mxd 5 년 전
부모
커밋
a7e166d487
29개의 변경된 파일3440개의 추가작업 그리고 23개의 파일을 삭제
  1. 9 23
      .gitignore
  2. 49 0
      pom.xml
  3. 92 0
      src/main/java/com/ssssssss/SqlFileListener.java
  4. 13 0
      src/main/java/com/ssssssss/TestApplication.java
  5. 16 0
      src/main/java/com/ssssssss/expression/DefaultExpressionEngine.java
  6. 118 0
      src/main/java/com/ssssssss/expression/ExpressionError.java
  7. 37 0
      src/main/java/com/ssssssss/expression/ExpressionTemplate.java
  8. 107 0
      src/main/java/com/ssssssss/expression/ExpressionTemplateContext.java
  9. 63 0
      src/main/java/com/ssssssss/expression/interpreter/AstInterpreter.java
  10. 361 0
      src/main/java/com/ssssssss/expression/interpreter/JavaReflection.java
  11. 39 0
      src/main/java/com/ssssssss/expression/interpreter/Reflection.java
  12. 1302 0
      src/main/java/com/ssssssss/expression/parsing/Ast.java
  13. 129 0
      src/main/java/com/ssssssss/expression/parsing/CharacterStream.java
  14. 218 0
      src/main/java/com/ssssssss/expression/parsing/Parser.java
  15. 145 0
      src/main/java/com/ssssssss/expression/parsing/Span.java
  16. 30 0
      src/main/java/com/ssssssss/expression/parsing/Token.java
  17. 131 0
      src/main/java/com/ssssssss/expression/parsing/TokenStream.java
  18. 101 0
      src/main/java/com/ssssssss/expression/parsing/TokenType.java
  19. 191 0
      src/main/java/com/ssssssss/expression/parsing/Tokenizer.java
  20. 4 0
      src/main/java/com/ssssssss/handler/CookieContext.java
  21. 18 0
      src/main/java/com/ssssssss/handler/HeaderContext.java
  22. 20 0
      src/main/java/com/ssssssss/handler/RequestContext.java
  23. 97 0
      src/main/java/com/ssssssss/handler/RequestHandler.java
  24. 11 0
      src/main/java/com/ssssssss/handler/SessionContext.java
  25. 78 0
      src/main/java/com/ssssssss/mapping/SqlMappingManager.java
  26. 49 0
      src/main/java/com/ssssssss/model/SqlMapping.java
  27. 10 0
      src/main/resources/application.properties
  28. 1 0
      src/main/resources/ssssssss/role/list.sql
  29. 1 0
      src/main/resources/ssssssss/test/select.sql

+ 9 - 23
.gitignore

@@ -1,23 +1,9 @@
-# Compiled class file
-*.class
-
-# Log file
-*.log
-
-# BlueJ files
-*.ctxt
-
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-
-# Package Files #
-*.jar
-*.war
-*.nar
-*.ear
-*.zip
-*.tar.gz
-*.rar
-
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
+target
+*.iml
+out/
+.idea
+.classpath
+.project
+.settings
+bin/
+.myeclipse

+ 49 - 0
pom.xml

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.1.5.RELEASE</version>
+        <relativePath/>
+    </parent>
+    <groupId>com.mxd</groupId>
+    <artifactId>sql-mvc</artifactId>
+    <version>0.0.1</version>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-text</artifactId>
+            <version>1.6</version>
+        </dependency>
+
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jdbc</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.6</version>
+        </dependency>
+    </dependencies>
+
+</project>

+ 92 - 0
src/main/java/com/ssssssss/SqlFileListener.java

@@ -0,0 +1,92 @@
+package com.ssssssss;
+
+import com.ssssssss.mapping.SqlMappingManager;
+import org.apache.commons.io.monitor.FileAlterationListener;
+import org.apache.commons.io.monitor.FileAlterationMonitor;
+import org.apache.commons.io.monitor.FileAlterationObserver;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.io.File;
+import java.net.URL;
+
+@Component
+public class SqlFileListener implements FileAlterationListener {
+
+    @Autowired
+    private SqlMappingManager manager;
+
+    @Value("${ssssssss.directory:/ssssssss}")
+    private String directory;
+
+    @PostConstruct
+    private void init(){
+        new Thread(()->{
+            try {
+                File file = new File(SqlFileListener.class.getResource(this.directory).getFile());
+                SqlFileListener.this.directory = file.getPath();
+                start(file.listFiles());
+                FileAlterationObserver observer = new FileAlterationObserver(file);
+                observer.addListener(SqlFileListener.this);
+                new FileAlterationMonitor(50,observer).start();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }).start();
+    }
+
+
+    @Override
+    public void onStart(FileAlterationObserver observer) {
+
+    }
+
+    private void start(File[] files){
+        if(files != null){
+            for (File file : files) {
+                if(file.isFile()){
+                    manager.register(this.directory,file);
+                }else{
+                    start(file.listFiles());
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onDirectoryCreate(File directory) {
+
+    }
+
+    @Override
+    public void onDirectoryChange(File directory) {
+
+    }
+
+    @Override
+    public void onDirectoryDelete(File directory) {
+
+    }
+
+    @Override
+    public void onFileCreate(File file) {
+        manager.register(this.directory,file);
+    }
+
+    @Override
+    public void onFileChange(File file) {
+        manager.register(this.directory,file);
+    }
+
+    @Override
+    public void onFileDelete(File file) {
+        manager.unregister(this.directory,file);
+    }
+
+    @Override
+    public void onStop(FileAlterationObserver observer) {
+
+    }
+}

+ 13 - 0
src/main/java/com/ssssssss/TestApplication.java

@@ -0,0 +1,13 @@
+package com.ssssssss;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class TestApplication {
+
+
+    public static void main(String[] args) throws InterruptedException {
+        SpringApplication.run(TestApplication.class);
+    }
+}

+ 16 - 0
src/main/java/com/ssssssss/expression/DefaultExpressionEngine.java

@@ -0,0 +1,16 @@
+package com.ssssssss.expression;
+
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+@Component
+public class DefaultExpressionEngine {
+	
+
+	public Object execute(String expression, Map<String, Object> variables) {
+		ExpressionTemplateContext context = new ExpressionTemplateContext(variables);
+		return ExpressionTemplate.create(expression).render(context);
+	}
+	
+}

+ 118 - 0
src/main/java/com/ssssssss/expression/ExpressionError.java

@@ -0,0 +1,118 @@
+
+package com.ssssssss.expression;
+
+import com.ssssssss.expression.parsing.Span;
+import com.ssssssss.expression.parsing.Span.Line;
+import com.ssssssss.expression.parsing.TokenStream;
+
+/** All errors reported by the library go through the static functions of this class. */
+public class ExpressionError {
+
+	/**
+	 * <p>
+	 * Create an error message based on the provided message and stream, highlighting the line on which the error happened. If the
+	 * stream has more tokens, the next token will be highlighted. Otherwise the end of the source of the stream will be
+	 * highlighted.
+	 * </p>
+	 *
+	 * <p>
+	 * Throws a {@link RuntimeException}
+	 * </p>
+	 */
+	public static void error (String message, TokenStream stream) {
+		if (stream.hasMore())
+			error(message, stream.consume().getSpan());
+		else {
+			String source = stream.getSource();
+			if (source == null)
+				error(message, new Span(" ", 0, 1));
+			else
+				error(message, new Span(source, source.length() - 1, source.length()));
+		}
+	}
+
+	/** Create an error message based on the provided message and location, highlighting the location in the line on which the
+	 * error happened. Throws a {@link TemplateException} **/
+	public static void error (String message, Span location, Throwable cause) {
+
+		Line line = location.getLine();
+		message = "Error (" + line.getLineNumber() + "): " + message + "\n\n";
+		message += line.getText();
+		message += "\n";
+
+		int errorStart = location.getStart() - line.getStart();
+		int errorEnd = errorStart + location.getText().length() - 1;
+		for (int i = 0, n = line.getText().length(); i < n; i++) {
+			boolean useTab = line.getText().charAt(i) == '\t';
+			message += i >= errorStart && i <= errorEnd ? "^" : useTab ? "\t" : " ";
+		}
+
+		if (cause == null)
+			throw new TemplateException(message, location);
+		else
+			throw new TemplateException(message, location, cause);
+	}
+
+	/** Create an error message based on the provided message and location, highlighting the location in the line on which the
+	 * error happened. Throws a {@link TemplateException} **/
+	public static void error (String message, Span location) {
+		error(message, location, null);
+	}
+
+	/** Exception thrown by all basis-template code via {@link ExpressionError#error(String, Span)}. In case an error happens deep inside a
+	 * list of included templates, the {@link #getMessage()} method will return a condensed error message. **/
+	public static class TemplateException extends RuntimeException {
+		private static final long serialVersionUID = 1L;
+		private final Span location;
+		private final String errorMessage;
+
+		private TemplateException (String message, Span location) {
+			super(message);
+			this.errorMessage = message;
+			this.location = location;
+		}
+
+		public TemplateException (String message, Span location, Throwable cause) {
+			super(message, cause);
+			this.errorMessage = message;
+			this.location = location;
+		}
+
+		/** Returns the location in the template at which the error happened. **/
+		public Span getLocation () {
+			return location;
+		}
+
+		@Override
+		public String getMessage () {
+			StringBuilder builder = new StringBuilder();
+
+			if (getCause() == null || getCause() == this) {
+				return super.getMessage();
+			}
+
+			builder.append(errorMessage.substring(0, errorMessage.indexOf('\n')));
+			builder.append("\n");
+
+			Throwable cause = getCause();
+			while (cause != null && cause != this) {
+				if (cause instanceof TemplateException) {
+					TemplateException ex = (TemplateException)cause;
+					if (ex.getCause() == null || ex.getCause() == ex)
+						builder.append(ex.errorMessage);
+					else
+						builder.append(ex.errorMessage.substring(0, ex.errorMessage.indexOf('\n')));
+					builder.append("\n");
+				}
+				cause = cause.getCause();
+			}
+			return builder.toString();
+		}
+	}
+	
+	public static class StringLiteralException extends RuntimeException {
+
+		private static final long serialVersionUID = 1L;
+		
+	}
+}

+ 37 - 0
src/main/java/com/ssssssss/expression/ExpressionTemplate.java

@@ -0,0 +1,37 @@
+
+package com.ssssssss.expression;
+import java.io.OutputStream;
+import java.util.List;
+
+import com.ssssssss.expression.interpreter.AstInterpreter;
+import com.ssssssss.expression.parsing.Ast;
+import com.ssssssss.expression.parsing.Ast.Node;
+import com.ssssssss.expression.parsing.Parser;
+
+
+/** A template is loaded by a {@link TemplateLoader} from a file marked up with the basis-template language. The template can be
+ * rendered to a {@link String} or {@link OutputStream} by calling one of the <code>render()</code> methods. The
+ * {@link ExpressionTemplateContext} passed to the <code>render()</code> methods is used to look up variable values referenced in the
+ * template. */
+public class ExpressionTemplate {
+	private final List<Node> nodes;
+
+	/** Internal. Created by {@link Parser}. **/
+	private ExpressionTemplate (List<Node> nodes) {
+		this.nodes = nodes;
+	}
+	
+	public static ExpressionTemplate create(String source){
+		return new ExpressionTemplate(Parser.parse(source));
+	}
+	
+	/** Internal. The AST nodes representing this template after parsing. See {@link Ast}. Used by {@link AstInterpreter}. **/
+	public List<Node> getNodes () {
+		return nodes;
+	}
+
+	/** Renders the template using the TemplateContext to resolve variable values referenced in the template. **/
+	public Object render (ExpressionTemplateContext context) {
+		return AstInterpreter.interpret(this, context);
+	}
+}

+ 107 - 0
src/main/java/com/ssssssss/expression/ExpressionTemplateContext.java

@@ -0,0 +1,107 @@
+
+package com.ssssssss.expression;
+
+import com.ssssssss.expression.interpreter.AstInterpreter;
+
+import java.util.*;
+
+
+/**
+ * <p>
+ * A template context stores mappings from variable names to user provided variable values. A {@link ExpressionTemplate} is given a context
+ * for rendering to resolve variable values it references in template expressions.
+ * </p>
+ *
+ * <p>
+ * Internally, a template context is a stack of these mappings, similar to scopes in a programming language, and used as such by
+ * the {@link AstInterpreter}.
+ * </p>
+ */
+public class ExpressionTemplateContext {
+	private final List<Map<String, Object>> scopes = new ArrayList<Map<String, Object>>();
+
+	/** Keeps track of previously allocated, unused scopes. New scopes are first tried to be retrieved from this pool to avoid
+	 * generating garbage. **/
+	private final List<Map<String, Object>> freeScopes = new ArrayList<Map<String, Object>>();
+
+	private final static ThreadLocal<ExpressionTemplateContext> CONTEXT_THREAD_LOCAL = new ThreadLocal<>();
+
+	public static ExpressionTemplateContext get(){
+		return CONTEXT_THREAD_LOCAL.get();
+	}
+
+	public static void remove(){
+		CONTEXT_THREAD_LOCAL.remove();
+	}
+
+	public static void set(ExpressionTemplateContext context){
+		CONTEXT_THREAD_LOCAL.set(context);
+	}
+
+	public ExpressionTemplateContext () {
+		push();
+	}
+	
+	public ExpressionTemplateContext(Map<String,Object> variables) {
+		this();
+		if(variables != null){
+			variables.forEach(this::set);
+		}
+	}
+
+	/** Sets the value of the variable with the given name. If the variable already exists in one of the scopes, that variable is
+	 * set. Otherwise the variable is set on the last pushed scope. */
+	public ExpressionTemplateContext set (String name, Object value) {
+		for (int i = scopes.size() - 1; i >= 0; i--) {
+			Map<String, Object> ctx = scopes.get(i);
+			if (ctx.isEmpty()) continue;
+			if (ctx.containsKey(name)) {
+				ctx.put(name, value);
+				return this;
+			}
+		}
+
+		scopes.get(scopes.size() - 1).put(name, value);
+		return this;
+	}
+
+	/** Sets the value of the variable with the given name on the last pushed scope **/
+	public ExpressionTemplateContext setOnCurrentScope (String name, Object value) {
+		scopes.get(scopes.size() - 1).put(name, value);
+		return this;
+	}
+
+	/** Internal. Returns the value of the variable with the given name, walking the scope stack from top to bottom, similar to how
+	 * scopes in programming languages are searched for variables. */
+	public Object get (String name) {
+		for (int i = scopes.size() - 1; i >= 0; i--) {
+			Map<String, Object> ctx = scopes.get(i);
+			if (ctx.isEmpty()) continue;
+			Object value = ctx.get(name);
+			if (value != null) return value;
+		}
+		return null;
+	}
+
+	/** Internal. Returns all variables currently defined in this context. */
+	public Set<String> getVariables () {
+		Set<String> variables = new HashSet<String>();
+		for (int i = 0, n = scopes.size(); i < n; i++) {
+			variables.addAll(scopes.get(i).keySet());
+		}
+		return variables;
+	}
+
+	/** Internal. Pushes a new "scope" onto the stack. **/
+	public void push () {
+		Map<String, Object> newScope = freeScopes.size() > 0 ? freeScopes.remove(freeScopes.size() - 1) : new HashMap<String, Object>();
+		scopes.add(newScope);
+	}
+
+	/** Internal. Pops the top of the "scope" stack. **/
+	public void pop () {
+		Map<String, Object> oldScope = scopes.remove(scopes.size() - 1);
+		oldScope.clear();
+		freeScopes.add(oldScope);
+	}
+}

+ 63 - 0
src/main/java/com/ssssssss/expression/interpreter/AstInterpreter.java

@@ -0,0 +1,63 @@
+
+package com.ssssssss.expression.interpreter;
+
+import com.ssssssss.expression.ExpressionError;
+import com.ssssssss.expression.ExpressionError.TemplateException;
+import com.ssssssss.expression.ExpressionTemplate;
+import com.ssssssss.expression.ExpressionTemplateContext;
+import com.ssssssss.expression.parsing.Ast;
+import com.ssssssss.expression.parsing.Ast.Node;
+import com.ssssssss.expression.parsing.Ast.Text;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * <p>
+ * Interprets a Template given a TemplateContext to lookup variable values in and writes the evaluation results to an output
+ * stream. Uses the global {@link Reflection} instance as returned by {@link Reflection#getInstance()} to access members and call
+ * methods.
+ * </p>
+ *
+ * <p>
+ * The interpeter traverses the AST as stored in {@link ExpressionTemplate#getNodes()}. the interpeter has a method for each AST node type
+ * (see {@link Ast} that evaluates that node. A node may return a value, to be used in the interpretation of a parent node or to
+ * be written to the output stream.
+ * </p>
+ **/
+public class AstInterpreter {
+	public static Object interpret (ExpressionTemplate template, ExpressionTemplateContext context) {
+		try {
+			return interpretNodeList(template.getNodes(), template, context);
+		} catch (Throwable t) {
+			if (t instanceof TemplateException)
+				throw (TemplateException)t;
+			else {
+				ExpressionError.error("执行表达式出错 " + t.getMessage(), template.getNodes().get(0).getSpan(),t);
+				return null; // never reached
+			}
+		} 
+	}
+
+	public static Object interpretNodeList (List<Node> nodes, ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+		String result = "";
+		for (int i = 0, n = nodes.size(); i < n; i++) {
+			Node node = nodes.get(i);
+			Object value = node.evaluate(template, context);
+			if(node instanceof Text){
+				result += node.getSpan().getText();
+			}else if(value == null){
+				if(i ==	 0 && i + 1 == n){
+					return null;
+				}
+				result += "null";
+			}else {
+				if(i ==0 && i + 1 ==n){
+					return value;
+				}
+				result += value;
+			}
+		}
+		return result;
+	}
+}

+ 361 - 0
src/main/java/com/ssssssss/expression/interpreter/JavaReflection.java

@@ -0,0 +1,361 @@
+
+package com.ssssssss.expression.interpreter;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class JavaReflection extends Reflection {
+	private final Map<Class<?>, Map<String, Field>> fieldCache = new ConcurrentHashMap<Class<?>, Map<String, Field>>();
+	private final Map<Class<?>, Map<MethodSignature, Method>> methodCache = new ConcurrentHashMap<Class<?>, Map<MethodSignature, Method>>();
+	private final Map<Class<?>, Map<String,List<Method>>> extensionmethodCache = new ConcurrentHashMap<>();
+
+	@SuppressWarnings("rawtypes")
+	@Override
+	public Object getField (Object obj, String name) {
+		Class cls = obj instanceof Class ? (Class)obj : obj.getClass();
+		Map<String, Field> fields = fieldCache.get(cls);
+		if (fields == null) {
+			fields = new ConcurrentHashMap<String, Field>();
+			fieldCache.put(cls, fields);
+		}
+
+		Field field = fields.get(name);
+		if (field == null) {
+			try {
+				field = cls.getDeclaredField(name);
+				field.setAccessible(true);
+				fields.put(name, field);
+			} catch (Throwable t) {
+				// fall through, try super classes
+			}
+
+			if (field == null) {
+				Class parentClass = cls.getSuperclass();
+				while (parentClass != Object.class && parentClass != null) {
+					try {
+						field = parentClass.getDeclaredField(name);
+						field.setAccessible(true);
+						fields.put(name, field);
+					} catch (NoSuchFieldException e) {
+						// fall through
+					}
+					parentClass = parentClass.getSuperclass();
+				}
+			}
+		}
+
+		return field;
+	}
+
+	@Override
+	public Object getFieldValue (Object obj, Object field) {
+		Field javaField = (Field)field;
+		try {
+			return javaField.get(obj);
+		} catch (Throwable e) {
+			throw new RuntimeException("Couldn't get value of field '" + javaField.getName() + "' from object of type '" + obj.getClass().getSimpleName() + "'");
+		}
+	}
+
+	@Override
+	public void registerExtensionClass(Class<?> target,Class<?> clazz){
+		Method[] methods = clazz.getDeclaredMethods();
+		if(methods != null){
+			Map<String, List<Method>> cachedMethodMap = extensionmethodCache.get(target);
+			if(cachedMethodMap == null){
+				cachedMethodMap = new HashMap<>();
+				extensionmethodCache.put(target,cachedMethodMap);
+			}
+			for (Method method : methods) {
+				if(Modifier.isStatic(method.getModifiers()) && method.getParameterCount() > 0){
+					List<Method> cachedList = cachedMethodMap.get(method.getName());
+					if(cachedList == null){
+						cachedList = new ArrayList<>();
+						cachedMethodMap.put(method.getName(), cachedList);
+					}
+					cachedList.add(method);
+				}
+			}
+		}
+	}
+
+	@Override
+	public Object getExtensionMethod(Object obj, String name, Object... arguments) {
+		Class<?> cls = obj instanceof Class ? (Class<?>)obj : obj.getClass();
+		if(cls.isArray()){
+			cls = Object[].class;
+		}
+		return getExtensionMethod(cls,name,arguments);
+	}
+
+	private Object getExtensionMethod(Class<?> cls, String name, Object... arguments) {
+		if(cls == null){
+			cls = Object.class;
+		}
+		Map<String, List<Method>> methodMap = extensionmethodCache.get(cls);
+		if(methodMap != null){
+			List<Method> methodList = methodMap.get(name);
+			if(methodList != null){
+				Class<?>[] parameterTypes = new Class[arguments.length + 1];
+				parameterTypes[0] = cls;
+				for (int i = 0; i < arguments.length; i++) {
+					parameterTypes[i + 1] = arguments[i] == null ? null : arguments[i].getClass();
+				}
+				return findMethod(methodList, parameterTypes);
+			}
+		}
+		if(cls != Object.class){
+			Class<?>[] interfaces = cls.getInterfaces();
+			if(interfaces != null){
+				for (Class<?> clazz : interfaces) {
+					Object method = getExtensionMethod(clazz,name,arguments);
+					if(method != null){
+						return method;
+					}
+				}
+			}
+			return getExtensionMethod(cls.getSuperclass(),name,arguments);
+		}
+		return null;
+	}
+
+	@Override
+	public Object getMethod (Object obj, String name, Object... arguments) {
+		Class<?> cls = obj instanceof Class ? (Class<?>)obj : obj.getClass();
+		Map<MethodSignature, Method> methods = methodCache.get(cls);
+		if (methods == null) {
+			methods = new ConcurrentHashMap<MethodSignature, Method>();
+			methodCache.put(cls, methods);
+		}
+
+		Class<?>[] parameterTypes = new Class[arguments.length];
+		for (int i = 0; i < arguments.length; i++) {
+			parameterTypes[i] = arguments[i] == null ? null : arguments[i].getClass();
+		}
+
+		JavaReflection.MethodSignature signature = new MethodSignature(name, parameterTypes);
+		Method method = methods.get(signature);
+
+		if (method == null) {
+			try {
+				if (name == null) {
+					method = findApply(cls);
+				} else {
+					method = findMethod(cls, name, parameterTypes);
+					if(method == null && parameterTypes != null){
+						method = findMethod(cls, name, new Class<?>[]{Object[].class});
+					}
+				}
+				method.setAccessible(true);
+				methods.put(signature, method);
+			} catch (Throwable e) {
+				// fall through
+			}
+
+			if (method == null) {
+				Class<?> parentClass = cls.getSuperclass();
+				while (parentClass != Object.class && parentClass != null) {
+					try {
+						if (name == null)
+							method = findApply(parentClass);
+						else {
+							method = findMethod(parentClass, name, parameterTypes);
+						}
+						method.setAccessible(true);
+						methods.put(signature, method);
+					} catch (Throwable e) {
+						// fall through
+					}
+					parentClass = parentClass.getSuperclass();
+				}
+			}
+		}
+
+		return method;
+	}
+
+	/** Returns the <code>apply()</code> method of a functional interface. **/
+	private static Method findApply (Class<?> cls) {
+		for (Method method : cls.getDeclaredMethods()) {
+			if (method.getName().equals("apply")) return method;
+		}
+		return null;
+	}
+
+	private static Method findMethod (List<Method> methods, Class<?>[] parameterTypes) {
+		Method foundMethod = null;
+		int foundScore = 0;
+		for (Method method : methods) {
+			// Check if the types match.
+			Class<?>[] otherTypes = method.getParameterTypes();
+			if(parameterTypes.length != otherTypes.length){
+				continue;
+			}
+			boolean match = true;
+			int score = 0;
+			for (int ii = 0, nn = parameterTypes.length; ii < nn; ii++) {
+				Class<?> type = parameterTypes[ii];
+				Class<?> otherType = otherTypes[ii];
+
+				if (!otherType.isAssignableFrom(type)) {
+					score++;
+					if (!isPrimitiveAssignableFrom(type, otherType)) {
+						score++;
+						if (!isCoercible(type, otherType)) {
+							match = false;
+							break;
+						} else {
+							score++;
+						}
+					}
+				}else if(type == null && otherType.isPrimitive()){
+					match = false;
+					break;
+				}
+			}
+			if (match) {
+				if (foundMethod == null) {
+					foundMethod = method;
+					foundScore = score;
+				} else {
+					if (score < foundScore) {
+						foundScore = score;
+						foundMethod = method;
+					}
+				}
+			}
+		}
+		return foundMethod;
+	}
+
+	/** Returns the method best matching the given signature, including type coercion, or null. **/
+	private static Method findMethod (Class<?> cls, String name, Class<?>[] parameterTypes) {
+		Method[] methods = cls.getDeclaredMethods();
+		List<Method> methodList = new ArrayList<>();
+		for (int i = 0, n = methods.length; i < n; i++) {
+			Method method = methods[i];
+
+			// if neither name or parameter list size match, bail on this method
+			if (!method.getName().equals(name)) continue;
+			if (method.getParameterTypes().length != parameterTypes.length) continue;
+			methodList.add(method);
+		}
+		return findMethod(methodList,parameterTypes);
+	}
+
+	/** Returns whether the from type can be assigned to the to type, assuming either type is a (boxed) primitive type. We can
+	 * relax the type constraint a little, as we'll invoke a method via reflection. That means the from type will always be boxed,
+	 * as the {@link Method#invoke(Object, Object...)} method takes objects. **/
+	private static boolean isPrimitiveAssignableFrom (Class<?> from, Class<?> to) {
+		if ((from == Boolean.class || from == boolean.class) && (to == boolean.class || to == Boolean.class)) return true;
+		if ((from == Integer.class || from == int.class) && (to == int.class || to == Integer.class)) return true;
+		if ((from == Float.class || from == float.class) && (to == float.class || to == Float.class)) return true;
+		if ((from == Double.class || from == double.class) && (to == double.class || to == Double.class)) return true;
+		if ((from == Byte.class || from == byte.class) && (to == byte.class || to == Byte.class)) return true;
+		if ((from == Short.class || from == short.class) && (to == short.class || to == Short.class)) return true;
+		if ((from == Long.class || from == long.class) && (to == long.class || to == Long.class)) return true;
+		if ((from == Character.class || from == char.class) && (to == char.class || to == Character.class)) return true;
+		return false;
+	}
+
+	public static String[] getStringTypes(Object[] objects){
+		String[] parameterTypes = new String[objects == null ? 0: objects.length];
+		if(objects != null){
+			for(int i=0,len = objects.length;i<len;i++){
+				Object value = objects[i];
+				parameterTypes[i] = value == null ? "null" : value.getClass().getSimpleName();
+			}
+		}
+		return parameterTypes;
+	}
+
+	/** Returns whether the from type can be coerced to the to type. The coercion rules follow those of Java. See JLS 5.1.2
+	 * https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html **/
+	private static boolean isCoercible (Class<?> from, Class<?> to) {
+		if (from == Integer.class || from == int.class) {
+			return to == float.class || to == Float.class || to == double.class || to == Double.class || to == long.class || to == Long.class;
+		}
+
+		if (from == Float.class || from == float.class) {
+			return to == double.class || to == Double.class;
+		}
+
+		if (from == Double.class || from == double.class) {
+			return false;
+		}
+
+		if (from == Character.class || from == char.class) {
+			return to == int.class || to == Integer.class || to == float.class || to == Float.class || to == double.class || to == Double.class || to == long.class
+				|| to == Long.class;
+		}
+
+		if (from == Byte.class || from == byte.class) {
+			return to == int.class || to == Integer.class || to == float.class || to == Float.class || to == double.class || to == Double.class || to == long.class
+				|| to == Long.class || to == short.class || to == Short.class;
+		}
+
+		if (from == Short.class || from == short.class) {
+			return to == int.class || to == Integer.class || to == float.class || to == Float.class || to == double.class || to == Double.class || to == long.class
+				|| to == Long.class;
+		}
+
+		if (from == Long.class || from == long.class) {
+			return to == float.class || to == Float.class || to == double.class || to == Double.class;
+		}
+
+		if(from == int[].class || from == Integer[].class){
+			return to == Object[].class || to == float[].class || to == Float[].class || to == double[].class || to == Double[].class || to == long[].class || to == Long[].class;
+		}
+
+		return false;
+	}
+
+	@Override
+	public Object callMethod (Object obj, Object method, Object... arguments) {
+		Method javaMethod = (Method)method;
+		try {
+			return javaMethod.invoke(obj, arguments);
+		} catch (Throwable t) {
+			throw new RuntimeException("Couldn't call method '" + javaMethod.getName() + "' with arguments '" + Arrays.toString(arguments)
+				+ "' on object of type '" + obj.getClass().getSimpleName() + "'.", t);
+		}
+	}
+
+	private static class MethodSignature {
+		private final String name;
+		@SuppressWarnings("rawtypes") private final Class[] parameters;
+		private final int hashCode;
+
+		@SuppressWarnings("rawtypes")
+		public MethodSignature (String name, Class[] parameters) {
+			this.name = name;
+			this.parameters = parameters;
+			final int prime = 31;
+			int hash = 1;
+			hash = prime * hash + ((name == null) ? 0 : name.hashCode());
+			hash = prime * hash + Arrays.hashCode(parameters);
+			hashCode = hash;
+		}
+
+		@Override
+		public int hashCode () {
+			return hashCode;
+		}
+
+		@Override
+		public boolean equals (Object obj) {
+			if (this == obj) return true;
+			if (obj == null) return false;
+			if (getClass() != obj.getClass()) return false;
+			JavaReflection.MethodSignature other = (JavaReflection.MethodSignature)obj;
+			if (name == null) {
+				if (other.name != null) return false;
+			} else if (!name.equals(other.name)) return false;
+			if (!Arrays.equals(parameters, other.parameters)) return false;
+			return true;
+		}
+	}
+}

+ 39 - 0
src/main/java/com/ssssssss/expression/interpreter/Reflection.java

@@ -0,0 +1,39 @@
+
+package com.ssssssss.expression.interpreter;
+
+/** Used by {@link AstInterpreter} to access fields and methods of objects. This is a singleton class used by all
+ * {@link AstInterpreter} instances. Replace the default implementation via {@link #setInstance(Reflection)}. The implementation
+ * must be thread-safe. */
+public abstract class Reflection {
+	private static Reflection instance = new JavaReflection();
+
+	/** Sets the Reflection instance to be used by all Template interpreters **/
+	public synchronized static void setInstance (Reflection reflection) {
+		instance = reflection;
+	}
+
+	/** Returns the Reflection instance used to fetch field and call methods **/
+	public synchronized static Reflection getInstance () {
+		return instance;
+	}
+
+	/** Returns an opaque handle to a field with the given name or null if the field could not be found **/
+	public abstract Object getField (Object obj, String name);
+
+	/** Returns an opaque handle to the method with the given name best matching the signature implied by the given arguments, or
+	 * null if the method could not be found. If obj is an instance of Class, the matching static method is returned. If the name
+	 * is null and the object is a {@link FunctionalInterface}, the first declared method on the object is returned. **/
+	public abstract Object getMethod (Object obj, String name, Object... arguments);
+	
+	public abstract Object getExtensionMethod (Object obj, String name,Object ... arguments);
+	
+	public abstract void registerExtensionClass(Class<?> target,Class<?> clazz);
+
+	/** Returns the value of the field from the object. The field must have been previously retrieved via
+	 * {@link #getField(Object, String)}. **/
+	public abstract Object getFieldValue (Object obj, Object field);
+
+	/** Calls the method on the object with the given arguments. The method must have been previously retrieved via
+	 * {@link #getMethod(Object, String, Object...)}. **/
+	public abstract Object callMethod (Object obj, Object method, Object... arguments);
+}

+ 1302 - 0
src/main/java/com/ssssssss/expression/parsing/Ast.java

@@ -0,0 +1,1302 @@
+
+package com.ssssssss.expression.parsing;
+
+import com.ssssssss.expression.ExpressionError;
+import com.ssssssss.expression.ExpressionError.TemplateException;
+import com.ssssssss.expression.ExpressionTemplate;
+import com.ssssssss.expression.ExpressionTemplateContext;
+import com.ssssssss.expression.interpreter.AstInterpreter;
+import com.ssssssss.expression.interpreter.JavaReflection;
+import com.ssssssss.expression.interpreter.Reflection;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+
+import javax.xml.transform.Source;
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.lang.reflect.InvocationTargetException;
+import java.util.*;
+
+
+/** Templates are parsed into an abstract syntax tree (AST) nodes by a Parser. This class contains all AST node types. */
+public abstract class Ast {
+
+	/** Base class for all AST nodes. A node minimally stores the {@link Span} that references its location in the
+	 * {@link Source}. **/
+	public abstract static class Node {
+		private final Span span;
+
+		public Node (Span span) {
+			this.span = span;
+		}
+
+		/** Returns the {@link Span} referencing this node's location in the {@link Source}. **/
+		public Span getSpan () {
+			return span;
+		}
+
+		@Override
+		public String toString () {
+			return span.getText();
+		}
+
+		public abstract Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException;
+	}
+
+	/** A text node represents an "un-templated" span in the source that should be emitted verbatim. **/
+	public static class Text extends Node {
+		private final String content;
+
+		public Text (Span text) {
+			super(text);
+			String unescapedValue = text.getText();
+			StringBuilder builder = new StringBuilder();
+
+			CharacterStream stream = new CharacterStream(unescapedValue);
+			while (stream.hasMore()) {
+				if (stream.match("\\{", true)) {
+					builder.append('{');
+				} else if (stream.match("\\}", true)) {
+					builder.append('}');
+				} else {
+					builder.append(stream.consume());
+				}
+			}
+			content = builder.toString();
+		}
+
+		/** Returns the UTF-8 representation of this text node. **/
+		public String getContent () {
+			return content;
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			return null;
+		}
+	}
+
+	/** All expressions are subclasses of this node type. Expressions are separated into unary operations (!, -), binary operations
+	 * (+, -, *, /, etc.) and ternary operations (?:). */
+	public abstract static class Expression extends Node {
+		public Expression (Span span) {
+			super(span);
+		}
+	}
+
+	/** An unary operation node represents a logical or numerical negation. **/
+	public static class UnaryOperation extends Expression {
+
+		public static enum UnaryOperator {
+			Not, Negate, Positive;
+
+			public static UnaryOperator getOperator (Token op) {
+				if (op.getType() == TokenType.Not) {
+					return UnaryOperator.Not;
+				}
+				if (op.getType() == TokenType.Plus) {
+					return UnaryOperator.Positive;
+				}
+				if (op.getType() == TokenType.Minus) {
+					return UnaryOperator.Negate;
+				}
+				ExpressionError.error("Unknown unary operator " + op + ".", op.getSpan());
+				return null; // not reached
+			}
+		}
+
+		private final UnaryOperator operator;
+		private final Expression operand;
+
+		public UnaryOperation (Token operator, Expression operand) {
+			super(operator.getSpan());
+			this.operator = UnaryOperator.getOperator(operator);
+			this.operand = operand;
+		}
+
+		public UnaryOperator getOperator () {
+			return operator;
+		}
+
+		public Expression getOperand () {
+			return operand;
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			Object operand = getOperand().evaluate(template, context);
+
+			if (getOperator() == UnaryOperator.Negate) {
+				if (operand instanceof Integer) {
+					return -(Integer)operand;
+				} else if (operand instanceof Float) {
+					return -(Float)operand;
+				} else if (operand instanceof Double) {
+					return -(Double)operand;
+				} else if (operand instanceof Byte) {
+					return -(Byte)operand;
+				} else if (operand instanceof Short) {
+					return -(Short)operand;
+				} else if (operand instanceof Long) {
+					return -(Long)operand;
+				} else {
+					ExpressionError.error("Operand of operator '" + getOperator().name() + "' must be a number, got " + operand, getSpan());
+					return null; // never reached
+				}
+			} else if (getOperator() == UnaryOperator.Not) {
+				if (!(operand instanceof Boolean)) {
+					ExpressionError.error("Operand of operator '" + getOperator().name() + "' must be a boolean", getSpan());
+				}
+				return !(Boolean)operand;
+			} else {
+				return operand;
+			}
+		}
+	}
+
+	/** A binary operation represents arithmetic operators, like addition or division, comparison operators, like less than or
+	 * equals, logical operators, like and, or an assignment. **/
+	public static class BinaryOperation extends Expression {
+
+		public static enum BinaryOperator {
+			Addition, Subtraction, Multiplication, Division, Modulo, Equal, NotEqual, Less, LessEqual, Greater, GreaterEqual, And, Or, Xor, Assignment;
+
+			public static BinaryOperator getOperator (Token op) {
+				if (op.getType() == TokenType.Plus) {
+					return BinaryOperator.Addition;
+				}
+				if (op.getType() == TokenType.Minus) {
+					return BinaryOperator.Subtraction;
+				}
+				if (op.getType() == TokenType.Asterisk) {
+					return BinaryOperator.Multiplication;
+				}
+				if (op.getType() == TokenType.ForwardSlash) {
+					return BinaryOperator.Division;
+				}
+				if (op.getType() == TokenType.Percentage) {
+					return BinaryOperator.Modulo;
+				}
+				if (op.getType() == TokenType.Equal) {
+					return BinaryOperator.Equal;
+				}
+				if (op.getType() == TokenType.NotEqual) {
+					return BinaryOperator.NotEqual;
+				}
+				if (op.getType() == TokenType.Less) {
+					return BinaryOperator.Less;
+				}
+				if (op.getType() == TokenType.LessEqual) {
+					return BinaryOperator.LessEqual;
+				}
+				if (op.getType() == TokenType.Greater) {
+					return BinaryOperator.Greater;
+				}
+				if (op.getType() == TokenType.GreaterEqual) {
+					return BinaryOperator.GreaterEqual;
+				}
+				if (op.getType() == TokenType.And) {
+					return BinaryOperator.And;
+				}
+				if (op.getType() == TokenType.Or) {
+					return BinaryOperator.Or;
+				}
+				if (op.getType() == TokenType.Xor) {
+					return BinaryOperator.Xor;
+				}
+				if (op.getType() == TokenType.Assignment) {
+					return BinaryOperator.Assignment;
+				}
+				ExpressionError.error("Unknown binary operator " + op + ".", op.getSpan());
+				return null; // not reached
+			}
+		}
+
+		private final Expression leftOperand;
+		private final BinaryOperator operator;
+		private final Expression rightOperand;
+
+		public BinaryOperation (Expression leftOperand, Token operator, Expression rightOperand) {
+			super(operator.getSpan());
+			this.leftOperand = leftOperand;
+			this.operator = BinaryOperator.getOperator(operator);
+			this.rightOperand = rightOperand;
+		}
+
+		public Expression getLeftOperand () {
+			return leftOperand;
+		}
+
+		public BinaryOperator getOperator () {
+			return operator;
+		}
+
+		public Expression getRightOperand () {
+			return rightOperand;
+		}
+
+		private Object evaluateAddition (Object left, Object right) {
+			if (left instanceof String || right instanceof String) {
+				return left.toString() + right.toString();
+			}
+			if (left instanceof Double || right instanceof Double) {
+				return ((Number)left).doubleValue() + ((Number)right).doubleValue();
+			}
+			if (left instanceof Float || right instanceof Float) {
+				return ((Number)left).floatValue() + ((Number)right).floatValue();
+			}
+			if (left instanceof Long || right instanceof Long) {
+				return ((Number)left).longValue() + ((Number)right).longValue();
+			}
+			if (left instanceof Integer || right instanceof Integer) {
+				return ((Number)left).intValue() + ((Number)right).intValue();
+			}
+			if (left instanceof Short || right instanceof Short) {
+				return ((Number)left).shortValue() + ((Number)right).shortValue();
+			}
+			if (left instanceof Byte || right instanceof Byte) {
+				return ((Number)left).byteValue() + ((Number)right).byteValue();
+			}
+
+			ExpressionError.error("Operands for addition operator must be numbers or strings, got " + left + ", " + right + ".", getSpan());
+			return null; // never reached
+		}
+
+		private Object evaluateSubtraction (Object left, Object right) {
+			if (left instanceof Double || right instanceof Double) {
+				return ((Number)left).doubleValue() - ((Number)right).doubleValue();
+			} else if (left instanceof Float || right instanceof Float) {
+				return ((Number)left).floatValue() - ((Number)right).floatValue();
+			} else if (left instanceof Long || right instanceof Long) {
+				return ((Number)left).longValue() - ((Number)right).longValue();
+			} else if (left instanceof Integer || right instanceof Integer) {
+				return ((Number)left).intValue() - ((Number)right).intValue();
+			} else if (left instanceof Short || right instanceof Short) {
+				return ((Number)left).shortValue() - ((Number)right).shortValue();
+			} else if (left instanceof Byte || right instanceof Byte) {
+				return ((Number)left).byteValue() - ((Number)right).byteValue();
+			} else {
+				ExpressionError.error("Operands for subtraction operator must be numbers" + left + ", " + right + ".", getSpan());
+				return null; // never reached
+			}
+		}
+
+		private Object evaluateMultiplication (Object left, Object right) {
+			if (left instanceof Double || right instanceof Double) {
+				return ((Number)left).doubleValue() * ((Number)right).doubleValue();
+			} else if (left instanceof Float || right instanceof Float) {
+				return ((Number)left).floatValue() * ((Number)right).floatValue();
+			} else if (left instanceof Long || right instanceof Long) {
+				return ((Number)left).longValue() * ((Number)right).longValue();
+			} else if (left instanceof Integer || right instanceof Integer) {
+				return ((Number)left).intValue() * ((Number)right).intValue();
+			} else if (left instanceof Short || right instanceof Short) {
+				return ((Number)left).shortValue() * ((Number)right).shortValue();
+			} else if (left instanceof Byte || right instanceof Byte) {
+				return ((Number)left).byteValue() * ((Number)right).byteValue();
+			} else {
+				ExpressionError.error("Operands for multiplication operator must be numbers" + left + ", " + right + ".", getSpan());
+				return null; // never reached
+			}
+		}
+
+		private Object evaluateDivision (Object left, Object right) {
+			if (left instanceof Double || right instanceof Double) {
+				return ((Number)left).doubleValue() / ((Number)right).doubleValue();
+			} else if (left instanceof Float || right instanceof Float) {
+				return ((Number)left).floatValue() / ((Number)right).floatValue();
+			} else if (left instanceof Long || right instanceof Long) {
+				return ((Number)left).longValue() / ((Number)right).longValue();
+			} else if (left instanceof Integer || right instanceof Integer) {
+				return ((Number)left).intValue() / ((Number)right).intValue();
+			} else if (left instanceof Short || right instanceof Short) {
+				return ((Number)left).shortValue() / ((Number)right).shortValue();
+			} else if (left instanceof Byte || right instanceof Byte) {
+				return ((Number)left).byteValue() / ((Number)right).byteValue();
+			} else {
+				ExpressionError.error("Operands for division operator must be numbers" + left + ", " + right + ".", getSpan());
+				return null; // never reached
+			}
+		}
+
+		private Object evaluateModulo (Object left, Object right) {
+			if (left instanceof Double || right instanceof Double) {
+				return ((Number)left).doubleValue() % ((Number)right).doubleValue();
+			} else if (left instanceof Float || right instanceof Float) {
+				return ((Number)left).floatValue() % ((Number)right).floatValue();
+			} else if (left instanceof Long || right instanceof Long) {
+				return ((Number)left).longValue() % ((Number)right).longValue();
+			} else if (left instanceof Integer || right instanceof Integer) {
+				return ((Number)left).intValue() % ((Number)right).intValue();
+			} else if (left instanceof Short || right instanceof Short) {
+				return ((Number)left).shortValue() % ((Number)right).shortValue();
+			} else if (left instanceof Byte || right instanceof Byte) {
+				return ((Number)left).byteValue() % ((Number)right).byteValue();
+			} else {
+				ExpressionError.error("Operands for modulo operator must be numbers" + left + ", " + right + ".", getSpan());
+				return null; // never reached
+			}
+		}
+
+		private boolean evaluateLess (Object left, Object right) {
+			if (left instanceof Double || right instanceof Double) {
+				return ((Number)left).doubleValue() < ((Number)right).doubleValue();
+			} else if (left instanceof Float || right instanceof Float) {
+				return ((Number)left).floatValue() < ((Number)right).floatValue();
+			} else if (left instanceof Long || right instanceof Long) {
+				return ((Number)left).longValue() < ((Number)right).longValue();
+			} else if (left instanceof Integer || right instanceof Integer) {
+				return ((Number)left).intValue() < ((Number)right).intValue();
+			} else if (left instanceof Short || right instanceof Short) {
+				return ((Number)left).shortValue() < ((Number)right).shortValue();
+			} else if (left instanceof Byte || right instanceof Byte) {
+				return ((Number)left).byteValue() < ((Number)right).byteValue();
+			} else {
+				ExpressionError.error("Operands for less operator must be numbers" + left + ", " + right + ".", getSpan());
+				return false; // never reached
+			}
+		}
+
+		private Object evaluateLessEqual (Object left, Object right) {
+			if (left instanceof Double || right instanceof Double) {
+				return ((Number)left).doubleValue() <= ((Number)right).doubleValue();
+			} else if (left instanceof Float || right instanceof Float) {
+				return ((Number)left).floatValue() <= ((Number)right).floatValue();
+			} else if (left instanceof Long || right instanceof Long) {
+				return ((Number)left).longValue() <= ((Number)right).longValue();
+			} else if (left instanceof Integer || right instanceof Integer) {
+				return ((Number)left).intValue() <= ((Number)right).intValue();
+			} else if (left instanceof Short || right instanceof Short) {
+				return ((Number)left).shortValue() <= ((Number)right).shortValue();
+			} else if (left instanceof Byte || right instanceof Byte) {
+				return ((Number)left).byteValue() <= ((Number)right).byteValue();
+			} else {
+				ExpressionError.error("Operands for less/equal operator must be numbers" + left + ", " + right + ".", getSpan());
+				return null; // never reached
+			}
+		}
+
+		private Object evaluateGreater (Object left, Object right) {
+			if (left instanceof Double || right instanceof Double) {
+				return ((Number)left).doubleValue() > ((Number)right).doubleValue();
+			} else if (left instanceof Float || right instanceof Float) {
+				return ((Number)left).floatValue() > ((Number)right).floatValue();
+			} else if (left instanceof Long || right instanceof Long) {
+				return ((Number)left).longValue() > ((Number)right).longValue();
+			} else if (left instanceof Integer || right instanceof Integer) {
+				return ((Number)left).intValue() > ((Number)right).intValue();
+			} else if (left instanceof Short || right instanceof Short) {
+				return ((Number)left).shortValue() > ((Number)right).shortValue();
+			} else if (left instanceof Byte || right instanceof Byte) {
+				return ((Number)left).byteValue() > ((Number)right).byteValue();
+			} else {
+				ExpressionError.error("Operands for greater operator must be numbers" + left + ", " + right + ".", getSpan());
+				return null; // never reached
+			}
+		}
+
+		private Object evaluateGreaterEqual (Object left, Object right) {
+			if (left instanceof Double || right instanceof Double) {
+				return ((Number)left).doubleValue() >= ((Number)right).doubleValue();
+			} else if (left instanceof Float || right instanceof Float) {
+				return ((Number)left).floatValue() >= ((Number)right).floatValue();
+			} else if (left instanceof Long || right instanceof Long) {
+				return ((Number)left).longValue() >= ((Number)right).longValue();
+			} else if (left instanceof Integer || right instanceof Integer) {
+				return ((Number)left).intValue() >= ((Number)right).intValue();
+			} else if (left instanceof Short || right instanceof Short) {
+				return ((Number)left).shortValue() >= ((Number)right).shortValue();
+			} else if (left instanceof Byte || right instanceof Byte) {
+				return ((Number)left).byteValue() >= ((Number)right).byteValue();
+			} else {
+				ExpressionError.error("Operands for greater/equal operator must be numbers" + left + ", " + right + ".", getSpan());
+				return null; // never reached
+			}
+		}
+
+		private Object evaluateAnd (Object left, ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			if (!(left instanceof Boolean)) {
+				ExpressionError.error("Left operand must be a boolean, got " + left + ".", getLeftOperand().getSpan());
+			}
+			if (!(Boolean)left) {
+				return false;
+			}
+			Object right = getRightOperand().evaluate(template, context);
+			if (!(right instanceof Boolean)) {
+				ExpressionError.error("Right operand must be a boolean, got " + right + ".", getRightOperand().getSpan());
+			}
+			return (Boolean)left && (Boolean)right;
+		}
+
+		private Object evaluateOr (Object left, ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			if (!(left instanceof Boolean)) {
+				ExpressionError.error("Left operand must be a boolean, got " + left + ".", getLeftOperand().getSpan());
+			}
+			if ((Boolean)left) {
+				return true;
+			}
+			Object right = getRightOperand().evaluate(template, context);
+			if (!(right instanceof Boolean)) {
+				ExpressionError.error("Right operand must be a boolean, got " + right + ".", getRightOperand().getSpan());
+			}
+			return (Boolean)left || (Boolean)right;
+		}
+
+		private Object evaluateXor (Object left, Object right) {
+			if (!(left instanceof Boolean)) {
+				ExpressionError.error("Left operand must be a boolean, got " + left + ".", getLeftOperand().getSpan());
+			}
+			if (!(right instanceof Boolean)) {
+				ExpressionError.error("Right operand must be a boolean, got " + right + ".", getRightOperand().getSpan());
+			}
+			return (Boolean)left ^ (Boolean)right;
+		}
+
+		private Object evaluateEqual (Object left, Object right) {
+			if (left != null) {
+				return left.equals(right);
+			}
+			if (right != null) {
+				return right.equals(left);
+			}
+			return true;
+		}
+
+		private Object evaluateNotEqual (Object left, Object right) {
+			return !(Boolean)evaluateEqual(left, right);
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			if (getOperator() == BinaryOperator.Assignment) {
+				if (!(getLeftOperand() instanceof VariableAccess)) {
+					ExpressionError.error("Can only assign to top-level variables in context.", getLeftOperand().getSpan());
+				}
+				Object value = getRightOperand().evaluate(template, context);
+				context.set(((VariableAccess)getLeftOperand()).getVariableName().getText(), value);
+				return null;
+			}
+
+			Object left = getLeftOperand().evaluate(template, context);
+			Object right = getOperator() == BinaryOperator.And || getOperator() == BinaryOperator.Or ? null : getRightOperand().evaluate(template, context);
+
+			switch (getOperator()) {
+			case Addition:
+				return evaluateAddition(left, right);
+			case Subtraction:
+				return evaluateSubtraction(left, right);
+			case Multiplication:
+				return evaluateMultiplication(left, right);
+			case Division:
+				return evaluateDivision(left, right);
+			case Modulo:
+				return evaluateModulo(left, right);
+			case Less:
+				return evaluateLess(left, right);
+			case LessEqual:
+				return evaluateLessEqual(left, right);
+			case Greater:
+				return evaluateGreater(left, right);
+			case GreaterEqual:
+				return evaluateGreaterEqual(left, right);
+			case Equal:
+				return evaluateEqual(left, right);
+			case NotEqual:
+				return evaluateNotEqual(left, right);
+			case And:
+				return evaluateAnd(left, template, context);
+			case Or:
+				return evaluateOr(left, template, context);
+			case Xor:
+				return evaluateXor(left, right);
+			default:
+				ExpressionError.error("Binary operator " + getOperator().name() + " not implemented", getSpan());
+				return null;
+			}
+		}
+	}
+
+	/** A ternary operation is an abbreviated if/then/else operation, and equivalent to the the ternary operator in Java. **/
+	public static class TernaryOperation extends Expression {
+		private final Expression condition;
+		private final Expression trueExpression;
+		private final Expression falseExpression;
+
+		public TernaryOperation (Expression condition, Expression trueExpression, Expression falseExpression) {
+			super(new Span(condition.getSpan(), falseExpression.getSpan()));
+			this.condition = condition;
+			this.trueExpression = trueExpression;
+			this.falseExpression = falseExpression;
+		}
+
+		public Expression getCondition () {
+			return condition;
+		}
+
+		public Expression getTrueExpression () {
+			return trueExpression;
+		}
+
+		public Expression getFalseExpression () {
+			return falseExpression;
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			Object condition = getCondition().evaluate(template, context);
+			if (!(condition instanceof Boolean)) {
+				ExpressionError.error("Condition of ternary operator must be a boolean, got " + condition + ".", getSpan());
+			}
+			return ((Boolean)condition) ? getTrueExpression().evaluate(template, context) : getFalseExpression().evaluate(template, context);
+		}
+	}
+
+	/** A null literal, with the single value <code>null</code> **/
+	public static class NullLiteral extends Expression {
+		public NullLiteral (Span span) {
+			super(span);
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			return null;
+		}
+	}
+
+	/** A boolean literal, with the values <code>true</code> and <code>false</code> **/
+	public static class BooleanLiteral extends Expression {
+		private final Boolean value;
+
+		public BooleanLiteral (Span literal) {
+			super(literal);
+			this.value = Boolean.parseBoolean(literal.getText());
+		}
+
+		public Boolean getValue () {
+			return value;
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			return value;
+		}
+	}
+
+	/** A double precision floating point literal. Must be marked with the <code>d</code> suffix, e.g. "1.0d". **/
+	public static class DoubleLiteral extends Expression {
+		private final Double value;
+
+		public DoubleLiteral (Span literal) {
+			super(literal);
+			this.value = Double.parseDouble(literal.getText().substring(0, literal.getText().length() - 1));
+		}
+
+		public Double getValue () {
+			return value;
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			return value;
+		}
+	}
+
+	/** A single precision floating point literla. May be optionally marked with the <code>f</code> suffix, e.g. "1.0f". **/
+	public static class FloatLiteral extends Expression {
+		private final Float value;
+
+		public FloatLiteral (Span literal) {
+			super(literal);
+			String text = literal.getText();
+			if (text.charAt(text.length() - 1) == 'f') {
+				text = text.substring(0, text.length() - 1);
+			}
+			this.value = Float.parseFloat(text);
+		}
+
+		public Float getValue () {
+			return value;
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			return value;
+		}
+	}
+
+	/** A byte literal. Must be marked with the <code>b</code> suffix, e.g. "123b". **/
+	public static class ByteLiteral extends Expression {
+		private final Byte value;
+
+		public ByteLiteral (Span literal) {
+			super(literal);
+			this.value = Byte.parseByte(literal.getText().substring(0, literal.getText().length() - 1));
+		}
+
+		public Byte getValue () {
+			return value;
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			return value;
+		}
+	}
+
+	/** A short literal. Must be marked with the <code>s</code> suffix, e.g. "123s". **/
+	public static class ShortLiteral extends Expression {
+		private final Short value;
+
+		public ShortLiteral (Span literal) {
+			super(literal);
+			this.value = Short.parseShort(literal.getText().substring(0, literal.getText().length() - 1));
+		}
+
+		public Short getValue () {
+			return value;
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			return value;
+		}
+	}
+
+	/** An integer literal. **/
+	public static class IntegerLiteral extends Expression {
+		private final Integer value;
+
+		public IntegerLiteral (Span literal) {
+			super(literal);
+			this.value = Integer.parseInt(literal.getText());
+		}
+
+		public Integer getValue () {
+			return value;
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			return value;
+		}
+	}
+
+	/** A long integer literal. Must be marked with the <code>l</code> suffix, e.g. "123l". **/
+	public static class LongLiteral extends Expression {
+		private final Long value;
+
+		public LongLiteral (Span literal) {
+			super(literal);
+			this.value = Long.parseLong(literal.getText().substring(0, literal.getText().length() - 1));
+		}
+
+		public Long getValue () {
+			return value;
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			return value;
+		}
+	}
+
+	/** A character literal, enclosed in single quotes. Supports escape sequences \n, \r,\t, \' and \\. **/
+	public static class CharacterLiteral extends Expression {
+		private final Character value;
+
+		public CharacterLiteral (Span literal) {
+			super(literal);
+
+			String text = literal.getText();
+			if (text.length() > 3) {
+				if (text.charAt(2) == 'n') {
+					value = '\n';
+				} else if (text.charAt(2) == 'r') {
+					value = '\r';
+				} else if (text.charAt(2) == 't') {
+					value = '\t';
+				} else if (text.charAt(2) == '\\') {
+					value = '\\';
+				} else if (text.charAt(2) == '\'') {
+					value = '\'';
+				} else {
+					ExpressionError.error("Unknown escape sequence '" + literal.getText() + "'.", literal);
+					value = 0; // never reached
+				}
+			} else {
+				this.value = literal.getText().charAt(1);
+			}
+		}
+
+		public Character getValue () {
+			return value;
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			return value;
+		}
+	}
+
+	/** A string literal, enclosed in double quotes. Supports escape sequences \n, \r, \t, \" and \\. **/
+	public static class StringLiteral extends Expression {
+		private final String value;
+
+		public StringLiteral (Span literal) {
+			super(literal);
+			String text = getSpan().getText();
+			String unescapedValue = text.substring(1, text.length() - 1);
+			StringBuilder builder = new StringBuilder();
+
+			CharacterStream stream = new CharacterStream(unescapedValue);
+			while (stream.hasMore()) {
+				if (stream.match("\\\\", true)) {
+					builder.append('\\');
+				} else if (stream.match("\\n", true)) {
+					builder.append('\n');
+				} else if (stream.match("\\r", true)) {
+					builder.append('\r');
+				} else if (stream.match("\\t", true)) {
+					builder.append('\t');
+				} else if (stream.match("\\\"", true)) {
+					builder.append('"');
+				} else {
+					builder.append(stream.consume());
+				}
+			}
+			value = builder.toString();
+		}
+
+		/** Returns the literal without quotes **/
+		public String getValue () {
+			return value;
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			return value;
+		}
+	}
+
+	/** Represents a top-level variable access by name. E.g. in the expression "a + 1", <code>a</code> would be encoded as a
+	 * VariableAccess node. Variables can be both read (in expressions) and written to (in assignments). Variable values are looked
+	 * up and written to a {@link ExpressionTemplateContext}. **/
+	public static class VariableAccess extends Expression {
+		public VariableAccess (Span name) {
+			super(name);
+		}
+
+		public Span getVariableName () {
+			return getSpan();
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			Object value = context.get(getSpan().getText());
+			//if (value == null) ExpressionError.error("找不到变量'" + getSpan().getText() + "'或变量值为null", getSpan());
+			return value;
+		}
+	}
+
+	/** Represents a map or array element access of the form <code>mapOrArray[keyOrIndex]</code>. Maps and arrays may only be read
+	 * from. **/
+	public static class MapOrArrayAccess extends Expression {
+		private final Expression mapOrArray;
+		private final Expression keyOrIndex;
+
+		public MapOrArrayAccess (Span span, Expression mapOrArray, Expression keyOrIndex) {
+			super(span);
+			this.mapOrArray = mapOrArray;
+			this.keyOrIndex = keyOrIndex;
+		}
+
+		/** Returns an expression that must evaluate to a map or array. **/
+		public Expression getMapOrArray () {
+			return mapOrArray;
+		}
+
+		/** Returns an expression that is used as the key or index to fetch a map or array element. **/
+		public Expression getKeyOrIndex () {
+			return keyOrIndex;
+		}
+
+		@SuppressWarnings("rawtypes")
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			Object mapOrArray = getMapOrArray().evaluate(template, context);
+			if (mapOrArray == null) {
+				return null;
+			}
+			Object keyOrIndex = getKeyOrIndex().evaluate(template, context);
+			if (keyOrIndex == null) {
+				return null;
+			}
+
+			if (mapOrArray instanceof Map) {
+				return ((Map)mapOrArray).get(keyOrIndex);
+			} else if (mapOrArray instanceof List) {
+				if (!(keyOrIndex instanceof Number)) {
+					ExpressionError.error("List index must be an integer, but was " + keyOrIndex.getClass().getSimpleName(), getKeyOrIndex().getSpan());
+				}
+				int index = ((Number)keyOrIndex).intValue();
+				return ((List)mapOrArray).get(index);
+			} else {
+				if (!(keyOrIndex instanceof Number)) {
+					ExpressionError.error("Array index must be an integer, but was " + keyOrIndex.getClass().getSimpleName(), getKeyOrIndex().getSpan());
+				}
+				int index = ((Number)keyOrIndex).intValue();
+				if (mapOrArray instanceof int[]) {
+					return ((int[])mapOrArray)[index];
+				} else if (mapOrArray instanceof float[]) {
+					return ((float[])mapOrArray)[index];
+				} else if (mapOrArray instanceof double[]) {
+					return ((double[])mapOrArray)[index];
+				} else if (mapOrArray instanceof boolean[]) {
+					return ((boolean[])mapOrArray)[index];
+				} else if (mapOrArray instanceof char[]) {
+					return ((char[])mapOrArray)[index];
+				} else if (mapOrArray instanceof short[]) {
+					return ((short[])mapOrArray)[index];
+				} else if (mapOrArray instanceof long[]) {
+					return ((long[])mapOrArray)[index];
+				} else if (mapOrArray instanceof byte[]) {
+					return ((byte[])mapOrArray)[index];
+				} else if (mapOrArray instanceof String) {
+					return Character.toString(((String)mapOrArray).charAt(index));
+				} else {
+					return ((Object[])mapOrArray)[index];
+				}
+			}
+		}
+	}
+
+	/** Represents an access of a member (field or method or entry in a map) of the form <code>object.member</code>. Members may
+	 * only be read from. **/
+	public static class MemberAccess extends Expression {
+		private final Expression object;
+		private final Span name;
+		private Object cachedMember;
+
+		public MemberAccess (Expression object, Span name) {
+			super(name);
+			this.object = object;
+			this.name = name;
+		}
+
+		/** Returns the object on which to access the member. **/
+		public Expression getObject () {
+			return object;
+		}
+
+		/** The name of the member. **/
+		public Span getName () {
+			return name;
+		}
+
+		/** Returns the cached member descriptor as returned by {@link Reflection#getField(Object, String)} or
+		 * {@link Reflection#getMethod(Object, String, Object...)}. See {@link #setCachedMember(Object)}. **/
+		public Object getCachedMember () {
+			return cachedMember;
+		}
+
+		/** Sets the member descriptor as returned by {@link Reflection#getField(Object, String)} or
+		 * {@link Reflection#getMethod(Object, String, Object...)} for faster member lookups. Called by {@link AstInterpreter} the
+		 * first time this node is evaluated. Subsequent evaluations can use the cached descriptor, avoiding a costly reflective
+		 * lookup. **/
+		public void setCachedMember (Object cachedMember) {
+			this.cachedMember = cachedMember;
+		}
+
+		@SuppressWarnings("rawtypes")
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			Object object = getObject().evaluate(template, context);
+			if (object == null) {
+				return null;
+			}
+
+			// special case for array.length
+			if (object.getClass().isArray() && getName().getText().equals("length")) {
+				return Array.getLength(object);
+			}
+
+			// special case for map, allows to do map.key instead of map[key]
+			if (object instanceof Map) {
+				Map map = (Map)object;
+				return map.get(getName().getText());
+			}
+
+			Object field = getCachedMember();
+			if (field != null) {
+				try {
+					return Reflection.getInstance().getFieldValue(object, field);
+				} catch (Throwable t) {
+					// fall through
+				}
+			}
+			String text = getName().getText();
+			field = Reflection.getInstance().getField(object, text);
+			if (field == null) {
+				String methodName = null;
+				if(text.length() > 1){
+					methodName = text.substring(0,1).toUpperCase() + text.substring(1);
+				}else{
+					methodName = text.toUpperCase();
+				}
+				MemberAccess access = new MemberAccess(this.object, new Span("get" + methodName));
+				MethodCall methodCall = new MethodCall(getName(),access, Collections.emptyList());
+				try {
+					return methodCall.evaluate(template, context);
+				} catch (TemplateException e) {
+					if(ExceptionUtils.indexOfThrowable(e, InvocationTargetException.class) > -1){
+						ExpressionError.error(String.format("在%s中调用方法get%s发生异常"
+								,object.getClass()
+								,methodName), getSpan(),e);
+						return null;
+					}
+					access = new MemberAccess(this.object, new Span("is" + methodName));
+					methodCall = new MethodCall(getName(),access, Collections.emptyList());
+					try {
+						return methodCall.evaluate(template, context);
+					} catch (TemplateException e1) {
+						if(ExceptionUtils.indexOfThrowable(e1, InvocationTargetException.class) > -1){
+							ExpressionError.error(String.format("在%s中调用方法is%s发生异常"
+									,object.getClass()
+									,methodName), getSpan(),e);
+							return null;
+						}
+						ExpressionError.error(String.format("在%s中找不到属性%s或者方法get%s、方法is%s"
+								,object.getClass()
+								,getName().getText()
+								,methodName
+								,methodName), getSpan());
+					}
+				}
+			}
+			setCachedMember(field);
+			return Reflection.getInstance().getFieldValue(object, field);
+		}
+	}
+
+	/** Represents a call to a top-level function. A function may either be a {@link FunctionalInterface} stored in a
+	 * {@link ExpressionTemplateContext}, or a {@link Macro} defined in a template. */
+	public static class FunctionCall extends Expression {
+		private final Expression function;
+		private final List<Expression> arguments;
+		private Object cachedFunction;
+		private final ThreadLocal<Object[]> cachedArguments;
+
+		public FunctionCall (Span span, Expression function, List<Expression> arguments) {
+			super(span);
+			this.function = function;
+			this.arguments = arguments;
+			this.cachedArguments = new ThreadLocal<Object[]>();
+		}
+
+		/** Return the expression that must evaluate to a {@link FunctionalInterface} or a {@link Macro}. **/
+		public Expression getFunction () {
+			return function;
+		}
+
+		/** Returns the list of expressions to be passed to the function as arguments. **/
+		public List<Expression> getArguments () {
+			return arguments;
+		}
+
+		/** Returns the cached "function" descriptor as returned by {@link Reflection#getMethod(Object, String, Object...)} or the
+		 * {@link Macro}. See {@link #setCachedFunction(Object)}. **/
+		public Object getCachedFunction () {
+			return cachedFunction;
+		}
+
+		/** Sets the "function" descriptor as returned by {@link Reflection#getMethod(Object, String, Object...)} for faster
+		 * lookups, or the {@link Macro} to be called. Called by {@link AstInterpreter} the first time this node is evaluated.
+		 * Subsequent evaluations can use the cached descriptor, avoiding a costly reflective lookup. **/
+		public void setCachedFunction (Object cachedFunction) {
+			this.cachedFunction = cachedFunction;
+		}
+
+		/** Returns a scratch buffer to store arguments in when calling the function in {@link AstInterpreter}. Avoids generating
+		 * garbage. **/
+		public Object[] getCachedArguments () {
+			Object[] args = cachedArguments.get();
+			if (args == null) {
+				args = new Object[arguments.size()];
+				cachedArguments.set(args);
+			}
+			return args;
+		}
+
+		/** Must be invoked when this node is done evaluating so we don't leak memory **/
+		public void clearCachedArguments () {
+			Object[] args = getCachedArguments();
+			for (int i = 0; i < args.length; i++) {
+				args[i] = null;
+			}
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			try {
+				Object[] argumentValues = getCachedArguments();
+				List<Expression> arguments = getArguments();
+				for (int i = 0, n = argumentValues.length; i < n; i++) {
+					Expression expr = arguments.get(i);
+					argumentValues[i] = expr.evaluate(template, context);
+				}
+
+				// This is a special case to handle template level macros. If a call to a macro is
+				// made, evaluating the function expression will result in an exception, as the
+				// function name can't be found in the context. Instead we need to manually check
+				// if the function expression is a VariableAccess and if so, if it can be found
+				// in the context.
+				Object function = null;
+				if (getFunction() instanceof VariableAccess) {
+					VariableAccess varAccess = (VariableAccess)getFunction();
+					function = context.get(varAccess.getVariableName().getText());
+				} else {
+					function = getFunction().evaluate(template, context);
+				}
+
+				if (function != null) {
+					Object method = getCachedFunction();
+					if (method != null) {
+						try {
+							return Reflection.getInstance().callMethod(function, method, argumentValues);
+						} catch (Throwable t) {
+							// fall through
+						}
+					}
+					method = Reflection.getInstance().getMethod(function, null, argumentValues);
+					if (method == null) {
+						ExpressionError.error("Couldn't find function.", getSpan());
+					}
+					setCachedFunction(method);
+					try {
+						return Reflection.getInstance().callMethod(function, method, argumentValues);
+					} catch (Throwable t) {
+						ExpressionError.error(t.getMessage(), getSpan(), t);
+						return null; // never reached
+					}
+				} else {
+					ExpressionError.error("Couldn't find function " + getFunction(), getSpan());
+					return null; // never reached
+				}
+			} finally {
+				clearCachedArguments();
+			}
+		}
+	}
+
+	/** Represents a call to a method of the form <code>object.method(a, b, c)</code>. **/
+	public static class MethodCall extends Expression {
+		private final MemberAccess method;
+		private final List<Expression> arguments;
+		private Object cachedMethod;
+		private final ThreadLocal<Object[]> cachedArguments;
+
+		public MethodCall (Span span, MemberAccess method, List<Expression> arguments) {
+			super(span);
+			this.method = method;
+			this.arguments = arguments;
+			this.cachedArguments = new ThreadLocal<Object[]>();
+		}
+
+		/** Returns the object on which to call the method. **/
+		public Expression getObject () {
+			return method.getObject();
+		}
+
+		/** Returns the method to call. **/
+		public MemberAccess getMethod () {
+			return method;
+		}
+
+		/** Returns the list of expressions to be passed to the function as arguments. **/
+		public List<Expression> getArguments () {
+			return arguments;
+		}
+
+		/** Returns the cached member descriptor as returned by {@link Reflection#getMethod(Object, String, Object...)}. See
+		 * {@link #setCachedMember(Object)}. **/
+		public Object getCachedMethod () {
+			return cachedMethod;
+		}
+
+		/** Sets the method descriptor as returned by {@link Reflection#getMethod(Object, String, Object...)} for faster lookups.
+		 * Called by {@link AstInterpreter} the first time this node is evaluated. Subsequent evaluations can use the cached
+		 * descriptor, avoiding a costly reflective lookup. **/
+		public void setCachedMethod (Object cachedMethod) {
+			this.cachedMethod = cachedMethod;
+		}
+
+		/** Returns a scratch buffer to store arguments in when calling the function in {@link AstInterpreter}. Avoids generating
+		 * garbage. **/
+		public Object[] getCachedArguments () {
+			Object[] args = cachedArguments.get();
+			if (args == null) {
+				args = new Object[arguments.size()];
+				cachedArguments.set(args);
+			}
+			return args;
+		}
+
+		/** Must be invoked when this node is done evaluating so we don't leak memory **/
+		public void clearCachedArguments () {
+			Object[] args = getCachedArguments();
+			for (int i = 0; i < args.length; i++) {
+				args[i] = null;
+			}
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			try {
+				Object object = getObject().evaluate(template, context);
+				if (object == null) {
+					return null;
+				}
+
+				Object[] argumentValues = getCachedArguments();
+				List<Expression> arguments = getArguments();
+				for (int i = 0, n = argumentValues.length; i < n; i++) {
+					Expression expr = arguments.get(i);
+					argumentValues[i] = expr.evaluate(template, context);
+				}
+				// Otherwise try to find a corresponding method or field pointing to a lambda.
+				Object method = getCachedMethod();
+				if (method != null) {
+					try {
+						return Reflection.getInstance().callMethod(object, method, argumentValues);
+					} catch (Throwable t) {
+						// fall through
+					}
+				}
+				
+				method = Reflection.getInstance().getMethod(object, getMethod().getName().getText(), argumentValues);
+				if (method != null) {
+					// found the method on the object, call it
+					setCachedMethod(method);
+					try {
+						return Reflection.getInstance().callMethod(object, method, argumentValues);
+					} catch (Throwable t) {
+						ExpressionError.error(t.getMessage(), getSpan(), t);
+						return null; // never reached
+					}
+				} 
+				method = Reflection.getInstance().getExtensionMethod(object, getMethod().getName().getText(), argumentValues);
+				if(method != null){
+					try {
+						int argumentLength = argumentValues == null ? 0 : argumentValues.length;
+						Object[] parameters = new Object[argumentLength + 1];
+						if(argumentLength > 0){
+							for (int i = 0; i < argumentLength; i++) {
+								parameters[i + 1] = argumentValues[i];
+							}
+						}
+						parameters[0] = object;
+						if(object.getClass().isArray()){
+							Object[] objs = new Object[Array.getLength(object)];
+							for (int i = 0,len = objs.length; i < len; i++) {
+								Array.set(objs, i, Array.get(object, i));
+							}
+							parameters[0] = objs;
+						}
+						return Reflection.getInstance().callMethod(object, method, parameters);
+					} catch (Throwable t) {
+						ExpressionError.error(t.getMessage(), getSpan(), t);
+						// fall through
+						return null;
+					}
+				}else {
+					// didn't find the method on the object, try to find a field pointing to a lambda
+					Object field = Reflection.getInstance().getField(object, getMethod().getName().getText());
+					if (field == null){
+						ExpressionError.error("在'" + object.getClass() + "'中找不到方法 " + getMethod().getName().getText() + "(" + StringUtils.join(JavaReflection.getStringTypes(argumentValues),",") + ")",
+							getSpan());
+					}
+					Object function = Reflection.getInstance().getFieldValue(object, field);
+					method = Reflection.getInstance().getMethod(function, null, argumentValues);
+					if (method == null){
+						ExpressionError.error("在'" + object.getClass() + "'中找不到方法 " + getMethod().getName().getText() + "("+ StringUtils.join(JavaReflection.getStringTypes(argumentValues),",") +")",
+								getSpan());
+					} 
+					try {
+						return Reflection.getInstance().callMethod(function, method, argumentValues);
+					} catch (Throwable t) {
+						ExpressionError.error(t.getMessage(), getSpan(), t);
+						return null; // never reached
+					}
+				}
+			} finally {
+				clearCachedArguments();
+			}
+		}
+	}
+
+	/** Represents a map literal of the form <code>{ key: value, key2: value, ... }</code> which can be nested. */
+	public static class MapLiteral extends Expression {
+		private final List<Token> keys;
+		private final List<Expression> values;
+
+		public MapLiteral (Span span, List<Token> keys, List<Expression> values) {
+			super(span);
+			this.keys = keys;
+			this.values = values;
+		}
+
+		public List<Token> getKeys () {
+			return keys;
+		}
+
+		public List<Expression> getValues () {
+			return values;
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			Map<String, Object> map = new HashMap<>();
+			for (int i = 0, n = keys.size(); i < n; i++) {
+				Object value = values.get(i).evaluate(template, context);
+				Token tokenKey = keys.get(i);
+				String key = tokenKey.getSpan().getText();
+				if(tokenKey.getType() == TokenType.StringLiteral){
+					key = (String) new StringLiteral(tokenKey.getSpan()).evaluate(template, context);
+				}else if(key != null && key.startsWith("$")){
+					Object objKey = context.get(key.substring(1));
+					if(objKey != null){
+						key = objKey.toString();
+					}else{
+						key = null;
+					}
+				}
+				map.put(key, value);
+			}
+			return map;
+		}
+	}
+
+	/** Represents a list literal of the form <code>[ value, value2, value3, ...]</code> which can be nested. */
+	public static class ListLiteral extends Expression {
+		public final List<Expression> values;
+
+		public ListLiteral (Span span, List<Expression> values) {
+			super(span);
+			this.values = values;
+		}
+
+		public List<Expression> getValues () {
+			return values;
+		}
+
+		@Override
+		public Object evaluate (ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
+			List<Object> list = new ArrayList<>();
+			for (int i = 0, n = values.size(); i < n; i++) {
+				list.add(values.get(i).evaluate(template, context));
+			}
+			return list;
+		}
+	}
+
+}

+ 129 - 0
src/main/java/com/ssssssss/expression/parsing/CharacterStream.java

@@ -0,0 +1,129 @@
+
+package com.ssssssss.expression.parsing;
+
+import javax.xml.transform.Source;
+
+/** Wraps a the content of a {@link Source} and handles traversing the contained characters. Manages a current {@link Span} via
+ * the {@link #startSpan()} and {@link #endSpan()} methods. */
+public class CharacterStream {
+	private final String source;
+	private int index = 0;
+	private final int end;
+
+	private int spanStart = 0;
+
+	public CharacterStream (String source) {
+		this(source, 0, source.length());
+	}
+
+	public CharacterStream (String source, int start, int end) {
+		if (start > end) throw new IllegalArgumentException("Start must be <= end.");
+		if (start < 0) throw new IndexOutOfBoundsException("Start must be >= 0.");
+		if (start > Math.max(0, source.length() - 1)) throw new IndexOutOfBoundsException("Start outside of string.");
+		if (end > source.length()) throw new IndexOutOfBoundsException("End outside of string.");
+
+		this.source = source;
+		this.index = start;
+		this.end = end;
+	}
+
+	/** Returns whether there are more characters in the stream **/
+	public boolean hasMore () {
+		return index < end;
+	}
+
+	/** Returns the next character without advancing the stream **/
+	public char peek () {
+		if (!hasMore()) throw new RuntimeException("No more characters in stream.");
+		return source.charAt(index++);
+	}
+
+	/** Returns the next character and advance the stream **/
+	public char consume () {
+		if (!hasMore()) throw new RuntimeException("No more characters in stream.");
+		return source.charAt(index++);
+	}
+
+	/** Matches the given needle with the next characters. Returns true if the needle is matched, false otherwise. If there's a
+	 * match and consume is true, the stream is advanced by the needle's length. */
+	public boolean match (String needle, boolean consume) {
+		int needleLength = needle.length();
+		if(needleLength + index >end){
+			return false;
+		}
+		for (int i = 0, j = index; i < needleLength; i++, j++) {
+			if (index >= end) return false;
+			if (needle.charAt(i) != source.charAt(j)) return false;
+		}
+		if (consume) index += needleLength;
+		return true;
+	}
+
+	/** Returns whether the next character is a digit and optionally consumes it. **/
+	public boolean matchDigit (boolean consume) {
+		if (index >= end) return false;
+		char c = source.charAt(index);
+		if (Character.isDigit(c)) {
+			if (consume) index++;
+			return true;
+		}
+		return false;
+	}
+
+	/** Returns whether the next character is the start of an identifier and optionally consumes it. Adheres to
+	 * {@link Character#isJavaIdentifierStart(char)}. **/
+	public boolean matchIdentifierStart (boolean consume) {
+		if (index >= end) return false;
+		char c = source.charAt(index);
+		if (Character.isJavaIdentifierStart(c) || c == '@') {
+			if (consume) index++;
+			return true;
+		}
+		return false;
+	}
+
+	/** Returns whether the next character is the start of an identifier and optionally consumes it. Adheres to
+	 * {@link Character#isJavaIdentifierPart(char)}. **/
+	public boolean matchIdentifierPart (boolean consume) {
+		if (index >= end) return false;
+		char c = source.charAt(index);
+		if (Character.isJavaIdentifierPart(c)) {
+			if (consume) index++;
+			return true;
+		}
+		return false;
+	}
+
+	/** Skips any number of successive whitespace characters. **/
+	public void skipWhiteSpace () {
+		while (true) {
+			if (index >= end) return;
+			char c = source.charAt(index);
+			if (c == ' ' || c == '\n' || c == '\r' || c == '\t') {
+				index++;
+				continue;
+			} else {
+				break;
+			}
+		}
+	}
+
+	/** Start a new Span at the current stream position. Call {@link #endSpan()} to complete the span. **/
+	public void startSpan () {
+		spanStart = index;
+	}
+
+	/** Completes the span started with {@link #startSpan()} at the current stream position. **/
+	public Span endSpan () {
+		return new Span(source, spanStart, index);
+	}
+
+	public boolean isSpanEmpty () {
+		return spanStart == this.index;
+	}
+
+	/** Returns the current character position in the stream. **/
+	public int getPosition () {
+		return index;
+	}
+}

+ 218 - 0
src/main/java/com/ssssssss/expression/parsing/Parser.java

@@ -0,0 +1,218 @@
+package com.ssssssss.expression.parsing;
+
+
+import com.ssssssss.expression.ExpressionError;
+import com.ssssssss.expression.ExpressionTemplate;
+import com.ssssssss.expression.parsing.Ast.*;
+
+import javax.xml.transform.Source;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/** Parses a {@link Source} into a {@link ExpressionTemplate}. The implementation is a simple recursive descent parser with a lookahead of
+ * 1. **/
+public class Parser {
+
+	/** Parses a {@link Source} into a {@link ExpressionTemplate}. **/
+	public static List<Node> parse (String source) {
+		List<Node> nodes = new ArrayList<Node>();
+		TokenStream stream = new TokenStream(new Tokenizer().tokenize(source));
+		while (stream.hasMore()) {
+			nodes.add(parseStatement(stream));
+		}
+		return nodes;
+	}
+
+	/** Parse a statement, which may either be a text block, if statement, for statement, while statement, macro definition,
+	 * include statement or an expression. **/
+	private static Node parseStatement (TokenStream tokens) {
+		Node result = null;
+
+		if (tokens.match(TokenType.TextBlock, false))
+			result = new Text(tokens.consume().getSpan());
+		else
+			result = parseExpression(tokens);
+
+		// consume semi-colons as statement delimiters
+		while (tokens.match(";", true))
+			;
+
+		return result;
+	}
+
+
+	private static Expression parseExpression (TokenStream stream) {
+		return parseTernaryOperator(stream);
+	}
+
+	private static Expression parseTernaryOperator (TokenStream stream) {
+		Expression condition = parseBinaryOperator(stream, 0);
+		if (stream.match(TokenType.Questionmark, true)) {
+			Expression trueExpression = parseTernaryOperator(stream);
+			stream.expect(TokenType.Colon);
+			Expression falseExpression = parseTernaryOperator(stream);
+			return new TernaryOperation(condition, trueExpression, falseExpression);
+		} else {
+			return condition;
+		}
+	}
+
+	private static final TokenType[][] binaryOperatorPrecedence = new TokenType[][] {new TokenType[] {TokenType.Assignment},
+		new TokenType[] {TokenType.Or, TokenType.And, TokenType.Xor}, new TokenType[] {TokenType.Equal, TokenType.NotEqual},
+		new TokenType[] {TokenType.Less, TokenType.LessEqual, TokenType.Greater, TokenType.GreaterEqual}, new TokenType[] {TokenType.Plus, TokenType.Minus},
+		new TokenType[] {TokenType.ForwardSlash, TokenType.Asterisk, TokenType.Percentage}};
+
+	private static Expression parseBinaryOperator (TokenStream stream, int level) {
+		int nextLevel = level + 1;
+		Expression left = nextLevel == binaryOperatorPrecedence.length ? parseUnaryOperator(stream) : parseBinaryOperator(stream, nextLevel);
+
+		TokenType[] operators = binaryOperatorPrecedence[level];
+		while (stream.hasMore() && stream.match(false, operators)) {
+			Token operator = stream.consume();
+			Expression right = nextLevel == binaryOperatorPrecedence.length ? parseUnaryOperator(stream) : parseBinaryOperator(stream, nextLevel);
+			left = new BinaryOperation(left, operator, right);
+		}
+
+		return left;
+	}
+
+	private static final TokenType[] unaryOperators = new TokenType[] {TokenType.Not, TokenType.Plus, TokenType.Minus};
+
+	private static Expression parseUnaryOperator (TokenStream stream) {
+		if (stream.match(false, unaryOperators)) {
+			return new UnaryOperation(stream.consume(), parseUnaryOperator(stream));
+		} else {
+			if (stream.match(TokenType.LeftParantheses, true)) {
+				Expression expression = parseExpression(stream);
+				stream.expect(TokenType.RightParantheses);
+				return expression;
+			} else {
+				return parseAccessOrCallOrLiteral(stream);
+			}
+		}
+	}
+
+	private static Expression parseAccessOrCallOrLiteral (TokenStream stream) {
+		if (stream.match(TokenType.Identifier, false)) {
+			return parseAccessOrCall(stream,TokenType.Identifier);
+		} else if (stream.match(TokenType.LeftCurly, false)) {
+			return parseMapLiteral(stream);
+		} else if (stream.match(TokenType.LeftBracket, false)) {
+			return parseListLiteral(stream);
+		} else if (stream.match(TokenType.StringLiteral, false)) {
+			if(stream.hasNext()){
+				if(stream.next().getType() == TokenType.Period){
+					stream.prev();
+					return parseAccessOrCall(stream,TokenType.StringLiteral);
+				}
+				stream.prev();
+			}
+			
+			return new StringLiteral(stream.expect(TokenType.StringLiteral).getSpan());
+		} else if (stream.match(TokenType.BooleanLiteral, false)) {
+			return new BooleanLiteral(stream.expect(TokenType.BooleanLiteral).getSpan());
+		} else if (stream.match(TokenType.DoubleLiteral, false)) {
+			return new DoubleLiteral(stream.expect(TokenType.DoubleLiteral).getSpan());
+		} else if (stream.match(TokenType.FloatLiteral, false)) {
+			return new FloatLiteral(stream.expect(TokenType.FloatLiteral).getSpan());
+		} else if (stream.match(TokenType.ByteLiteral, false)) {
+			return new ByteLiteral(stream.expect(TokenType.ByteLiteral).getSpan());
+		} else if (stream.match(TokenType.ShortLiteral, false)) {
+			return new ShortLiteral(stream.expect(TokenType.ShortLiteral).getSpan());
+		} else if (stream.match(TokenType.IntegerLiteral, false)) {
+			return new IntegerLiteral(stream.expect(TokenType.IntegerLiteral).getSpan());
+		} else if (stream.match(TokenType.LongLiteral, false)) {
+			return new LongLiteral(stream.expect(TokenType.LongLiteral).getSpan());
+		} else if (stream.match(TokenType.CharacterLiteral, false)) {
+			return new CharacterLiteral(stream.expect(TokenType.CharacterLiteral).getSpan());
+		} else if (stream.match(TokenType.NullLiteral, false)) {
+			return new NullLiteral(stream.expect(TokenType.NullLiteral).getSpan());
+		} else {
+			ExpressionError.error("Expected a variable, field, map, array, function or method call, or literal.", stream);
+			return null; // not reached
+		}
+	}
+
+	private static Expression parseMapLiteral (TokenStream stream) {
+		Span openCurly = stream.expect(TokenType.LeftCurly).getSpan();
+
+		List<Token> keys = new ArrayList<>();
+		List<Expression> values = new ArrayList<>();
+		while (stream.hasMore() && !stream.match("}", false)) {
+			if(stream.match(TokenType.StringLiteral, false)){
+				keys.add(stream.expect(TokenType.StringLiteral));
+			}else{
+				keys.add(stream.expect(TokenType.Identifier));
+			}
+			
+			stream.expect(":");
+			values.add(parseExpression(stream));
+			if (!stream.match("}", false)) stream.expect(TokenType.Comma);
+		}
+		Span closeCurly = stream.expect("}").getSpan();
+		return new MapLiteral(new Span(openCurly, closeCurly), keys, values);
+	}
+
+	private static Expression parseListLiteral (TokenStream stream) {
+		Span openBracket = stream.expect(TokenType.LeftBracket).getSpan();
+
+		List<Expression> values = new ArrayList<>();
+		while (stream.hasMore() && !stream.match(TokenType.RightBracket, false)) {
+			values.add(parseExpression(stream));
+			if (!stream.match(TokenType.RightBracket, false)) stream.expect(TokenType.Comma);
+		}
+
+		Span closeBracket = stream.expect(TokenType.RightBracket).getSpan();
+		return new ListLiteral(new Span(openBracket, closeBracket), values);
+	}
+
+	private static Expression parseAccessOrCall (TokenStream stream,TokenType tokenType) {
+		//Span identifier = stream.expect(TokenType.Identifier);
+		//Expression result = new VariableAccess(identifier);
+		Span identifier = stream.expect(tokenType).getSpan();
+		Expression result = tokenType == TokenType.StringLiteral ? new StringLiteral(identifier) :new VariableAccess(identifier);
+
+		while (stream.hasMore() && stream.match(false, TokenType.LeftParantheses, TokenType.LeftBracket, TokenType.Period)) {
+
+			// function or method call
+			if (stream.match(TokenType.LeftParantheses, false)) {
+				List<Expression> arguments = parseArguments(stream);
+				Span closingSpan = stream.expect(TokenType.RightParantheses).getSpan();
+				if (result instanceof VariableAccess || result instanceof MapOrArrayAccess)
+					result = new FunctionCall(new Span(result.getSpan(), closingSpan), result, arguments);
+				else if (result instanceof MemberAccess) {
+					result = new MethodCall(new Span(result.getSpan(), closingSpan), (MemberAccess)result, arguments);
+				} else {
+					ExpressionError.error("Expected a variable, field or method.", stream);
+				}
+			}
+
+			// map or array access
+			else if (stream.match(TokenType.LeftBracket, true)) {
+				Expression keyOrIndex = parseExpression(stream);
+				Span closingSpan = stream.expect(TokenType.RightBracket).getSpan();
+				result = new MapOrArrayAccess(new Span(result.getSpan(), closingSpan), result, keyOrIndex);
+			}
+
+			// field or method access
+			else if (stream.match(TokenType.Period, true)) {
+				identifier = stream.expect(TokenType.Identifier).getSpan();
+				result = new MemberAccess(result, identifier);
+			}
+		}
+
+		return result;
+	}
+
+	/** Does not consume the closing parentheses. **/
+	private static List<Expression> parseArguments (TokenStream stream) {
+		stream.expect(TokenType.LeftParantheses);
+		List<Expression> arguments = new ArrayList<Expression>();
+		while (stream.hasMore() && !stream.match(TokenType.RightParantheses, false)) {
+			arguments.add(parseExpression(stream));
+			if (!stream.match(TokenType.RightParantheses, false)) stream.expect(TokenType.Comma);
+		}
+		return arguments;
+	}
+}

+ 145 - 0
src/main/java/com/ssssssss/expression/parsing/Span.java

@@ -0,0 +1,145 @@
+
+package com.ssssssss.expression.parsing;
+
+/** A span within a source string denoted by start and end index, with the latter being exclusive. */
+public class Span {
+	/** the source string this span refers to **/
+	private final String source;
+
+	/** start index in source string, starting at 0 **/
+	private int start;
+
+	/** end index in source string, exclusive, starting at 0 **/
+	private int end;
+
+	/** Cached String instance to reduce pressure on GC **/
+	private final String cachedText;
+
+	public Span (String source) {
+		this(source, 0, source.length());
+	}
+
+	public Span (String source, int start, int end) {
+		if (start > end) throw new IllegalArgumentException("Start must be <= end.");
+		if (start < 0) throw new IndexOutOfBoundsException("Start must be >= 0.");
+		if (start > source.length() - 1) 
+			throw new IndexOutOfBoundsException("Start outside of string.");
+		if (end >source.length()) throw new IndexOutOfBoundsException("End outside of string.");
+
+		this.source = source;
+		this.start = start;
+		this.end = end;
+		this.cachedText = source.substring(start, end);
+	}
+
+	public Span (Span start, Span end) {
+		if (!start.source.equals(end.source)) throw new IllegalArgumentException("The two spans do not reference the same source.");
+		if (start.start > end.end) throw new IllegalArgumentException("Start must be <= end.");
+		if (start.start < 0) throw new IndexOutOfBoundsException("Start must be >= 0.");
+		if (start.start > start.source.length() - 1) throw new IndexOutOfBoundsException("Start outside of string.");
+		if (end.end > start.source.length()) throw new IndexOutOfBoundsException("End outside of string.");
+
+		this.source = start.source;
+		this.start = start.start;
+		this.end = end.end;
+		this.cachedText = source.substring(this.start, this.end);
+	}
+
+	/** Returns the text referenced by this span **/
+	public String getText () {
+		return cachedText;
+	}
+
+	/** Returns the index of the first character of this span. **/
+	public int getStart () {
+		return start;
+	}
+
+	/** Returns the index of the last character of this span plus 1. **/
+	public int getEnd () {
+		return end;
+	}
+
+	/** Returns the source string this span references. **/
+	public String getSource () {
+		return source;
+	}
+
+	@Override
+	public String toString () {
+		return "Span [text=" + getText() + ", start=" + start + ", end=" + end + "]";
+	}
+
+	/** Returns the line this span is on. Does not return a correct result for spans across multiple lines. **/
+	public Line getLine () {
+		int lineStart = start;
+		while (true) {
+			if (lineStart < 0) break;
+			char c = source.charAt(lineStart);
+			if (c == '\n') {
+				lineStart = lineStart + 1;
+				break;
+			}
+			lineStart--;
+		}
+		if (lineStart < 0) lineStart = 0;
+
+		int lineEnd = end;
+		while (true) {
+			if (lineEnd > source.length() - 1) break;
+			char c = source.charAt(lineEnd);
+			if (c == '\n') {
+				break;
+			}
+			lineEnd++;
+		}
+
+		int lineNumber = 0;
+		int idx = lineStart;
+		while (idx > 0) {
+			char c = source.charAt(idx);
+			if (c == '\n') {
+				lineNumber++;
+			}
+			idx--;
+		}
+		lineNumber++;
+
+		return new Line(source, lineStart, lineEnd, lineNumber);
+	}
+
+	/** A line within a Source **/
+	public static class Line {
+		private final String source;
+		private final int start;
+		private final int end;
+		private final int lineNumber;
+
+		public Line (String source, int start, int end, int lineNumber) {
+			this.source = source;
+			this.start = start;
+			this.end = end;
+			this.lineNumber = lineNumber;
+		}
+
+		public String getSource () {
+			return source;
+		}
+
+		public int getStart () {
+			return start;
+		}
+
+		public int getEnd () {
+			return end;
+		}
+
+		public int getLineNumber () {
+			return lineNumber;
+		}
+
+		public String getText () {
+			return source.substring(start, end);
+		}
+	}
+}

+ 30 - 0
src/main/java/com/ssssssss/expression/parsing/Token.java

@@ -0,0 +1,30 @@
+
+package com.ssssssss.expression.parsing;
+
+/** A token produced by the {@link Tokenizer}. */
+public class Token {
+	private final TokenType type;
+	private final Span span;
+
+	public Token (TokenType type, Span span) {
+		this.type = type;
+		this.span = span;
+	}
+
+	public TokenType getType () {
+		return type;
+	}
+
+	public Span getSpan () {
+		return span;
+	}
+
+	public String getText () {
+		return span.getText();
+	}
+
+	@Override
+	public String toString () {
+		return "Token [type=" + type + ", span=" + span + "]";
+	}
+}

+ 131 - 0
src/main/java/com/ssssssss/expression/parsing/TokenStream.java

@@ -0,0 +1,131 @@
+
+package com.ssssssss.expression.parsing;
+
+import com.ssssssss.expression.ExpressionError;
+
+import javax.xml.transform.Source;
+import java.util.List;
+
+
+/** Iterates over a list of {@link Token} instances, provides methods to match expected tokens and throw errors in case of a
+ * mismatch. */
+public class TokenStream {
+	private final List<Token> tokens;
+	private int index;
+	private final int end;
+
+	public TokenStream (List<Token> tokens) {
+		this.tokens = tokens;
+		this.index = 0;
+		this.end = tokens.size();
+	}
+
+	/** Returns whether there are more tokens in the stream. **/
+	public boolean hasMore () {
+		return index < end;
+	}
+	
+	public boolean hasNext(){
+		return index + 1 < end;
+	}
+	
+	public boolean hasPrev(){
+		return index > 0;
+	}
+
+	/** Consumes the next token and returns it. **/
+	public Token consume () {
+		if (!hasMore()) throw new RuntimeException("Reached the end of the source.");
+		return tokens.get(index++);
+	}
+	
+	public Token next(){
+		if (!hasMore()) throw new RuntimeException("Reached the end of the source.");
+		return tokens.get(++index);
+	}
+	
+	public Token prev(){
+		if(index == 0){
+			throw new RuntimeException("Reached the end of the source.");
+		}
+		return tokens.get(--index);
+	}
+
+	/** Checks if the next token has the give type and optionally consumes, or throws an error if the next token did not match the
+	 * type. */
+	public Token expect (TokenType type) {
+		boolean result = match(type, true);
+		if (!result) {
+			Token token = index < tokens.size() ? tokens.get(index) : null;
+			Span span = token != null ? token.getSpan() : null;
+			if (span == null)
+				ExpressionError.error("Expected '" + type.getError() + "', but reached the end of the source.", this);
+			else
+				ExpressionError.error("Expected '" + type.getError() + "', but got '" + token.getText() + "'", span);
+			return null; // never reached
+		} else {
+			return tokens.get(index - 1);
+		}
+	}
+
+	/** Checks if the next token matches the given text and optionally consumes, or throws an error if the next token did not match
+	 * the text. */
+	public Token expect (String text) {
+		boolean result = match(text, true);
+		if (!result) {
+			Token token = index < tokens.size() ? tokens.get(index) : null;
+			Span span = token != null ? token.getSpan() : null;
+			if (span == null)
+				ExpressionError.error("Expected '" + text + "', but reached the end of the source.", this);
+			else
+				ExpressionError.error("Expected '" + text + "', but got '" + token.getText() + "'", span);
+			return null; // never reached
+		} else {
+			return tokens.get(index - 1);
+		}
+	}
+
+	/** Matches and optionally consumes the next token in case of a match. Returns whether the token matched. */
+	public boolean match (TokenType type, boolean consume) {
+		if (index >= end) return false;
+		if (tokens.get(index).getType() == type) {
+			if (consume) index++;
+			return true;
+		}
+		return false;
+	}
+
+	/** Matches and optionally consumes the next token in case of a match. Returns whether the token matched. */
+	public boolean match (String text, boolean consume) {
+		if (index >= end) return false;
+		if (tokens.get(index).getText().equals(text)) {
+			if (consume) index++;
+			return true;
+		}
+		return false;
+	}
+
+	/** Matches any of the token types and optionally consumes the next token in case of a match. Returns whether the token
+	 * matched. */
+	public boolean match (boolean consume, TokenType... types) {
+		for (TokenType type : types) {
+			if (match(type, consume)) return true;
+		}
+		return false;
+	}
+
+	/** Matches any of the token texts and optionally consumes the next token in case of a match. Returns whether the token
+	 * matched. */
+	public boolean match (boolean consume, String... tokenTexts) {
+		for (String text : tokenTexts) {
+			if (match(text, consume)) return true;
+		}
+		return false;
+	}
+
+	/** Returns the {@link Source} this stream wraps. */
+	public String getSource () {
+		if (tokens.size() == 0) return null;
+		return tokens.get(0).getSpan().getSource();
+	}
+}

+ 101 - 0
src/main/java/com/ssssssss/expression/parsing/TokenType.java

@@ -0,0 +1,101 @@
+
+package com.ssssssss.expression.parsing;
+
+import java.util.Arrays;
+import java.util.Comparator;
+
+/** Enumeration of token types. A token type consists of a representation for error messages, and may optionally specify a literal
+ * to be used by the {@link CharacterStream} to recognize the token. Token types are sorted by their literal length to easy
+ * matching of token types with common prefixes, e.g. "<" and "<=". Token types with longer literals are matched first. */
+public enum TokenType {
+	// @off
+	TextBlock("a text block"),
+	Period(".", "."),
+	Comma(",", ","),
+	Semicolon(";", ";"),
+	Colon(":", ":"),
+	Plus("+", "+"),
+	Minus("-", "-"),
+	Asterisk("*", "*"),
+	ForwardSlash("/", "/"),
+	PostSlash("\\", "\\"),
+	Percentage("%", "%"),
+	LeftParantheses("(", ")"),
+	RightParantheses(")", ")"),
+	LeftBracket("[", "["),
+	RightBracket("]", "]"),
+	LeftCurly("{", "{"),
+	RightCurly("}"), // special treatment!
+	Less("<", "<"),
+	Greater(">", ">"),
+	LessEqual("<=", "<="),
+	GreaterEqual(">=", ">="),
+	Equal("==", "=="),
+	NotEqual("!=", "!="),
+	Assignment("=", "="),
+	And("&&", "&&"),
+	Or("||", "||"),
+	Xor("^", "^"),
+	Not("!", "!"),
+	Questionmark("?", "?"),
+	DoubleQuote("\"", "\""),
+	SingleQuote("'", "'"),
+	BooleanLiteral("true or false"),
+	DoubleLiteral("a double floating point number"),
+	FloatLiteral("a floating point number"),
+	LongLiteral("a long integer number"),
+	IntegerLiteral("an integer number"),
+	ShortLiteral("a short integer number"),
+	ByteLiteral("a byte integer number"),
+	CharacterLiteral("a character"),
+	StringLiteral("a string"),
+	NullLiteral("null"),
+	Identifier("an identifier");
+	// @on
+
+	private static TokenType[] values;
+
+	static {
+		// Sort the token types by their literal length. The character stream uses this
+		// this order to match tokens with the longest length first.
+		values = TokenType.values();
+		Arrays.sort(values, new Comparator<TokenType>() {
+			@Override
+			public int compare (TokenType o1, TokenType o2) {
+				if (o1.literal == null && o2.literal == null) return 0;
+				if (o1.literal == null && o2.literal != null) return 1;
+				if (o1.literal != null && o2.literal == null) return -1;
+				return o2.literal.length() - o1.literal.length();
+			}
+		});
+	}
+
+	private final String literal;
+	private final String error;
+
+	TokenType (String error) {
+		this.literal = null;
+		this.error = error;
+	}
+
+	TokenType (String literal, String error) {
+		this.literal = literal;
+		this.error = error;
+	}
+
+	/** The literal to match, may be null. **/
+	public String getLiteral () {
+		return literal;
+	}
+
+	/** The error string to use when reporting this token type in an error message. **/
+	public String getError () {
+		return error;
+	}
+
+	/** Returns an array of token types, sorted in descending order based on their literal length. This is used by the
+	 * {@link CharacterStream} to match token types with the longest literal first. E.g. "<=" will be matched before "<". **/
+	public static TokenType[] getSortedValues () {
+		return values;
+	}
+}

+ 191 - 0
src/main/java/com/ssssssss/expression/parsing/Tokenizer.java

@@ -0,0 +1,191 @@
+
+package com.ssssssss.expression.parsing;
+
+import com.ssssssss.expression.ExpressionError;
+import com.ssssssss.expression.ExpressionError.StringLiteralException;
+import com.ssssssss.expression.ExpressionError.TemplateException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class Tokenizer {
+
+	/** Tokenizes the source into tokens with a {@link TokenType}. Text blocks not enclosed in {{ }} are returned as a single token
+	 * of type {@link TokenType.TextBlock}. {{ and }} are not returned as individual tokens. See {@link TokenType} for the list of
+	 * tokens this tokenizer understands. */
+	public List<Token> tokenize (String source) {
+		List<Token> tokens = new ArrayList<Token>();
+		if (source.length() == 0) return tokens;
+		CharacterStream stream = new CharacterStream(source);
+		stream.startSpan();
+
+		RuntimeException re = null;
+		while (stream.hasMore()) {
+			if (stream.match("${", false)) {
+				if (!stream.isSpanEmpty()) tokens.add(new Token(TokenType.TextBlock, stream.endSpan()));
+				stream.startSpan();
+				boolean isContinue = false;
+				do{
+					while (!stream.match("}", true)) {
+						if (!stream.hasMore()) ExpressionError.error("Did not find closing }.", stream.endSpan());
+						stream.consume();
+					}
+					try{
+						tokens.addAll(tokenizeCodeSpan(stream.endSpan()));
+						isContinue = false;
+						re = null;
+					}catch(TemplateException e){
+						re = e;
+						if(e.getCause() != null || stream.hasMore()){
+							isContinue = true;
+						}
+					}
+					
+				}while(isContinue);
+				if(re != null){
+					throw re;
+				}
+				stream.startSpan();
+			} else {
+				stream.consume();
+			}
+		}
+		if (!stream.isSpanEmpty()) tokens.add(new Token(TokenType.TextBlock, stream.endSpan()));
+		return tokens;
+	}
+
+	private static List<Token> tokenizeCodeSpan (Span span) {
+		String source = span.getSource();
+		CharacterStream stream = new CharacterStream(source, span.getStart(), span.getEnd());
+		List<Token> tokens = new ArrayList<Token>();
+
+		// match opening tag and throw it away
+		if (!stream.match("${", true)) ExpressionError.error("Expected ${", new Span(source, stream.getPosition(), stream.getPosition() + 1));
+		int leftCount = 0;
+		int rightCount = 0;
+		outer:
+		while (stream.hasMore()) {
+			// skip whitespace
+			stream.skipWhiteSpace();
+
+			// Number literal, both integers and floats. Number literals may be suffixed by a type identifier.
+			if (stream.matchDigit(false)) {
+				TokenType type = TokenType.IntegerLiteral;
+				stream.startSpan();
+				while (stream.matchDigit(true))
+					;
+				if (stream.match(TokenType.Period.getLiteral(), true)) {
+					type = TokenType.FloatLiteral;
+					while (stream.matchDigit(true))
+						;
+				}
+				if (stream.match("b", true) || stream.match("B", true)) {
+					if (type == TokenType.FloatLiteral) ExpressionError.error("Byte literal can not have a decimal point.", stream.endSpan());
+					type = TokenType.ByteLiteral;
+				} else if (stream.match("s", true) || stream.match("S", true)) {
+					if (type == TokenType.FloatLiteral) ExpressionError.error("Short literal can not have a decimal point.", stream.endSpan());
+					type = TokenType.ShortLiteral;
+				} else if (stream.match("l", true) || stream.match("L", true)) {
+					if (type == TokenType.FloatLiteral) ExpressionError.error("Long literal can not have a decimal point.", stream.endSpan());
+					type = TokenType.LongLiteral;
+				} else if (stream.match("f", true) || stream.match("F", true)) {
+					type = TokenType.FloatLiteral;
+				} else if (stream.match("d", true) || stream.match("D", true)) {
+					type = TokenType.DoubleLiteral;
+				}
+				Span numberSpan = stream.endSpan();
+				tokens.add(new Token(type, numberSpan));
+				continue;
+			}
+
+			// String literal
+			if (stream.match(TokenType.SingleQuote.getLiteral(), true)) {
+				stream.startSpan();
+				boolean matchedEndQuote = false;
+				while (stream.hasMore()) {
+					// Note: escape sequences like \n are parsed in StringLiteral
+					if (stream.match("\\", true)) {
+						stream.consume();
+					}
+					if (stream.match(TokenType.SingleQuote.getLiteral(), true)) {
+						matchedEndQuote = true;
+						break;
+					}
+					stream.consume();
+				}
+				if (!matchedEndQuote) ExpressionError.error("字符串没有结束符\'", stream.endSpan(),new StringLiteralException());
+				Span stringSpan = stream.endSpan();
+				stringSpan = new Span(stringSpan.getSource(), stringSpan.getStart() - 1, stringSpan.getEnd());
+				tokens.add(new Token(TokenType.StringLiteral, stringSpan));
+				continue;
+			}
+
+			// String literal
+			if (stream.match(TokenType.DoubleQuote.getLiteral(), true)) {
+				stream.startSpan();
+				boolean matchedEndQuote = false;
+				while (stream.hasMore()) {
+					// Note: escape sequences like \n are parsed in StringLiteral
+					if (stream.match("\\", true)) {
+						stream.consume();
+					}
+					if (stream.match(TokenType.DoubleQuote.getLiteral(), true)) {
+						matchedEndQuote = true;
+						break;
+					}
+					stream.consume();
+				}
+				if (!matchedEndQuote) ExpressionError.error("字符串没有结束符\"", stream.endSpan(),new StringLiteralException());
+				Span stringSpan = stream.endSpan();
+				stringSpan = new Span(stringSpan.getSource(), stringSpan.getStart() - 1, stringSpan.getEnd());
+				tokens.add(new Token(TokenType.StringLiteral, stringSpan));
+				continue;
+			}
+			
+			// Identifier, keyword, boolean literal, or null literal
+			if (stream.matchIdentifierStart(true)) {
+				stream.startSpan();
+				while (stream.matchIdentifierPart(true))
+					;
+				Span identifierSpan = stream.endSpan();
+				identifierSpan = new Span(identifierSpan.getSource(), identifierSpan.getStart() - 1, identifierSpan.getEnd());
+
+				if (identifierSpan.getText().equals("true") || identifierSpan.getText().equals("false")) {
+					tokens.add(new Token(TokenType.BooleanLiteral, identifierSpan));
+				} else if (identifierSpan.getText().equals("null")) {
+					tokens.add(new Token(TokenType.NullLiteral, identifierSpan));
+				} else {
+					tokens.add(new Token(TokenType.Identifier, identifierSpan));
+				}
+				continue;
+			}
+
+			// Simple tokens
+			for (TokenType t : TokenType.getSortedValues()) {
+				if (t.getLiteral() != null) {
+					if (stream.match(t.getLiteral(), true)) {
+						if(t == TokenType.LeftCurly){
+							leftCount ++;
+						}
+						tokens.add(new Token(t, new Span(source, stream.getPosition() - t.getLiteral().length(), stream.getPosition())));
+						continue outer;
+					}
+				}
+			}
+			if(leftCount!=rightCount&&stream.match("}", true)){
+				rightCount++;
+				tokens.add(new Token(TokenType.RightCurly, new Span(source, stream.getPosition() - 1, stream.getPosition())));
+				continue outer;
+			}
+			// match closing tag
+			if (stream.match("}", false)) break;
+
+			ExpressionError.error("Unknown token", new Span(source, stream.getPosition(), stream.getPosition() + 1));
+		}
+
+		// code spans must end with }
+		if (!stream.match("}", true)) ExpressionError.error("Expected }", new Span(source, stream.getPosition(), stream.getPosition() + 1));
+		return tokens;
+	}
+}

+ 4 - 0
src/main/java/com/ssssssss/handler/CookieContext.java

@@ -0,0 +1,4 @@
+package com.ssssssss.handler;
+
+public class CookieContext {
+}

+ 18 - 0
src/main/java/com/ssssssss/handler/HeaderContext.java

@@ -0,0 +1,18 @@
+package com.ssssssss.handler;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+
+public class HeaderContext extends HashMap<String,Object> {
+
+    private HttpServletRequest request;
+
+    public HeaderContext(HttpServletRequest request) {
+        this.request = request;
+    }
+
+    @Override
+    public Object get(Object key){
+        return request.getHeader(key.toString());
+    }
+}

+ 20 - 0
src/main/java/com/ssssssss/handler/RequestContext.java

@@ -0,0 +1,20 @@
+package com.ssssssss.handler;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Enumeration;
+import java.util.HashMap;
+
+public class RequestContext extends HashMap<String,Object> {
+
+    private HttpServletRequest request;
+
+    public RequestContext(HttpServletRequest request) {
+        this.request = request;
+        Enumeration<String> parameterNames = request.getParameterNames();
+        while (parameterNames.hasMoreElements()){
+            String key = parameterNames.nextElement();
+            put(key,request.getParameter(key));
+        }
+        put("header",new HeaderContext(request));
+    }
+}

+ 97 - 0
src/main/java/com/ssssssss/handler/RequestHandler.java

@@ -0,0 +1,97 @@
+package com.ssssssss.handler;
+
+import com.ssssssss.expression.DefaultExpressionEngine;
+import com.ssssssss.mapping.SqlMappingManager;
+import com.ssssssss.model.SqlMapping;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Component
+public class RequestHandler {
+
+    @Autowired
+    private SqlMappingManager manager;
+
+    @Autowired
+    private JdbcTemplate template;
+
+    private Pattern optionRegx = Pattern.compile("\\[(.*?)\\]");
+
+    private Pattern expressionRegx = Pattern.compile("#\\{(.*?)\\}");
+
+    @ResponseBody
+    public Object request(HttpServletRequest request) {
+        String mapping = request.getServletPath();
+        if(mapping.endsWith("/")){
+            mapping = mapping.substring(0,mapping.length() - 1);
+        }
+        SqlMapping sqlMapping = manager.getSqlMapping(mapping);
+        return invoke(sqlMapping,new RequestContext(request));
+    }
+
+    private Object invoke(SqlMapping sqlMapping, Map<String,Object> context){
+        String sql = sqlMapping.getSql();
+        List<String> options = extract(sql, optionRegx);
+        DefaultExpressionEngine engine = new DefaultExpressionEngine();
+        List<Object> params = new ArrayList<>();
+        Map<String,Object> cachedValues = new HashMap<>();
+        for (String option : options) {
+            List<String> expressions = extract(option, expressionRegx);
+            boolean hasNull = false;
+            for (String expression : expressions) {
+                Object val = engine.execute("${" + expression + "}", context);
+                cachedValues.put(expression,val);
+                if(val == null){
+                    hasNull = true;
+                    break;
+                }
+            }
+            sql = sql.replaceFirst(optionRegx.pattern() ,hasNull ? "":"$1");
+        }
+        List<String> expressions = extract(sql, expressionRegx);
+        for (String expression : expressions) {
+            Object val = cachedValues.get(expression);
+            if(!cachedValues.containsKey(expression)){
+                val = engine.execute("${" + expression + "}", context);
+            }
+            sql = sql.replaceFirst(expressionRegx.pattern(),"?");
+            params.add(val);
+        }
+        System.out.println(sql);
+        System.out.println(params);
+        if(params.size() == 0){
+            return template.queryForList(sql);
+        }
+        return template.queryForList(sql,params.toArray());
+    }
+
+    public List<String> extract(String sql, Pattern pattern){
+        Matcher matcher = pattern.matcher(sql);
+        List<String> results = new ArrayList<>();
+        while(matcher.find()){
+            results.add(matcher.group(1));
+        }
+        return results;
+    }
+
+    public static void main(String[] args) {
+        String sql = "select * from sys_user where user_id = #{userId} [and user_name = #{userName.substring(2)}] and abc = #{abc}";
+        SqlMapping mapping = new SqlMapping(sql,null,null);
+        new RequestHandler().invoke(mapping,new HashMap<String,Object>(){
+            {
+                put("userId","123");
+               // put("userName","abcd");
+            }
+        });
+    }
+}

+ 11 - 0
src/main/java/com/ssssssss/handler/SessionContext.java

@@ -0,0 +1,11 @@
+package com.ssssssss.handler;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import java.util.HashMap;
+
+public class SessionContext {
+
+    private HttpServletRequest request;
+
+}

+ 78 - 0
src/main/java/com/ssssssss/mapping/SqlMappingManager.java

@@ -0,0 +1,78 @@
+package com.ssssssss.mapping;
+
+import com.ssssssss.handler.RequestHandler;
+import com.ssssssss.model.SqlMapping;
+import org.apache.commons.io.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class SqlMappingManager {
+
+    private static Logger logger = LoggerFactory.getLogger(SqlMappingManager.class);
+
+    private Map<String, SqlMapping> sqlMaps = new HashMap<>();
+
+    private Method requestInvokeMethod = RequestHandler.class.getDeclaredMethod("request", HttpServletRequest.class);
+
+    @Autowired
+    private RequestHandler requestInvokeHandler;
+
+    @Autowired
+    private RequestMappingHandlerMapping requestMappingHandlerMapping;
+
+    public SqlMappingManager() throws NoSuchMethodException {
+    }
+
+
+    public void register(String directory,File file){
+        String mapping = convertToRequestMapping(directory, file.getPath());
+        try {
+            String sql = FileUtils.readFileToString(file,"UTF-8");
+            boolean hasRegisted = sqlMaps.containsKey(mapping);
+            sqlMaps.put(mapping,new SqlMapping(sql,mapping,file.getPath()));
+            if(!hasRegisted){
+                logger.info("注册{}",mapping);
+                requestMappingHandlerMapping.registerMapping(RequestMappingInfo.paths(mapping).build(),requestInvokeHandler,requestInvokeMethod);
+            }else{
+                logger.info("刷新{}",mapping);
+            }
+
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void unregister(String directory,File file){
+        String mapping = convertToRequestMapping(directory, file.getPath());
+        logger.info("取消注册{}",mapping);
+        sqlMaps.remove(mapping);
+        requestMappingHandlerMapping.unregisterMapping(RequestMappingInfo.paths(mapping).build());
+    }
+
+    public SqlMapping getSqlMapping(String mapping){
+        return sqlMaps.get(mapping);
+    }
+
+    private String convertToRequestMapping(String dir,String file){
+        String mapping = file.substring(dir.length());
+        int index = mapping.lastIndexOf(".");
+        if(index > -1){
+            mapping = mapping.substring(0,index);
+        }
+        mapping = mapping.replace("\\","/");
+        return mapping;
+    }
+
+}

+ 49 - 0
src/main/java/com/ssssssss/model/SqlMapping.java

@@ -0,0 +1,49 @@
+package com.ssssssss.model;
+
+public class SqlMapping {
+
+    /**
+     * SQL语句
+     */
+    private String sql;
+
+    /**
+     * 访问路径
+     */
+    private String path;
+
+    /**
+     * 文件路径
+     */
+    private String file;
+
+    public SqlMapping(String sql, String path, String file) {
+        this.sql = sql;
+        this.path = path;
+        this.file = file;
+    }
+
+    public String getSql() {
+        return sql;
+    }
+
+    public void setSql(String sql) {
+        this.sql = sql;
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    public void setPath(String path) {
+        this.path = path;
+    }
+
+    public String getFile() {
+        return file;
+    }
+
+    public void setFile(String file) {
+        this.file = file;
+    }
+}

+ 10 - 0
src/main/resources/application.properties

@@ -0,0 +1,10 @@
+server.port=9999
+server.servlet.context-path=/ssssssss
+
+
+spring.datasource.url=jdbc:mysql://localhost/ssssssss?useSSL=false&useUnicode=true&characterEncoding=UTF8
+spring.datasource.username=root
+spring.datasource.password=123456789
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+spring.datasource.initialSize=5
+spring.datasource.minIdle=5

+ 1 - 0
src/main/resources/ssssssss/role/list.sql

@@ -0,0 +1 @@
+select name,#{header.Host} host from sys_role where name = #{name} limit 8

+ 1 - 0
src/main/resources/ssssssss/test/select.sql

@@ -0,0 +1 @@
+select name from sys_user [where id = #{id}] limit 10