/** * Copyright 2010-2016 Ralph Schaer <ralphschaer@gmail.com> * * 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 ch.ralscha.extdirectspring.util; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.charset.Charset; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.context.ApplicationContext; import org.springframework.lang.UsesJava8; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.DigestUtils; import org.springframework.util.ReflectionUtils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import ch.ralscha.extdirectspring.bean.ExtDirectRequest; import ch.ralscha.extdirectspring.bean.api.PollingProvider; import ch.ralscha.extdirectspring.bean.api.RemotingApi; import ch.ralscha.extdirectspring.controller.ConfigurationService; import ch.ralscha.extdirectspring.controller.RouterController; /** * Utility class */ public final class ExtDirectSpringUtil { public static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); private static Class<?> javaUtilOptionalClass = null; static { try { javaUtilOptionalClass = ClassUtils.forName("java.util.Optional", RouterController.class.getClassLoader()); } catch (ClassNotFoundException ex) { // Java 8 not available - Optional references simply not supported then. } } private ExtDirectSpringUtil() { // singleton } /** * Checks if two objects are equal. Returns true if both objects are null * * @param a object one * @param b object two * @return true if objects are equal */ public static boolean equal(Object a, Object b) { return a == b || a != null && a.equals(b); } /** * Checks if the request is a multipart request * * @param request the HTTP servlet request * @return true if request is a Multipart request (file upload) */ public static boolean isMultipart(HttpServletRequest request) { if (!"post".equals(request.getMethod().toLowerCase())) { return false; } String contentType = request.getContentType(); return contentType != null && contentType.toLowerCase().startsWith("multipart/"); } /** * Invokes a method on a Spring managed bean. * * @param context a Spring application context * @param beanName the name of the bean * @param methodInfo the methodInfo object * @param params the parameters * @return the result of the method invocation * @throws IllegalArgumentException if there is no bean in the context * @throws IllegalAccessException * @throws InvocationTargetException */ public static Object invoke(ApplicationContext context, String beanName, MethodInfo methodInfo, final Object[] params) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { Object bean = context.getBean(beanName); Method handlerMethod = methodInfo.getMethod(); ReflectionUtils.makeAccessible(handlerMethod); Object result = handlerMethod.invoke(bean, params); if (result != null && result.getClass().equals(javaUtilOptionalClass)) { return OptionalUnwrapper.unwrap(result); } return result; } /** * Inner class to avoid a hard dependency on Java 8. */ @UsesJava8 private static class OptionalUnwrapper { public static Object unwrap(Object optionalObject) { return ((Optional<?>) optionalObject).orElse(null); } } public static Object invoke(HttpServletRequest request, HttpServletResponse response, Locale locale, ApplicationContext context, ExtDirectRequest directRequest, ParametersResolver parametersResolver, MethodInfoCache cache) throws Exception { MethodInfo methodInfo = cache.get(directRequest.getAction(), directRequest.getMethod()); Object[] resolvedParams = parametersResolver.resolveParameters(request, response, locale, directRequest, methodInfo); return invoke(context, directRequest.getAction(), methodInfo, resolvedParams); } /** * Converts a stacktrace into a String * * @param t a Throwable * @return the whole stacktrace in a String */ public static String getStackTrace(Throwable t) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw, true); t.printStackTrace(pw); pw.flush(); sw.flush(); return sw.toString(); } private final static long secondsInAMonth = 30L * 24L * 60L * 60L; /** * Adds Expires, ETag and Cache-Control response headers. * * @param response the HTTP servlet response * @param etag the calculated etag (md5) of the response * @param month number of months the response can be cached. Added to the Expires and * Cache-Control header. If null defaults to 6 months. */ public static void addCacheHeaders(HttpServletResponse response, String etag, Integer month) { Assert.notNull(etag, "ETag must not be null"); long seconds; if (month != null) { seconds = month * secondsInAMonth; } else { seconds = 6L * secondsInAMonth; } response.setDateHeader("Expires", System.currentTimeMillis() + seconds * 1000L); response.setHeader("ETag", etag); response.setHeader("Cache-Control", "public, max-age=" + seconds); } /** * Checks etag and sends back HTTP status 304 if not modified. If modified sets * content type and content length, adds cache headers ( * {@link #addCacheHeaders(HttpServletResponse, String, Integer)}), writes the data * into the {@link HttpServletResponse#getOutputStream()} and flushes it. * * @param request the HTTP servlet request * @param response the HTTP servlet response * @param data the response data * @param contentType the content type of the data (i.e. * "application/javascript;charset=UTF-8") * @throws IOException */ public static void handleCacheableResponse(HttpServletRequest request, HttpServletResponse response, byte[] data, String contentType) throws IOException { String ifNoneMatch = request.getHeader("If-None-Match"); String etag = "\"0" + DigestUtils.md5DigestAsHex(data) + "\""; addCacheHeaders(response, etag, 6); if (etag.equals(ifNoneMatch)) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); return; } response.setContentType(contentType); response.setContentLength(data.length); @SuppressWarnings("resource") ServletOutputStream out = response.getOutputStream(); out.write(data); out.flush(); } /** * Returns the api configuration as a String. Uses "REMOTING_API" and "POLLING_URLS" * for the variable names * @param ctx The spring applicationcontext * @return the api configuration * @throws JsonProcessingException */ public static String generateApiString(ApplicationContext ctx) throws JsonProcessingException { return generateApiString(ctx, "REMOTING_API", "POLLING_URLS"); } /** * Returns the api configuration as a String * @param ctx The spring applicationcontext * @param remotingVarName name of the variable for the remoting configuration (e.g. * REMOTING_API) * @param pollingApiVarName name of the variable for the polling configuration (e.g. * POLLING_URLS) * @return the api configuration * @throws JsonProcessingException */ public static String generateApiString(ApplicationContext ctx, String remotingVarName, String pollingApiVarName) throws JsonProcessingException { RemotingApi remotingApi = new RemotingApi(ctx.getBean(ConfigurationService.class) .getConfiguration().getProviderType(), "router", null); for (Map.Entry<MethodInfoCache.Key, MethodInfo> entry : ctx .getBean(MethodInfoCache.class)) { MethodInfo methodInfo = entry.getValue(); if (methodInfo.getAction() != null) { remotingApi.addAction(entry.getKey().getBeanName(), methodInfo.getAction()); } else if (methodInfo.getPollingProvider() != null) { remotingApi.addPollingProvider(methodInfo.getPollingProvider()); } } remotingApi.sort(); StringBuilder extDirectConfig = new StringBuilder(100); extDirectConfig.append("var ").append(remotingVarName).append(" = "); extDirectConfig.append(new ObjectMapper().writer().withDefaultPrettyPrinter() .writeValueAsString(remotingApi)); extDirectConfig.append(";"); List<PollingProvider> pollingProviders = remotingApi.getPollingProviders(); if (!pollingProviders.isEmpty()) { extDirectConfig.append("\n\nvar ").append(pollingApiVarName).append(" = {\n"); for (int i = 0; i < pollingProviders.size(); i++) { extDirectConfig.append(" \""); extDirectConfig.append(pollingProviders.get(i).getEvent()); extDirectConfig.append("\" : \"poll/"); extDirectConfig.append(pollingProviders.get(i).getBeanName()); extDirectConfig.append("/"); extDirectConfig.append(pollingProviders.get(i).getMethod()); extDirectConfig.append("/"); extDirectConfig.append(pollingProviders.get(i).getEvent()); extDirectConfig.append("\""); if (i < pollingProviders.size() - 1) { extDirectConfig.append(",\n"); } } extDirectConfig.append("\n};"); } return extDirectConfig.toString(); } }