package com.revolsys.ui.web.rest.interceptor; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.net.URLDecoder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.Principal; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.BiFunction; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.util.ClassUtils; import org.springframework.util.FileCopyUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ValueConstants; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartRequest; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.support.RequestContextUtils; import org.springframework.web.util.WebUtils; import com.revolsys.datatype.DataType; import com.revolsys.datatype.DataTypes; import com.revolsys.io.Reader; import com.revolsys.io.Writer; import com.revolsys.logging.Logs; import com.revolsys.ui.web.annotation.RequestAttribute; import com.revolsys.ui.web.utils.HttpServletUtils; import com.revolsys.util.Exceptions; import com.revolsys.util.Property; import com.revolsys.util.WrappedException; import com.revolsys.util.function.Function3; public class WebMethodHandler { private static final Map<Class<?>, WebParameterHandler> CLASS_HANDLERS = new HashMap<>(); private static final Map<Class<?>, Function3<WebAnnotationMethodHandlerAdapter, Parameter, Annotation, WebParameterHandler>> ANNOTATION_HANDLERS = new HashMap<>(); static { // TODO Map final WebParameterHandler requestHandler = (request, response) -> { return request; }; CLASS_HANDLERS.put(ServletRequest.class, requestHandler); CLASS_HANDLERS.put(HttpServletRequest.class, requestHandler); final WebParameterHandler responseHandler = (request, response) -> { return response; }; CLASS_HANDLERS.put(ServletResponse.class, responseHandler); CLASS_HANDLERS.put(HttpServletResponse.class, responseHandler); CLASS_HANDLERS.put(HttpSession.class, (request, response) -> { return request.getSession(); }); CLASS_HANDLERS.put(Principal.class, (request, response) -> { return request.getUserPrincipal(); }); CLASS_HANDLERS.put(Locale.class, (request, response) -> { return RequestContextUtils.getLocale(request); }); CLASS_HANDLERS.put(InputStream.class, (request, response) -> { try { return request.getInputStream(); } catch (final Exception e) { return Exceptions.throwUncheckedException(e); } }); CLASS_HANDLERS.put(OutputStream.class, (request, response) -> { try { return response.getOutputStream(); } catch (final Exception e) { return Exceptions.throwUncheckedException(e); } }); CLASS_HANDLERS.put(Reader.class, (request, response) -> { try { return request.getReader(); } catch (final Exception e) { return Exceptions.throwUncheckedException(e); } }); CLASS_HANDLERS.put(Writer.class, (request, response) -> { try { return response.getWriter(); } catch (final Exception e) { return Exceptions.throwUncheckedException(e); } }); ANNOTATION_HANDLERS.put(RequestBody.class, WebMethodHandler::body); ANNOTATION_HANDLERS.put(CookieValue.class, WebMethodHandler::cookie); ANNOTATION_HANDLERS.put(RequestHeader.class, WebMethodHandler::requestHeader); ANNOTATION_HANDLERS.put(RequestParam.class, WebMethodHandler::requestParameter); ANNOTATION_HANDLERS.put(RequestAttribute.class, WebMethodHandler::requestAttribute); ANNOTATION_HANDLERS.put(PathVariable.class, WebMethodHandler::pathVariable); } @SuppressWarnings({ "unchecked", "rawtypes" }) public static WebParameterHandler body(final WebAnnotationMethodHandlerAdapter adapter, final Parameter parameter, final Annotation annotation) { final boolean required = ((RequestBody)annotation).required(); final String parameterName = parameter.getName(); final Class parameterClass = parameter.getType(); final DataType dataType = DataTypes.getDataType(parameterClass); return WebParameterHandler.function( // parameterName, // (request, response) -> { try { final HttpInputMessage inputMessage = new ServletServerHttpRequest(request); MediaType contentType = MediaTypeUtil.getContentType(request); if (contentType == null) { contentType = MediaType.APPLICATION_FORM_URLENCODED; } if (!MediaType.APPLICATION_FORM_URLENCODED.includes(contentType) && !MediaType.MULTIPART_FORM_DATA.includes(contentType)) { contentType = MediaTypeUtil.getRequestMediaType(request, adapter.mediaTypes, adapter.mediaTypeOrder, adapter.urlPathHelper, adapter.parameterName, adapter.defaultMediaType, ""); } final HttpHeaders headers = inputMessage.getHeaders(); if (contentType == null) { final StringBuilder builder = new StringBuilder( ClassUtils.getShortName(parameterClass)); final String paramName = parameterName; if (paramName != null) { builder.append(' '); builder.append(paramName); } throw new HttpMediaTypeNotSupportedException("Cannot extract @RequestBody parameter (" + builder.toString() + "): no Content-Type found"); } else { HttpServletUtils.setContentTypeWithCharset(headers, contentType); } final List<MediaType> allSupportedMediaTypes = new ArrayList<>(); if (adapter.messageConverters != null) { for (final HttpMessageConverter<?> messageConverter : adapter.messageConverters) { allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); if (messageConverter.canRead(parameterClass, contentType)) { return messageConverter.read(parameterClass, inputMessage); } } String body = null; if (MediaType.APPLICATION_FORM_URLENCODED.includes(contentType)) { Charset charset = contentType.getCharSet(); if (charset == null) { charset = StandardCharsets.UTF_8; } final String urlBody = FileCopyUtils .copyToString(new InputStreamReader(inputMessage.getBody(), charset)); final String[] pairs = StringUtils.tokenizeToStringArray(urlBody, "&"); final MultiValueMap<String, String> values = new LinkedMultiValueMap<>(pairs.length); for (final String pair : pairs) { final int idx = pair.indexOf('='); if (idx == -1) { values.add(URLDecoder.decode(pair, charset.name()), null); } else { final String name = URLDecoder.decode(pair.substring(0, idx), charset.name()); final String value = URLDecoder.decode(pair.substring(idx + 1), charset.name()); values.add(name, value); } } body = values.getFirst("body"); } else if (request instanceof MultipartHttpServletRequest) { final MultipartHttpServletRequest multiPartRequest = (MultipartHttpServletRequest)request; final MultipartFile bodyFile = multiPartRequest.getFile("body"); contentType = MediaTypeUtil.getRequestMediaType(request, adapter.mediaTypes, adapter.mediaTypeOrder, adapter.urlPathHelper, adapter.parameterName, adapter.defaultMediaType, bodyFile.getOriginalFilename()); HttpServletUtils.setContentTypeWithCharset(headers, contentType); final HttpInputMessage newInputMessage = new HttpInputMessage() { @Override public InputStream getBody() throws IOException { return bodyFile.getInputStream(); } @Override public HttpHeaders getHeaders() { return headers; } }; for (final HttpMessageConverter<?> messageConverter : adapter.messageConverters) { if (messageConverter.canRead(parameterClass, contentType)) { return messageConverter.read(parameterClass, newInputMessage); } } } if (body == null) { body = request.getParameter("body"); } if (body != null) { contentType = MediaTypeUtil.getRequestMediaType(request, adapter.mediaTypes, adapter.mediaTypeOrder, adapter.urlPathHelper, adapter.parameterName, adapter.defaultMediaType, ""); HttpServletUtils.setContentTypeWithCharset(headers, contentType); byte[] bytes; bytes = body.getBytes(); final InputStream bodyIn = new ByteArrayInputStream(bytes); final HttpInputMessage newInputMessage = new HttpInputMessage() { @Override public InputStream getBody() throws IOException { return bodyIn; } @Override public HttpHeaders getHeaders() { return headers; } }; for (final HttpMessageConverter<?> messageConverter : adapter.messageConverters) { if (messageConverter.canRead(parameterClass, contentType)) { return messageConverter.read(parameterClass, newInputMessage); } } } } throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes); } catch (final Exception e) { return Exceptions.throwUncheckedException(e); } }, // dataType, // required, // null// ); } public static WebParameterHandler cookie(final WebAnnotationMethodHandlerAdapter adapter, final Parameter parameter, final Annotation annotation) { final Class<?> parameterClass = parameter.getType(); final DataType dataType = DataTypes.getDataType(parameterClass); final CookieValue cookieValue = (CookieValue)annotation; final String name = getName(parameter, cookieValue.value()); final boolean required = cookieValue.required(); final Object defaultValue = parseDefaultValueAttribute(dataType, cookieValue.defaultValue()); BiFunction<HttpServletRequest, HttpServletResponse, Object> function; if (Cookie.class.equals(parameterClass)) { function = (request, response) -> { final Cookie cookie = WebUtils.getCookie(request, name); return cookie; }; } else { function = (request, response) -> { final Cookie cookie = WebUtils.getCookie(request, name); if (cookie == null) { return null; } else { return cookie.getValue(); } }; } return WebParameterHandler.function( // name, // function, // dataType, // required, // defaultValue // ); } private static String getName(final Parameter parameter, final String name) { if (Property.hasValue(name)) { return name; } else { return parameter.getName(); } } public static Object parseDefaultValueAttribute(final DataType dataType, final String value) { if (ValueConstants.DEFAULT_NONE.equals(value)) { return null; } else { return dataType.toObject(value); } } @SuppressWarnings("unchecked") public static WebParameterHandler pathVariable(final WebAnnotationMethodHandlerAdapter adapter, final Parameter parameter, final Annotation annotation) { final Class<?> parameterClass = parameter.getType(); final DataType dataType = DataTypes.getDataType(parameterClass); final PathVariable pathVariable = (PathVariable)annotation; final String name = getName(parameter, pathVariable.value()); return WebParameterHandler.function( // name, // (request, response) -> { final Map<String, String> uriTemplateVariables = (Map<String, String>)request .getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); if (uriTemplateVariables == null) { return null; } else { return uriTemplateVariables.get(name); } }, // dataType, // true, // null // ); } public static WebParameterHandler requestAttribute( final WebAnnotationMethodHandlerAdapter adapter, final Parameter parameter, final Annotation annotation) { final Class<?> parameterClass = parameter.getType(); final DataType dataType = DataTypes.getDataType(parameterClass); final RequestAttribute requestAttribute = (RequestAttribute)annotation; final String name = getName(parameter, requestAttribute.value()); final boolean required = requestAttribute.required(); final Object defaultValue = parseDefaultValueAttribute(dataType, requestAttribute.defaultValue()); return WebParameterHandler.function( // name, // (request, response) -> { return request.getAttribute(name); }, // dataType, // required, // defaultValue // ); } public static WebParameterHandler requestHeader(final WebAnnotationMethodHandlerAdapter adapter, final Parameter parameter, final Annotation annotation) { final Class<?> parameterClass = parameter.getType(); final DataType dataType = DataTypes.getDataType(parameterClass); final RequestHeader requestHeader = (RequestHeader)annotation; final String name = getName(parameter, requestHeader.value()); final boolean required = requestHeader.required(); final Object defaultValue = parseDefaultValueAttribute(dataType, requestHeader.defaultValue()); return WebParameterHandler.function( // name, // (request, response) -> { return request.getHeader(name); }, // dataType, // required, // defaultValue // ); } public static WebParameterHandler requestParameter( final WebAnnotationMethodHandlerAdapter adapter, final Parameter parameter, final Annotation annotation) { final RequestParam requestParam = (RequestParam)annotation; final String name = getName(parameter, requestParam.value()); final boolean required = requestParam.required(); final String defaultValueString = requestParam.defaultValue(); final Class<?> parameterClass = parameter.getType(); final DataType dataType = DataTypes.getDataType(parameterClass); BiFunction<HttpServletRequest, HttpServletResponse, Object> function; Object defaultValue = null; if (List.class.equals(parameterClass)) { if (ValueConstants.DEFAULT_NONE.equals(defaultValueString)) { final ParameterizedType parameterizedType = (ParameterizedType)parameter .getParameterizedType(); final Type[] typeParameters = parameterizedType.getActualTypeArguments(); final Type elementType = typeParameters[0]; if (MultipartFile.class.equals(elementType)) { function = (request, response) -> { if (request instanceof MultipartRequest) { final MultipartRequest multipartRequest = (MultipartRequest)request; return multipartRequest.getFiles(name); } else { return Collections.emptyList(); } }; } else { final DataType elementDataType = DataTypes.getDataType(elementType); function = (request, response) -> { final List<Object> list = new ArrayList<>(); final String[] parameterValues = request.getParameterValues(name); if (parameterValues != null) { for (final String stringValue : parameterValues) { final Object value = elementDataType.toObject(stringValue); list.add(value); } } return list; }; } } else { throw new IllegalArgumentException("RequestParam.defaultValue not allowed for " + name); } } else if (parameterClass.isArray()) { if (ValueConstants.DEFAULT_NONE.equals(defaultValueString)) { final Class<?> elementClass = parameterClass.getComponentType(); if (MultipartFile.class.equals(elementClass)) { function = (request, response) -> { if (request instanceof MultipartRequest) { final MultipartRequest multipartRequest = (MultipartRequest)request; final List<MultipartFile> files = multipartRequest.getFiles(name); return files.toArray(); } else { return new MultipartFile[0]; } }; } else { final DataType elementDataType = DataTypes.getDataType(elementClass); function = (request, response) -> { final String[] parameterValues = request.getParameterValues(name); int length; if (parameterValues == null) { length = 0; } else { length = parameterValues.length; } final Object array = Array.newInstance(elementClass, length); for (int i = 0; i < length; i++) { final String stringValue = parameterValues[i]; final Object value = elementDataType.toObject(stringValue); Array.set(array, i, value); } return array; }; } } else { throw new IllegalArgumentException("RequestParam.defaultValue not allowed for " + name); } } else { defaultValue = parseDefaultValueAttribute(dataType, defaultValueString); if (MultipartFile.class.equals(parameterClass)) { function = (request, response) -> { if (request instanceof MultipartRequest) { final MultipartRequest multipartRequest = (MultipartRequest)request; return multipartRequest.getFile(name); } else { return null; } }; } else { function = (request, response) -> { return request.getParameter(name); }; } } return WebParameterHandler.function( // name, // function, // dataType, // required, // defaultValue // ); } private final Method method; private final int parameterCount; private final WebParameterHandler[] parameterHandlers; private final WebAnnotationMethodHandlerAdapter adapter; public WebMethodHandler(final WebAnnotationMethodHandlerAdapter adapter, final Method method) { this.adapter = adapter; this.method = method; final Parameter[] parameters = method.getParameters(); this.parameterCount = parameters.length; this.parameterHandlers = new WebParameterHandler[this.parameterCount]; for (int i = 0; i < this.parameterCount; i++) { final Parameter parameter = parameters[i]; this.parameterHandlers[i] = newParameterHandler(parameter); } } public Method getMethod() { return this.method; } public Object invokeMethod(final Object handler, final HttpServletRequest request, final HttpServletResponse response) throws Exception { final Object[] parameters = new Object[this.parameterCount]; for (int i = 0; i < this.parameterHandlers.length; i++) { try { final WebParameterHandler webParameterHandler = this.parameterHandlers[i]; final Object parameterValue = webParameterHandler.getParameter(request, response); parameters[i] = parameterValue; } catch (final WrappedException e) { throw (Exception)e.getCause(); } } try { return this.method.invoke(handler, parameters); } catch (final Throwable e) { return Exceptions.throwUncheckedException(e); } } protected WebParameterHandler newParameterHandler(final Parameter parameter) { final Annotation[] annotations = parameter.getAnnotations(); final Class<?> parameterClass = parameter.getType(); WebParameterHandler parameterHandler = CLASS_HANDLERS.get(parameterClass); for (final Annotation annotation : annotations) { final Class<? extends Annotation> annotationClass = annotation.annotationType(); final Function3<WebAnnotationMethodHandlerAdapter, Parameter, Annotation, WebParameterHandler> factory = ANNOTATION_HANDLERS .get(annotationClass); if (factory != null) { final WebParameterHandler currentParameterHandler = factory.apply(this.adapter, parameter, annotation); if (currentParameterHandler != null) { if (parameterHandler == null) { parameterHandler = currentParameterHandler; } else { throw new IllegalArgumentException( "Multiple matches for parameter: " + parameter + " " + annotation); } } } } if (parameterHandler == null) { parameterHandler = CLASS_HANDLERS.get(parameterClass); if (parameterHandler == null) { Logs.warn(this, "No handler for: " + parameter); return WebParameterHandler.fixed(null); } } return parameterHandler; } }