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 02d2e5e6..76388a03 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,6 +15,7 @@ */ package com.google.api.server.spi.request; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.EndpointsContext; import com.google.api.server.spi.IoUtil; @@ -24,6 +25,8 @@ 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; @@ -39,12 +42,13 @@ 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; import java.util.Map.Entry; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; @@ -159,12 +163,63 @@ public Object[] read() throws ServiceException { } } return deserializeParams(body); + } catch (InvalidFormatException e) { + logger.atInfo().withCause(e).log("Unable to read request parameter(s)"); + throw translate(e); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | IOException e) { logger.atInfo().withCause(e).log("Unable to read request parameter(s)"); - throw new BadRequestException(e); + throw new BadRequestException("Parse error", "parseError", "global"); + } + } + + private BadRequestException translate(InvalidFormatException e) { + String messagePattern = "Invalid {0} value \"{1}\".{2}"; + String reason = "parseError"; + if (e.getTargetType().isEnum()) { + return new BadRequestException( + MessageFormat.format(messagePattern, "enum", + e.getValue(), + " Valid values are " + Arrays.toString(e.getTargetType().getEnumConstants())), + reason + ); + } else if (isNumber(e.getTargetType())) { + return new BadRequestException( + MessageFormat.format(messagePattern, "number", e.getValue(), ""), + reason + ); + } else if (isBoolean(e.getTargetType())) { + return new BadRequestException( + MessageFormat.format(messagePattern,"boolean", e.getValue(), " Valid values are [true, false]"), + reason + ); + } else if (isDate(e.getTargetType())) { + return new BadRequestException( + MessageFormat.format(messagePattern, "date", e.getValue(), ""), + reason + ); + } else { + return new BadRequestException("Parse error", reason, "global"); } } + 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 6b6761b9..6839c5f8 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,6 +16,7 @@ package com.google.api.server.spi.request; import com.fasterxml.jackson.core.Base64Variants; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; import com.google.api.server.spi.ConfiguredObjectMapper; import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.EndpointsContext; @@ -61,8 +62,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -257,9 +256,13 @@ private static class DateDeserializer extends JsonDeserializer { @Override public Date deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException { - com.google.api.client.util.DateTime date = - new com.google.api.client.util.DateTime(jsonParser.readValueAs(String.class)); - return new Date(date.getValue()); + String value = jsonParser.readValueAs(String.class); + try { + com.google.api.client.util.DateTime date = new com.google.api.client.util.DateTime(value); + return new Date(date.getValue()); + } catch (NumberFormatException e) { + throw InvalidFormatException.from(jsonParser, e.getMessage(), value, Date.class); + } } } @@ -267,7 +270,12 @@ private static class DateAndTimeDeserializer extends JsonDeserializer { 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 e55ebc15..a820b2a9 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 @@ -21,6 +21,7 @@ import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.EndpointsContext; import com.google.api.server.spi.ServiceContext; +import com.google.api.server.spi.ServiceException; import com.google.api.server.spi.TypeLoader; import com.google.api.server.spi.config.Api; import com.google.api.server.spi.config.ApiMethod; @@ -39,7 +40,9 @@ import java.util.ArrayList; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.springframework.mock.web.MockHttpServletRequest; @@ -48,6 +51,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.Objects; @@ -67,6 +71,9 @@ public class RestServletRequestParamReaderTest { private ApiSerializationConfig serializationConfig; private ApiConfig apiConfig; private ApiMethodConfig methodConfig; + + @Rule + public ExpectedException thrown = ExpectedException.none(); @Before public void setUp() throws Exception { @@ -169,7 +176,127 @@ public void nonObjectRequest() throws Exception { // expected } } + + @Test + public void parseIntegerError() throws ServiceException { + request.setContent("{\"objInt\":\"invalid\"}".getBytes(StandardCharsets.UTF_8)); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + thrown.expect(BadRequestException.class); + thrown.expectMessage("Invalid number"); + reader.read(); + } + + @Test + public void parseIntError() throws ServiceException { + request.setContent("{\"simpleInt\":\"invalid\"}".getBytes(StandardCharsets.UTF_8)); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + thrown.expect(BadRequestException.class); + thrown.expectMessage("Invalid number value \"invalid\""); + reader.read(); + } + + @Test + public void parseLongError() throws ServiceException { + request.setContent("{\"objLong\":\"invalid\"}".getBytes(StandardCharsets.UTF_8)); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + thrown.expect(BadRequestException.class); + thrown.expectMessage("Invalid number"); + reader.read(); + } + + @Test + public void parsePrimitiveLongError() throws ServiceException { + request.setContent("{\"simpleLong\":\"invalid\"}".getBytes(StandardCharsets.UTF_8)); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + thrown.expect(BadRequestException.class); + thrown.expectMessage("Invalid number value \"invalid\""); + reader.read(); + } + + @Test + public void parseFloatError() throws ServiceException { + request.setContent("{\"objFloat\":\"invalid\"}".getBytes(StandardCharsets.UTF_8)); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + thrown.expect(BadRequestException.class); + thrown.expectMessage("Invalid number"); + reader.read(); + } + + @Test + public void parsePrimitiveFloatError() throws ServiceException { + request.setContent("{\"simpleFloat\":\"invalid\"}".getBytes(StandardCharsets.UTF_8)); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + thrown.expect(BadRequestException.class); + thrown.expectMessage("Invalid number value \"invalid\""); + reader.read(); + } + @Test + public void parseDoubleError() throws ServiceException { + request.setContent("{\"objDouble\":\"invalid\"}".getBytes(StandardCharsets.UTF_8)); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + thrown.expect(BadRequestException.class); + thrown.expectMessage("Invalid number"); + reader.read(); + } + + @Test + public void parsePrimitiveDoubleError() throws ServiceException { + request.setContent("{\"simpleDouble\":\"invalid\"}".getBytes(StandardCharsets.UTF_8)); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + thrown.expect(BadRequestException.class); + thrown.expectMessage("Invalid number value \"invalid\""); + reader.read(); + } + + @Test + public void parseBooleanError() throws ServiceException { + request.setContent("{\"objBoolean\":\"invalid\"}".getBytes(StandardCharsets.UTF_8)); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + thrown.expect(BadRequestException.class); + thrown.expectMessage("Invalid boolean value"); + reader.read(); + } + + @Test + public void parsePrimitiveBooleanError() throws ServiceException { + request.setContent("{\"simpleBoolean\":\"invalid\"}".getBytes(StandardCharsets.UTF_8)); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + thrown.expect(BadRequestException.class); + thrown.expectMessage("Invalid boolean value \"invalid\""); + reader.read(); + } + + @Test + public void parseEnumError() throws ServiceException { + request.setContent("{\"simpleEnum\":\"invalidEnum\"}".getBytes(StandardCharsets.UTF_8)); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + thrown.expect(BadRequestException.class); + thrown.expectMessage("Invalid enum value \"invalidEnum\". Valid values are [One, Two, Three]"); + reader.read(); + } + + @Test + public void parseDateError() throws ServiceException { + request.setContent("{\"objDate\":\"invalidDate\"}".getBytes(StandardCharsets.UTF_8)); + RestServletRequestParamReader reader = createReader(ImmutableMap.of("path", "1234")); + + thrown.expect(BadRequestException.class); + thrown.expectMessage("Invalid date value \"invalidDate\"."); + reader.read(); + } + @Test public void gzippedRequest() throws Exception { request.addParameter("path", "1234"); @@ -237,9 +364,29 @@ private RestServletRequestParamReader createReader(Map rawPathPa serializationConfig, methodConfig); } + public enum TestEnum { + One, Two, Three + } + public static class TestResource { public SimpleDate query; - + + public int simpleInt; + public long simpleLong; + public float simpleFloat; + public double simpleDouble; + public boolean simpleBoolean; + + public Integer objInt; + public Long objLong; + public Float objFloat; + public Double objDouble; + public Boolean objBoolean; + + public Date objDate; + + public TestEnum simpleEnum; + public TestResource() {} public TestResource(SimpleDate query) { 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 5061f134..d56bb0ac 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 @@ -901,8 +901,8 @@ private void verifySimpleDateSerializationFails(String simpleDateString) try { readParameters( "{" + TestEndpoint.NAME_DATE_AND_TIME + ":\"" + simpleDateString + "\"}", method); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException expected) {} + fail("Expected BadRequestException"); + } catch (BadRequestException expected) {} } private Calendar getCalendarFromDate(Date date) {