├── src
├── main
│ ├── webapp
│ │ ├── assets
│ │ │ └── main.css
│ │ ├── view
│ │ │ ├── hello.jsp
│ │ │ ├── route.jsp
│ │ │ └── index.jsp
│ │ └── WEB-INF
│ │ │ ├── web.xml
│ │ │ └── route_tag.tld
│ ├── resources
│ │ ├── simplelogger.properties
│ │ └── routes
│ └── java
│ │ └── com
│ │ └── codemacro
│ │ └── webdemo
│ │ ├── MyWebApp.java
│ │ ├── result
│ │ ├── Result.java
│ │ ├── JSPRouteTag.java
│ │ └── JSPResult.java
│ │ ├── test
│ │ └── TestController.java
│ │ ├── BaseController.java
│ │ ├── MyServletFilter.java
│ │ ├── RouteConf.java
│ │ └── ActionManager.java
└── test
│ └── java
│ └── com
│ └── codemacro
│ └── webdemo
│ └── AppTest.java
├── README.md
└── pom.xml
/src/main/webapp/assets/main.css:
--------------------------------------------------------------------------------
1 | body {
2 |
3 | }
--------------------------------------------------------------------------------
/src/main/resources/simplelogger.properties:
--------------------------------------------------------------------------------
1 | org.slf4j.simpleLogger.defaultLogLevel=debug
2 |
--------------------------------------------------------------------------------
/src/main/webapp/view/hello.jsp:
--------------------------------------------------------------------------------
1 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
2 |
3 |
4 | hello page
5 |
6 |
7 | hello ${name}
8 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/main/webapp/view/route.jsp:
--------------------------------------------------------------------------------
1 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
2 | <%@ taglib prefix="route" uri="/myweb-router" %>
3 |
4 |
5 | route test page
6 |
7 |
8 | index
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/main/resources/routes:
--------------------------------------------------------------------------------
1 | GET /index com.codemacro.webdemo.test.TestController.index
2 | GET /404 com.codemacro.webdemo.test.TestController.code404
3 | GET /obj com.codemacro.webdemo.test.TestController.object
4 | GET /index.jsp com.codemacro.webdemo.test.TestController.template
5 | GET /hello com.codemacro.webdemo.test.TestController.hello
6 | GET /route com.codemacro.webdemo.test.TestController.route
7 | POST /hello com.codemacro.webdemo.test.TestController.sayHello
8 |
9 |
--------------------------------------------------------------------------------
/src/main/java/com/codemacro/webdemo/MyWebApp.java:
--------------------------------------------------------------------------------
1 | package com.codemacro.webdemo;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 |
6 | public class MyWebApp {
7 | private static Logger logger = LoggerFactory.getLogger(MyWebApp.class);
8 | private ActionManager actionMgr;
9 |
10 | public void startup(String appName) {
11 | logger.info("MyWebApp <{}> initializing...", appName);
12 | actionMgr = new ActionManager(appName);
13 | RouteConf.load(actionMgr);
14 | }
15 |
16 | public ActionManager getActionManager() {
17 | return actionMgr;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/web.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | MyWebFilter
6 | com.codemacro.webdemo.MyServletFilter
7 |
8 |
9 |
10 | MyWebFilter
11 | /*
12 |
13 |
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | A minimum web framework based on Servlet.
2 |
3 | See `TestController` and `routes` for example.
4 |
5 | public class TestController extends BaseController {
6 |
7 | public Result index() {
8 | return ok("hello world");
9 | }
10 | }
11 |
12 | // routes
13 | GET /index com.codemacro.webdemo.test.TestController.index
14 |
15 | Compile and package to war:
16 |
17 | mvn package
18 |
19 | Deploy a servlet server (Jetty/Tomcat).
20 |
21 |
22 | ## Deploy on Jetty
23 |
24 | 1. download jetty (v9.2)
25 | 2. put .war to `webapps` path
26 | 3. `java -jar start.jar`
27 | 4. locate: `http://localhost:8080/you-war-file-name`
28 |
--------------------------------------------------------------------------------
/src/main/webapp/view/index.jsp:
--------------------------------------------------------------------------------
1 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
2 |
3 |
4 | index page
5 |
6 |
7 | hello ${name}
8 |
9 |
By EL
10 |
11 | ${lang}|
12 |
13 |
14 |
15 | ${lang}|
16 |
17 |
18 |
19 |
By scriptlets
20 | <% String[] langs = (String[]) request.getAttribute("langs"); %>
21 | <% if (langs != null) { %>
22 | <% for (String lang : langs) { %>
23 | <%= lang %>|
24 | <% } } %>
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/main/java/com/codemacro/webdemo/result/Result.java:
--------------------------------------------------------------------------------
1 | package com.codemacro.webdemo.result;
2 |
3 | import java.io.IOException;
4 | import java.io.PrintWriter;
5 |
6 | import javax.servlet.ServletException;
7 | import javax.servlet.http.HttpServletResponse;
8 |
9 | public class Result {
10 | protected HttpServletResponse response;
11 | protected Object result;
12 |
13 | public Result(HttpServletResponse resp, Object result) {
14 | this.response = resp;
15 | this.result = result;
16 | }
17 |
18 | public void render() throws IOException, ServletException {
19 | PrintWriter writer = response.getWriter();
20 | writer.append(result.toString());
21 | writer.close();
22 | }
23 | }
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/route_tag.tld:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | 1.0
7 | 1.1
8 | URLRouteTags
9 | /myweb-router
10 |
11 |
12 |
13 | reverse
14 | com.codemacro.webdemo.result.JSPRouteTag
15 |
16 |
17 |
18 | action
19 | true
20 |
21 | true
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/test/java/com/codemacro/webdemo/AppTest.java:
--------------------------------------------------------------------------------
1 | package com.codemacro.webdemo;
2 |
3 | import junit.framework.Test;
4 | import junit.framework.TestCase;
5 | import junit.framework.TestSuite;
6 |
7 | /**
8 | * Unit test for simple App.
9 | */
10 | public class AppTest
11 | extends TestCase
12 | {
13 | /**
14 | * Create the test case
15 | *
16 | * @param testName name of the test case
17 | */
18 | public AppTest( String testName )
19 | {
20 | super( testName );
21 | }
22 |
23 | /**
24 | * @return the suite of tests being tested
25 | */
26 | public static Test suite()
27 | {
28 | return new TestSuite( AppTest.class );
29 | }
30 |
31 | /**
32 | * Rigourous Test :-)
33 | */
34 | public void testApp()
35 | {
36 | assertTrue( true );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/com/codemacro/webdemo/result/JSPRouteTag.java:
--------------------------------------------------------------------------------
1 | package com.codemacro.webdemo.result;
2 |
3 | import java.io.IOException;
4 | import java.util.HashMap;
5 | import java.util.Map;
6 |
7 | import javax.servlet.jsp.JspContext;
8 | import javax.servlet.jsp.JspException;
9 | import javax.servlet.jsp.JspWriter;
10 | import javax.servlet.jsp.tagext.DynamicAttributes;
11 | import javax.servlet.jsp.tagext.SimpleTagSupport;
12 |
13 | import com.codemacro.webdemo.ActionManager;
14 |
15 | public class JSPRouteTag extends SimpleTagSupport implements DynamicAttributes {
16 | public static final String ACTION_MGR = "__actionmgr__";
17 | private String action;
18 | private Map attrMap = new HashMap();
19 |
20 | @Override
21 | public void doTag() throws IOException {
22 | JspContext context = getJspContext();
23 | ActionManager actionMgr = (ActionManager) context.findAttribute(ACTION_MGR);
24 | JspWriter out = context.getOut();
25 | String uri = actionMgr.getReverseAction(action, attrMap);
26 | out.println(uri);
27 | }
28 |
29 | @Override
30 | public void setDynamicAttribute(String uri, String name, Object value) throws JspException {
31 | attrMap.put(name, value);
32 | }
33 |
34 | public void setAction(String action) {
35 | this.action = action;
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/com/codemacro/webdemo/test/TestController.java:
--------------------------------------------------------------------------------
1 | package com.codemacro.webdemo.test;
2 |
3 | import java.util.Arrays;
4 |
5 | import com.codemacro.webdemo.BaseController;
6 | import com.codemacro.webdemo.result.Result;
7 |
8 | public class TestController extends BaseController {
9 |
10 | public Result index() {
11 | return ok("hello world");
12 | }
13 |
14 | public Result code404() {
15 | return status(404, "not found");
16 | }
17 |
18 | public Result object() {
19 | class Obj {
20 | @Override
21 | public String toString() {
22 | return "object: " + hashCode();
23 | }
24 | }
25 | return ok(new Obj());
26 | }
27 |
28 | public Result template() {
29 | String[] langs = new String[] {"c++", "java", "python"};
30 | return ok(jsp("index.jsp")
31 | .put("name", "kevin")
32 | .put("langs", langs)
33 | .put("langList", Arrays.asList(langs))
34 | );
35 | }
36 |
37 | public Result hello() {
38 | String name = getQueryString("name");
39 | return ok(jsp("hello.jsp").put("name", name));
40 | }
41 |
42 | public Result sayHello() {
43 | String name = getQueryString("name");
44 | return ok(jsp("hello.jsp").put("name", name));
45 | }
46 |
47 | public Result route() {
48 | return ok(jsp("route.jsp"));
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/com/codemacro/webdemo/result/JSPResult.java:
--------------------------------------------------------------------------------
1 | package com.codemacro.webdemo.result;
2 |
3 | import java.io.IOException;
4 | import java.util.Map;
5 | import java.util.TreeMap;
6 |
7 | import javax.servlet.ServletException;
8 | import javax.servlet.http.HttpServletRequest;
9 | import javax.servlet.http.HttpServletResponse;
10 |
11 | import com.codemacro.webdemo.ActionManager;
12 |
13 | public class JSPResult extends Result {
14 | private static final String BASE = "/view/";
15 | private HttpServletRequest request;
16 | private String file;
17 | private Map content;
18 |
19 | public JSPResult(HttpServletRequest req, HttpServletResponse resp, String file,
20 | ActionManager actionMgr) {
21 | super(resp, null);
22 | this.request = req;
23 | this.content = new TreeMap();
24 | this.file = BASE + file;
25 | put(JSPRouteTag.ACTION_MGR, actionMgr);
26 | }
27 |
28 | public JSPResult put(String key, Object value) {
29 | content.put(key, value);
30 | return this;
31 | }
32 |
33 | @Override
34 | public void render() throws IOException, ServletException {
35 | for (Map.Entry entry : content.entrySet()) {
36 | request.setAttribute(entry.getKey(), entry.getValue());
37 | }
38 | request.getRequestDispatcher(file).forward(request, response);
39 | }
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/src/main/java/com/codemacro/webdemo/BaseController.java:
--------------------------------------------------------------------------------
1 | package com.codemacro.webdemo;
2 |
3 | import java.util.Map;
4 | import javax.servlet.http.HttpServletRequest;
5 | import javax.servlet.http.HttpServletResponse;
6 |
7 | import com.codemacro.webdemo.result.JSPResult;
8 | import com.codemacro.webdemo.result.Result;
9 |
10 | public class BaseController {
11 | private HttpServletRequest request;
12 | private HttpServletResponse response;
13 | private ActionManager actionMgr;
14 |
15 | public void init(HttpServletRequest req, HttpServletResponse resp, ActionManager actionMgr) {
16 | this.request = req;
17 | this.response = resp;
18 | this.actionMgr = actionMgr;
19 | }
20 |
21 | @SuppressWarnings("unchecked")
22 | protected Map getQueryStrings() {
23 | return request.getParameterMap();
24 | }
25 |
26 | protected String getQueryString(String key) {
27 | return request.getParameter(key);
28 | }
29 |
30 | protected Result status(int code, String text) {
31 | response.setStatus(code);
32 | return new Result(response, text);
33 | }
34 |
35 | protected Result ok(Object obj) {
36 | return new Result(response, obj);
37 | }
38 |
39 | protected Result ok(Result result) {
40 | return result;
41 | }
42 |
43 | protected JSPResult jsp(String file) {
44 | return new JSPResult(request, response, file, actionMgr);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/com/codemacro/webdemo/MyServletFilter.java:
--------------------------------------------------------------------------------
1 | package com.codemacro.webdemo;
2 |
3 | import java.io.IOException;
4 |
5 | import javax.servlet.Filter;
6 | import javax.servlet.FilterChain;
7 | import javax.servlet.FilterConfig;
8 | import javax.servlet.ServletContext;
9 | import javax.servlet.ServletException;
10 | import javax.servlet.ServletRequest;
11 | import javax.servlet.ServletResponse;
12 | import javax.servlet.http.HttpServletRequest;
13 | import javax.servlet.http.HttpServletResponse;
14 |
15 | import org.slf4j.Logger;
16 | import org.slf4j.LoggerFactory;
17 |
18 | public class MyServletFilter implements Filter {
19 | private static Logger logger = LoggerFactory.getLogger(MyServletFilter.class);
20 | private MyWebApp app;
21 |
22 | public void destroy() {
23 | logger.info("MyWeb exits");
24 | }
25 |
26 | public void doFilter(ServletRequest req, ServletResponse res,
27 | FilterChain chain) throws IOException, ServletException {
28 | boolean ret = app.getActionManager().invoke((HttpServletRequest)req, (HttpServletResponse)res);
29 | if (!ret) {
30 | chain.doFilter(req, res);
31 | }
32 | }
33 |
34 | public void init(FilterConfig conf) throws ServletException {
35 | app = new MyWebApp();
36 | app.startup(getAppName(conf.getServletContext()));
37 | logger.info("MyWeb startup");
38 | }
39 |
40 | private String getAppName(ServletContext context) {
41 | return context.getServletContextName();
42 | }
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/src/main/java/com/codemacro/webdemo/RouteConf.java:
--------------------------------------------------------------------------------
1 | package com.codemacro.webdemo;
2 |
3 | import java.io.BufferedReader;
4 | import java.io.IOException;
5 | import java.io.InputStreamReader;
6 | import java.util.regex.Matcher;
7 | import java.util.regex.Pattern;
8 |
9 | import org.slf4j.Logger;
10 | import org.slf4j.LoggerFactory;
11 |
12 | public class RouteConf {
13 | private static final String FILE = "/routes";
14 | private static final Pattern LineR = Pattern.compile("(\\S+)\\s+(\\S+)\\s+(\\S+)");
15 | private static Logger logger = LoggerFactory.getLogger(RouteConf.class);
16 |
17 | public static void load(ActionManager actionMgr) {
18 | logger.info("load routes...");
19 | try (BufferedReader br = new BufferedReader(new InputStreamReader(
20 | RouteConf.class.getResourceAsStream(FILE)))) {
21 | String line;
22 | while ((line = br.readLine()) != null) {
23 | parseLine(actionMgr, line);
24 | }
25 | } catch (IOException e) {
26 | logger.error("load routes failed: {}", e);
27 | }
28 | }
29 |
30 | private static void parseLine(ActionManager actionMgr, String line) {
31 | if (line.startsWith("#")) {
32 | return ;
33 | }
34 | Matcher m = LineR.matcher(line);
35 | if (!m.find()) {
36 | return ;
37 | }
38 | String method = m.group(1);
39 | String uri = m.group(2);
40 | String clazzFunc = m.group(3);
41 | int pos = clazzFunc.lastIndexOf('.');
42 | String clazz = clazzFunc.substring(0, pos);
43 | String func = clazzFunc.substring(pos + 1);
44 | logger.debug("parse line {} {} {} {}", method, uri, clazz, func);
45 | actionMgr.registerAction(clazz, func, uri, method);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 |
5 | web-demo
6 | web-demo
7 | 0.0.1-SNAPSHOT
8 | war
9 |
10 | web-demo
11 | http://maven.apache.org
12 |
13 |
14 | UTF-8
15 | 1.7
16 | 1.7
17 |
18 |
19 |
20 |
21 | javax.servlet
22 | servlet-api
23 | 2.5
24 | jar
25 | compile
26 |
27 |
28 | javax.servlet
29 | jsp-api
30 | 2.0
31 | true
32 |
33 |
34 | org.slf4j
35 | slf4j-api
36 | 1.7.5
37 |
38 |
39 | org.slf4j
40 | slf4j-simple
41 | 1.7.5
42 |
43 |
44 | junit
45 | junit
46 | 3.8.1
47 | test
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/main/java/com/codemacro/webdemo/ActionManager.java:
--------------------------------------------------------------------------------
1 | package com.codemacro.webdemo;
2 |
3 | import java.io.IOException;
4 | import java.io.PrintWriter;
5 | import java.io.UnsupportedEncodingException;
6 | import java.lang.reflect.Method;
7 | import java.net.URLEncoder;
8 | import java.util.HashMap;
9 | import java.util.Map;
10 |
11 | import javax.servlet.http.HttpServletRequest;
12 | import javax.servlet.http.HttpServletResponse;
13 |
14 | import org.slf4j.Logger;
15 | import org.slf4j.LoggerFactory;
16 |
17 | import com.codemacro.webdemo.result.Result;
18 |
19 | public class ActionManager {
20 | private static Logger logger = LoggerFactory.getLogger(ActionManager.class);
21 | public static enum HttpMethod { GET, POST };
22 |
23 | private static class ActionKey {
24 | public String uri;
25 | public HttpMethod httpMethod;
26 |
27 | public ActionKey(String uri, HttpMethod m) {
28 | this.uri = uri;
29 | this.httpMethod = m;
30 | }
31 |
32 | @Override
33 | public int hashCode() {
34 | return httpMethod.hashCode() | uri.hashCode();
35 | }
36 |
37 | @Override
38 | public boolean equals(Object that) {
39 | if (that == this) {
40 | return true;
41 | }
42 | if (!(that instanceof ActionKey)) {
43 | return false;
44 | }
45 | ActionKey athat = (ActionKey) that;
46 | return athat.httpMethod.equals(httpMethod) && athat.uri.equals(uri);
47 | }
48 |
49 | @Override
50 | public String toString() {
51 | return "ActionKey[uri=" + uri + ", method=" + httpMethod + "]";
52 | }
53 | }
54 |
55 | private static class ActionValue {
56 | public Class extends BaseController> clazz;
57 | public Method method;
58 |
59 | public ActionValue(Class extends BaseController> clazz, Method m) {
60 | this.clazz = clazz;
61 | this.method = m;
62 | }
63 |
64 | @Override
65 | public String toString() {
66 | return "ActionValue[m=" + clazz.getName() + "." + method.getName() + "]";
67 | }
68 | }
69 |
70 | private Map actions = new HashMap();
71 | private String appName;
72 |
73 | public ActionManager(String appName) {
74 | this.appName = appName;
75 | }
76 |
77 | public String getAppName() {
78 | return appName;
79 | }
80 |
81 | @SuppressWarnings("unchecked")
82 | public void registerAction(String clazName, String methodName, String uri, String method) {
83 | try {
84 | uri = "/" + appName + uri;
85 | Class extends BaseController> clazz = (Class extends BaseController>) loadClass(clazName);
86 | Method m = clazz.getMethod(methodName, (Class>[])null);
87 | if (m.getReturnType() != Result.class) {
88 | throw new RuntimeException("action method return type mismatch: " + uri);
89 | }
90 | ActionKey k = new ActionKey(uri, getMethod(method));
91 | ActionValue v = new ActionValue(clazz, m);
92 | logger.debug("register action {} {} {} {}", clazName, methodName, uri, method);
93 | actions.put(k, v);
94 | } catch (Exception e) {
95 | throw new RuntimeException("registerAction failed: " + uri, e);
96 | }
97 | }
98 |
99 | public boolean invoke(HttpServletRequest req, HttpServletResponse resp) throws IOException {
100 | String uri = req.getRequestURI();
101 | String method = req.getMethod().toUpperCase();
102 | try {
103 | ActionValue v = getAction(uri, method);
104 | if (v == null) {
105 | return false;
106 | }
107 | BaseController ctl = (BaseController) v.clazz.newInstance();
108 | ctl.init(req, resp, this);
109 | logger.debug("invoke action {}", uri);
110 | Result result = (Result) v.method.invoke(ctl, (Object[]) null);
111 | result.render();
112 | } catch (Exception e) {
113 | PrintWriter writer = resp.getWriter();
114 | writer.append("action exception: " + uri);
115 | writer.append("\n");
116 | writer.append(e.getMessage());
117 | writer.close();
118 | logger.warn("action exception: {}\n{}", e, actions);
119 | }
120 | return true;
121 | }
122 |
123 | public ActionValue getAction(String uri, String method) {
124 | ActionKey k = new ActionKey(uri, getMethod(method));
125 | return actions.get(k);
126 | }
127 |
128 | public String getReverseAction(String clazzMethod, Map args) {
129 | String argStr = formatQueryParams(args);
130 | for (Map.Entry action : actions.entrySet()) {
131 | ActionValue av = action.getValue();
132 | if ((av.clazz.getName() + "." + av.method.getName()).equals(clazzMethod)) {
133 | return action.getKey().uri + (argStr == "" ? "" : "?" + argStr);
134 | }
135 | }
136 | return "";
137 | }
138 |
139 | static public HttpMethod getMethod(String m) {
140 | switch (m) {
141 | case "GET": return HttpMethod.GET;
142 | case "POST": return HttpMethod.POST;
143 | }
144 | return HttpMethod.GET;
145 | }
146 |
147 | private Class> loadClass(String fullName) throws ClassNotFoundException {
148 | return Class.forName(fullName);
149 | }
150 |
151 | private String formatQueryParams(Map args) {
152 | if (args == null) {
153 | return "";
154 | }
155 | String s = "";
156 | for (Map.Entry entry : args.entrySet()) {
157 | String kv = encode(entry.getKey()) + "=" + encode(entry.getValue().toString());
158 | if (s == "") {
159 | s = kv;
160 | } else {
161 | s += "&" + kv;
162 | }
163 | }
164 | return s;
165 | }
166 |
167 | private String encode(String s) {
168 | try {
169 | return URLEncoder.encode(s, "UTF-8");
170 | } catch (UnsupportedEncodingException e) {
171 | return "";
172 | }
173 | }
174 | }
175 |
176 |
177 |
--------------------------------------------------------------------------------