/*
* Copyright 2012 XueSong Guo.
*
* 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 cn.webwheel;
import cn.webwheel.results.SimpleResult;
import cn.webwheel.results.SimpleResultInterpreter;
import cn.webwheel.results.TemplateResult;
import cn.webwheel.results.TemplateResultInterpreter;
import cn.webwheel.setters.*;
import javax.servlet.FilterChain;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* Entrance of WebWheel MVC. There must be a class inherit this abstract class in each WebWheel application.<br/
* Subclass must have default constructor(no argument), for being instantiated.
*/
abstract public class Main {
/**
* current servlet context
*/
protected ServletContext servletContext;
protected Map<Class, ResultInterpreter> interpreterMap = new HashMap<Class, ResultInterpreter>();
protected Map<String, ActionInfo> actionMap = new HashMap<String, ActionInfo>();
protected ActionSetter actionSetter = new ActionSetter();
protected Map<Pattern, String> rewritePatterns = new HashMap<Pattern, String>();
protected final SetterConfig defSetterConfig = new SetterConfig();
/**
* When action method is an instance method, action object is created by this method.
* <p>
* The default implement use {@link Class#newInstance()} to instantiate the action object, and set {@link WebContext} to it if action class inherited from {@link WebContextAware}.<br/>
* Subclass can use IOC container to get action object.
* @see WebContextAware
* @see WebContext
* @return action instance
*/
public <T> T createAction(WebContext ctx, Class<T> type) {
T action;
try {
action = type.newInstance();
} catch (Exception e) {
throw new RuntimeException("can not create instance of " + type, e);
}
if (action instanceof WebContextAware) {
((WebContextAware) action).setWebContext(ctx);
}
return action;
}
/**
* WebWheel MVC application initializing method.<br/>
* The default implement configures 2 {@link ResultInterpreter}s and 28 {@link Setter}s.<br/>
* <p>
* <b>result interpreter</b>:<br/>
* {@link TemplateResult} to {@link TemplateResultInterpreter}<br/>
* {@link SimpleResult} to {@link SimpleResultInterpreter}<br/>
* <br/>
* <b>http parameter binding setter</b>:<br/>
* <br/>
* String<br/>
* String[]<br/>
* <br/>
* boolean<br/>
* Boolean<br/>
* boolean[]<br/>
* <br/>
* int<br/>
* Integer<br/>
* int[]<br/>
* <br/>
* long<br/>
* Long<br/>
* long[]<br/>
* <br/>
* float<br/>
* Float<br/>
* float[]<br/>
* <br/>
* double<br/>
* Double<br/>
* double[]<br/>
* <br/>
* File<br/>
* File[]<br/>
* <br/>
* FileEx<br/>
* FileEx[]<br/>
* <br/>
* Map<String, Object><br/>
* Map<String, String><br/>
* Map<String, String[]><br/>
* Map<String, File><br/>
* Map<String, File[]><br/>
* Map<String, FileEx><br/>
* Map<String, FileEx[]>
* @see FileEx
*/
protected void init() throws ServletException {
File root = new File(servletContext.getRealPath("/"));
interpret(TemplateResult.class).by(new TemplateResultInterpreter(root, null));
interpret(SimpleResult.class).by(new SimpleResultInterpreter());
set(String.class).by(new StringSetter());
set(String[].class).by(new StringArraySetter());
set(boolean.class).by(new BooleanSetter(Boolean.FALSE));
set(Boolean.class).by(new BooleanSetter(null));
set(boolean[].class).by(new BooleanArraySetter());
set(byte.class).by(new ByteSetter((byte) 0));
set(Byte.class).by(new ByteSetter(null));
set(byte[].class).by(new ByteArraySetter());
set(short.class).by(new ShortSetter((short) 0));
set(Short.class).by(new ShortSetter(null));
set(short[].class).by(new ShortArraySetter());
set(int.class).by(new IntSetter(0));
set(Integer.class).by(new IntSetter(null));
set(int[].class).by(new IntArraySetter());
set(long.class).by(new LongSetter(0L));
set(Long.class).by(new LongSetter(null));
set(long[].class).by(new LongArraySetter());
set(float.class).by(new FloatSetter(0f));
set(Float.class).by(new FloatSetter(null));
set(float[].class).by(new FloatArraySetter());
set(double.class).by(new DoubleSetter(0.0));
set(Double.class).by(new DoubleSetter(null));
set(double[].class).by(new DoubleArraySetter());
set(File.class).by(new FileSetter());
set(File[].class).by(new FileArraySetter());
set(FileEx.class).by(new FileExSetter());
set(FileEx[].class).by(new FileExArraySetter());
set(new TypeLiteral<Map<String, Object>>(){}.getType()).by(new MapOSetter());
set(new TypeLiteral<Map<String, String>>(){}.getType()).by(new MapSSetter());
set(new TypeLiteral<Map<String, String[]>>(){}.getType()).by(new MapSASetter());
set(new TypeLiteral<Map<String, File>>(){}.getType()).by(new MapFSetter());
set(new TypeLiteral<Map<String, File[]>>(){}.getType()).by(new MapFASetter());
set(new TypeLiteral<Map<String, FileEx>>(){}.getType()).by(new MapFxSetter());
set(new TypeLiteral<Map<String, FileEx[]>>(){}.getType()).by(new MapFxASetter());
}
/**
* Destroy method. To be invoked in {@link cn.webwheel.WebWheelFilter#destroy()}
*/
protected void destroy() {}
/**
* According action result's type, find appropriate interpreter to interpret the result instance.
* <p>
* The finding procedure is from bottom to up in class inheritance diagram.
* @param ctx current context
* @param result result object of action method
* @return interpreter is found
* @throws IOException
* @throws ServletException
*/
@SuppressWarnings("unchecked")
protected boolean interpretResult(WebContext ctx, Object result) throws IOException, ServletException {
if (result == null) {
return false;
}
Class cls = result.getClass();
do {
ResultInterpreter it = interpreterMap.get(cls);
if (it != null) {
it.interpret(result, ctx);
return true;
}
for (Class inf : cls.getInterfaces()) {
it = interpreterMap.get(inf);
if (it != null) {
it.interpret(result, ctx);
return true;
}
}
} while ((cls = cls.getSuperclass()) != null);
return false;
}
/**
* Invoke action method.
* @param ctx current context
* @param ai action info
* @param action action object, may be null for static action method
* @return result of action method
*/
protected Object executeAction(WebContext ctx, ActionInfo ai, Object action) throws Throwable {
try {
Object[] args = actionSetter.set(action, ai, ctx.getRequest());
try {
return ai.actionMethod.invoke(action == null ? ai.actionMethod.getDeclaringClass() : action, args);
} catch (IllegalAccessException ignored) {
return null;
} catch (InvocationTargetException e) {
throw e.getCause();
}
} finally {
actionSetter.clear(ctx.getRequest());
}
}
/**
* Get http url(after http forward or include)
*/
protected String pathFor(HttpServletRequest request) {
String path = (String) request.getAttribute("javax.servlet.include.servlet_path");
if (path != null) {
String info = (String) request.getAttribute("javax.servlet.include.path_info");
if (info != null) {
path += info;
}
} else {
path = request.getServletPath();
String info = request.getPathInfo();
if (info != null) {
path += info;
}
}
return path;
}
/**
* Http request url rewriting.
* @see ActionBinder#rest(String)
* @param url http url
* @return url after rewrite
*/
protected String handleRewrite(String url) {
for (Map.Entry<Pattern, String> entry : rewritePatterns.entrySet()) {
Matcher matcher = entry.getKey().matcher(url);
if (matcher.matches()) {
return matcher.replaceAll(entry.getValue());
}
}
return null;
}
/**
* http request handling procedure.
*/
@SuppressWarnings("unchecked")
protected void process(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
String path = pathFor(request);
String rewrite = handleRewrite(path);
if (rewrite != null) {
request.getRequestDispatcher(rewrite).forward(request, response);
return;
}
ActionInfo ai = actionMap.get(path);
if (ai == null) {
filterChain.doFilter(request, response);
return;
}
WebContextImpl ctx = new WebContextImpl(path, request, response);
Object action = null;
if (!Modifier.isStatic(ai.getActionMethod().getModifiers())) {
action = createAction(ctx, ai.actionClass);
if (action == null) {
filterChain.doFilter(request, response);
return;
}
}
Object result;
try {
if (ai.getActionMethod().getReturnType() == void.class) {
executeAction(ctx, ai, action);
return;
}
result = executeAction(ctx, ai, action);
} catch (RuntimeException e) {
throw e;
} catch (IOException e) {
throw e;
} catch (ServletException e) {
throw e;
} catch (Throwable e) {
throw new ServletException(e);
}
boolean solved = interpretResult(ctx, result);
if (!solved) {
if (result == null) {
filterChain.doFilter(request, response);
} else {
response.getWriter().write(result.toString());
}
}
}
/**
* Map a http url to action method, optionally using url rewrite.
* <p>
* <b>example:</b><br/>
* map("/index.html").with(PageIndex.class, "renderPage");
* map("/article.html?id=$1").rest("/article/(\\d+)").with(Article.class, Article.class.getMethod("viewArticle"));
* @see ActionBinder#rest(String)
* @see ActionBinder#with(Class, String)
* @see ActionBinder#with(Class, Method)
* @param path http url
* @return binder
*/
protected ActionBinder map(String path) {
return new ActionBinder(path);
}
protected class ActionBinder {
private String path;
private ActionBinder(String path) {
this.path = path;
}
/**
* Implement a simple url rewrite.
* <p>
* Using such procedure:<br/>
* Pattern.compile(rest).matcher(url).replaceAll(path)
* @param rest url pattern.
* @return binder
* @throws PatternSyntaxException rest pattern is wrong
*/
public ActionBinder rest(String rest) throws PatternSyntaxException {
Pattern pat = Pattern.compile(rest);
for (Pattern p : rewritePatterns.keySet()) {
if (p.pattern().equals(rest)) {
throw new IllegalArgumentException("duplicated rewrite path: " + rest);
}
}
rewritePatterns.put(pat, path);
return this;
}
/**
* Map url to action method name.
* <p>
* There's must only one method named the parameter.
* @param actionClass action class
* @param methodName action method name
* @return http parameter binding config
*/
public SetterConfig with(Class actionClass, String methodName) throws NoSuchMethodException {
Method m = null;
for (Method mtd : actionClass.getMethods()) {
if (mtd.getName().equals(methodName)) {
if (m != null) {
throw new NoSuchMethodException("duplicated method named: " + methodName + " in " + actionClass);
}
m = mtd;
}
}
return with(actionClass, m);
}
/**
* Map url to action method.
* @param actionClass action class
* @param method action method
* @return http parameter binding config
*/
@SuppressWarnings("unchecked")
public SetterConfig with(Class actionClass, Method method) {
if ((actionClass.isMemberClass() && !Modifier.isStatic(actionClass.getModifiers()))
|| actionClass.isAnonymousClass() || actionClass.isLocalClass()
|| !Modifier.isPublic(actionClass.getModifiers())
|| Modifier.isAbstract(actionClass.getModifiers())) {
throw new IllegalArgumentException("action class signature wrong: " + actionClass);
}
if (!Modifier.isPublic(method.getModifiers())) {
throw new IllegalArgumentException("action method signature wrong: " + method);
}
ActionInfo ai = new ActionInfo(actionClass, method);
int i = path.indexOf('?');
String realPath = i == -1 ? path : path.substring(0, i);
if (actionMap.put(realPath, ai) != null) {
throw new RuntimeException("duplicated action for path: " + realPath);
}
return ai.setterConfig = new SetterConfig(defSetterConfig);
}
}
/**
* Map a result type to result interpreter.
* @param resultType result class type
* @param <T> result class type
* @return binder
*/
protected <T> ResultTypeBinder<T> interpret(Class<T> resultType) {
return new ResultTypeBinder<T>(resultType);
}
/**
* Map a type to http parameter binding setter.
* @param type parameter type, may be generic type.
* @see TypeLiteral
* @return binder
*/
protected SetterBinder set(Type type) {
return new SetterBinder(type);
}
protected class SetterBinder {
private Type type;
private SetterBinder(Type type) {
this.type = type;
}
/**
* map http parameter binding setter
*/
public void by(Setter setter) {
actionSetter.addSetter(type, setter);
}
}
/**
* Find action method under certain package recursively.
* <p>
* Action method must be marked by {@link Action}(may be through parent class).<br/>
* Url will be the package path under rootpkg.<br/>
* <b>example</b><br/>
* action class:
* <p><blockquote><pre>
* package com.my.app.web.user;
* public class insert {
* {@code @}Action
* public Object act() {...}
* }
* </pre></blockquote><p>
* This action method will be mapped to url: /user/insert.act
* @see #map(String)
* @see Action
* @param rootpkg action class package
*/
@SuppressWarnings("deprecation")
final protected void autoMap(String rootpkg) {
try {
Enumeration<URL> enm = getClass().getClassLoader().getResources(rootpkg.replace('.', '/'));
while (enm.hasMoreElements()) {
URL url = enm.nextElement();
if (url.getProtocol().equals("file")) {
autoMap(rootpkg.replace('.', '/'), rootpkg, new File(URLDecoder.decode(url.getFile())));
} else if (url.getProtocol().equals("jar")) {
String file = URLDecoder.decode(url.getFile());
String root = file.substring(file.lastIndexOf('!') + 2);
file = file.substring(0, file.length() - root.length() - 2);
URL jarurl = new URL(file);
if (jarurl.getProtocol().equals("file")) {
JarFile jarFile = new JarFile(URLDecoder.decode(jarurl.getFile()));
try {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if (!name.endsWith(".class")) continue;
if (!name.startsWith(root + '/')) continue;
name = name.substring(0, name.length() - 6);
name = name.replace('/', '.');
int i = name.lastIndexOf('.');
autoMap(root, name.substring(0, i), name.substring(i + 1));
}
} finally {
jarFile.close();
}
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void autoMap(String root, String pkg, File dir) {
File[] files = dir.listFiles();
if (files == null) return;
for (File file : files) {
String fn = file.getName();
if (file.isDirectory()) {
autoMap(root, pkg + "." + fn, file);
} else if (fn.endsWith(".class")) {
autoMap(root, pkg, fn.substring(0, fn.length() - 6));
}
}
}
private void getActions(List<Action> list, Set<Class> set, Class cls, Method method) {
if (cls == null || !set.add(cls)) return;
for (Method m : cls.getDeclaredMethods()) {
if (!m.getName().equals(method.getName())) continue;
if (!Arrays.equals(m.getParameterTypes(), method.getParameterTypes())) continue;
Action action = m.getAnnotation(Action.class);
if (action != null) {
list.add(action);
}
break;
}
for (Class i : cls.getInterfaces()) {
getActions(list, set, i, method);
}
getActions(list, set, cls.getSuperclass(), method);
}
private Action getAction(Class cls, Method method) {
if (Modifier.isStatic(method.getModifiers())) {
return method.getAnnotation(Action.class);
}
ArrayList<Action> actions = new ArrayList<Action>();
getActions(actions, new HashSet<Class>(), cls, method);
if (actions.isEmpty()) return null;
if (actions.get(0).disabled()) return null;
if (actions.size() == 1) return actions.get(0);
ActionImpl action = new ActionImpl();
for (Action act : actions) {
if (action.merge(act)) {
return action;
}
}
return action;
}
@SuppressWarnings("unchecked")
private void autoMap(String root, String pkg, String name) {
Class cls;
try {
cls = Class.forName(pkg + "." + name);
} catch (ClassNotFoundException e) {
return;
}
if (cls.isMemberClass() && !Modifier.isStatic(cls.getModifiers())) {
return;
}
if (cls.isAnonymousClass() || cls.isLocalClass()
|| !Modifier.isPublic(cls.getModifiers())
|| Modifier.isAbstract(cls.getModifiers())) {
return;
}
name = name.replace('$', '.');
for (Method method : cls.getMethods()) {
String pathPrefix = pkg.substring(root.length()).replace('.', '/') + '/';
String path = pathPrefix + name + '.' + method.getName();
Action action = getAction(cls, method);
if (action == null) continue;
if (!action.value().isEmpty()) {
if (action.value().startsWith("?")) {
path = path + action.value();
} else if (action.value().startsWith(".")) {
path = pathPrefix + name + action.value();
} else if (!action.value().startsWith("/")) {
path = pathPrefix + action.value();
} else {
path = action.value();
}
}
ActionBinder binder = map(path);
if (!action.rest().isEmpty()) {
binder = binder.rest(action.rest());
}
SetterConfig cfg = binder.with(cls, method);
if (!action.charset().isEmpty()) {
cfg = cfg.setCharset(action.charset());
}
if (action.fileUploadFileSizeMax() != 0) {
cfg = cfg.setFileUploadFileSizeMax(action.fileUploadFileSizeMax());
}
if (action.fileUploadSizeMax() != 0) {
cfg.setFileUploadSizeMax(action.fileUploadSizeMax());
}
}
}
private static class ActionImpl implements Action {
String map = "";
String rest = "";
String charset = "";
int fileUploadSizeMax;
int fileUploadFileSizeMax;
boolean merge(Action act) {
if (act.disabled()) return true;
if (map.isEmpty()) {
map = act.value();
}
if (rest.isEmpty()) {
rest = act.rest();
}
if (charset.isEmpty()) {
charset = act.charset();
}
if (fileUploadSizeMax == 0) {
fileUploadSizeMax = act.fileUploadSizeMax();
}
if (fileUploadFileSizeMax == 0) {
fileUploadFileSizeMax = act.fileUploadFileSizeMax();
}
return !map.isEmpty() && !rest.isEmpty() && !charset.isEmpty() && fileUploadSizeMax != 0 && fileUploadFileSizeMax != 0;
}
@Override
public boolean disabled() {
return false;
}
@Override
public String value() {
return map;
}
@Override
public String rest() {
return rest;
}
@Override
public String charset() {
return charset;
}
@Override
public int fileUploadSizeMax() {
return fileUploadSizeMax;
}
@Override
public int fileUploadFileSizeMax() {
return fileUploadFileSizeMax;
}
@Override
public Class<? extends Annotation> annotationType() {
return Action.class;
}
}
protected class ResultTypeBinder<T> {
private Class<T> resultType;
private ResultTypeBinder(Class<T> resultType) {
this.resultType = resultType;
}
/**
* map result interpreter
*/
public void by(ResultInterpreter<? extends T> interpreterClass) {
if (interpreterClass == null) {
interpreterMap.remove(resultType);
} else {
interpreterMap.put(resultType, interpreterClass);
}
}
}
private class WebContextImpl implements WebContext {
private String path;
private HttpServletRequest request;
private HttpServletResponse response;
private WebContextImpl(String path, HttpServletRequest request, HttpServletResponse response) {
this.path = path;
this.request = request;
this.response = response;
}
@Override
public String getPath() {
return path;
}
@Override
public Main getMain() {
return Main.this;
}
@Override
public ServletContext getContext() {
return servletContext;
}
@Override
public HttpServletRequest getRequest() {
return request;
}
@Override
public HttpServletResponse getResponse() {
return response;
}
}
}