From f7cee57859c7dd04d875f6cd38b2685cee0d1688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Denis?= Date: Wed, 16 Oct 2019 16:30:13 +0200 Subject: [PATCH] Refactor param reader to handle properly name conflicts between params and body - Do not "merge" body with query and path params - Improve errors on JSON parsing errors --- .../RestServletRequestParamReader.java | 100 +++++------------- .../request/ServletRequestParamReader.java | 70 +++++++++++- .../api/server/spi/EndpointsServletTest.java | 9 +- .../RestServletRequestParamReaderTest.java | 52 ++++++--- .../ServletRequestParamReaderTest.java | 61 +++++++++-- .../api/server/spi/testing/TestEndpoint.java | 27 +++++ 6 files changed, 216 insertions(+), 103 deletions(-) diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/request/RestServletRequestParamReader.java b/endpoints-framework/src/main/java/com/google/api/server/spi/request/RestServletRequestParamReader.java index ef39d459..6308cc65 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/request/RestServletRequestParamReader.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/request/RestServletRequestParamReader.java @@ -15,7 +15,7 @@ */ package com.google.api.server.spi.request; -import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.EndpointsContext; import com.google.api.server.spi.IoUtil; @@ -25,8 +25,6 @@ import com.google.api.server.spi.config.model.ApiParameterConfig; import com.google.api.server.spi.config.model.ApiSerializationConfig; import com.google.api.server.spi.response.BadRequestException; -import com.google.api.server.spi.types.DateAndTime; -import com.google.api.server.spi.types.SimpleDate; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableMap; @@ -42,9 +40,6 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; -import java.text.MessageFormat; -import java.util.Arrays; -import java.util.Date; import java.util.Enumeration; import java.util.List; import java.util.Map; @@ -91,7 +86,8 @@ public Object[] read() throws ServiceException { return new Object[0]; } HttpServletRequest servletRequest = endpointsContext.getRequest(); - JsonNode node; + ObjectNode body = (ObjectNode) objectReader.createObjectNode(); + ObjectNode params = (ObjectNode) objectReader.createObjectNode(); // multipart/form-data requests can be used for requests which have no resource body. In // this case, each part represents a named parameter instead. if (ServletFileUpload.isMultipartContent(servletRequest)) { @@ -107,7 +103,7 @@ public Object[] read() throws ServiceException { throw new BadRequestException("unable to parse multipart form field"); } } - node = obj; + params = obj; } catch (FileUploadException e) { throw new BadRequestException("unable to parse multipart request", e); } @@ -117,102 +113,60 @@ public Object[] read() throws ServiceException { // Unlike the Lily protocol, which essentially always requires a JSON body to exist (due to // path and query parameters being injected into the body), bodies are optional here, so we // create an empty body and inject named parameters to make deserialize work. - node = Strings.isEmptyOrWhitespace(requestBody) ? objectReader.createObjectNode() - : objectReader.readTree(requestBody); - } - if (!node.isObject()) { - throw new BadRequestException("expected a JSON object body"); + if (!Strings.isEmptyOrWhitespace(requestBody)) { + JsonNode node = objectReader.readTree(requestBody); + if (!node.isObject()) { + throw new BadRequestException("expected a JSON object body"); + } + body = (ObjectNode) node; + } } - ObjectNode body = (ObjectNode) node; Map> parameterMap = getParameterMap(method); // First add query parameters, then add path parameters. If the parameters already exist in // the resource, then the they aren't added to the body object. For compatibility reasons, // the order of precedence is resource field > query parameter > path parameter. for (Enumeration e = servletRequest.getParameterNames(); e.hasMoreElements(); ) { String parameterName = (String) e.nextElement(); - if (!body.has(parameterName)) { - Class parameterClass = parameterMap.get(parameterName); - ApiParameterConfig parameterConfig = parameterConfigMap.get(parameterName); - if (parameterClass != null && parameterConfig.isRepeated()) { - ArrayNode values = body.putArray(parameterName); - for (String value : servletRequest.getParameterValues(parameterName)) { - values.add(value); - } - } else { - body.put(parameterName, servletRequest.getParameterValues(parameterName)[0]); + Class parameterClass = parameterMap.get(parameterName); + ApiParameterConfig parameterConfig = parameterConfigMap.get(parameterName); + if (parameterClass != null && parameterConfig.isRepeated()) { + ArrayNode values = params.putArray(parameterName); + for (String value : servletRequest.getParameterValues(parameterName)) { + values.add(value); } + } else { + params.put(parameterName, servletRequest.getParameterValues(parameterName)[0]); } } for (Entry entry : rawPathParameters.entrySet()) { String parameterName = entry.getKey(); Class parameterClass = parameterMap.get(parameterName); - if (parameterClass != null && !body.has(parameterName)) { + if (parameterClass != null && !params.has(parameterName)) { if (parameterConfigMap.get(parameterName).isRepeated()) { - ArrayNode values = body.putArray(parameterName); + ArrayNode values = params.putArray(parameterName); for (String value : COMPOSITE_PATH_SPLITTER.split(entry.getValue())) { values.add(value); } } else { - body.put(parameterName, entry.getValue()); + params.put(parameterName, entry.getValue()); } } } for (Entry entry : parameterConfigMap.entrySet()) { - if (!body.has(entry.getKey()) && entry.getValue().getDefaultValue() != null) { - body.put(entry.getKey(), entry.getValue().getDefaultValue()); + if (!params.has(entry.getKey()) && entry.getValue().getDefaultValue() != null) { + params.put(entry.getKey(), entry.getValue().getDefaultValue()); } } - return deserializeParams(body); - } catch (InvalidFormatException e) { + return deserializeParams(body, params); + } catch (MismatchedInputException e) { logger.atInfo().withCause(e).log("Unable to read request parameter(s)"); - throw translate(e); + throw translateJsonException(e); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | IOException e) { logger.atInfo().withCause(e).log("Unable to read request parameter(s)"); throw new BadRequestException("Parse error", "parseError", e); } } - - private BadRequestException translate(InvalidFormatException e) { - String messagePattern = "Invalid {0} value \"{1}\".{2}"; - String message; - String reason = "parseError"; - if (e.getTargetType().isEnum()) { - message = MessageFormat.format(messagePattern, "enum", - e.getValue(), - " Valid values are " + Arrays.toString(e.getTargetType().getEnumConstants()) - ); - } else if (isNumber(e.getTargetType())) { - message = MessageFormat.format(messagePattern, "number", e.getValue(), ""); - } else if (isBoolean(e.getTargetType())) { - message = MessageFormat.format(messagePattern,"boolean", e.getValue(), " Valid values are [true, false]"); - } else if (isDate(e.getTargetType())) { - message = MessageFormat.format(messagePattern, "date", e.getValue(), ""); - } else { - message = "Parse error"; - } - - return new BadRequestException(message, reason, e); - } - - private boolean isBoolean(Class clazz) { - return Boolean.class.equals(clazz) || boolean.class.equals(clazz); - } - - private boolean isDate(Class clazz) { - return Date.class.isAssignableFrom(clazz) - || DateAndTime.class.equals(clazz) - || SimpleDate.class.equals(clazz); - } - - private boolean isNumber(Class clazz) { - return Number.class.isAssignableFrom(clazz) - || byte.class.equals(clazz) - || int.class.equals(clazz) - || long.class.equals(clazz) - || float.class.equals(clazz) - || double.class.equals(clazz); - } private static ImmutableMap> getParameterMap(EndpointMethod method) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/request/ServletRequestParamReader.java b/endpoints-framework/src/main/java/com/google/api/server/spi/request/ServletRequestParamReader.java index 6839c5f8..c8f85ddc 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/request/ServletRequestParamReader.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/request/ServletRequestParamReader.java @@ -16,7 +16,10 @@ package com.google.api.server.spi.request; import com.fasterxml.jackson.core.Base64Variants; +import com.fasterxml.jackson.databind.JsonMappingException.Reference; import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.api.server.spi.ConfiguredObjectMapper; import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.EndpointsContext; @@ -54,6 +57,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -65,6 +69,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; @@ -128,7 +133,7 @@ protected static List getParameterNames(EndpointMethod endpointMethod) return parameterNames; } - protected Object[] deserializeParams(JsonNode node) throws IOException, IllegalAccessException, + protected Object[] deserializeParams(JsonNode body, JsonNode parameters) throws IOException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, ServiceException { EndpointMethod method = getMethod(); Class[] paramClasses = method.getParameterClasses(); @@ -174,13 +179,13 @@ protected Object[] deserializeParams(JsonNode node) throws IOException, IllegalA } else { String name = parameterNames.get(i); if (Strings.isNullOrEmpty(name)) { - params[i] = (node == null) ? null : objectReader.forType(clazz).readValue(node); + params[i] = (body == null) ? null : objectReader.forType(clazz).readValue(body); logger.atFine().log("deserialize: %s %s injected into unnamed param[%d]", clazz, params[i], i); } else if (StandardParameters.isStandardParamName(name)) { - params[i] = getStandardParamValue(node, name); + params[i] = getStandardParamValue(parameters, name); } else { - JsonNode nodeValue = node.get(name); + JsonNode nodeValue = parameters.get(name); if (nodeValue == null) { params[i] = null; } else { @@ -356,10 +361,65 @@ public Object[] read() throws ServiceException { return new Object[0]; } JsonNode node = objectReader.readTree(requestBody); - return deserializeParams(node); + if (!node.isObject()) { + throw new BadRequestException("expected a JSON object body"); + } + //this convention comes from gapi.client to separate params and body + JsonNode resource = node.get("resource"); + ((ObjectNode) node).remove("resource"); + return deserializeParams(resource, node); + } catch (MismatchedInputException e) { + logger.atInfo().withCause(e).log("Unable to read request parameter(s)"); + throw translateJsonException(e); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | IOException e) { throw new BadRequestException(e); } } + + BadRequestException translateJsonException(MismatchedInputException e) { + String reason = "parseError"; + Class targetType = e.getTargetType(); + String fieldPath = e.getPath().stream().map(Reference::getFieldName) + .collect(Collectors.joining(".")); + String message = "Parse error at '" + fieldPath + + "' ('" + targetType.getSimpleName() + "' type)"; + String messagePattern = ": invalid {0} value \"{1}\".{2}"; + if (e instanceof InvalidFormatException) { + Object value = ((InvalidFormatException) e).getValue(); + if (targetType.isEnum()) { + message += MessageFormat.format(messagePattern, "enum", + value, + " Valid values are " + Arrays.toString(targetType.getEnumConstants()) + ); + } else if (isNumber(targetType)) { + message += MessageFormat.format(messagePattern, "number", value, ""); + } else if (isBoolean(targetType)) { + message += MessageFormat.format(messagePattern,"boolean", value, " Valid values are [true, false]"); + } else if (isDate(targetType)) { + message += MessageFormat.format(messagePattern, "date", value, ""); + } + } + + return new BadRequestException(message, reason, e); + } + + private boolean isBoolean(Class clazz) { + return Boolean.class.equals(clazz) || boolean.class.equals(clazz); + } + + private boolean isDate(Class clazz) { + return Date.class.isAssignableFrom(clazz) + || DateAndTime.class.equals(clazz) + || SimpleDate.class.equals(clazz); + } + + private boolean isNumber(Class clazz) { + return Number.class.isAssignableFrom(clazz) + || byte.class.equals(clazz) + || int.class.equals(clazz) + || long.class.equals(clazz) + || float.class.equals(clazz) + || double.class.equals(clazz); + } } diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/EndpointsServletTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/EndpointsServletTest.java index c45a6d54..30e1e29a 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/EndpointsServletTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/EndpointsServletTest.java @@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import java.nio.charset.StandardCharsets; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -102,7 +103,7 @@ public void empty() throws IOException { public void echo() throws IOException { req.setRequestURI("/_ah/api/test/v2/echo"); req.setMethod("POST"); - req.setParameter("x", "1"); + req.setContent("{\"x\":1}".getBytes(StandardCharsets.UTF_8)); servlet.service(req, resp); @@ -117,7 +118,7 @@ public void echo() throws IOException { public void contentLengthHeaderNull() throws IOException { req.setRequestURI("/_ah/api/test/v2/echo"); req.setMethod("POST"); - req.setParameter("x", "1"); + req.setContent("{\"x\":1}".getBytes(StandardCharsets.UTF_8)); servlet.service(req, resp); @@ -133,7 +134,7 @@ public void contentLengthHeaderPresent() throws IOException, ServletException { req.setRequestURI("/_ah/api/test/v2/echo"); req.setMethod("POST"); - req.setParameter("x", "1"); + req.setContent("{\"x\":1}".getBytes(StandardCharsets.UTF_8)); servlet.service(req, resp); @@ -145,7 +146,7 @@ public void methodOverride() throws IOException { req.setRequestURI("/_ah/api/test/v2/increment"); req.setMethod("POST"); req.addHeader("X-HTTP-Method-Override", "PATCH"); - req.setParameter("x", "1"); + req.setContent("{\"x\":1}".getBytes(StandardCharsets.UTF_8)); servlet.service(req, resp); diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/request/RestServletRequestParamReaderTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/request/RestServletRequestParamReaderTest.java index 43c11380..20427e81 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/request/RestServletRequestParamReaderTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/request/RestServletRequestParamReaderTest.java @@ -183,7 +183,8 @@ public void parseIntegerError() throws ServiceException { RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); thrown.expect(BadRequestException.class); - thrown.expectMessage("Invalid number"); + thrown.expectMessage("at 'objInt'"); + thrown.expectMessage("invalid number"); reader.read(); } @@ -193,7 +194,8 @@ public void parseIntError() throws ServiceException { RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); thrown.expect(BadRequestException.class); - thrown.expectMessage("Invalid number value \"invalid\""); + thrown.expectMessage("at 'simpleInt'"); + thrown.expectMessage("invalid number value \"invalid\""); reader.read(); } @@ -203,7 +205,8 @@ public void parseLongError() throws ServiceException { RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); thrown.expect(BadRequestException.class); - thrown.expectMessage("Invalid number"); + thrown.expectMessage("at 'objLong'"); + thrown.expectMessage("invalid number"); reader.read(); } @@ -213,7 +216,8 @@ public void parsePrimitiveLongError() throws ServiceException { RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); thrown.expect(BadRequestException.class); - thrown.expectMessage("Invalid number value \"invalid\""); + thrown.expectMessage("at 'simpleLong'"); + thrown.expectMessage("invalid number value \"invalid\""); reader.read(); } @@ -223,7 +227,8 @@ public void parseFloatError() throws ServiceException { RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); thrown.expect(BadRequestException.class); - thrown.expectMessage("Invalid number"); + thrown.expectMessage("at 'objFloat'"); + thrown.expectMessage("invalid number"); reader.read(); } @@ -233,7 +238,8 @@ public void parsePrimitiveFloatError() throws ServiceException { RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); thrown.expect(BadRequestException.class); - thrown.expectMessage("Invalid number value \"invalid\""); + thrown.expectMessage("at 'simpleFloat'"); + thrown.expectMessage("invalid number value \"invalid\""); reader.read(); } @@ -243,7 +249,8 @@ public void parseDoubleError() throws ServiceException { RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); thrown.expect(BadRequestException.class); - thrown.expectMessage("Invalid number"); + thrown.expectMessage("at 'objDouble'"); + thrown.expectMessage("invalid number"); reader.read(); } @@ -253,7 +260,8 @@ public void parsePrimitiveDoubleError() throws ServiceException { RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); thrown.expect(BadRequestException.class); - thrown.expectMessage("Invalid number value \"invalid\""); + thrown.expectMessage("at 'simpleDouble'"); + thrown.expectMessage("invalid number value \"invalid\""); reader.read(); } @@ -263,7 +271,8 @@ public void parseBooleanError() throws ServiceException { RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); thrown.expect(BadRequestException.class); - thrown.expectMessage("Invalid boolean value"); + thrown.expectMessage("at 'objBoolean'"); + thrown.expectMessage("invalid boolean value"); reader.read(); } @@ -273,7 +282,8 @@ public void parsePrimitiveBooleanError() throws ServiceException { RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); thrown.expect(BadRequestException.class); - thrown.expectMessage("Invalid boolean value \"invalid\""); + thrown.expectMessage("at 'simpleBoolean'"); + thrown.expectMessage("invalid boolean value \"invalid\""); reader.read(); } @@ -283,7 +293,8 @@ public void parseEnumError() throws ServiceException { RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); thrown.expect(BadRequestException.class); - thrown.expectMessage("Invalid enum value \"invalidEnum\". Valid values are [One, Two, Three]"); + thrown.expectMessage("at 'simpleEnum'"); + thrown.expectMessage("invalid enum value \"invalidEnum\". Valid values are [One, Two, Three]"); reader.read(); } @@ -293,7 +304,19 @@ public void parseDateError() throws ServiceException { RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); thrown.expect(BadRequestException.class); - thrown.expectMessage("Invalid date value \"invalidDate\"."); + thrown.expectMessage("at 'objDate'"); + thrown.expectMessage("invalid date value \"invalidDate\"."); + reader.read(); + } + + @Test + public void parseNestedError() throws ServiceException { + request.setContent("{\"nested\":{\"simpleInt\": \"abc\"}}".getBytes(StandardCharsets.UTF_8)); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + thrown.expect(BadRequestException.class); + thrown.expectMessage("at 'nested.simpleInt'"); + thrown.expectMessage("invalid number value \"abc\"."); reader.read(); } @@ -379,6 +402,10 @@ public enum TestEnum { One, Two, Three } + public static class NestedResource { + public int simpleInt; + } + public static class TestResource { public SimpleDate query; @@ -397,6 +424,7 @@ public static class TestResource { public Date objDate; public TestEnum simpleEnum; + public NestedResource nested; public TestResource() {} diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/request/ServletRequestParamReaderTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/request/ServletRequestParamReaderTest.java index d56bb0ac..3ff35b06 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/request/ServletRequestParamReaderTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/request/ServletRequestParamReaderTest.java @@ -47,6 +47,7 @@ import java.util.Arrays; import java.util.Calendar; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.Iterator; @@ -110,9 +111,9 @@ public void testRead() throws Exception { .put(TestEndpoint.NAME_LONG_OBJECT, String.valueOf(VALUE_LONG)) .put(TestEndpoint.NAME_FLOAT_OBJECT, String.valueOf(VALUE_FLOAT)) .put(TestEndpoint.NAME_DOUBLE_OBJECT, String.valueOf(VALUE_DOUBLE)) - .put("stringValue", "321") - .put("integerValue", "321") - .put("more", "999").build()); + .put("more", "999").build(), ImmutableMap.of( + "stringValue", "321", + "integerValue", "321")); assertEquals(VALUE_STRING, params[0]); assertEquals(VALUE_BOOLEAN, params[1]); @@ -156,8 +157,7 @@ public void testReadMissingParameters() throws Exception { .put(TestEndpoint.NAME_INTEGER_OBJECT, String.valueOf(VALUE_INTEGER)) .put(TestEndpoint.NAME_FLOAT, String.valueOf(VALUE_FLOAT)) .put(TestEndpoint.NAME_FLOAT_OBJECT, String.valueOf(VALUE_FLOAT)) - .put("stringValue", "321") - .put("more", "999").build()); + .put("more", "999").build(), ImmutableMap.of("stringValue", "321")); assertEquals(VALUE_STRING, params[0]); assertEquals(VALUE_BOOLEAN, params[1]); @@ -177,7 +177,7 @@ public void testReadMissingParameters() throws Exception { assertEquals(request, params[14]); } - private Object[] readExecuteMethod(ImmutableMap parameters) throws Exception { + private Object[] readExecuteMethod(ImmutableMap parameters, ImmutableMap resource) throws Exception { Method method = TestEndpoint.class.getDeclaredMethod("succeed", String.class, boolean.class, int.class, long.class, float.class, double.class, Boolean.class, Integer.class, Long.class, Float.class, Double.class, @@ -187,7 +187,15 @@ private Object[] readExecuteMethod(ImmutableMap parameters) thro for (Map.Entry entry : parameters.entrySet()) { builder.append(String.format("\"%s\":%s,", entry.getKey(), entry.getValue())); } - builder.replace(builder.length() - 1, builder.length(), "}"); + if (!resource.isEmpty()) { + builder.append("\"resource\":{"); + for (Map.Entry entry : resource.entrySet()) { + builder.append(String.format("\"%s\":%s,", entry.getKey(), entry.getValue())); + } + builder.replace(builder.length() - 1, builder.length(), "}}"); + } else { + builder.replace(builder.length() - 1, builder.length(), "}"); + } Object[] params = readParameters(builder.toString(), method); assertEquals(15, params.length); return params; @@ -556,8 +564,8 @@ public void foo(@Named("str") String string, } String requestString = "{\"str\":\"hello\",\"" + TestEndpoint.NAME_STRING + "\":\"" + VALUE_STRING + "\",\"" + TestEndpoint.NAME_INTEGER + "\":" + VALUE_INTEGER - + ",\"integer_array\":[1,2,3]," + "\"integer_collection\":[4,5,6], \"stringValue\":" - + "\"321\", \"integerValue\":321}"; + + ",\"integer_array\":[1,2,3]," + "\"integer_collection\":[4,5,6],\"resource\":{\"stringValue\":" + + "\"321\", \"integerValue\":321}}"; Method method = TestMultipleResources.class.getDeclaredMethod("foo", String.class, Integer[].class, Collection.class, Request.class); @@ -788,6 +796,41 @@ public void prettyPrint(@Named("prettyPrint") String prettyPrint) {} assertEquals(1, params.length); assertEquals(true, params[0]); } + + @Test + public void testNameInParamsAndResource() throws Exception { + class TestNameInParamsAndResource { + @SuppressWarnings("unused") + public void test(@Named("stringValue") List string, + @Nullable @Named("integerValue") List integer, Request resource) {} + } + Object[] params = readParameters( + "{\"stringValue\": [\"fromParams\"], \"integerValue\": [1,2,3], " + + "\"resource\": {\"stringValue\": \"abc\", \"integerValue\": 42}}", + TestNameInParamsAndResource.class + .getDeclaredMethod("test", List.class, List.class, Request.class)); + assertEquals(3, params.length); + assertEquals(Collections.singletonList("fromParams"), params[0]); + assertEquals(ImmutableList.of(1,2,3), params[1]); + assertEquals(new Request("abc", 42), params[2]); + } + + @Test + public void testTypeMismatch() throws Exception { + class TesTypeMismatch { + @SuppressWarnings("unused") + public void test(Request request) {} + } + try { + readParameters( + "{\"resource\": {\"integerValue\": [42]}}", + TesTypeMismatch.class + .getDeclaredMethod("test", Request.class)); + fail("expected bad request exception"); + } catch (BadRequestException e) { + assertEquals("Parse error at 'integerValue' ('int' type)", e.getMessage()); + } + } @Test public void testUserInjectionThrowsExceptionIfRequired() throws Exception { diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/TestEndpoint.java b/test-utils/src/main/java/com/google/api/server/spi/testing/TestEndpoint.java index 97ce8f1d..d7f0a28e 100644 --- a/test-utils/src/main/java/com/google/api/server/spi/testing/TestEndpoint.java +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/TestEndpoint.java @@ -29,6 +29,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Objects; import javax.inject.Named; import javax.servlet.http.HttpServletRequest; @@ -59,6 +60,14 @@ public static class Request { private String string; private Integer integer = -1; + public Request() { + } + + public Request(String string, Integer integer) { + this.string = string; + this.integer = integer; + } + public void setStringValue(String string) { this.string = string; } @@ -74,6 +83,24 @@ public String getStringValue() { public Integer getIntegerValue() { return integer; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Request request = (Request) o; + return Objects.equals(string, request.string) && + Objects.equals(integer, request.integer); + } + + @Override + public int hashCode() { + return Objects.hash(string, integer); + } } public enum TestEnum {