/* Copyright 2006 - 2010 Under Dusken 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 no.dusken.aranea.web.spring; import org.apache.commons.lang.StringUtils; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.Controller; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; /** * ChainsController uses the Chain of Responsibility Pattern to delegate control * to a chain of Controller objects that are passed into it at configuration * time. A ChainsController also takes an additional policy setting that decides * whether it should delegate to the Controller chain in a serial or parallel * manner. A ChainsController is itself a Controller, which means that a * ChainsController can be passed in as a parameter to another ChainsController. * This allows for very fine-grained control over the entire controller chain. * * @author Sujit Pal * @author Erlend Hamnaberg<erlenha@underdusken.no> * @version $Revision$ */ public class ChainedController implements Controller { private List<Controller> controllers; private boolean parallel = false; private String viewName; private static class CallableController implements Callable<ModelAndView> { private Controller controller; private HttpServletRequest request; private HttpServletResponse response; public CallableController(Controller controller, HttpServletRequest request, HttpServletResponse response) { this.controller = controller; this.request = request; this.response = response; } public ModelAndView call() throws Exception { return controller.handleRequest(request, response); } } /** * Set a List of Controller objects that must be chained. * * @param controllers a List of Controllers. */ public void setControllerChain(List<Controller> controllers) { this.controllers = controllers; } /** * Set the policy using which the Controller objects must be invoked. By * default, parallel is set to false, which means that the chain of * Controllers will be called serially. If set to true, the * ChainedController will spawn a thread for each of the Controllers in the * chain, and execute all threads simultaneously. In either case, an * exception thrown by one of the Controllers in the chain will result in an * exception thrown by the ChainsController itself. * * @param parallel if true, serial if false. */ public void setParallel(boolean parallel) { this.parallel = parallel; } /** * Allows declarative setting of the final view name. This may also be * programatically defined by subclassing the ChainedController. * * @param viewName the name of the view the controller will forward to. */ public void setViewName(String viewName) { this.viewName = viewName; } /** * Handles the request and builds a ModelAndView object to forward to the * view layer. Delegates to the private handleRequestXXX() methods based on * whether parallel is set to true or false. * * @param request the HttpServletRequest object. * @param response the HttpServletResponse object. * @return a ModelAndView object. * @throws Exception if one is thrown by the controllers in the chain. */ public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { return (parallel ? handleRequestParallely(request, response) : handleRequestSequentially(request, response)); } /** * Spawns multiple threads, one for each controller in the list of * controllers, and within each thread, delegates to the controller's * handleRequest() method. Once all the threads are complete, the * ModelAndView objects returned from each of the handleRequest() methods * are merged into a single view. The view name for the model is set to the * specified view name. If an exception is thrown by any of the controllers * in the chain, this exception is propagated up from the handleRequest() * method of the ChainedController. * * @param request the HttpServletRequest object. * @param response the HttpServletResponse object. * @return a merged ModelAndView object. * @throws Exception if one is thrown from the controllers in the chain. */ @SuppressWarnings("unchecked") private ModelAndView handleRequestParallely(HttpServletRequest request, HttpServletResponse response) throws Exception { ExecutorService service = Executors.newCachedThreadPool(); int numberOfControllers = controllers.size(); CallableController[] callables = new CallableController[numberOfControllers]; Future<ModelAndView>[] futures = new Future[numberOfControllers]; for (int i = 0; i < numberOfControllers; i++) { callables[i] = new CallableController(controllers.get(i), request, response); futures[i] = service.submit(callables[i]); } ModelAndView mergedModel = new ModelAndView(); for (Future<ModelAndView> future : futures) { ModelAndView model = future.get(); if (model != null) { mergedModel.addAllObjects(model.getModel()); } } if (StringUtils.isNotEmpty(this.viewName)) { mergedModel.setViewName(this.viewName); } return mergedModel; } /** * Calls the handleRequest controller for each of the Controllers in the * chain sequentially, merging the ModelAndView objects returned after each * call and returning the merged ModelAndView object. An exception thrown by * any of the controllers in the chain will propagate upwards through the * handleRequest() method of the ChainedController. The ChainedController * itself does not support any communication between the controllers in the * chain, but this can be effected by the controllers posting to a common * accessible object such as the ApplicationContext. Note that this will * introduce coupling between the controllers and will be difficult to * arrange into a parallel chain. A controller can stop processing of the * chain by returning a null ModelAndView object. Enhanced with adding the * chained views to the controller. This enables the controller to render * the views from the chained controllers. * * @param request the HttpServletRequest object. * @param response the HttpServletResponse object. * @return the merged ModelAndView object for all the controllers. * @throws Exception if one is thrown by one of the controllers in the chain. */ public ModelAndView handleRequestSequentially(HttpServletRequest request, HttpServletResponse response) throws Exception { ModelAndView mergedModel = new ModelAndView(); List<String> mergedViews = new ArrayList<String>(); for (Controller controller : controllers) { try { ModelAndView model = controller .handleRequest(request, response); if (model == null) { // chain will stop if a controller returns a null // ModelAndView object. break; } mergedModel.addAllObjects(model.getModel()); mergedViews.add(model.getViewName()); } catch (Exception e) { throw new Exception("Controller: " + controller.getClass().getName() + " threw exception: " + e.getMessage(), e); } } mergedModel.addObject("views", mergedViews); if (StringUtils.isNotEmpty(this.viewName)) { mergedModel.setViewName(this.viewName); } return mergedModel; } }