Java tutorial
/* * Copyright 2016 Google Inc. All Rights Reserved. * * Licensed 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 com.google.api.server.spi.request; import com.google.api.server.spi.ConfiguredObjectMapper; import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.IoUtil; import com.google.api.server.spi.ServiceException; import com.google.api.server.spi.auth.common.User; import com.google.api.server.spi.config.Named; import com.google.api.server.spi.config.annotationreader.AnnotationUtil; import com.google.api.server.spi.config.model.ApiSerializationConfig; import com.google.api.server.spi.config.model.StandardParameters; 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.appengine.api.datastore.Blob; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.reflect.TypeToken; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.module.SimpleModule; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; 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; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; /** * Reads parameters from an {@link HttpServletRequest}. */ public class ServletRequestParamReader extends AbstractParamReader { private static final Logger logger = Logger.getLogger(ServletRequestParamReader.class.getName()); private static final Set<SimpleModule> READER_MODULES; private static final String APPENGINE_USER_CLASS_NAME = "com.google.appengine.api.users.User"; static { Set<SimpleModule> modules = new LinkedHashSet<>(); SimpleModule dateModule = new SimpleModule("dateModule", new Version(1, 0, 0, null, null, null)); dateModule.addDeserializer(Date.class, new DateDeserializer()); modules.add(dateModule); SimpleModule simpleDateModule = new SimpleModule("simpleDateModule", new Version(1, 0, 0, null, null, null)); simpleDateModule.addDeserializer(SimpleDate.class, new SimpleDateDeserializer()); modules.add(simpleDateModule); SimpleModule dateAndTimeModule = new SimpleModule("dateAndTimeModule", new Version(1, 0, 0, null, null, null)); dateAndTimeModule.addDeserializer(DateAndTime.class, new DateAndTimeDeserializer()); modules.add(dateAndTimeModule); try { // Attempt to load the Blob class, which may not exist outside of App Engine Standard. ServletRequestParamReader.class.getClassLoader().loadClass("com.google.appengine.api.datastore.Blob"); SimpleModule blobModule = new SimpleModule("blobModule", new Version(1, 0, 0, null, null, null)); blobModule.addDeserializer(Blob.class, new BlobDeserializer()); modules.add(blobModule); } catch (ClassNotFoundException e) { // Ignore this error, since we can function without the Blob deserializer. } READER_MODULES = Collections.unmodifiableSet(modules); } protected static List<String> getParameterNames(EndpointMethod endpointMethod) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { List<String> parameterNames = endpointMethod.getParameterNames(); if (parameterNames == null) { Method method = endpointMethod.getMethod(); parameterNames = new ArrayList<>(); for (int parameter = 0; parameter < method.getParameterTypes().length; parameter++) { Annotation annotation = AnnotationUtil.getNamedParameter(method, parameter, Named.class); if (annotation == null) { parameterNames.add(null); } else { parameterNames.add((String) annotation.getClass().getMethod("value").invoke(annotation)); } } endpointMethod.setParameterNames(parameterNames); } return parameterNames; } protected Object[] deserializeParams(JsonNode node) throws IOException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, ServiceException { EndpointMethod method = getMethod(); Class<?>[] paramClasses = method.getParameterClasses(); TypeToken<?>[] paramTypes = method.getParameterTypes(); Object[] params = new Object[paramClasses.length]; List<String> parameterNames = getParameterNames(method); for (int i = 0; i < paramClasses.length; i++) { TypeToken<?> type = paramTypes[i]; Class<?> clazz = paramClasses[i]; if (User.class.isAssignableFrom(clazz)) { // User type parameter requires no Named annotation (ignored if present) User user = getUser(); if (user == null || clazz.isAssignableFrom(user.getClass())) { params[i] = user; logger.log(Level.FINE, "deserialize: User injected into param[{0}]", i); } else { logger.log(Level.WARNING, "deserialize: User object of type {0} is not assignable to {1}. User will be null.", new Object[] { user.getClass().getName(), clazz.getName() }); } } else if (APPENGINE_USER_CLASS_NAME.equals(clazz.getName())) { // User type parameter requires no Named annotation (ignored if present) params[i] = getAppEngineUser(); logger.log(Level.FINE, "deserialize: App Engine User injected into param[{0}]", i); } else if (clazz == HttpServletRequest.class) { // HttpServletRequest type parameter requires no Named annotation (ignored if present) params[i] = servletRequest; logger.log(Level.FINE, "deserialize: HttpServletRequest injected into param[{0}]", i); } else if (clazz == ServletContext.class) { // ServletContext type parameter requires no Named annotation (ignored if present) params[i] = servletContext; logger.log(Level.FINE, "deserialize: ServletContext {0} injected into param[{1}]", new Object[] { params[i], i }); } else { String name = parameterNames.get(i); if (Strings.isNullOrEmpty(name)) { params[i] = (node == null) ? null : objectReader.forType(clazz).readValue(node); logger.log(Level.FINE, "deserialize: {0} {1} injected into unnamed param[{2}]", new Object[] { clazz, params[i], i }); } else if (StandardParameters.isStandardParamName(name)) { params[i] = getStandardParamValue(node, name); } else { JsonNode nodeValue = node.get(name); if (nodeValue == null) { params[i] = null; } else { // Check for collection type if (Collection.class.isAssignableFrom(clazz) && type.getType() instanceof ParameterizedType) { params[i] = deserializeCollection(clazz, (ParameterizedType) type.getType(), nodeValue); } else { params[i] = objectReader.forType(clazz).readValue(nodeValue); } } logger.log(Level.FINE, "deserialize: {0} {1} injected into param[{2}] named {3}", new Object[] { clazz, params[i], i, name }); } } } return params; } @VisibleForTesting User getUser() throws ServiceException { return Auth.from(servletRequest).authenticate(); } @VisibleForTesting com.google.appengine.api.users.User getAppEngineUser() throws ServiceException { return Auth.from(servletRequest).authenticateAppEngineUser(); } private Object getStandardParamValue(JsonNode body, String paramName) { if (!StandardParameters.isStandardParamName(paramName)) { throw new IllegalArgumentException("paramName"); } else if (StandardParameters.USER_IP.equals(paramName)) { return servletRequest.getRemoteAddr(); } else if (StandardParameters.PRETTY_PRINT.equals(paramName)) { return StandardParameters.shouldPrettyPrint(servletRequest); } JsonNode value = body.get(paramName); if (value == null && StandardParameters.ALT.equals(paramName)) { return "json"; } return value != null ? value.asText() : null; } private <T> Collection<T> deserializeCollection(Class<?> clazz, ParameterizedType collectionType, JsonNode nodeValue) throws IOException { @SuppressWarnings("unchecked") Class<? extends Collection<T>> collectionClass = (Class<? extends Collection<T>>) clazz; @SuppressWarnings("unchecked") Class<T> paramClass = (Class<T>) EndpointMethod .getClassFromType(collectionType.getActualTypeArguments()[0]); @SuppressWarnings("unchecked") Class<T[]> arrayClazz = (Class<T[]>) Array.newInstance(paramClass, 0).getClass(); Collection<T> collection = objectReader.forType(collectionClass).readValue(objectReader.createArrayNode()); if (nodeValue != null) { T[] array = objectReader.forType(arrayClazz).readValue(nodeValue); collection.addAll(Arrays.asList(array)); } return collection; } private static class DateDeserializer extends JsonDeserializer<Date> { @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()); } } private static class DateAndTimeDeserializer extends JsonDeserializer<DateAndTime> { @Override public DateAndTime deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException { return DateAndTime.parseRfc3339String(jsonParser.readValueAs(String.class)); } } private static class SimpleDateDeserializer extends JsonDeserializer<SimpleDate> { Pattern pattern = Pattern.compile("^(\\d{4})-(\\d{2})-(\\d{2})$"); @Override public SimpleDate deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException { String value = jsonParser.readValueAs(String.class).trim(); Matcher matcher = pattern.matcher(value); if (matcher.find()) { int year = Integer.parseInt(matcher.group(1)); int month = Integer.parseInt(matcher.group(2)); int day = Integer.parseInt(matcher.group(3)); return new SimpleDate(year, month, day); } else { throw new IllegalArgumentException("String is not an RFC3339 formated date (yyyy-mm-dd): " + value); } } } private static class BlobDeserializer extends JsonDeserializer<Blob> { @Override public Blob deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException { return new Blob(jsonParser.getBinaryValue()); } } protected final HttpServletRequest servletRequest; private final ServletContext servletContext; protected final ObjectReader objectReader; public ServletRequestParamReader(EndpointMethod method, HttpServletRequest servletRequest, ServletContext servletContext, ApiSerializationConfig serializationConfig) { super(method); this.servletRequest = servletRequest; this.servletContext = servletContext; LinkedHashSet<SimpleModule> modules = new LinkedHashSet<>(); modules.addAll(READER_MODULES); this.objectReader = ConfiguredObjectMapper.builder().apiSerializationConfig(serializationConfig) .addRegisteredModules(modules).build().reader(); } @Override public Object[] read() throws ServiceException { // Assumes input stream to be encoded in UTF-8 // TODO: Take charset from content-type as encoding try { String requestBody = IoUtil.readStream(servletRequest.getInputStream()); logger.log(Level.FINE, "requestBody=" + requestBody); if (requestBody == null || requestBody.trim().isEmpty()) { return new Object[0]; } JsonNode node = objectReader.readTree(requestBody); return deserializeParams(node); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | IOException e) { throw new BadRequestException(e); } } }