/*******************************************************************************
* Copyright (c) 2012, Directors of the Tyndale STEP Project
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* Neither the name of the Tyndale House, Cambridge (www.TyndaleHouse.com)
* nor the names of its contributors may be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package com.tyndalehouse.step.rest.framework;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;
import com.tyndalehouse.step.core.exceptions.StepInternalException;
import com.tyndalehouse.step.core.models.ClientSession;
import com.tyndalehouse.step.core.service.AppManagerService;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Provider;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import static java.lang.String.format;
/**
* The FrontController acts like a minimal REST server. The paths are resolved as follows:
* <p/>
* /step-web/rest/controllerName/methodName/arg1/arg2/arg3
*
* @author chrisburrell
*/
@MultipartConfig
@Singleton
public class FrontController extends AbstractAjaxController {
public static final String UTF_8_ENCODING = "UTF-8";
private static final String EXTERNAL_CONTROLLER_SUB_PACKAGE = "external";
private static final Logger LOGGER = LoggerFactory.getLogger(FrontController.class);
private static final String CONTROLLER_PACKAGE = "com.tyndalehouse.step.rest.controllers";
private static final char PACKAGE_SEPARATOR = '.';
private static final long serialVersionUID = 7898656504631346047L;
private static final String CONTROLLER_SUFFIX = "Controller";
private final transient Injector guiceInjector;
private final transient Map<String, Method> methodNames = new HashMap<String, Method>();
private final transient Map<String, Object> controllers = new HashMap<String, Object>();
/**
* creates the front controller which will dispatch all the requests
* <p/>
*
* @param guiceInjector the injector used to call the relevant controllers
* @param errorResolver the error resolver is the object that helps us translate errors for the client
* @param clientSessionProvider the client session provider
*/
@Inject
public FrontController(final Injector guiceInjector,
final AppManagerService appManagerService,
final ClientErrorResolver errorResolver,
final Provider<ClientSession> clientSessionProvider,
final Provider<ObjectMapper> objectMapperProvider) {
super(appManagerService, clientSessionProvider, errorResolver, objectMapperProvider);
this.guiceInjector = guiceInjector;
}
/**
* Invokes the method on the controller instance and returns JSON-ed results
*
* @return byte array representation of the return value
*/
@Override
protected Object invokeMethod(HttpServletRequest servletRequest) throws Exception {
StepRequest sr = new StepRequest(servletRequest, UTF_8_ENCODING);
return invokeMethodWithStepRequest(sr);
}
/**
* @param sr allows to pass a StepRequest instead of the normal HttpServletRequest
* @return the object as a result of the call
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
Object invokeMethodWithStepRequest(final StepRequest sr) throws IllegalAccessException, InvocationTargetException {
LOGGER.debug("The cache was missed so invoking method now...");
// controller instance on which to call a method
final Object controllerInstance = getController(sr.getControllerName(), sr.isExternal());
// resolve method
final Method controllerMethod = getControllerMethod(sr.getMethodName(), controllerInstance,
sr.getArgs(), sr.getCacheKey().getMethodKey());
// invoke the three together
return controllerMethod.invoke(controllerInstance, (Object[]) sr.getArgs());
}
/**
* Retrieves a controller, either from the cache, or from Guice.
*
* @param controllerName the name of the controller (used as the key for the cache)
* @param isExternal indicates whether the request should be found in the external controllers
* @return the controller object
*/
Object getController(final String controllerName, final boolean isExternal) {
Object controllerInstance = this.controllers.get(controllerName);
// if retrieving yields null, get controller from Guice, and put in cache
if (controllerInstance == null) {
// make up the full class name
final String packageName = CONTROLLER_PACKAGE;
final StringBuilder className = new StringBuilder(packageName.length() + controllerName.length()
+ CONTROLLER_SUFFIX.length() + 1);
className.append(packageName);
className.append(PACKAGE_SEPARATOR);
if (isExternal) {
className.append(EXTERNAL_CONTROLLER_SUB_PACKAGE);
className.append(PACKAGE_SEPARATOR);
}
className.append(Character.toUpperCase(controllerName.charAt(0)));
className.append(controllerName.substring(1));
className.append(CONTROLLER_SUFFIX);
try {
final Class<?> controllerClass = Class.forName(className.toString());
controllerInstance = this.guiceInjector.getInstance(controllerClass);
// we use the controller name as it came in to key the map
this.controllers.put(controllerName, controllerInstance);
} catch (final ClassNotFoundException e) {
throw new StepInternalException("Unable to find a controller for " + className, e);
}
}
return controllerInstance;
}
/**
* Returns the method to be invoked upon the controller
*
* @param methodName the method name
* @param controllerInstance the instance of the controller
* @param args the list of arguments, required to resolve the correct method if they have arguments
* @param cacheKey the key to retrieve in the cache
* @return the method to be invoked
*/
Method getControllerMethod(final String methodName, final Object controllerInstance, final Object[] args,
final String cacheKey) {
final Class<?> controllerClass = controllerInstance.getClass();
// retrieve method from cache, or put in cache if not there
Method controllerMethod = this.methodNames.get(cacheKey);
if (controllerMethod == null) {
// resolve method
try {
final Class<?>[] classes = getClasses(args);
controllerMethod = controllerClass.getMethod(methodName, classes);
// put method in cache
this.methodNames.put(cacheKey, controllerMethod);
} catch (final NoSuchMethodException e) {
throw new StepInternalException("Unable to find matching method for " + methodName, e);
}
}
return controllerMethod;
}
/**
* @param args a number of arguments
* @return an array of classes matching the list of arguments passed in
*/
Class<?>[] getClasses(final Object[] args) {
if (args == null) {
return new Class<?>[0];
}
final Class<?>[] classes = new Class<?>[args.length];
for (int ii = 0; ii < classes.length; ii++) {
classes[ii] = args[ii].getClass();
}
return classes;
}
}