/*
* Copyright 2014 K.M. Kim
*
* 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 com.skp.milonga.servlet.handler;
import java.io.File;
import java.io.FileReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.FileSystemManager;
import org.apache.commons.vfs2.VFS;
import org.apache.commons.vfs2.impl.DefaultFileMonitor;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.NativeFunction;
import org.mozilla.javascript.debug.Debugger;
import org.mozilla.javascript.tools.debugger.Dim;
import org.mozilla.javascript.tools.shell.Global;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import com.skp.milonga.interpret.JsUserFileListener;
import com.skp.milonga.rhino.debug.RhinoDebuggerFactory;
/**
* Milonga Core Bean
*
* @author kminkim
*
*/
public class AtmosRequestMappingHandlerMapping extends
RequestMappingHandlerMapping {
public static final String ATMOS_JS_FILE_NAME = "atmos.js";
/*
* storage of url-handler mapping infos
*/
private HandlerMappingInfoStorage handlerMappingInfoStorage = new AtmosRequestMappingInfoStorage();
/*
* Atmos library file stream.
*/
private InputStream atmosLibraryStream = getClass().getClassLoader()
.getResourceAsStream(ATMOS_JS_FILE_NAME);
/*
* Location of user-scripting javascript files. This should be directory.
*/
private String[] userSourceLocations;
/*
* state of source code auto-refreshable
*/
private boolean autoRefreshable;
private Debugger debugger;
private Global global;
@Override
protected void initHandlerMethods() {
if (logger.isDebugEnabled()) {
logger.debug("Looking for request mappings in application context: "
+ getApplicationContext());
}
detectHandlerMethods();
handlerMethodsInitialized(getHandlerMethods());
processAutoRefresh();
}
public void reInitHandlerMethods() {
detectHandlerMethods();
handlerMethodsInitialized(getHandlerMethods());
logger.info("[Milonga] Refreshing Javascript source is done. All handler methods re-registered.");
}
/**
* read user-defined javascript files in configured directory, and register
* handler methods in those files as Spring MVC handler.
*/
protected void detectHandlerMethods() {
processAtmostRequestMappingInfo();
try {
registerNativeFunctionHandlers(
handlerMappingInfoStorage.getHandlerMappingInfos(),
NativeFunctionResponseBodyHandler.class);
registerNativeFunctionHandlers(
handlerMappingInfoStorage.getHandlerWithViewMappingInfos(),
NativeFunctionModelAndViewHandler.class);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* register handlers
*
* @param mappingInfos
* @param handlerClassType
* @throws SecurityException
* @throws NoSuchMethodException
*/
private void registerNativeFunctionHandlers(
Map<String, HandlerDefinition> mappingInfos,
Class<? extends AbstractNativeFunctionHandler> handlerClassType)
throws SecurityException, NoSuchMethodException {
Iterator<Entry<String, HandlerDefinition>> iterator = mappingInfos.entrySet()
.iterator();
while (iterator.hasNext()) {
Entry<String, HandlerDefinition> mappingInfo = iterator.next();
String url = mappingInfo.getKey();
HandlerDefinition handlerDefinition = mappingInfo.getValue();
registerNativeFunctionHandler(url, handlerDefinition, handlerClassType);
}
}
/**
* register handler
* @param url
* @param handlerDefinition
* @param handlerClassType
* @throws SecurityException
* @throws NoSuchMethodException
*/
private void registerNativeFunctionHandler(String url,
HandlerDefinition handlerDefinition,
Class<? extends AbstractNativeFunctionHandler> handlerClassType)
throws SecurityException, NoSuchMethodException {
NativeFunction atmosFunction = (NativeFunction) handlerDefinition
.getHandler();
Object atmosHandler = getHandler(atmosFunction, handlerClassType);
Class<?> handlerType = (atmosHandler instanceof String) ? getApplicationContext()
.getType((String) atmosHandler) : atmosHandler.getClass();
if (atmosHandler instanceof NativeFunctionModelAndViewHandler) {
((NativeFunctionModelAndViewHandler) atmosHandler)
.setViewName(handlerMappingInfoStorage.getViewName(url));
}
final Class<?> userType = ClassUtils.getUserClass(handlerType);
Method method = userType.getMethod(
AbstractNativeFunctionHandler.HANDLER_METHOD_NAME,
HttpServletRequest.class, HttpServletResponse.class);
RequestMethodsRequestCondition requestMethodsRequestCondition = getRequestMethodsRequestCondition(handlerDefinition
.getHttpMethods());
RequestMappingInfo mapping = new RequestMappingInfo(
new PatternsRequestCondition(url),
requestMethodsRequestCondition, null, null, null,
/* new ProducesRequestCondition("application/xml") */null, null);
registerHandlerMethod(atmosHandler, method, mapping);
}
/**
* convert httpMethods String array to RequestMethod array
*
* @param httpMethods
* @return
*/
private RequestMethodsRequestCondition getRequestMethodsRequestCondition(
String[] httpMethods) {
RequestMethod[] requestMethods = new RequestMethod[httpMethods.length];
for (int i = 0; i < requestMethods.length; i++) {
requestMethods[i] = RequestMethod.valueOf(httpMethods[i]);
}
return new RequestMethodsRequestCondition(requestMethods);
}
/**
* Process all user scripting javascript files in configured location, then
* url-handler mapping infos gotta be stored in memory.
*/
private void processAtmostRequestMappingInfo() {
Context cx = Context.enter();
global = new Global(cx);
// javascript library loading
/*
* List<String> modulePath = new ArrayList<String>();
* modulePath.add(getServletContextPath() + atmosLibraryLocation);
* global.installRequire(cx, modulePath, false);
*/
try {
// optimization level -1 means interpret mode
cx.setOptimizationLevel(-1);
if (debugger == null) {
debugger = RhinoDebuggerFactory.create();
}
//Debugger debugger = RhinoDebuggerFactory.create();
cx.setDebugger(debugger, new Dim.ContextData());
atmosLibraryStream = getClass().getClassLoader()
.getResourceAsStream(ATMOS_JS_FILE_NAME);
InputStreamReader isr = new InputStreamReader(atmosLibraryStream);
// define Spring application context to context variable
global.defineProperty("context", getApplicationContext(), 0);
cx.evaluateReader(global, isr, ATMOS_JS_FILE_NAME, 1, null);
/*
* execute all user scripting javascript files in configured
* location, then url-handler informations gotta be stored in
* memory.
*/
for (String userSourceLocation : userSourceLocations) {
File dir = new File(getServletContextPath() + userSourceLocation);
if (dir.isDirectory()) {
String[] fileArray = dir.list();
for (String fileName : fileArray) {
File jsFile = new File(dir.getAbsolutePath() + "/"
+ fileName);
if (jsFile.isFile()) {
FileReader reader = new FileReader(jsFile);
global.defineProperty("mappingInfo",
handlerMappingInfoStorage, 0);
cx.evaluateReader(global, reader, fileName, 1, null);
}
}
}
else {
FileReader reader = new FileReader(dir);
global.defineProperty("mappingInfo",
handlerMappingInfoStorage, 0);
cx.evaluateReader(global, reader, dir.getName(), 1, null);
}
}
atmosLibraryStream.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
private String getServletContextPath() {
return getServletContext().getRealPath("/") + "/";
}
/**
* Convert Javascript function to Spring handler and return it.
*
* @param atmosFunction
* Javascript function
* @param handlerTypeClass
* handler type to return
* @return
*/
private Object getHandler(NativeFunction atmosFunction,
Class<? extends AbstractNativeFunctionHandler> handlerTypeClass) {
Object handler = null;
try {
Constructor<?> handlerConst = handlerTypeClass
.getConstructor(NativeFunction.class, Global.class);
handler = handlerConst.newInstance(atmosFunction, global);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return handler;
}
private void processAutoRefresh() {
if (autoRefreshable) {
launchJsFileMonitor();
logger.info("[Milonga] Javascript code auto-refreshable enabled.");
} else {
logger.info("[Milonga] Javascript code auto-refreshable disabled.");
}
}
private void launchJsFileMonitor() {
try {
FileSystemManager fsManager = VFS.getManager();
JsUserFileListener fileListener = new JsUserFileListener();
fileListener.setApplicationContext(getApplicationContext());
DefaultFileMonitor fileMonitor = new DefaultFileMonitor(fileListener);
for (String userSourceLocation : userSourceLocations) {
FileObject listenDir = fsManager.resolveFile(getServletContextPath() + userSourceLocation);
fileMonitor.setRecursive(true);
fileMonitor.addFile(listenDir);
}
fileMonitor.start();
} catch (FileSystemException e) {
logger.error(
"[Milonga] Launching javascript source watcher is failed. Interpreter mode is not available.",
e);
}
}
/**
* Setter of requestMappingInfo
*/
public void setHandlerMappingInfoStorage(
HandlerMappingInfoStorage handlerMappingInfoStorage) {
this.handlerMappingInfoStorage = handlerMappingInfoStorage;
}
public HandlerMappingInfoStorage getHandlerMappingInfoStorage() {
return handlerMappingInfoStorage;
}
/**
* Setter of userSourceLocation
*/
public void setUserSourceLocations(String[] userSourceLocations) {
this.userSourceLocations = userSourceLocations;
}
public String[] getUserSourceLocations() {
return userSourceLocations;
}
public void setAutoRefreshable(boolean autoRefreshable) {
this.autoRefreshable = autoRefreshable;
}
}