/** * 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.controller; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.util.FileCopyUtils; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.util.WebUtils; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; import ch.ralscha.extdirectspring.annotation.ExtDirectMethod; import ch.ralscha.extdirectspring.annotation.ExtDirectMethodType; import ch.ralscha.extdirectspring.bean.BaseResponse; import ch.ralscha.extdirectspring.bean.EdFormLoadResult; import ch.ralscha.extdirectspring.bean.EdFormPostResult; import ch.ralscha.extdirectspring.bean.EdStoreResult; import ch.ralscha.extdirectspring.bean.ExtDirectFormLoadResult; import ch.ralscha.extdirectspring.bean.ExtDirectFormPostResult; import ch.ralscha.extdirectspring.bean.ExtDirectPollResponse; import ch.ralscha.extdirectspring.bean.ExtDirectRequest; import ch.ralscha.extdirectspring.bean.ExtDirectResponse; import ch.ralscha.extdirectspring.bean.ExtDirectResponseRaw; import ch.ralscha.extdirectspring.bean.ExtDirectStoreResult; import ch.ralscha.extdirectspring.bean.JsonViewHint; import ch.ralscha.extdirectspring.bean.ModelAndJsonView; import ch.ralscha.extdirectspring.util.ExtDirectSpringUtil; import ch.ralscha.extdirectspring.util.MethodInfo; import ch.ralscha.extdirectspring.util.MethodInfoCache; /** * Main router controller that handles polling, form handler and normal Ext Direct calls. */ @Controller public class RouterController { public static final MediaType APPLICATION_JSON = new MediaType("application", "json", ExtDirectSpringUtil.UTF8_CHARSET); public static final MediaType TEXT_HTML = new MediaType("text", "html", ExtDirectSpringUtil.UTF8_CHARSET); private static final Log log = LogFactory.getLog(RouterController.class); private final RequestMappingHandlerAdapter handlerAdapter; private final ConfigurationService configurationService; private final MethodInfoCache methodInfoCache; @Autowired(required = false) private Set<ExtRequestListener> extRequestListeners; @Autowired public RouterController(RequestMappingHandlerAdapter handlerAdapter, ConfigurationService configurationService, MethodInfoCache methodInfoCache) { this.handlerAdapter = handlerAdapter; this.configurationService = configurationService; this.methodInfoCache = methodInfoCache; } @RequestMapping(value = "/poll/{beanName}/{method}/{event}") public void poll(@PathVariable("beanName") String beanName, @PathVariable("method") String method, @PathVariable("event") String event, HttpServletRequest request, HttpServletResponse response, Locale locale) throws Exception { ExtDirectPollResponse directPollResponse = new ExtDirectPollResponse(); directPollResponse.setName(event); MethodInfo methodInfo = this.methodInfoCache.get(beanName, method); boolean streamResponse; Class<?> jsonView = null; if (methodInfo != null) { streamResponse = this.configurationService.getConfiguration() .isStreamResponse() || methodInfo.isStreamResponse(); try { Object[] parameters = this.configurationService.getParametersResolver() .prepareParameters(request, response, locale, methodInfo); if (this.configurationService.getConfiguration().isSynchronizeOnSession() || methodInfo.isSynchronizeOnSession()) { HttpSession session = request.getSession(false); if (session != null) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { Object result = ExtDirectSpringUtil.invoke( this.configurationService.getApplicationContext(), beanName, methodInfo, parameters); if (result instanceof ModelAndJsonView) { ModelAndJsonView modelAndJsonView = (ModelAndJsonView) result; directPollResponse.setData(modelAndJsonView.getModel()); jsonView = getJsonView(modelAndJsonView, methodInfo.getJsonView()); } else { directPollResponse.setData(result); jsonView = getJsonView(result, methodInfo.getJsonView()); } } } else { Object result = ExtDirectSpringUtil.invoke( this.configurationService.getApplicationContext(), beanName, methodInfo, parameters); if (result instanceof ModelAndJsonView) { ModelAndJsonView modelAndJsonView = (ModelAndJsonView) result; directPollResponse.setData(modelAndJsonView.getModel()); jsonView = getJsonView(modelAndJsonView, methodInfo.getJsonView()); } else { directPollResponse.setData(result); jsonView = getJsonView(result, methodInfo.getJsonView()); } } } else { Object result = ExtDirectSpringUtil.invoke( this.configurationService.getApplicationContext(), beanName, methodInfo, parameters); if (result instanceof ModelAndJsonView) { ModelAndJsonView modelAndJsonView = (ModelAndJsonView) result; directPollResponse.setData(modelAndJsonView.getModel()); jsonView = getJsonView(modelAndJsonView, methodInfo.getJsonView()); } else { directPollResponse.setData(result); jsonView = getJsonView(result, methodInfo.getJsonView()); } } } catch (Exception e) { log.error("Error polling method '" + beanName + "." + method + "'", e.getCause() != null ? e.getCause() : e); directPollResponse.setData( handleException(methodInfo, directPollResponse, e, request)); } } else { log.error("Error invoking method '" + beanName + "." + method + "'. Method or Bean not found"); handleMethodNotFoundError(directPollResponse, beanName, method); streamResponse = this.configurationService.getConfiguration() .isStreamResponse(); } writeJsonResponse(response, directPollResponse, jsonView, streamResponse); } @RequestMapping(value = "/router", method = RequestMethod.POST, params = "extAction") public String router(HttpServletRequest request, HttpServletResponse response, @RequestParam("extAction") String extAction, @RequestParam("extMethod") String extMethod) throws IOException { ExtDirectResponse directResponse = new ExtDirectResponse(request); MethodInfo methodInfo = this.methodInfoCache.get(extAction, extMethod); boolean streamResponse; if (methodInfo != null && methodInfo.getForwardPath() != null) { return methodInfo.getForwardPath(); } else if (methodInfo != null && methodInfo.getHandlerMethod() != null) { streamResponse = this.configurationService.getConfiguration() .isStreamResponse() || methodInfo.isStreamResponse(); HandlerMethod handlerMethod = methodInfo.getHandlerMethod(); try { ModelAndView modelAndView; if (this.configurationService.getConfiguration().isSynchronizeOnSession() || methodInfo.isSynchronizeOnSession()) { HttpSession session = request.getSession(false); if (session != null) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { modelAndView = this.handlerAdapter.handle(request, response, handlerMethod); } } else { modelAndView = this.handlerAdapter.handle(request, response, handlerMethod); } } else { modelAndView = this.handlerAdapter.handle(request, response, handlerMethod); } Map<String, Object> model = modelAndView.getModel(); if (model.containsKey("extDirectFormPostResult")) { ExtDirectFormPostResult formPostResult = (ExtDirectFormPostResult) model .get("extDirectFormPostResult"); directResponse.setResult(formPostResult.getResult()); directResponse.setJsonView( getJsonView(formPostResult, methodInfo.getJsonView())); } else if (model.containsKey("edFormPostResult")) { EdFormPostResult formPostResult = (EdFormPostResult) model .get("edFormPostResult"); directResponse.setResult(formPostResult.result()); directResponse.setJsonView( getJsonView(formPostResult, methodInfo.getJsonView())); } } catch (Exception e) { log.error("Error calling method: " + extMethod, e.getCause() != null ? e.getCause() : e); directResponse.setResult( handleException(methodInfo, directResponse, e, request)); } } else { streamResponse = this.configurationService.getConfiguration() .isStreamResponse(); log.error("Error invoking method '" + extAction + "." + extMethod + "'. Method or Bean not found"); handleMethodNotFoundError(directResponse, extAction, extMethod); } writeJsonResponse(response, directResponse, null, streamResponse, ExtDirectSpringUtil.isMultipart(request)); return null; } @RequestMapping(value = "/router", method = RequestMethod.POST, params = "!extAction") public void router(HttpServletRequest request, HttpServletResponse response, Locale locale) throws IOException { Object requestData = this.configurationService.getJsonHandler() .readValue(request.getInputStream(), Object.class); List<ExtDirectRequest> directRequests = null; if (requestData instanceof Map) { directRequests = Collections.singletonList(this.configurationService .getJsonHandler().convertValue(requestData, ExtDirectRequest.class)); } else if (requestData instanceof List) { directRequests = new ArrayList<ExtDirectRequest>(); for (Object oneRequest : (List<?>) requestData) { directRequests.add(this.configurationService.getJsonHandler() .convertValue(oneRequest, ExtDirectRequest.class)); } } if (directRequests != null) { if (directRequests.size() == 1) { handleMethodCallOne(directRequests.get(0), request, response, locale); } else if (this.configurationService.getConfiguration() .getBatchedMethodsExecutionPolicy() == BatchedMethodsExecutionPolicy.SEQUENTIAL) { handleMethodCallsSequential(directRequests, request, response, locale); } else if (this.configurationService.getConfiguration() .getBatchedMethodsExecutionPolicy() == BatchedMethodsExecutionPolicy.CONCURRENT) { handleMethodCallsConcurrent(directRequests, request, response, locale); } } } private void handleMethodCallsConcurrent(List<ExtDirectRequest> directRequests, HttpServletRequest request, HttpServletResponse response, Locale locale) throws IOException { List<Future<ExtDirectResponse>> futures = new ArrayList<Future<ExtDirectResponse>>( directRequests.size()); for (ExtDirectRequest directRequest : directRequests) { Callable<ExtDirectResponse> callable = createMethodCallCallable(directRequest, request, response, locale); futures.add(this.configurationService.getConfiguration() .getBatchedMethodsExecutorService().submit(callable)); } ObjectMapper objectMapper = this.configurationService.getJsonHandler() .getMapper(); List<Object> directResponses = new ArrayList<Object>(directRequests.size()); boolean streamResponse = this.configurationService.getConfiguration() .isStreamResponse(); for (Future<ExtDirectResponse> future : futures) { try { ExtDirectResponse directResponse = future.get(); streamResponse = streamResponse || directResponse.isStreamResponse(); Class<?> jsonView = directResponse.getJsonView(); if (jsonView == null) { directResponses.add(directResponse); } else { String jsonResult = objectMapper.writerWithView(jsonView) .writeValueAsString(directResponse.getResult()); directResponses .add(new ExtDirectResponseRaw(directResponse, jsonResult)); } } catch (InterruptedException e) { log.error("Error invoking method", e); } catch (ExecutionException e) { log.error("Error invoking method", e); } } writeJsonResponse(response, directResponses, null, streamResponse); } private Callable<ExtDirectResponse> createMethodCallCallable( final ExtDirectRequest directRequest, final HttpServletRequest request, final HttpServletResponse response, final Locale locale) { return new Callable<ExtDirectResponse>() { @Override public ExtDirectResponse call() throws Exception { return handleMethodCall(directRequest, request, response, locale); } }; } private void handleMethodCallOne(ExtDirectRequest directRequest, HttpServletRequest request, HttpServletResponse response, Locale locale) throws IOException { ExtDirectResponse directResponse = handleMethodCall(directRequest, request, response, locale); boolean streamResponse = this.configurationService.getConfiguration() .isStreamResponse() || directResponse.isStreamResponse(); Class<?> jsonView = directResponse.getJsonView(); Object responseObject; if (jsonView == null) { responseObject = Collections.singleton(directResponse); } else { ObjectMapper objectMapper = this.configurationService.getJsonHandler() .getMapper(); String jsonResult = objectMapper.writerWithView(jsonView) .writeValueAsString(directResponse.getResult()); responseObject = Collections .singleton(new ExtDirectResponseRaw(directResponse, jsonResult)); } writeJsonResponse(response, responseObject, null, streamResponse); } private void handleMethodCallsSequential(List<ExtDirectRequest> directRequests, HttpServletRequest request, HttpServletResponse response, Locale locale) throws IOException { List<Object> directResponses = new ArrayList<Object>(directRequests.size()); boolean streamResponse = this.configurationService.getConfiguration() .isStreamResponse(); ObjectMapper objectMapper = this.configurationService.getJsonHandler() .getMapper(); for (ExtDirectRequest directRequest : directRequests) { ExtDirectResponse directResponse = handleMethodCall(directRequest, request, response, locale); streamResponse = streamResponse || directResponse.isStreamResponse(); Class<?> jsonView = directResponse.getJsonView(); if (jsonView == null) { directResponses.add(directResponse); } else { String jsonResult = objectMapper.writerWithView(jsonView) .writeValueAsString(directResponse.getResult()); directResponses.add(new ExtDirectResponseRaw(directResponse, jsonResult)); } } writeJsonResponse(response, directResponses, null, streamResponse); } @SuppressWarnings({ "rawtypes", "unchecked" }) ExtDirectResponse handleMethodCall(ExtDirectRequest directRequest, HttpServletRequest request, HttpServletResponse response, Locale locale) { ExtDirectResponse directResponse = new ExtDirectResponse(directRequest); notifyExtRequestListenersBeforeRequest(directRequest, directResponse, request, response, locale); try { MethodInfo methodInfo = this.methodInfoCache.get(directRequest.getAction(), directRequest.getMethod()); if (methodInfo != null) { try { directResponse.setStreamResponse(methodInfo.isStreamResponse()); Object result = processRemotingRequest(request, response, locale, directRequest, methodInfo); if (result != null) { ModelAndJsonView modelAndJsonView = null; if (result instanceof ModelAndJsonView) { modelAndJsonView = (ModelAndJsonView) result; result = modelAndJsonView.getModel(); } if (methodInfo.isType(ExtDirectMethodType.FORM_LOAD) && !(result instanceof ExtDirectFormLoadResult) && !(result instanceof EdFormLoadResult)) { ExtDirectFormLoadResult formLoadResult = new ExtDirectFormLoadResult( result); if (result instanceof JsonViewHint) { formLoadResult.setJsonView( ((JsonViewHint) result).getJsonView()); } result = formLoadResult; } else if ((methodInfo.isType(ExtDirectMethodType.STORE_MODIFY) || methodInfo.isType(ExtDirectMethodType.STORE_READ)) && !(result instanceof ExtDirectStoreResult) && !(result instanceof EdStoreResult) && this.configurationService.getConfiguration() .isAlwaysWrapStoreResponse()) { if (result instanceof Collection) { result = new ExtDirectStoreResult((Collection) result); } else { result = new ExtDirectStoreResult(result); } } else if (methodInfo.isType(ExtDirectMethodType.FORM_POST_JSON)) { if (result instanceof ExtDirectFormPostResult) { ExtDirectFormPostResult formPostResult = (ExtDirectFormPostResult) result; result = formPostResult.getResult(); } else if (result instanceof EdFormPostResult) { EdFormPostResult formPostResult = (EdFormPostResult) result; result = formPostResult.result(); } } directResponse.setResult(result); if (modelAndJsonView != null) { directResponse.setJsonView(getJsonView(modelAndJsonView, methodInfo.getJsonView())); } else { directResponse.setJsonView( getJsonView(result, methodInfo.getJsonView())); } } else { if (methodInfo.isType(ExtDirectMethodType.STORE_MODIFY) || methodInfo.isType(ExtDirectMethodType.STORE_READ)) { directResponse.setResult(Collections.emptyList()); } } } catch (Exception e) { log.error("Error calling method: " + directRequest.getMethod(), e.getCause() != null ? e.getCause() : e); directResponse.setResult( handleException(methodInfo, directResponse, e, request)); } } else { log.error("Error invoking method '" + directRequest.getAction() + "." + directRequest.getMethod() + "'. Method or Bean not found"); handleMethodNotFoundError(directResponse, directRequest.getAction(), directRequest.getMethod()); } return directResponse; } finally { notifyExtRequestListenersAfterRequest(directRequest, directResponse, request, response, locale); } } private void notifyExtRequestListenersBeforeRequest(ExtDirectRequest directRequest, ExtDirectResponse directResponse, HttpServletRequest request, HttpServletResponse response, Locale locale) { if (this.extRequestListeners != null) { for (ExtRequestListener extrl : this.extRequestListeners) { extrl.beforeRequest(directRequest, directResponse, request, response, locale); } } } private void notifyExtRequestListenersAfterRequest(ExtDirectRequest directRequest, ExtDirectResponse directResponse, HttpServletRequest request, HttpServletResponse response, Locale locale) { if (this.extRequestListeners != null) { for (ExtRequestListener extrl : this.extRequestListeners) { extrl.afterRequest(directRequest, directResponse, request, response, locale); } } } public void writeJsonResponse(HttpServletRequest request, HttpServletResponse response, Object responseObject, Class<?> jsonView) throws IOException { writeJsonResponse(response, responseObject, jsonView, this.configurationService.getConfiguration().isStreamResponse(), ExtDirectSpringUtil.isMultipart(request)); } private void writeJsonResponse(HttpServletResponse response, Object responseObject, Class<?> jsonView, boolean streamResponse) throws IOException { writeJsonResponse(response, responseObject, jsonView, streamResponse, false); } @SuppressWarnings("resource") public void writeJsonResponse(HttpServletResponse response, Object responseObject, Class<?> jsonView, boolean streamResponse, boolean isMultipart) throws IOException { ObjectMapper objectMapper = this.configurationService.getJsonHandler() .getMapper(); if (isMultipart) { response.setContentType(RouterController.TEXT_HTML.toString()); response.setCharacterEncoding(RouterController.TEXT_HTML.getCharset().name()); ByteArrayOutputStream bos = new ByteArrayOutputStream(1024); bos.write( "<html><body><textarea>".getBytes(ExtDirectSpringUtil.UTF8_CHARSET)); String responseJson; if (jsonView == null) { responseJson = objectMapper.writeValueAsString(responseObject); } else { responseJson = objectMapper.writerWithView(jsonView) .writeValueAsString(responseObject); } responseJson = responseJson.replace(""", "\\""); bos.write(responseJson.getBytes(ExtDirectSpringUtil.UTF8_CHARSET)); String frameDomain = this.configurationService.getConfiguration() .getFrameDomain(); String frameDomainScript = ""; if (frameDomain != null) { frameDomainScript = String.format(this.configurationService .getConfiguration().getFrameDomainScript(), frameDomain); } bos.write(("</textarea>" + frameDomainScript + "</body></html>") .getBytes(ExtDirectSpringUtil.UTF8_CHARSET)); response.setContentLength(bos.size()); FileCopyUtils.copy(bos.toByteArray(), response.getOutputStream()); } else { response.setContentType(APPLICATION_JSON.toString()); response.setCharacterEncoding(APPLICATION_JSON.getCharset().name()); ServletOutputStream outputStream = response.getOutputStream(); if (!streamResponse) { ByteArrayOutputStream bos = new ByteArrayOutputStream(1024); JsonGenerator jsonGenerator = objectMapper.getFactory() .createGenerator(bos, JsonEncoding.UTF8); if (jsonView == null) { objectMapper.writeValue(jsonGenerator, responseObject); } else { objectMapper.writerWithView(jsonView).writeValue(jsonGenerator, responseObject); } response.setContentLength(bos.size()); outputStream.write(bos.toByteArray()); jsonGenerator.close(); } else { JsonGenerator jsonGenerator = objectMapper.getFactory() .createGenerator(outputStream, JsonEncoding.UTF8); if (jsonView == null) { objectMapper.writeValue(jsonGenerator, responseObject); } else { objectMapper.writerWithView(jsonView).writeValue(jsonGenerator, responseObject); } jsonGenerator.close(); } outputStream.flush(); } } private Object processRemotingRequest(HttpServletRequest request, HttpServletResponse response, Locale locale, ExtDirectRequest directRequest, MethodInfo methodInfo) throws Exception { Object[] parameters = this.configurationService.getParametersResolver() .resolveParameters(request, response, locale, directRequest, methodInfo); if (this.configurationService.getConfiguration().isSynchronizeOnSession() || methodInfo.isSynchronizeOnSession()) { HttpSession session = request.getSession(false); if (session != null) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { return ExtDirectSpringUtil.invoke( this.configurationService.getApplicationContext(), directRequest.getAction(), methodInfo, parameters); } } } return ExtDirectSpringUtil.invoke( this.configurationService.getApplicationContext(), directRequest.getAction(), methodInfo, parameters); } private Object handleException(MethodInfo methodInfo, BaseResponse response, Exception e, HttpServletRequest request) { return this.configurationService.getRouterExceptionHandler() .handleException(methodInfo, response, e, request); } private void handleMethodNotFoundError(BaseResponse response, String beanName, String methodName) { response.setType("exception"); response.setMessage(this.configurationService.getConfiguration() .getDefaultExceptionMessage()); if (this.configurationService.getConfiguration().isSendStacktrace()) { response.setWhere( "Bean or Method '" + beanName + "." + methodName + "' not found"); } else { response.setWhere(null); } } private static Class<?> getJsonView(Object result, Class<?> defaultJsonView) { if (result instanceof JsonViewHint) { Class<?> jsonView = ((JsonViewHint) result).getJsonView(); if (jsonView != null) { if (jsonView != ExtDirectMethod.NoJsonView.class) { return jsonView; } return null; } } return defaultJsonView; } }