This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.3 by this push:
new 7bec3c215b [Cherry-pick to branch-1.3] [#12783] fix(server): return
JSON errors for the whole pre-resource-method WebApplicationException family
(#12878) (#12910)
7bec3c215b is described below
commit 7bec3c215b2af003434fd674e96922f73fb4d2e2
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Sep 4 15:54:57 2026 +0800
[Cherry-pick to branch-1.3] [#12783] fix(server): return JSON errors for
the whole pre-resource-method WebApplicationException family (#12878) (#12910)
**Cherry-pick Information:**
- Original commit: deb1e2d21f007535e21d67fd7e5f8e8f09240f3c
- Target branch: `branch-1.3`
- Status: ⚠️ **Has conflicts - manual resolution required**
**Do not merge** until conflict markers are resolved and the
`cherry-pick-conflict` label is removed.
Please review and resolve the conflicts before merging.
---------
Co-authored-by: Jerry Shao <[email protected]>
Co-authored-by: Claude Sonnet 5 <[email protected]>
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
Co-authored-by: Jerry Shao <[email protected]>
---
.../integration/test/JsonErrorHandlerIT.java | 64 +++++++++++++-----
.../apache/gravitino/server/GravitinoServer.java | 2 +
.../gravitino/server/web/VersioningFilter.java | 12 +++-
.../web/mapper/WebApplicationExceptionMapper.java | 77 ++++++++++++++++++++++
.../gravitino/server/web/TestVersioningFilter.java | 38 +++++++++--
.../mapper/TestWebApplicationExceptionMapper.java | 67 +++++++++++++++++++
6 files changed, 236 insertions(+), 24 deletions(-)
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/JsonErrorHandlerIT.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/JsonErrorHandlerIT.java
index 797f9eda3c..c3841ae975 100644
---
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/JsonErrorHandlerIT.java
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/JsonErrorHandlerIT.java
@@ -30,13 +30,16 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
- * Integration test verifying that a malformed typed path parameter on the
metadata API (e.g. a
- * non-numeric model version) returns the same structured JSON {@link
ErrorResponse} used by every
- * other error on the API, instead of Jetty's default HTML error page.
+ * Integration test verifying that requests rejected before reaching a
resource method (malformed
+ * typed parameters, unmatched routes, wrong HTTP methods, an unsupported API
version) return the
+ * same structured JSON {@link ErrorResponse} used by every other error on the
API, instead of
+ * Jetty's default HTML error page.
*
- * <p>Jersey fails to convert the path segment into the resource method's
typed {@code @PathParam}
- * before any resource method runs, so the metalake/catalog/schema/model in
the URL need not
- * actually exist for this failure to occur.
+ * <p>Most of these cases are handled by Jersey itself — converting a typed
+ * {@code @PathParam}/{@code @QueryParam}, matching a route, matching an HTTP
method to a resource —
+ * before any resource method runs. The unsupported-API-version case is
rejected earlier still, by
+ * {@code VersioningFilter} before the request ever reaches Jersey. Either
way, the
+ * metalake/catalog/schema/model in the URL need not actually exist for these
failures to occur.
*/
public class JsonErrorHandlerIT extends BaseIT {
@@ -45,42 +48,69 @@ public class JsonErrorHandlerIT extends BaseIT {
@Test
public void testMalformedModelVersionReturnsJsonErrorBody() throws Exception
{
HttpResponse<String> response =
-
sendGet("/api/metalakes/m/catalogs/c/schemas/s/models/mo/versions/abc");
+ sendRequest("GET",
"/api/metalakes/m/catalogs/c/schemas/s/models/mo/versions/abc");
Assertions.assertEquals(404, response.statusCode());
- assertJsonNotFoundBody(response, "PathParamException");
+ assertJsonErrorBody(response, ErrorConstants.NOT_FOUND_CODE,
"PathParamException");
}
@Test
public void testMalformedModelVersionUriReturnsJsonErrorBody() throws
Exception {
HttpResponse<String> response =
-
sendGet("/api/metalakes/m/catalogs/c/schemas/s/models/mo/versions/abc/uri");
+ sendRequest("GET",
"/api/metalakes/m/catalogs/c/schemas/s/models/mo/versions/abc/uri");
Assertions.assertEquals(404, response.statusCode());
- assertJsonNotFoundBody(response, "PathParamException");
+ assertJsonErrorBody(response, ErrorConstants.NOT_FOUND_CODE,
"PathParamException");
}
@Test
public void testUnknownApiRouteStillReturnsJsonErrorBody() throws Exception {
// A route that Jersey cannot match at all is a different failure (no
@PathParam conversion
// is even attempted), but it must be covered by the same fix.
- HttpResponse<String> response = sendGet("/api/v99/nonexistent/route");
+ HttpResponse<String> response = sendRequest("GET",
"/api/v99/nonexistent/route");
Assertions.assertEquals(404, response.statusCode());
- assertJsonNotFoundBody(response, "NotFoundException");
+ assertJsonErrorBody(response, ErrorConstants.NOT_FOUND_CODE,
"NotFoundException");
}
- private HttpResponse<String> sendGet(String path) throws Exception {
+ @Test
+ public void testWrongHttpMethodReturnsJsonErrorBody() throws Exception {
+ // /api/version only supports GET; POSTing to it never reaches a resource
method either.
+ HttpResponse<String> response = sendRequest("POST", "/api/version");
+
+ Assertions.assertEquals(405, response.statusCode());
+ assertJsonErrorBody(
+ response, ErrorConstants.UNSUPPORTED_OPERATION_CODE,
"UnsupportedOperationException");
+ }
+
+ @Test
+ public void testUnsupportedApiVersionReturnsJsonErrorBody() throws Exception
{
+ // VersioningFilter rejects an unrecognized API version before the request
ever reaches
+ // Jersey, so this is not covered by any JAX-RS ExceptionMapper.
HttpRequest request =
HttpRequest.newBuilder()
- .uri(new URI("http://localhost:" + getGravitinoServerPort() +
path))
+ .uri(new URI("http://localhost:" + getGravitinoServerPort() +
"/api/version"))
+ .header("Accept", "application/vnd.gravitino.v99+json")
.GET()
.build();
+ HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
+
+ Assertions.assertEquals(406, response.statusCode());
+ assertJsonErrorBody(
+ response, ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
"IllegalArgumentException");
+ }
+
+ private HttpResponse<String> sendRequest(String method, String path) throws
Exception {
+ HttpRequest request =
+ HttpRequest.newBuilder()
+ .uri(new URI("http://localhost:" + getGravitinoServerPort() +
path))
+ .method(method, HttpRequest.BodyPublishers.noBody())
+ .build();
return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
}
- private void assertJsonNotFoundBody(HttpResponse<String> response, String
expectedType)
- throws Exception {
+ private void assertJsonErrorBody(
+ HttpResponse<String> response, int expectedCode, String expectedType)
throws Exception {
String contentType =
response.headers().firstValue("Content-Type").orElse("");
Assertions.assertTrue(
contentType.contains("application/json"),
@@ -90,7 +120,7 @@ public class JsonErrorHandlerIT extends BaseIT {
ErrorResponse errorResponse =
ObjectMapperProvider.objectMapper().readValue(response.body(),
ErrorResponse.class);
- Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE,
errorResponse.getCode());
+ Assertions.assertEquals(expectedCode, errorResponse.getCode());
Assertions.assertEquals(expectedType, errorResponse.getType());
Assertions.assertFalse(errorResponse.getMessage().isEmpty());
}
diff --git
a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
index 31f820af32..831422ebc6 100644
--- a/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
+++ b/server/src/main/java/org/apache/gravitino/server/GravitinoServer.java
@@ -64,6 +64,7 @@ import
org.apache.gravitino.server.web.mapper.JsonParseExceptionMapper;
import org.apache.gravitino.server.web.mapper.JsonProcessingExceptionMapper;
import org.apache.gravitino.server.web.mapper.NotFoundExceptionMapper;
import org.apache.gravitino.server.web.mapper.ParamExceptionMapper;
+import org.apache.gravitino.server.web.mapper.WebApplicationExceptionMapper;
import org.apache.gravitino.server.web.ui.WebUIFilter;
import org.apache.gravitino.stats.StatisticDispatcher;
import org.apache.gravitino.tag.TagDispatcher;
@@ -169,6 +170,7 @@ public class GravitinoServer extends ResourceConfig {
register(JsonMappingExceptionMapper.class);
register(ParamExceptionMapper.class);
register(NotFoundExceptionMapper.class);
+ register(WebApplicationExceptionMapper.class);
register(ObjectMapperProvider.class).register(JacksonFeature.class);
property(CommonProperties.JSON_JACKSON_DISABLED_MODULES,
"DefaultScalaModule");
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/VersioningFilter.java
b/server/src/main/java/org/apache/gravitino/server/web/VersioningFilter.java
index 7c2c71b32d..b2263e7f40 100644
--- a/server/src/main/java/org/apache/gravitino/server/web/VersioningFilter.java
+++ b/server/src/main/java/org/apache/gravitino/server/web/VersioningFilter.java
@@ -19,6 +19,7 @@
package org.apache.gravitino.server.web;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
@@ -36,6 +37,7 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
+import org.apache.gravitino.dto.responses.ErrorResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -104,9 +106,17 @@ public class VersioningFilter implements Filter {
if (!ApiVersion.isSupportedVersion(version)) {
LOG.error("Unsupported version v{} in Request Header {}.", version,
value);
+ String message = String.format("Unsupported version v%d in request
header", version);
+ ErrorResponse errorResponse =
ErrorResponse.illegalArguments(message);
+ // Write the JSON ErrorResponse directly instead of calling
+ // HttpServletResponse#sendError, so this filter -- which runs
before Jersey ever sees
+ // the request -- doesn't fall through to Jetty's default HTML error
page.
HttpServletResponse resp = (HttpServletResponse) response;
- resp.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "Unsupported
version");
+ resp.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
+ resp.setContentType("application/json");
+ resp.setCharacterEncoding(StandardCharsets.UTF_8.name());
+ ObjectMapperProvider.objectMapper().writeValue(resp.getWriter(),
errorResponse);
} else {
chain.doFilter(request, response);
}
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/mapper/WebApplicationExceptionMapper.java
b/server/src/main/java/org/apache/gravitino/server/web/mapper/WebApplicationExceptionMapper.java
new file mode 100644
index 0000000000..94ed1b48c4
--- /dev/null
+++
b/server/src/main/java/org/apache/gravitino/server/web/mapper/WebApplicationExceptionMapper.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.server.web.mapper;
+
+import javax.annotation.Priority;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.eclipse.jetty.http.HttpStatus;
+
+/**
+ * WebApplicationExceptionMapper returns a structured JSON error body for any
{@link
+ * WebApplicationException} that JAX-RS/Jersey raises itself before a resource
method runs (e.g. a
+ * wrong HTTP method or an unacceptable {@code Accept}/{@code Content-Type}
header), instead of
+ * letting the servlet container fall back to Jetty's default HTML error page.
+ *
+ * <p>{@link org.glassfish.jersey.server.ParamException} and {@link
javax.ws.rs.NotFoundException}
+ * are subtypes of {@link WebApplicationException} with their own, more
specific mappers ({@link
+ * ParamExceptionMapper}, {@link NotFoundExceptionMapper}); JAX-RS always
prefers the mapper
+ * registered for the nearest type in the exception's class hierarchy, so this
mapper only applies
+ * to every other case in the family.
+ *
+ * <p>Whatever entity {@code exception.getResponse()} may already carry is
deliberately not
+ * preserved: every error under {@code /api/*} must be the same {@link
ErrorResponse} JSON shape, so
+ * an exception that needs to convey more than a status code should do so
through {@link
+ * WebApplicationException#getMessage()}, which this mapper does read.
+ */
+@Priority(1)
+public class WebApplicationExceptionMapper implements
ExceptionMapper<WebApplicationException> {
+
+ @Override
+ public Response toResponse(WebApplicationException exception) {
+ int status = exception.getResponse().getStatus();
+ String message =
+ StringUtils.isBlank(exception.getMessage())
+ ? HttpStatus.getMessage(status)
+ : exception.getMessage();
+
+ return Response.fromResponse(exception.getResponse())
+ .entity(toErrorResponse(status, message))
+ .type(MediaType.APPLICATION_JSON)
+ .build();
+ }
+
+ private static ErrorResponse toErrorResponse(int status, String message) {
+ switch (status) {
+ case HttpServletResponse.SC_BAD_REQUEST:
+ return ErrorResponse.illegalArguments(message);
+ case HttpServletResponse.SC_FORBIDDEN:
+ return ErrorResponse.forbidden(message, null);
+ case HttpServletResponse.SC_METHOD_NOT_ALLOWED:
+ return ErrorResponse.unsupportedOperation(message);
+ default:
+ return ErrorResponse.restError(message);
+ }
+ }
+}
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/TestVersioningFilter.java
b/server/src/test/java/org/apache/gravitino/server/web/TestVersioningFilter.java
index 6da270ea4f..d7cb3ba303 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/TestVersioningFilter.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/TestVersioningFilter.java
@@ -31,6 +31,8 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
@@ -40,12 +42,30 @@ import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.apache.gravitino.dto.responses.ErrorConstants;
+import org.apache.gravitino.dto.responses.ErrorResponse;
import
org.apache.gravitino.server.web.VersioningFilter.MutableHttpServletRequest;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
public class TestVersioningFilter {
+ private static StringWriter stubWriter(HttpServletResponse mockResponse)
throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(stringWriter));
+ return stringWriter;
+ }
+
+ private static void assertUnsupportedVersionResponse(
+ HttpServletResponse mockResponse, StringWriter writer) throws
IOException {
+ verify(mockResponse).setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
+ verify(mockResponse).setContentType("application/json");
+ ErrorResponse errorResponse =
+ ObjectMapperProvider.objectMapper().readValue(writer.toString(),
ErrorResponse.class);
+ assertEquals(ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
errorResponse.getCode());
+ assertTrue(errorResponse.getMessage().contains("Unsupported version"));
+ }
+
@Test
public void testDoFilterWithSupportedVersion() throws ServletException,
IOException {
VersioningFilter filter = new VersioningFilter();
@@ -75,11 +95,12 @@ public class TestVersioningFilter {
.thenReturn(
new
Vector<>(Collections.singletonList("application/vnd.gravitino.v2+json"))
.elements());
+ StringWriter writer = stubWriter(mockResponse);
filter.doFilter(mockRequest, mockResponse, mockChain);
verify(mockChain, never()).doFilter(any(), any());
- verify(mockResponse).sendError(HttpServletResponse.SC_NOT_ACCEPTABLE,
"Unsupported version");
+ assertUnsupportedVersionResponse(mockResponse, writer);
}
@Test
@@ -145,10 +166,11 @@ public class TestVersioningFilter {
verify(mockChain).doFilter(any(), any());
reset(mockChain, mockResponse);
+ StringWriter writer = stubWriter(mockResponse);
filter.doFilter(mockRequest, mockResponse, mockChain);
verify(mockChain, never()).doFilter(any(), any());
- verify(mockResponse).sendError(HttpServletResponse.SC_NOT_ACCEPTABLE,
"Unsupported version");
+ assertUnsupportedVersionResponse(mockResponse, writer);
}
@Test
@@ -229,16 +251,18 @@ public class TestVersioningFilter {
verify(mockChain).doFilter(any(), any());
reset(mockChain, mockResponse);
+ StringWriter writer1 = stubWriter(mockResponse);
filter.doFilter(mockRequest, mockResponse, mockChain);
verify(mockChain, never()).doFilter(any(), any());
- verify(mockResponse).sendError(HttpServletResponse.SC_NOT_ACCEPTABLE,
"Unsupported version");
+ assertUnsupportedVersionResponse(mockResponse, writer1);
reset(mockChain, mockResponse);
+ StringWriter writer2 = stubWriter(mockResponse);
filter.doFilter(mockRequest, mockResponse, mockChain);
verify(mockChain, never()).doFilter(any(), any());
- verify(mockResponse).sendError(HttpServletResponse.SC_NOT_ACCEPTABLE,
"Unsupported version");
+ assertUnsupportedVersionResponse(mockResponse, writer2);
}
@Test
@@ -285,9 +309,10 @@ public class TestVersioningFilter {
"application/vnd.gravitino.v2+json, application/json"))
.elements());
+ StringWriter writer1 = stubWriter(mockResponse);
filter.doFilter(mockRequest, mockResponse, mockChain);
verify(mockChain, never()).doFilter(any(), any());
- verify(mockResponse).sendError(HttpServletResponse.SC_NOT_ACCEPTABLE,
"Unsupported version");
+ assertUnsupportedVersionResponse(mockResponse, writer1);
reset(mockChain, mockResponse);
@@ -297,8 +322,9 @@ public class TestVersioningFilter {
Collections.singletonList(
"application/vnd.gravitino.v3+json; q=0.9,
application/json"))
.elements());
+ StringWriter writer2 = stubWriter(mockResponse);
filter.doFilter(mockRequest, mockResponse, mockChain);
verify(mockChain, never()).doFilter(any(), any());
- verify(mockResponse).sendError(HttpServletResponse.SC_NOT_ACCEPTABLE,
"Unsupported version");
+ assertUnsupportedVersionResponse(mockResponse, writer2);
}
}
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/mapper/TestWebApplicationExceptionMapper.java
b/server/src/test/java/org/apache/gravitino/server/web/mapper/TestWebApplicationExceptionMapper.java
new file mode 100644
index 0000000000..5aa2dc7d9c
--- /dev/null
+++
b/server/src/test/java/org/apache/gravitino/server/web/mapper/TestWebApplicationExceptionMapper.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.server.web.mapper;
+
+import javax.ws.rs.NotAcceptableException;
+import javax.ws.rs.NotAllowedException;
+import javax.ws.rs.NotSupportedException;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.dto.responses.ErrorConstants;
+import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestWebApplicationExceptionMapper {
+
+ private final WebApplicationExceptionMapper mapper = new
WebApplicationExceptionMapper();
+
+ @Test
+ public void testWrongHttpMethodReturnsJsonBody() {
+ Response response = mapper.toResponse(new NotAllowedException("GET"));
+ ErrorResponse entity = (ErrorResponse) response.getEntity();
+
+ Assertions.assertEquals(
+ Response.Status.METHOD_NOT_ALLOWED.getStatusCode(),
response.getStatus());
+ Assertions.assertEquals(ErrorConstants.UNSUPPORTED_OPERATION_CODE,
entity.getCode());
+ Assertions.assertFalse(entity.getMessage().isEmpty());
+ Assertions.assertTrue(
+ response.getHeaderString("Allow") != null
+ && response.getHeaderString("Allow").contains("GET"));
+ }
+
+ @Test
+ public void testUnsupportedMediaTypeReturnsJsonBody() {
+ Response response = mapper.toResponse(new
NotSupportedException("unsupported content type"));
+ ErrorResponse entity = (ErrorResponse) response.getEntity();
+
+ Assertions.assertEquals(
+ Response.Status.UNSUPPORTED_MEDIA_TYPE.getStatusCode(),
response.getStatus());
+ Assertions.assertEquals(ErrorConstants.REST_ERROR_CODE, entity.getCode());
+ Assertions.assertEquals("unsupported content type", entity.getMessage());
+ }
+
+ @Test
+ public void testNotAcceptableReturnsJsonBody() {
+ Response response = mapper.toResponse(new NotAcceptableException());
+ ErrorResponse entity = (ErrorResponse) response.getEntity();
+
+ Assertions.assertEquals(Response.Status.NOT_ACCEPTABLE.getStatusCode(),
response.getStatus());
+ Assertions.assertFalse(entity.getMessage().isEmpty());
+ }
+}