浏览代码

JFinal 1.2 release :)

JamesZhan 13 年之前
父节点
当前提交
6cc8038cd5
共有 37 个文件被更改,包括 448 次插入370 次删除
  1. 3 2
      README.rst
  2. 17 15
      WebRoot/WEB-INF/lib/1_lib_description.txt
  3. 1 1
      src/com/jfinal/config/Constants.java
  4. 0 1
      src/com/jfinal/core/ActionReporter.java
  5. 6 4
      src/com/jfinal/core/Config.java
  6. 1 1
      src/com/jfinal/core/Const.java
  7. 4 4
      src/com/jfinal/core/Controller.java
  8. 14 4
      src/com/jfinal/core/JFinal.java
  9. 8 1
      src/com/jfinal/core/JFinalFilter.java
  10. 1 1
      src/com/jfinal/core/ModelInjector.java
  11. 14 3
      src/com/jfinal/ext/interceptor/Restful.java
  12. 1 0
      src/com/jfinal/ext/interceptor/SessionInViewInterceptor.java
  13. 6 7
      src/com/jfinal/ext/kit/SessionIdGenerator.java
  14. 0 109
      src/com/jfinal/ext/render/JsonWithContentTypeRender.java
  15. 1 1
      src/com/jfinal/i18n/I18N.java
  16. 3 1
      src/com/jfinal/kit/JsonKit.java
  17. 3 0
      src/com/jfinal/kit/PathKit.java
  18. 12 0
      src/com/jfinal/plugin/activerecord/ActiveRecordPlugin.java
  19. 12 0
      src/com/jfinal/plugin/activerecord/CPI.java
  20. 3 3
      src/com/jfinal/plugin/activerecord/Db.java
  21. 5 7
      src/com/jfinal/plugin/activerecord/Model.java
  22. 20 1
      src/com/jfinal/plugin/activerecord/TableInfo.java
  23. 1 3
      src/com/jfinal/plugin/activerecord/dialect/MysqlDialect.java
  24. 1 1
      src/com/jfinal/plugin/activerecord/dialect/OracleDialect.java
  25. 6 8
      src/com/jfinal/plugin/activerecord/dialect/PostgreSqlDialect.java
  26. 0 1
      src/com/jfinal/plugin/ehcache/CacheInterceptor.java
  27. 2 0
      src/com/jfinal/plugin/spring/Inject.java
  28. 18 19
      src/com/jfinal/plugin/spring/IocInterceptor.java
  29. 11 1
      src/com/jfinal/render/JsonRender.java
  30. 53 62
      src/com/jfinal/render/JspRender.java
  31. 7 7
      src/com/jfinal/render/Redirect301Render.java
  32. 5 7
      src/com/jfinal/render/RedirectRender.java
  33. 4 4
      src/com/jfinal/render/RenderFactory.java
  34. 0 1
      src/com/jfinal/render/VelocityRender.java
  35. 1 0
      src/com/jfinal/server/IServer.java
  36. 70 90
      src/com/jfinal/server/JettyServer.java
  37. 134 0
      src/com/jfinal/server/Scanner.java

+ 3 - 2
README.rst

@@ -8,6 +8,7 @@ JFinal有如下主要特点
 ------------------------
 #. MVC架构,设计精巧,使用简单
 #. 遵循COC原则,零配置,无xml
+#. 独创Db + Record模式,灵活便利
 #. ActiveRecord支持,使数据库开发极致快速
 #. 自动加载修改后的java文件,开发过程中无需重启web server
 #. AOP支持,拦截器配置灵活,功能强大
@@ -15,9 +16,9 @@ JFinal有如下主要特点
 #. 多视图支持,支持FreeMarker、JSP、Velocity
 #. 强大的Validator后端校验功能
 #. 功能齐全,拥有struts2的绝大部分功能
-#. 体积小仅198K,且无第三方依赖
+#. 体积小仅218K,且无第三方依赖
 
-**JFinal 极速开发QQ群欢迎您的加入: 196337924**
+**JFinal 极速开发QQ群欢迎您的加入: 283446146**
 
 **以下是JFinal实现Blog管理的示例:**
 

+ 17 - 15
WebRoot/WEB-INF/lib/1_lib_description.txt

@@ -1,9 +1,11 @@
 JFinal 自身对第三方无依赖,但当需要第三方功能支持时则需要添加相应的 jar 文件
 
-1:只有 jfinal-xxx-bin.jar 文件是必需的。其它jar文件按需所用。
+1:只有 jfinal-bin-xx.jar 文件是必需的。其它jar文件按需所用。
 
-2:jetty-server-6.1.26.jar 用来支持无需额外安装 tomcat jetty 等 web server即可开始开发,
-同时它也是支持热部署的必要包。
+2:jetty-server-8.1.8.jar 用来支持无需额外安装 tomcat jetty 等 web server
+   即可开始开发,同时它也是支持热部署的必要包。jetty-server-8.1.8.jar 中
+   包含:jetty-8.1.8发行版"/lib"目录下所有jetty模块jar包、servlet-api-3.0.jar、
+   "/lib/jsp"下所有jar包。
 
 3:freemarker-2.3.16.jar 支持 FreeMarker 视图类型。
 
@@ -11,25 +13,25 @@ JFinal 自身对第三方无依赖,但当需要第三方功能支持时则需
 
 5:cos-26Dec2008.jar 支持文件上传功能。
 
-6: jstl-1.2.jar 支持 jsp 标记,仅使用jsp视图时才需要,此文件包括了 jstl-1.2.jar starandard.jar
+6:mysql-connector-java-5.1.20-bin.jar 支持 mysql 数据库
 
-7:mysql-connector-java-5.1.20-bin.jar 支持 mysql 数据库
+7:c3p0-0.9.1.2.jar 数据库连接池
 
-8:c3p0-0.9.1.2.jar 数据库连接池。
+8:ehcache-core-2.5.2.jar、slf4j-api-1.6.1.jar、slf4j-log4j12-1.6.1.jar 支持 EhCache。
+   在使用EhCache时需要有ehcache.xml文件。
 
-9:ehcache-core-2.5.2.jar、slf4j-api-1.6.1.jar、slf4j-jdk14-1.6.1.jar 支持 EhCache。在使用EhCache时需要有ehcache.xml文件。
+9:以org.springframework 打头的所有 jar 包支持 SpringPlugin
 
-10:oracle-jdbc6dms.jar 支持 oracle 数据库
+10:sqlite-jdbc-3.7.2.jar 支持 Sqlite 数据库
 
-11:以org.springframework 打头的所有 jar 包支持 SpringPlugin
+11:druid-0.2.6.jar 支持 Druid 数据库连接池
 
-12:sqlite-jdbc-3.7.2.jar 支持 Sqlite 数据库
+12:ojdbc6.jar Oracle Database 11g Release 2 (11.2.0.3) JDBC Driver
 
-13:druid-0.2.6.jar 支持 Druid 数据库连接池
-
-14:log4j-1.2.16.jar 支持 log4j 日志,当此文件不存在时,自动切换至 JDK Logger,注意,log4j需要相应的配置文件
-log4j.properties, 否则当log4j-1.2.16.jar 存在而 log4j.properties 不存在时无日志输出。jdk logger 需要的logging.properties文件
-也在此目录下提供了
+13:log4j-1.2.16.jar 支持 log4j 日志,当此文件不存在时,自动切换至 JDK Logger,
+   注意,log4j需要相应的配置文件 log4j.properties,否则当log4j-1.2.16.jar 存在
+   而log4j.properties 不存在时无日志输出。jdk logger 需要的logging.properties文件
+   也在此目录下提供了
 
 
 

+ 1 - 1
src/com/jfinal/config/Constants.java

@@ -108,7 +108,7 @@ final public class Constants {
 	}
 	
 	/**
-	 * Set urlPara separator. The default value is "_"
+	 * Set urlPara separator. The default value is "-"
 	 * @param urlParaSeparator the urlPara separator
 	 */
 	public void setUrlParaSeparator(String urlParaSeparator) {

+ 0 - 1
src/com/jfinal/core/ActionReporter.java

@@ -74,7 +74,6 @@ final class ActionReporter {
 		
 		// print all parameters
 		HttpServletRequest request = controller.getRequest();
-		@SuppressWarnings("unchecked")
 		Enumeration<String> e = request.getParameterNames();
 		if (e.hasMoreElements()) {
 			sb.append("Parameter   : ");

+ 6 - 4
src/com/jfinal/core/Config.java

@@ -77,13 +77,15 @@ class Config {
 				try {
 					boolean success = plugin.start();
 					if (!success) {
-						log.error("Plugin start error: " + plugin.getClass().getName());
-						throw new RuntimeException("Plugin start error: " + plugin.getClass().getName());
+						String message = "Plugin start error: " + plugin.getClass().getName();
+						log.error(message);
+						throw new RuntimeException(message);
 					}
 				}
 				catch (Exception e) {
-					log.error("Plugin start error: " + plugin.getClass().getName(), e);
-					throw new RuntimeException("Plugin start error: " + plugin.getClass().getName(), e);
+					String message = "Plugin start error: " + plugin.getClass().getName() + ". \n" + e.getMessage();
+					log.error(message, e);
+					throw new RuntimeException(message, e);
 				}
 			}
 		}

+ 1 - 1
src/com/jfinal/core/Const.java

@@ -24,7 +24,7 @@ import com.jfinal.render.ViewType;
  */
 public interface Const {
 	
-	String JFINAL_VERSION = "1.1.6";
+	String JFINAL_VERSION = "1.2";
 	
 	ViewType DEFAULT_VIEW_TYPE = ViewType.FREE_MARKER;
 	

+ 4 - 4
src/com/jfinal/core/Controller.java

@@ -948,8 +948,8 @@ public abstract class Controller {
 	/**
 	 * Redirect to url
 	 */
-	public void redirect(String url, boolean withOutQueryString) {
-		render = renderFactory.getRedirectRender(url, withOutQueryString);
+	public void redirect(String url, boolean withQueryString) {
+		render = renderFactory.getRedirectRender(url, withQueryString);
 	}
 	
 	/**
@@ -970,8 +970,8 @@ public abstract class Controller {
 	/**
 	 * Render with url and 301 status
 	 */
-	public void redirect301(String url, boolean withOutQueryString) {
-		render = renderFactory.getRedirect301Render(url, withOutQueryString);
+	public void redirect301(String url, boolean withQueryString) {
+		render = renderFactory.getRedirect301Render(url, withQueryString);
 	}
 	
 	/**

+ 14 - 4
src/com/jfinal/core/JFinal.java

@@ -28,6 +28,7 @@ import com.jfinal.kit.PathKit;
 import com.jfinal.plugin.IPlugin;
 import com.jfinal.plugin.activerecord.ActiveRecordPlugin;
 import com.jfinal.render.RenderFactory;
+import com.jfinal.server.IServer;
 import com.jfinal.server.ServerFactory;
 import com.jfinal.token.ITokenCache;
 import com.jfinal.token.TokenManager;
@@ -42,6 +43,7 @@ public final class JFinal {
 	private ActionMapping actionMapping;
 	private Handler handler;
 	private ServletContext servletContext;
+	private static IServer server;
 	
 	Handler getHandler() {
 		return handler;
@@ -153,11 +155,17 @@ public final class JFinal {
 	}
 	
 	public static void start() {
-		ServerFactory.getServer().start();
+		server = ServerFactory.getServer();
+		server.start();
 	}
 	
 	public static void start(String webAppDir, int port, String context, int scanIntervalSeconds) {
-		ServerFactory.getServer(webAppDir, port, context, scanIntervalSeconds).start();
+		server = ServerFactory.getServer(webAppDir, port, context, scanIntervalSeconds);
+		server.start();
+	}
+	
+	public static void stop() {
+		server.stop();
 	}
 	
 	/**
@@ -166,14 +174,16 @@ public final class JFinal {
 	 */
 	public static void main(String[] args) {
 		if (args == null || args.length == 0) {
-			ServerFactory.getServer().start();
+			server = ServerFactory.getServer();
+			server.start();
 		}
 		else {
 			String webAppDir = args[0];
 			int port = Integer.parseInt(args[1]);
 			String context = args[2];
 			int scanIntervalSeconds = Integer.parseInt(args[3]);
-			ServerFactory.getServer(webAppDir, port, context, scanIntervalSeconds).start();
+			server = ServerFactory.getServer(webAppDir, port, context, scanIntervalSeconds);
+			server.start();
 		}
 	}
 	

+ 8 - 1
src/com/jfinal/core/JFinalFilter.java

@@ -41,6 +41,7 @@ public final class JFinalFilter implements Filter {
 	private Constants constants;
 	private static final JFinal jfinal = JFinal.me();
 	private static Logger log;
+	private int contextPathLength;
 	
 	public void init(FilterConfig filterConfig) throws ServletException {
 		createJFinalConfig(filterConfig.getInitParameter("configClass"));
@@ -52,6 +53,9 @@ public final class JFinalFilter implements Filter {
 		constants = Config.getConstants();
 		encoding = constants.getEncoding();
 		jfinalConfig.afterJFinalStart();
+		
+		String contextPath = filterConfig.getServletContext().getContextPath();
+		contextPathLength = (contextPath == null || "/".equals(contextPath) ? 0 : contextPath.length());
 	}
 	
 	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
@@ -59,7 +63,10 @@ public final class JFinalFilter implements Filter {
 		HttpServletResponse response = (HttpServletResponse)res;
 		request.setCharacterEncoding(encoding);
 		
-		String target = request.getServletPath();
+		String target = request.getRequestURI();
+		if (contextPathLength != 0)
+			target = target.substring(contextPathLength);
+		
 		boolean[] isHandled = {false};
 		try {
 			handler.handle(target, request, response, isHandled);

+ 1 - 1
src/com/jfinal/core/ModelInjector.java

@@ -77,7 +77,7 @@ final class ModelInjector {
 		}
 	}
 	
-	@SuppressWarnings({ "unchecked", "rawtypes" })
+	@SuppressWarnings("rawtypes")
 	private static final void injectActiveRecordModel(Model<?> model, String modelName, HttpServletRequest request, boolean skipConvertError) {
 		TableInfo tableInfo = TableInfoMapping.me().getTableInfo(model.getClass());
 		

+ 14 - 3
src/com/jfinal/ext/interceptor/Restful.java

@@ -28,6 +28,7 @@ import com.jfinal.core.Controller;
  */
 public class Restful implements Interceptor {
 	
+	private static final String isRestfulForwardKey = "_isRestfulForward_";
 	private Set<String> set = new HashSet<String>() {
 		private static final long serialVersionUID = 2717581127375143508L;{
 		// add edit 与  JFinal 原有规则相同
@@ -48,31 +49,41 @@ public class Restful implements Interceptor {
 	 */
 	public void intercept(ActionInvocation ai) {
 		// 阻止 JFinal 原有规则 action 请求
+		Controller controller = ai.getController();
+		Boolean isRestfulForward = controller.getAttr(isRestfulForwardKey);
 		String methodName = ai.getMethodName();
-		if (set.contains(methodName)) {
+		if (set.contains(methodName) && isRestfulForward== null) {
 			ai.getController().renderError404();
 			return ;
 		}
 		
-		Controller controller = ai.getController();
+		if (isRestfulForward != null && isRestfulForward) {
+			ai.invoke();
+			return ;
+		}
+		
 		String controllerKey = ai.getControllerKey();
 		String method = controller.getRequest().getMethod().toUpperCase();
 		String urlPara = controller.getPara();
 		if ("GET".equals(method)) {
-			if (urlPara != null) {
+			if (urlPara != null && !"edit".equals(methodName)) {
+				controller.setAttr(isRestfulForwardKey, Boolean.TRUE);
 				controller.forwardAction(controllerKey + "/show/" + urlPara);
 				return ;
 			}
 		}
 		else if ("POST".equals(method)) {
+			controller.setAttr(isRestfulForwardKey, Boolean.TRUE);
 			controller.forwardAction(controllerKey + "/save");
 			return ;
 		}
 		else if ("PUT".equals(method)) {
+			controller.setAttr(isRestfulForwardKey, Boolean.TRUE);
 			controller.forwardAction(controllerKey + "/update/" + urlPara);
 			return ;
 		}
 		else if ("DELETE".equals(method)) {
+			controller.setAttr(isRestfulForwardKey, Boolean.TRUE);
 			controller.forwardAction(controllerKey + "/delete/" + urlPara);
 			return ;
 		}

+ 1 - 0
src/com/jfinal/ext/interceptor/SessionInViewInterceptor.java

@@ -69,6 +69,7 @@ class JFinalSession extends HashMap implements HttpSession {
 		return session.getAttribute(key);
 	}
 	
+	@SuppressWarnings("unchecked")
 	public Enumeration getAttributeNames() {
 		return session.getAttributeNames();
 	}

+ 6 - 7
src/com/jfinal/ext/kit/SessionIdGenerator.java

@@ -19,20 +19,19 @@ package com.jfinal.ext.kit;
 import java.security.SecureRandom;
 import java.util.Random;
 import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
 
 /**
- * SessionIdGenerator.
+ * SessionIdKit.
  */
-public class SessionIdGenerator {
+public class SessionIdKit {
 	
     protected static Random random;
     private static boolean weakRandom;
     private static volatile Object lock = new Object();
     
-    private static final SessionIdGenerator me = new SessionIdGenerator();
+    private static final SessionIdKit me = new SessionIdKit();
     
-    private SessionIdGenerator() {
+    private SessionIdKit() {
     	try {
 			// This operation may block on some systems with low entropy. See
 			// this page
@@ -48,11 +47,11 @@ public class SessionIdGenerator {
 		}
     }
     
-    public static final SessionIdGenerator me() {
+    public static final SessionIdKit me() {
     	return me;
     }
     
-	public String generate(HttpServletRequest request, HttpServletResponse response) {
+	public String generate(HttpServletRequest request) {
         synchronized (lock) {
             String id = null;
             while (id == null || id.length() == 0) {	//)||idInUse(id))

+ 0 - 109
src/com/jfinal/ext/render/JsonWithContentTypeRender.java

@@ -1,109 +0,0 @@
-/**
- * Copyright (c) 2011-2012, James Zhan 詹波 (jfinal@126.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 com.jfinal.ext.render;
-
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.Map;
-import com.jfinal.kit.JsonKit;
-import com.jfinal.render.Render;
-import com.jfinal.render.RenderException;
-
-/**
- * JsonRenderWithContentType
- */
-public class JsonWithContentTypeRender extends Render {
-	
-	private static final long serialVersionUID = 3672646716279300085L;
-	private String key;
-	private Object value;
-	private String[] attrs;
-	private String contentType;
-	
-	public JsonWithContentTypeRender(String contentType) {
-		this.contentType = contentType;
-	}
-	
-	public JsonWithContentTypeRender(String key, Object value, String contentType) {
-		this.key = key;
-		this.value = value;
-		this.contentType = contentType;
-	}
-	
-	public JsonWithContentTypeRender(String[] attrs, String contentType) {
-		this.attrs = attrs;
-		this.contentType = contentType;
-	}
-	
-	public void render() {
-		String jsonText = buildJsonText();
-		PrintWriter writer = null;
-		try {
-			response.setHeader("Pragma", "no-cache");	// HTTP/1.0 caches might not implement Cache-Control and might only implement Pragma: no-cache
-			response.setHeader("Cache-Control", "no-cache");
-			response.setDateHeader("Expires", 0);
-			
-			response.setContentType(contentType + ";charset=" + getEncoding());
-			writer = response.getWriter();
-	        writer.write(jsonText);
-	        writer.flush();
-		} catch (IOException e) {
-			throw new RenderException(e);
-		}
-		finally {
-			if (writer != null)
-				writer.close();
-		}
-	}
-	
-	private static final int depth = 8;
-	
-	@SuppressWarnings({"rawtypes", "unchecked"})
-	private String buildJsonText() {
-		Map map = new HashMap();
-		if (key != null) {
-			map.put(key, value);
-		}
-		else if (attrs != null) {
-			for (String key : attrs)
-				map.put(key, request.getAttribute(key));
-		}
-		else {
-			Enumeration<String> attrs = request.getAttributeNames();
-			while (attrs.hasMoreElements()) {
-				String key = attrs.nextElement();
-				Object value = request.getAttribute(key);
-				map.put(key, value);
-			}
-		}
-		
-		return JsonKit.mapToJson(map, depth);
-	}
-}
-
-
-
-
-
-
-
-
-
-
-

+ 1 - 1
src/com/jfinal/i18n/I18N.java

@@ -30,7 +30,7 @@ import com.jfinal.core.Const;
  * 1: Config parameters in JFinalConfig
  * 2: Init I18N in JFinal 
  * 3: I18N support text with Locale
- * 4: ActionContext use I18N.getText(...) with Local setting in I18nInterceptor
+ * 4: Controller use I18N.getText(...) with Local setting in I18nInterceptor
  * 5: The resource file in WEB-INF/classes
  * 
  * important: Locale can create with language like new Locale("xxx");

+ 3 - 1
src/com/jfinal/kit/JsonKit.java

@@ -39,6 +39,8 @@ import com.jfinal.plugin.activerecord.Record;
 @SuppressWarnings({"rawtypes", "unchecked"})
 public class JsonKit {
 	
+	private static final int DEFAULT_DEPTH = 8;
+	
 	public static String mapToJson(Map map, int depth) {
 		if(map == null)
 			return "null";
@@ -156,7 +158,7 @@ public class JsonKit {
 	}
 	
 	public static String toJson(Object value) {
-		return toJson(value, 8);
+		return toJson(value, DEFAULT_DEPTH);
 	}
 	
 	public static String toJson(Object value, int depth) {

+ 3 - 0
src/com/jfinal/kit/PathKit.java

@@ -61,6 +61,9 @@ public class PathKit {
 	}
 	
 	public static void setWebRootPath(String webRootPath) {
+		if (webRootPath == null)
+			return ;
+		
 		if (webRootPath.endsWith(File.separator))
 			webRootPath = webRootPath.substring(0, webRootPath.length() - 1);
 		PathKit.webRootPath = webRootPath;

+ 12 - 0
src/com/jfinal/plugin/activerecord/ActiveRecordPlugin.java

@@ -80,6 +80,18 @@ public class ActiveRecordPlugin implements IPlugin {
 		DbKit.setTransactionLevel(transactionLevel);
 	}
 	
+	/**
+	 * Set transaction level define in java.sql.Connection
+	 * @param transactionLevel only be 0, 1, 2, 4, 8
+	 */
+	public ActiveRecordPlugin setTransactionLevel(int transactionLevel) {
+		int t = transactionLevel;
+		if (t != 0 && t != 1  && t != 2  && t != 4  && t != 8)
+			throw new IllegalArgumentException("The transactionLevel only be 0, 1, 2, 4, 8");
+		DbKit.setTransactionLevel(transactionLevel);
+		return this;
+	}
+	
 	public ActiveRecordPlugin addMapping(String tableName, String primaryKey, Class<? extends Model<?>> modelClass) {
 		tableMappings.add(new TableInfo(tableName, primaryKey, modelClass));
 		return this;

+ 12 - 0
src/com/jfinal/plugin/activerecord/CPI.java

@@ -47,4 +47,16 @@ public abstract class CPI {
 	public static final Map<String, Object> getColumns(Record record) {
 		return record.getColumns();
 	} */
+	
+	public static List<Record> find(Connection conn, String sql, Object... paras) throws SQLException {
+		return Db.find(conn, sql, paras);
+	}
+	
+	public static Page<Record> paginate(Connection conn, int pageNumber, int pageSize, String select, String sqlExceptSelect, Object... paras) throws SQLException {
+		return Db.paginate(conn, pageNumber, pageSize, select, sqlExceptSelect, paras);
+	}
+	
+	public static int update(Connection conn, String sql, Object... paras) throws SQLException {
+		return Db.update(conn, sql, paras);
+	}
 }

+ 3 - 3
src/com/jfinal/plugin/activerecord/Db.java

@@ -125,7 +125,7 @@ public class Db {
 	 */
 	public static <T> T queryFirst(String sql, Object... paras) {
 		List<T> result = query(sql, paras);
-		return (T) (result.size() > 0 ? result.get(0) : null);
+		return (result.size() > 0 ? result.get(0) : null);
 	}
 	
 	/**
@@ -135,7 +135,7 @@ public class Db {
 	public static <T> T queryFirst(String sql) {
 		// return queryFirst(sql, NULL_PARA_ARRAY);
 		List<T> result = query(sql, NULL_PARA_ARRAY);
-		return (T) (result.size() > 0 ? result.get(0) : null);
+		return (result.size() > 0 ? result.get(0) : null);
 	}
 	
 	// 26 queryXxx method below -----------------------------------------------
@@ -668,7 +668,7 @@ public class Db {
 	static boolean update(Connection conn, String tableName, String primaryKey, Record record) throws SQLException {
 		Object id = record.get(primaryKey);
 		if (id == null)
-			throw new ActiveRecordException("You can't update model whitout Primary Key.");
+			throw new ActiveRecordException("You can't update model without Primary Key.");
 		
 		StringBuilder sql = new StringBuilder();
 		List<Object> paras = new ArrayList<Object>();

+ 5 - 7
src/com/jfinal/plugin/activerecord/Model.java

@@ -74,11 +74,9 @@ public abstract class Model<M extends Model> implements Serializable {
 		if (tableInfoMapping.getTableInfo(getClass()).hasColumnLabel(attr)) {
 			attrs.put(attr, value);
 			getModifyFlag().add(attr);	// Add modify flag, update() need this flag.
+			return (M)this;
 		}
-		else {
-			throw new ActiveRecordException("The attribute name is not exists: " + attr);
-		}
-		return (M)this;
+		throw new ActiveRecordException("The attribute name is not exists: " + attr);
 	}
 	
 	/**
@@ -93,7 +91,7 @@ public abstract class Model<M extends Model> implements Serializable {
 	 * Get attribute of any mysql type
 	 */
 	public <T> T get(String attr) {
-		return (T)attrs.get(attr);
+		return (T)(attrs.get(attr));
 	}
 	
 	/**
@@ -331,7 +329,7 @@ public abstract class Model<M extends Model> implements Serializable {
 		String pKey = tInfo.getPrimaryKey();
 		Object id = attrs.get(pKey);
 		if (id == null)
-			throw new ActiveRecordException("You can't delete model whitout id.");
+			throw new ActiveRecordException("You can't delete model without id.");
 		return deleteById(tInfo, id);
 	}
 	
@@ -363,7 +361,7 @@ public abstract class Model<M extends Model> implements Serializable {
 		String pKey = tableInfo.getPrimaryKey();
 		Object id = attrs.get(pKey);
 		if (id == null)
-			throw new ActiveRecordException("You can't update model whitout Primary Key.");
+			throw new ActiveRecordException("You can't update model without Primary Key.");
 		
 		StringBuilder sql = new StringBuilder();
 		List<Object> paras = new ArrayList<Object>();

+ 20 - 1
src/com/jfinal/plugin/activerecord/TableInfo.java

@@ -26,6 +26,8 @@ public class TableInfo {
 	
 	private String tableName;
 	private String primaryKey;
+	private String secondaryKey = null;
+	
 	@SuppressWarnings("unchecked")
 	private Map<String, Class<?>> columnTypeMap = DbKit.containerFactory.getAttrsMap();	//	new HashMap<String, Class<?>>();
 	
@@ -71,10 +73,27 @@ public class TableInfo {
 			throw new IllegalArgumentException("Model class can not be null.");
 		
 		this.tableName = tableName.trim();
-		this.primaryKey = primaryKey.trim();
+		setPrimaryKey(primaryKey.trim());	// this.primaryKey = primaryKey.trim();
 		this.modelClass = modelClass;
 	}
 	
+	private void setPrimaryKey(String primaryKey) {
+		String[] keyArr = primaryKey.split(",");
+		if (keyArr.length > 1) {
+			if (StringKit.isBlank(keyArr[0]) || StringKit.isBlank(keyArr[1]))
+				throw new IllegalArgumentException("The composite primary key can not be blank.");
+			this.primaryKey = keyArr[0].trim();
+			this.secondaryKey = keyArr[1].trim();
+		}
+		else {
+			this.primaryKey = primaryKey;
+		}
+	}
+	
+	public String getSecondaryKey() {
+		return secondaryKey;
+	}
+	
 	public Class<? extends Model<?>> getModelClass() {
 		return modelClass;
 	}

+ 1 - 3
src/com/jfinal/plugin/activerecord/dialect/MysqlDialect.java

@@ -84,9 +84,7 @@ public class MysqlDialect extends Dialect {
 			for (int i=0; i<columnsArray.length; i++) {
 				if (i > 0)
 					sql.append(", ");
-				sql.append("`");
-				sql.append(columnsArray[i].trim());
-				sql.append("`");
+				sql.append("`").append(columnsArray[i].trim()).append("`");
 			}
 		}
 		sql.append(" from `");

+ 1 - 1
src/com/jfinal/plugin/activerecord/dialect/OracleDialect.java

@@ -49,7 +49,7 @@ public class OracleDialect extends Dialect {
 				}
 				sql.append(colName);
 				Object value = e.getValue();
-				if(colName.equalsIgnoreCase(pKey) && value instanceof String && (((String)value).endsWith(".nextval"))) {
+				if(value instanceof String && colName.equalsIgnoreCase(pKey) && ((String)value).endsWith(".nextval")) {
 				    temp.append(value);
 				}else{
 				    temp.append("?");

+ 6 - 8
src/com/jfinal/plugin/activerecord/dialect/PostgreSqlDialect.java

@@ -51,26 +51,26 @@ public class PostgreSqlDialect extends Dialect {
 	}
 	
 	public String forModelDeleteById(TableInfo tInfo) {
-		String pKey = tInfo.getPrimaryKey();
+		String primaryKey = tInfo.getPrimaryKey();
 		StringBuilder sql = new StringBuilder(45);
 		sql.append("delete from \"");
 		sql.append(tInfo.getTableName());
-		sql.append("\" where \"").append(pKey).append("\" = ?");
+		sql.append("\" where \"").append(primaryKey).append("\" = ?");
 		return sql.toString();
 	}
 	
-	public void forModelUpdate(TableInfo tableInfo, Map<String, Object> attrs, Set<String> modifyFlag, String pKey, Object id, StringBuilder sql, List<Object> paras) {
+	public void forModelUpdate(TableInfo tableInfo, Map<String, Object> attrs, Set<String> modifyFlag, String primaryKey, Object id, StringBuilder sql, List<Object> paras) {
 		sql.append("update \"").append(tableInfo.getTableName()).append("\" set ");
 		for (Entry<String, Object> e : attrs.entrySet()) {
 			String colName = e.getKey();
-			if (!pKey.equalsIgnoreCase(colName) && modifyFlag.contains(colName) && tableInfo.hasColumnLabel(colName)) {
+			if (!primaryKey.equalsIgnoreCase(colName) && modifyFlag.contains(colName) && tableInfo.hasColumnLabel(colName)) {
 				if (paras.size() > 0)
 					sql.append(", ");
 				sql.append("\"").append(colName).append("\" = ? ");
 				paras.add(e.getValue());
 			}
 		}
-		sql.append(" where \"").append(pKey).append("\" = ?");
+		sql.append(" where \"").append(primaryKey).append("\" = ?");
 		paras.add(id);
 	}
 	
@@ -84,9 +84,7 @@ public class PostgreSqlDialect extends Dialect {
 			for (int i=0; i<columnsArray.length; i++) {
 				if (i > 0)
 					sql.append(", ");
-				sql.append("\"");
-				sql.append(columnsArray[i].trim());
-				sql.append("\"");
+				sql.append("\"").append(columnsArray[i].trim()).append("\"");
 			}
 		}
 		sql.append(" from \"");

+ 0 - 1
src/com/jfinal/plugin/ehcache/CacheInterceptor.java

@@ -95,7 +95,6 @@ public class CacheInterceptor implements Interceptor {
 		return sb.toString();
 	}
 	
-	@SuppressWarnings("unchecked")
 	private void cacheAction(String cacheName, String cacheKey, Controller controller) {
 		HttpServletRequest request = controller.getRequest();
 		Map<String, Object> cacheData = new HashMap<String, Object>();

+ 2 - 0
src/com/jfinal/plugin/spring/Inject.java

@@ -39,10 +39,12 @@ public class Inject {
 	@Target({ElementType.FIELD})
 	public static @interface BY_NAME {}
 	
+	/*
 	@Inherited
 	@Retention(RetentionPolicy.RUNTIME)
 	@Target({ElementType.FIELD})
 	public static @interface IGNORE {}
+	*/
 }
 
 

+ 18 - 19
src/com/jfinal/plugin/spring/IocInterceptor.java

@@ -32,26 +32,25 @@ public class IocInterceptor implements Interceptor {
 	public void intercept(ActionInvocation ai) {
 		Controller controller = ai.getController();
 		Field[] fields = controller.getClass().getDeclaredFields();
-		for (Field field : fields)
-			injectField(controller, field);
+		for (Field field : fields) {
+			Object bean = null;
+			if (field.isAnnotationPresent(Inject.BY_NAME.class))
+				bean = ctx.getBean(field.getName());
+			else if (field.isAnnotationPresent(Inject.BY_TYPE.class))
+				bean = ctx.getBean(field.getType());
+			else
+				continue ;
+			
+			try {
+				if (bean != null) {
+					field.setAccessible(true);
+					field.set(controller, bean);
+				}
+			} catch (Exception e) {
+				throw new RuntimeException(e);
+			}
+		}
 		
 		ai.invoke();
 	}
-	
-	private void injectField(Controller controller, Field field) {
-		Object bean = null;
-		if (field.isAnnotationPresent(Inject.BY_NAME.class))
-			bean = ctx.getBean(field.getName());
-		else if (field.isAnnotationPresent(Inject.IGNORE.class))
-			return ;
-		else
-			bean = ctx.getBean(field.getType());
-		
-		try {
-			field.setAccessible(true);
-			field.set(controller, bean);
-		} catch (Exception e) {
-			throw new RuntimeException(e);
-		}
-	}
 }

+ 11 - 1
src/com/jfinal/render/JsonRender.java

@@ -25,6 +25,9 @@ import com.jfinal.kit.JsonKit;
 
 /**
  * JsonRender.
+ * <p>
+ * IE 不支持content type 为 application/json, 在 ajax 上传文件完成后返回 json时 ie 提示下载文件,<br>
+ * 解决办法是使用: render(new JsonRender(params).forIE());
  */
 public class JsonRender extends Render {
 	
@@ -40,6 +43,13 @@ public class JsonRender extends Render {
 	 * 2: ie 不支持 application/json, 在 ajax 上传文件完成后返回 json时 ie 提示下载文件
 	 */
 	private static final String contentType = "application/json;charset=" + getEncoding();
+	private static final String contentTypeForIE = "text/html;charset=" + getEncoding();
+	private boolean forIE = false;
+	
+	public JsonRender forIE() {
+		forIE = true;
+		return this;
+	}
 	
 	private String jsonText;
 	private String[] attrs;
@@ -83,7 +93,7 @@ public class JsonRender extends Render {
 			response.setHeader("Cache-Control", "no-cache");
 			response.setDateHeader("Expires", 0);
 			
-			response.setContentType(contentType);
+			response.setContentType(forIE ? contentTypeForIE : contentType);
 			writer = response.getWriter();
 	        writer.write(jsonText);
 	        writer.flush();

+ 53 - 62
src/com/jfinal/render/JspRender.java

@@ -59,86 +59,77 @@ public class JspRender extends Render {
 		}
 	}
 	
+	private static int DEPTH = 8;
+	
 	private void supportActiveRecord(HttpServletRequest request) {
 		for (Enumeration<String> attrs = request.getAttributeNames(); attrs.hasMoreElements();) {
 			String key = attrs.nextElement();
 			Object value = request.getAttribute(key);
-			if (value instanceof Model) {
-				request.setAttribute(key, handleModel((Model)value));
-			}
-			else if (value instanceof Record) {
-				request.setAttribute(key, handleRecord((Record)value));
-			}
-			else if (value instanceof List) {
-				request.setAttribute(key, handleList((List)value));
-			}
-			else if (value instanceof Page) {
-				request.setAttribute(key, handlePage((Page)value));
-			}
-			else if (value instanceof Model[]) {
-				request.setAttribute(key, handleModelArray((Model[])value));
-			}
-			else if (value instanceof Record[]) {
-				request.setAttribute(key, handleRecordArray((Record[])value)); 
-			}
+			request.setAttribute(key, handleObject(value, DEPTH));
 		}
 	}
 	
-	private List handleList(List list) {
-		if (list != null && list.size() > 0) {
-			Object o = list.get(0);
-			if (o instanceof Model)
-				return handleModelList((List<Model>)list);
-			else if (o instanceof Record)
-				return handleRecordList((List<Record>)list);
-		}
-		return list;
-	}
-	
-	private Object handlePage(Page page) {
-		Map<String, Object> result = new HashMap<String, Object>();
-		result.put("list", handleList(page.getList()));
-		result.put("pageNumber", page.getPageNumber());
-		result.put("pageSize", page.getPageSize());
-		result.put("totalPage", page.getTotalPage());
-		result.put("totalRow", page.getTotalRow());
-		return result;
-	}
-	
-	private Map<String, Object> handleModel(Model model) {
-		// handleGetterMethod(CPI.getAttrs(model), model.getClass().getMethods());
-		return CPI.getAttrs(model);
-	}
-	
-	private Map<String, Object> handleRecord(Record record) {
-		return record.getColumns();
+	private Object handleObject(Object value, int depth) {
+		if(value == null || (depth--) <= 0)
+			return value;
+		
+		if (value instanceof List)
+			return handleList((List)value, depth);
+		else if (value instanceof Model)
+			return handleMap(CPI.getAttrs((Model)value), depth);
+		else if (value instanceof Record)
+			return handleMap(((Record)value).getColumns(), depth);
+		else if(value instanceof Map)
+			return handleMap((Map)value, depth);
+		else if (value instanceof Page)
+			return handlePage((Page)value, depth);
+		else if (value instanceof Object[])
+			return handleArray((Object[])value, depth);
+		else
+			return value;
 	}
 	
-	private List<Map<String, Object>> handleModelList(List<Model> list) {
-		List<Map<String, Object>> result = new ArrayList<Map<String, Object>>(list.size());
-		for (Model model : list)
-			result.add(CPI.getAttrs(model));
+	private Map handleMap(Map map, int depth) {
+		if (map == null || map.size() == 0)
+			return map;
+		
+		Map<Object, Object> result = map;
+		for (Map.Entry<Object, Object> e : result.entrySet()) {
+			Object key = e.getKey();
+			Object value = e.getValue();
+			value = handleObject(value, depth);
+			result.put(key, value);
+		}
 		return result;
 	}
 	
-	private List<Map<String, Object>> handleRecordList(List<Record> list) {
-		List<Map<String, Object>> result = new ArrayList<Map<String, Object>>(list.size());
-		for (Record record : list)
-			result.add(record.getColumns());
+	private List handleList(List list, int depth) {
+		if (list == null || list.size() == 0)
+			return list;
+		
+		List result = new ArrayList(list.size());
+		for (Object value : list)
+			result.add(handleObject(value, depth));
 		return result;
 	}
 	
-	private List<Map<String, Object>> handleModelArray(Model[] array) {	// should be? : Map<String, Object>[]
-		List<Map<String, Object>> result = new ArrayList<Map<String, Object>>(array.length);
-		for (Model model : array)
-			result.add(CPI.getAttrs(model));
+	private Object handlePage(Page page, int depth) {
+		Map<String, Object> result = new HashMap<String, Object>();
+		result.put("list", handleList(page.getList(), depth));
+		result.put("pageNumber", page.getPageNumber());
+		result.put("pageSize", page.getPageSize());
+		result.put("totalPage", page.getTotalPage());
+		result.put("totalRow", page.getTotalRow());
 		return result;
 	}
 	
-	private List<Map<String, Object>> handleRecordArray(Record[] array) {
-		List<Map<String, Object>> result = new ArrayList<Map<String, Object>>(array.length);
-		for (Record record : array)
-			result.add(record.getColumns());
+	private List handleArray(Object[] array, int depth) {
+		if (array == null || array.length == 0)
+			return new ArrayList(0);
+		
+		List result = new ArrayList(array.length);
+		for (int i=0; i<array.length; i++)
+			result.add(handleObject(array[i], depth));
 		return result;
 	}
 }

+ 7 - 7
src/com/jfinal/render/Redirect301Render.java

@@ -25,23 +25,23 @@ public class Redirect301Render extends Render {
 	
 	private static final long serialVersionUID = -6822589387282014944L;
 	private String url;
-	private boolean withOutQueryString;
+	private boolean withQueryString;
 	
 	public Redirect301Render(String url) {
 		this.url = url;
-		this.withOutQueryString = false;
+		this.withQueryString = false;
 	}
 	
-	public Redirect301Render(String url, boolean withOutQueryString) {
+	public Redirect301Render(String url, boolean withQueryString) {
 		this.url = url;
-		this.withOutQueryString = withOutQueryString;
+		this.withQueryString = withQueryString;
 	}
 	
 	public void render() {
-		if (withOutQueryString == false) {
+		if (withQueryString) {
 			String queryString = request.getQueryString();
-			queryString = (queryString == null ? "" : "?" + queryString);
-			url = url + queryString;
+			if (queryString != null)
+				url = url + "?" + queryString;
 		}
 		
 		response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);

+ 5 - 7
src/com/jfinal/render/RedirectRender.java

@@ -25,23 +25,21 @@ public class RedirectRender extends Render {
 	
 	private static final long serialVersionUID = -3120354341585834890L;
 	private String url;
-	private boolean withOutQueryString;
+	private boolean withQueryString;
 	
 	public RedirectRender(String url) {
 		this.url = url;
-		this.withOutQueryString = false;
+		this.withQueryString = false;
 	}
 	
-	public RedirectRender(String url, boolean withOutQueryString) {
+	public RedirectRender(String url, boolean withQueryString) {
 		this.url = url;
-		this.withOutQueryString =  withOutQueryString;
+		this.withQueryString =  withQueryString;
 	}
 	
 	public void render() {
-		if (withOutQueryString == false) {
+		if (withQueryString) {
 			String queryString = request.getQueryString();
-			// queryString = (queryString == null ? "" : "?" + queryString);
-			// url = url + queryString;
 			if (queryString != null)
 				url = url + "?" + queryString;
 		}

+ 4 - 4
src/com/jfinal/render/RenderFactory.java

@@ -203,16 +203,16 @@ public class RenderFactory {
 		return new RedirectRender(url);
 	}
 	
-	public Render getRedirectRender(String url, boolean withOutQueryString) {
-		return new RedirectRender(url, withOutQueryString);
+	public Render getRedirectRender(String url, boolean withQueryString) {
+		return new RedirectRender(url, withQueryString);
 	}
 	
 	public Render getRedirect301Render(String url) {
 		return new Redirect301Render(url);
 	}
 	
-	public Render getRedirect301Render(String url, boolean withOutQueryString) {
-		return new Redirect301Render(url, withOutQueryString);
+	public Render getRedirect301Render(String url, boolean withQueryString) {
+		return new Redirect301Render(url, withQueryString);
 	}
 	
 	public Render getNullRender() {

+ 0 - 1
src/com/jfinal/render/VelocityRender.java

@@ -32,7 +32,6 @@ import org.apache.velocity.exception.ResourceNotFoundException;
 /**
  * VelocityRender.
  */
-@SuppressWarnings({"unchecked"})
 public class VelocityRender extends Render {
 	
 	private static final long serialVersionUID = 2195369405439638708L;

+ 1 - 0
src/com/jfinal/server/IServer.java

@@ -18,4 +18,5 @@ package com.jfinal.server;
 
 public interface IServer {
 	void start();
+	void stop();
 }

+ 70 - 90
src/com/jfinal/server/JettyServer.java

@@ -18,30 +18,21 @@ package com.jfinal.server;
 
 import java.io.File;
 import java.io.IOException;
-import java.lang.management.ManagementFactory;
 import java.net.DatagramSocket;
 import java.net.ServerSocket;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import javax.management.MBeanServer;
-import org.mortbay.jetty.Server;
-import org.mortbay.jetty.nio.SelectChannelConnector;
-import org.mortbay.jetty.webapp.WebAppContext;
-import org.mortbay.management.MBeanContainer;
-import org.mortbay.util.Scanner;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.SessionManager;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+import org.eclipse.jetty.server.session.HashSessionManager;
+import org.eclipse.jetty.server.session.SessionHandler;
+import org.eclipse.jetty.webapp.WebAppContext;
+import com.jfinal.core.Const;
 import com.jfinal.kit.PathKit;
+import com.jfinal.kit.StringKit;
 
 /**
  * JettyServer is used to config and start jetty web server.
- * Jetty version 6.1.26
- */
-/*
- * 1: project dir (no use)
- * 2: port
- * 3: context
- * 4: webapp dir
- * 5: scan interval senconds
+ * Jetty version 8.1.8
  */
 class JettyServer implements IServer {
 	
@@ -49,108 +40,79 @@ class JettyServer implements IServer {
 	private int port;
 	private String context;
 	private int scanIntervalSeconds;
-	private boolean isStarted = false;
+	private boolean running = false;
 	private Server server;
-	private WebAppContext web;
-	private boolean enablescanner = true;
+	private WebAppContext webApp;
 	
 	JettyServer(String webAppDir, int port, String context, int scanIntervalSeconds) {
-		this.webAppDir = webAppDir;
-		this.port = port;
-		this.context = context;
-		this.scanIntervalSeconds = scanIntervalSeconds;
-		checkConfig();
-	}
-	
-	private void checkConfig() {
+		if (webAppDir == null)
+			throw new IllegalStateException("Invalid webAppDir of web server: " + webAppDir);
 		if (port < 0 || port > 65536)
 			throw new IllegalArgumentException("Invalid port of web server: " + port);
-		
-		if (scanIntervalSeconds < 1)
-			enablescanner = false;
-		
-		if (context == null)
+		if (StringKit.isBlank(context))
 			throw new IllegalStateException("Invalid context of web server: " + context);
 		
-		if (webAppDir == null)
-			throw new IllegalStateException("Invalid context of web server: " + webAppDir);
+		this.webAppDir = webAppDir;
+		this.port = port;
+		this.context = context;
+		this.scanIntervalSeconds = scanIntervalSeconds;
 	}
 	
 	public void start() {
-		if (! isStarted) {
-			try {
-				doStart();
-			} catch (Exception e) {
-				e.printStackTrace();
-			}
-			isStarted = true;
+		if (!running) {
+			try {doStart();} catch (Exception e) {e.printStackTrace();}
+			running = true;
 		}
-		else {
-			throw new RuntimeException("Server already started.");
+	}
+	
+	public void stop() {
+		if (running) {
+			try {server.stop();} catch (Exception e) {e.printStackTrace();}
+			running = false;
 		}
 	}
 	
-	private void doStart() throws Exception {
-		String context = this.context;
-		String webAppDir = this.webAppDir;
-		Integer port = this.port;
-		Integer scanIntervalSeconds = this.scanIntervalSeconds;
+	private void doStart() {
+		if (!available(port))
+			throw new IllegalStateException("port: " + port + " already in use!");
 		
+		System.out.println("Starting JFinal " + Const.JFINAL_VERSION);
 		server = new Server();
+		SelectChannelConnector connector = new SelectChannelConnector();
+		connector.setPort(port);
+		server.addConnector(connector);
+		webApp = new WebAppContext();
+		webApp.setContextPath(context);
+		webApp.setResourceBase(webAppDir);	// webApp.setWar(webAppDir);
+		webApp.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
+		webApp.setInitParameter("org.eclipse.jetty.servlet.Default.useFileMappedBuffer", "false");	// webApp.setInitParams(Collections.singletonMap("org.mortbay.jetty.servlet.Default.useFileMappedBuffer", "false"));
+		persistSession(webApp);
 		
-		if (port != null) {
-			if (!available(port)) {
-				throw new IllegalStateException("port: " + port + " already in use!");
-			}
-			SelectChannelConnector connector = new SelectChannelConnector();
-			connector.setPort(port);
-			
-			server.addConnector(connector);
-		}
-		
-		web = new WebAppContext();
-		
-		// 警告: 设置成 true 无法支持热加载
-		// web.setParentLoaderPriority(false);
-		web.setContextPath(context);
-		web.setWar(webAppDir);
-		web.setInitParams(Collections.singletonMap("org.mortbay.jetty.servlet.Default.useFileMappedBuffer", "false"));
-		server.addHandler(web);
-		
-		MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
-		MBeanContainer mBeanContainer = new MBeanContainer(mBeanServer);
-		server.getContainer().addEventListener(mBeanContainer);
-		mBeanContainer.start();
+		server.setHandler(webApp);
 		
 		// configureScanner
-		if (enablescanner) {
-			final ArrayList<File> scanList = new ArrayList<File>();
-			scanList.add(new File(PathKit.getRootClassPath()));
-			Scanner scanner = new Scanner();
-			scanner.setReportExistingFilesOnStartup(false);
-			scanner.setScanInterval(scanIntervalSeconds);
-			scanner.setScanDirs(scanList);
-			scanner.addListener(new Scanner.BulkListener() {
-				
-				public void filesChanged(@SuppressWarnings("rawtypes") List changes) {
+		if (scanIntervalSeconds > 0) {
+			Scanner scanner = new Scanner(PathKit.getRootClassPath(), scanIntervalSeconds) {
+				public void onChange() {
 					try {
-						System.err.println("Loading changes ......");
-						web.stop();
-						web.start();
-						System.err.println("Loading complete.\n");
-						
+						System.err.println("\nLoading changes ......");
+						webApp.stop();
+						webApp.start();
+						System.err.println("Loading complete.");
 					} catch (Exception e) {
 						System.err.println("Error reconfiguring/restarting webapp after change in watched files");
 						e.printStackTrace();
 					}
 				}
-			});
-			System.err.println("Starting scanner at interval of " + scanIntervalSeconds + " seconds.");
+			};
+			System.out.println("Starting scanner at interval of " + scanIntervalSeconds + " seconds.");
 			scanner.start();
 		}
 		
 		try {
+			System.out.println("Starting web server on port: " + port);
 			server.start();
+			System.out.println("Starting Complete. Welcome To The JFinal World :)");
 			server.join();
 		} catch (Exception e) {
 			e.printStackTrace();
@@ -159,6 +121,24 @@ class JettyServer implements IServer {
 		return;
 	}
 	
+	private void persistSession(WebAppContext webApp) {
+		String storeDir = PathKit.getWebRootPath() + "/../../session_data" + context;
+		if ("\\".equals(File.separator))
+			storeDir = storeDir.replaceAll("/", "\\\\");
+		
+		SessionManager sm = webApp.getSessionHandler().getSessionManager();
+		if (sm instanceof HashSessionManager) {
+			((HashSessionManager)sm).setStoreDirectory(new File(storeDir));
+			return ;
+		}
+		
+		HashSessionManager hsm = new HashSessionManager();
+		hsm.setStoreDirectory(new File(storeDir));
+		SessionHandler sh = new SessionHandler();
+		sh.setSessionManager(hsm);
+		webApp.setSessionHandler(sh);
+	}
+	
 	private static boolean available(int port) {
 		if (port <= 0) {
 			throw new IllegalArgumentException("Invalid start port: " + port);

+ 134 - 0
src/com/jfinal/server/Scanner.java

@@ -0,0 +1,134 @@
+/**
+ * Copyright (c) 2011-2012, James Zhan 詹波 (jfinal@126.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 com.jfinal.server;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+import com.jfinal.kit.StringKit;
+
+/**
+ * Scanner.
+ */
+public abstract class Scanner {
+	
+	private Timer timer;
+	private TimerTask task;
+	private File rootDir;
+	private int interval;
+	private boolean running = false;
+	
+	private final Map<String,TimeSize> preScan = new HashMap<String,TimeSize> ();
+	private final Map<String,TimeSize> curScan = new HashMap<String,TimeSize> ();
+	
+	public Scanner(String rootDir, int interval) {
+		if (StringKit.isBlank(rootDir))
+			throw new IllegalArgumentException("The parameter rootDir can not be blank.");
+		this.rootDir = new File(rootDir);
+		if (!this.rootDir.isDirectory())
+			throw new IllegalArgumentException("The directory " + rootDir + " is not exists.");
+		if (interval <= 0)
+			throw new IllegalArgumentException("The parameter interval must more than zero.");
+		this.interval = interval;
+	}
+	
+	public abstract void onChange();
+	
+	private void working() {
+		scan(rootDir);
+		compare();
+		
+		preScan.clear();
+		preScan.putAll(curScan);
+		curScan.clear();
+	}
+	
+	private void scan(File file) {
+		if (file == null || !file.exists())
+			return ;
+		
+		if (file.isFile()) {
+			try {
+				curScan.put(file.getCanonicalPath(), new TimeSize(file.lastModified(),file.length()));
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}
+		else if (file.isDirectory()) {
+			File[] fs = file.listFiles();
+			if (fs != null)
+				for (File f : fs)
+					scan(f);
+		}
+	}
+	
+	private void compare() {
+		if (preScan.size() == 0)
+			return;
+		
+		if (!preScan.equals(curScan))
+			onChange();
+	}
+	
+	public void start() {
+		if (!running) {
+			timer = new Timer("JFinal-Scanner", true);
+			task = new TimerTask() {public void run() {working();}};
+			timer.schedule(task, 1010L * interval, 1010L * interval);
+			running = true;
+		}
+	}
+	
+	public void stop() {
+		if (running) {
+			timer.cancel();
+			task.cancel();
+			running = false;
+		}
+	}
+}
+
+class TimeSize {
+	
+	final long time;
+	final long size;
+	
+	public TimeSize(long time, long size) {
+		this.time = time;
+		this.size = size;
+	}
+	
+	public int hashCode() {
+		return (int)(time ^ size);
+	}
+	
+	public boolean equals(Object o) {
+		if (o instanceof TimeSize) {
+			TimeSize ts = (TimeSize)o;
+			return ts.time == this.time && ts.size == this.size;
+		}
+		return false;
+	}
+	
+	public String toString() {
+		return "[t=" + time + ", s=" + size + "]";
+	}
+}
+