diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1cd561d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,52 @@
+# maven #
+target
+
+logs
+
+# eclipse #
+.settings
+.project
+.classpath
+.log
+
+# windows #
+Thumbs.db
+
+# Mac #
+.DS_Store
+
+# Package Files #
+*.war
+*.ear
+
+# idea #
+.idea
+*.iml
+
+plan.txt
+
+*.class
+
+# Package Files #
+*.jar
+
+
+*.bak
+*.tmp
+*.log
+/bin/
+build.sh
+integration-repo
+/build/
+
+# IDEA metadata and output dirs
+*.ipr
+*.iws
+
+/webapp/WEB-INF/classes/
+/webapp/WEB-INF/test-classes/
+/webapp/WEB-INF/target/
+
+a_little_config_pro.txt
+
+dev_plan.txt
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 44e61dd..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,191 +0,0 @@
-Apache License
-Version 2.0, January 2004
-http://www.apache.org/licenses/
-
-TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-1. Definitions.
-
-"License" shall mean the terms and conditions for use, reproduction, and
-distribution as defined by Sections 1 through 9 of this document.
-
-"Licensor" shall mean the copyright owner or entity authorized by the copyright
-owner that is granting the License.
-
-"Legal Entity" shall mean the union of the acting entity and all other entities
-that control, are controlled by, or are under common control with that entity.
-For the purposes of this definition, "control" means (i) the power, direct or
-indirect, to cause the direction or management of such entity, whether by
-contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
-outstanding shares, or (iii) beneficial ownership of such entity.
-
-"You" (or "Your") shall mean an individual or Legal Entity exercising
-permissions granted by this License.
-
-"Source" form shall mean the preferred form for making modifications, including
-but not limited to software source code, documentation source, and configuration
-files.
-
-"Object" form shall mean any form resulting from mechanical transformation or
-translation of a Source form, including but not limited to compiled object code,
-generated documentation, and conversions to other media types.
-
-"Work" shall mean the work of authorship, whether in Source or Object form, made
-available under the License, as indicated by a copyright notice that is included
-in or attached to the work (an example is provided in the Appendix below).
-
-"Derivative Works" shall mean any work, whether in Source or Object form, that
-is based on (or derived from) the Work and for which the editorial revisions,
-annotations, elaborations, or other modifications represent, as a whole, an
-original work of authorship. For the purposes of this License, Derivative Works
-shall not include works that remain separable from, or merely link (or bind by
-name) to the interfaces of, the Work and Derivative Works thereof.
-
-"Contribution" shall mean any work of authorship, including the original version
-of the Work and any modifications or additions to that Work or Derivative Works
-thereof, that is intentionally submitted to Licensor for inclusion in the Work
-by the copyright owner or by an individual or Legal Entity authorized to submit
-on behalf of the copyright owner. For the purposes of this definition,
-"submitted" means any form of electronic, verbal, or written communication sent
-to the Licensor or its representatives, including but not limited to
-communication on electronic mailing lists, source code control systems, and
-issue tracking systems that are managed by, or on behalf of, the Licensor for
-the purpose of discussing and improving the Work, but excluding communication
-that is conspicuously marked or otherwise designated in writing by the copyright
-owner as "Not a Contribution."
-
-"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
-of whom a Contribution has been received by Licensor and subsequently
-incorporated within the Work.
-
-2. Grant of Copyright License.
-
-Subject to the terms and conditions of this License, each Contributor hereby
-grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
-irrevocable copyright license to reproduce, prepare Derivative Works of,
-publicly display, publicly perform, sublicense, and distribute the Work and such
-Derivative Works in Source or Object form.
-
-3. Grant of Patent License.
-
-Subject to the terms and conditions of this License, each Contributor hereby
-grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
-irrevocable (except as stated in this section) patent license to make, have
-made, use, offer to sell, sell, import, and otherwise transfer the Work, where
-such license applies only to those patent claims licensable by such Contributor
-that are necessarily infringed by their Contribution(s) alone or by combination
-of their Contribution(s) with the Work to which such Contribution(s) was
-submitted. If You institute patent litigation against any entity (including a
-cross-claim or counterclaim in a lawsuit) alleging that the Work or a
-Contribution incorporated within the Work constitutes direct or contributory
-patent infringement, then any patent licenses granted to You under this License
-for that Work shall terminate as of the date such litigation is filed.
-
-4. Redistribution.
-
-You may reproduce and distribute copies of the Work or Derivative Works thereof
-in any medium, with or without modifications, and in Source or Object form,
-provided that You meet the following conditions:
-
-You must give any other recipients of the Work or Derivative Works a copy of
-this License; and
-You must cause any modified files to carry prominent notices stating that You
-changed the files; and
-You must retain, in the Source form of any Derivative Works that You distribute,
-all copyright, patent, trademark, and attribution notices from the Source form
-of the Work, excluding those notices that do not pertain to any part of the
-Derivative Works; and
-If the Work includes a "NOTICE" text file as part of its distribution, then any
-Derivative Works that You distribute must include a readable copy of the
-attribution notices contained within such NOTICE file, excluding those notices
-that do not pertain to any part of the Derivative Works, in at least one of the
-following places: within a NOTICE text file distributed as part of the
-Derivative Works; within the Source form or documentation, if provided along
-with the Derivative Works; or, within a display generated by the Derivative
-Works, if and wherever such third-party notices normally appear. The contents of
-the NOTICE file are for informational purposes only and do not modify the
-License. You may add Your own attribution notices within Derivative Works that
-You distribute, alongside or as an addendum to the NOTICE text from the Work,
-provided that such additional attribution notices cannot be construed as
-modifying the License.
-You may add Your own copyright statement to Your modifications and may provide
-additional or different license terms and conditions for use, reproduction, or
-distribution of Your modifications, or for any such Derivative Works as a whole,
-provided Your use, reproduction, and distribution of the Work otherwise complies
-with the conditions stated in this License.
-
-5. Submission of Contributions.
-
-Unless You explicitly state otherwise, any Contribution intentionally submitted
-for inclusion in the Work by You to the Licensor shall be under the terms and
-conditions of this License, without any additional terms or conditions.
-Notwithstanding the above, nothing herein shall supersede or modify the terms of
-any separate license agreement you may have executed with Licensor regarding
-such Contributions.
-
-6. Trademarks.
-
-This License does not grant permission to use the trade names, trademarks,
-service marks, or product names of the Licensor, except as required for
-reasonable and customary use in describing the origin of the Work and
-reproducing the content of the NOTICE file.
-
-7. Disclaimer of Warranty.
-
-Unless required by applicable law or agreed to in writing, Licensor provides the
-Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
-including, without limitation, any warranties or conditions of TITLE,
-NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
-solely responsible for determining the appropriateness of using or
-redistributing the Work and assume any risks associated with Your exercise of
-permissions under this License.
-
-8. Limitation of Liability.
-
-In no event and under no legal theory, whether in tort (including negligence),
-contract, or otherwise, unless required by applicable law (such as deliberate
-and grossly negligent acts) or agreed to in writing, shall any Contributor be
-liable to You for damages, including any direct, indirect, special, incidental,
-or consequential damages of any character arising as a result of this License or
-out of the use or inability to use the Work (including but not limited to
-damages for loss of goodwill, work stoppage, computer failure or malfunction, or
-any and all other commercial damages or losses), even if such Contributor has
-been advised of the possibility of such damages.
-
-9. Accepting Warranty or Additional Liability.
-
-While redistributing the Work or Derivative Works thereof, You may choose to
-offer, and charge a fee for, acceptance of support, warranty, indemnity, or
-other liability obligations and/or rights consistent with this License. However,
-in accepting such obligations, You may act only on Your own behalf and on Your
-sole responsibility, not on behalf of any other Contributor, and only if You
-agree to indemnify, defend, and hold each Contributor harmless for any liability
-incurred by, or claims asserted against, such Contributor by reason of your
-accepting any such warranty or additional liability.
-
-END OF TERMS AND CONDITIONS
-
-APPENDIX: How to apply the Apache License to your work
-
-To apply the Apache License to your work, attach the following boilerplate
-notice, with the fields enclosed by brackets "{}" replaced with your own
-identifying information. (Don't include the brackets!) The text should be
-enclosed in the appropriate comment syntax for the file format. We also
-recommend that a file or class name and description of purpose be included on
-the same "printed page" as the copyright notice for easier identification within
-third-party archives.
-
- Copyright 2017 JFinal
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
\ No newline at end of file
diff --git a/README.md b/README.md
index 8c0a977..55d7a5a 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,42 @@
-#enjoy
+### Enjoy
+
+Enjoy 是基于 Java 语言的极轻量极魔板引擎。极轻量级仅 171K 并且不依赖任何第三方。极简设计仅 if、for、set、define、include、render 六个核心指令,让学习成本低到极致。独创 DKFF(Dynamic Key Feature Forward) 词法分析算法与 DLRD (Double Layer Recursive Descent)语法分析算法,避免使用 javacc、antlr、jflex 生成器,令代码量少到极致。
+
+#### Enjoy 主要特点
+- 体积小,仅 167K,且不依赖于任何第三方
+- 消灭传统模板引擎中大量繁杂概念,仅六个核心指令,学习成本极低
+- 独创 DKFF 词法分析算法与 DLRD 语法分析算法,避免使用javacc、antlr
+- 功能强大,极为简单覆盖掉 freemarker、velocity 的核心功能
+- 扩展性强,支持多种扩展方式,且是唯一支持指令级扩展的模板引擎
+- 与 java 打通式设计,在模板中与 java 交互极为方便
+- 贴近 java 使用直觉,为 java 开发者量身打造
+- 回归模板引擎渲染 View 数据的本质,采用指令式设计,避免 view 层表达复杂逻辑
+
+
+#### 简单示例:
+
+**1. 在 spring 中的配置**
+
+```java
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**2.详细使用方法见 jfinal 手册**
+read me 正在补充,详细使用文档请下载 jfinal.com 官网的 jfinal 手册[http://www.jfinal.com](http://www.jfinal.com)
+
+**JFinal 官方网站:[http://www.jfinal.com](http://www.jfinal.com)**
+
+
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..af8053d
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,117 @@
+
+ 4.0.0
+ com.jfinal
+ enjoy
+ jar
+ enjoy
+ 3.2-SNAPSHOT
+ http://www.jfinal.com
+ Enjoy is a simple, light, rapid, independent, extensible Java Template Engine.
+
+
+ UTF-8
+ UTF-8
+
+
+
+ Git Issue
+ http://git.oschina.net/jfinal/enjoy/issues
+
+
+
+ The Apache Software License, Version 2.0
+ http://apache.org/licenses/LICENSE-2.0.txt
+
+
+
+
+ jfinal
+ James
+ jfinal@126.com
+ http://jfinal.com/user/1
+
+
+
+ scm:git:git@github.com:jfinal/enjoy.git
+ scm:git:git@github.com:jfinal/enjoy.git
+ git@github.com:jfinal/enjoy.git
+
+
+
+ org.sonatype.oss
+ oss-parent
+ 7
+
+
+
+
+
+
+ junit
+ junit
+ 4.8.2
+ test
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+ org.springframework
+ spring-webmvc
+ 4.3.8.RELEASE
+ provided
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.6.1
+
+ 1.6
+ 1.6
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.1.2
+
+
+ attach-sources
+ verify
+
+ jar-no-fork
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 1.1
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+ false
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/com/jfinal/kit/ElKit.java b/src/main/java/com/jfinal/kit/ElKit.java
new file mode 100644
index 0000000..4a0fbfa
--- /dev/null
+++ b/src/main/java/com/jfinal/kit/ElKit.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.kit;
+
+import java.io.Writer;
+import java.util.Map;
+import com.jfinal.template.Directive;
+import com.jfinal.template.Engine;
+import com.jfinal.template.Env;
+import com.jfinal.template.Template;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * EL 表达式语言求值工具类
+ *
+ *
+ * 1:不带参示例
+ * Integer value = ElKit.eval("1 + 2 * 3");
+ *
+ * 2:带参示例
+ * Kv data = Kv.by("a", 2).set("b", 3);
+ * Integer value = ElKit.eval("1 + a * b", data);
+ *
+ */
+public class ElKit {
+
+ private static Engine engine = new Engine();
+ private static final String RETURN_VALUE_KEY = "_RETURN_VALUE_";
+
+ static {
+ engine.addDirective("eval", new InnerEvalDirective());
+ }
+
+ public Engine getEngine() {
+ return engine;
+ }
+
+ public static T eval(String expr) {
+ return eval(expr, Kv.create());
+ }
+
+ @SuppressWarnings("unchecked")
+ public static T eval(String expr, Map, ?> data) {
+ String stringTemplate = "#eval(" + expr + ")";
+ Template template = engine.getTemplateByString(stringTemplate);
+ template.render(data, null);
+ return (T)data.get(RETURN_VALUE_KEY);
+ }
+
+ public static class InnerEvalDirective extends Directive {
+ public void exec(Env env, Scope scope, Writer writer) {
+ Object value = exprList.eval(scope);
+ scope.set(RETURN_VALUE_KEY, value);
+ }
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/kit/HashKit.java b/src/main/java/com/jfinal/kit/HashKit.java
new file mode 100644
index 0000000..4dee65e
--- /dev/null
+++ b/src/main/java/com/jfinal/kit/HashKit.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.kit;
+
+import java.security.MessageDigest;
+
+public class HashKit {
+
+ private static final java.security.SecureRandom random = new java.security.SecureRandom();
+ private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
+ private static final char[] CHAR_ARRAY = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
+
+ public static String md5(String srcStr){
+ return hash("MD5", srcStr);
+ }
+
+ public static String sha1(String srcStr){
+ return hash("SHA-1", srcStr);
+ }
+
+ public static String sha256(String srcStr){
+ return hash("SHA-256", srcStr);
+ }
+
+ public static String sha384(String srcStr){
+ return hash("SHA-384", srcStr);
+ }
+
+ public static String sha512(String srcStr){
+ return hash("SHA-512", srcStr);
+ }
+
+ public static String hash(String algorithm, String srcStr) {
+ try {
+ MessageDigest md = MessageDigest.getInstance(algorithm);
+ byte[] bytes = md.digest(srcStr.getBytes("utf-8"));
+ return toHex(bytes);
+ }
+ catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String toHex(byte[] bytes) {
+ StringBuilder ret = new StringBuilder(bytes.length * 2);
+ for (int i=0; i> 4) & 0x0f]);
+ ret.append(HEX_DIGITS[bytes[i] & 0x0f]);
+ }
+ return ret.toString();
+ }
+
+ /**
+ * md5 128bit 16bytes
+ * sha1 160bit 20bytes
+ * sha256 256bit 32bytes
+ * sha384 384bit 48bytes
+ * sha512 512bit 64bytes
+ */
+ public static String generateSalt(int saltLength) {
+ StringBuilder salt = new StringBuilder(saltLength);
+ for (int i=0; i set;
+
+ public static final JavaKeyword me = createSharedInstance();
+
+ private static JavaKeyword createSharedInstance() {
+ JavaKeyword jk = new JavaKeyword();
+ jk.set = Collections.unmodifiableSet(jk.set); // 共享对象不让修改
+ return jk;
+ }
+
+ public JavaKeyword() {
+ set = new HashSet();
+ for (String keyword : keywordArray) {
+ set.add(keyword);
+ }
+ }
+
+ public JavaKeyword addKeyword(String keyword) {
+ if (StrKit.notBlank(keyword)) {
+ set.add(keyword);
+ }
+ return this;
+ }
+
+ public JavaKeyword removeKeyword(String keyword) {
+ set.remove(keyword);
+ return this;
+ }
+
+ public boolean contains(String str) {
+ return set.contains(str);
+ }
+}
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/kit/Kv.java b/src/main/java/com/jfinal/kit/Kv.java
new file mode 100644
index 0000000..6c7b2bd
--- /dev/null
+++ b/src/main/java/com/jfinal/kit/Kv.java
@@ -0,0 +1,197 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.kit;
+
+import java.util.HashMap;
+import java.util.Map;
+// import com.jfinal.json.Json;
+
+/**
+ * Kv (Key Value)
+ *
+ * Example:
+ * Kv para = Kv.by("id", 123);
+ * User user = user.findFirst(getSqlPara("find", para));
+ */
+@SuppressWarnings({"serial", "rawtypes", "unchecked"})
+public class Kv extends HashMap {
+
+ @Deprecated
+ private static final String STATE_OK = "isOk";
+ @Deprecated
+ private static final String STATE_FAIL = "isFail";
+
+ public Kv() {
+ }
+
+ public static Kv by(Object key, Object value) {
+ return new Kv().set(key, value);
+ }
+
+ public static Kv create() {
+ return new Kv();
+ }
+
+ @Deprecated
+ public static Kv ok() {
+ return new Kv().setOk();
+ }
+
+ @Deprecated
+ public static Kv ok(Object key, Object value) {
+ return ok().set(key, value);
+ }
+
+ @Deprecated
+ public static Kv fail() {
+ return new Kv().setFail();
+ }
+
+ @Deprecated
+ public static Kv fail(Object key, Object value) {
+ return fail().set(key, value);
+ }
+
+ @Deprecated
+ public Kv setOk() {
+ super.put(STATE_OK, Boolean.TRUE);
+ super.put(STATE_FAIL, Boolean.FALSE);
+ return this;
+ }
+
+ @Deprecated
+ public Kv setFail() {
+ super.put(STATE_FAIL, Boolean.TRUE);
+ super.put(STATE_OK, Boolean.FALSE);
+ return this;
+ }
+
+ @Deprecated
+ public boolean isOk() {
+ Boolean isOk = (Boolean)get(STATE_OK);
+ if (isOk != null) {
+ return isOk;
+ }
+ Boolean isFail = (Boolean)get(STATE_FAIL);
+ if (isFail != null) {
+ return !isFail;
+ }
+
+ throw new IllegalStateException("调用 isOk() 之前,必须先调用 ok()、fail() 或者 setOk()、setFail() 方法");
+ }
+
+ @Deprecated
+ public boolean isFail() {
+ Boolean isFail = (Boolean)get(STATE_FAIL);
+ if (isFail != null) {
+ return isFail;
+ }
+ Boolean isOk = (Boolean)get(STATE_OK);
+ if (isOk != null) {
+ return !isOk;
+ }
+
+ throw new IllegalStateException("调用 isFail() 之前,必须先调用 ok()、fail() 或者 setOk()、setFail() 方法");
+ }
+
+ public Kv set(Object key, Object value) {
+ super.put(key, value);
+ return this;
+ }
+
+ public Kv set(Map map) {
+ super.putAll(map);
+ return this;
+ }
+
+ public Kv set(Kv kv) {
+ super.putAll(kv);
+ return this;
+ }
+
+ public Kv delete(Object key) {
+ super.remove(key);
+ return this;
+ }
+
+ public T getAs(Object key) {
+ return (T)get(key);
+ }
+
+ public String getStr(Object key) {
+ Object s = get(key);
+ return s != null ? s.toString() : null;
+ }
+
+ public Integer getInt(Object key) {
+ Number n = (Number)get(key);
+ return n != null ? n.intValue() : null;
+ }
+
+ public Long getLong(Object key) {
+ Number n = (Number)get(key);
+ return n != null ? n.longValue() : null;
+ }
+
+ public Number getNumber(Object key) {
+ return (Number)get(key);
+ }
+
+ public Boolean getBoolean(Object key) {
+ return (Boolean)get(key);
+ }
+
+ /**
+ * key 存在,并且 value 不为 null
+ */
+ public boolean notNull(Object key) {
+ return get(key) != null;
+ }
+
+ /**
+ * key 不存在,或者 key 存在但 value 为null
+ */
+ public boolean isNull(Object key) {
+ return get(key) == null;
+ }
+
+ /**
+ * key 存在,并且 value 为 true,则返回 true
+ */
+ public boolean isTrue(Object key) {
+ Object value = get(key);
+ return (value instanceof Boolean && ((Boolean)value == true));
+ }
+
+ /**
+ * key 存在,并且 value 为 false,则返回 true
+ */
+ public boolean isFalse(Object key) {
+ Object value = get(key);
+ return (value instanceof Boolean && ((Boolean)value == false));
+ }
+
+// public String toJson() {
+// return Json.getJson().toJson(this);
+// }
+
+ public boolean equals(Object kv) {
+ return kv instanceof Kv && super.equals(kv);
+ }
+}
+
+
diff --git a/src/main/java/com/jfinal/kit/ReflectKit.java b/src/main/java/com/jfinal/kit/ReflectKit.java
new file mode 100644
index 0000000..9b7d2a4
--- /dev/null
+++ b/src/main/java/com/jfinal/kit/ReflectKit.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.kit;
+
+/**
+ * 反射工具类
+ */
+public class ReflectKit {
+
+ public static Object newInstance(Class> clazz) {
+ try {
+ return clazz.newInstance();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/kit/StrKit.java b/src/main/java/com/jfinal/kit/StrKit.java
new file mode 100644
index 0000000..1f00ab5
--- /dev/null
+++ b/src/main/java/com/jfinal/kit/StrKit.java
@@ -0,0 +1,165 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.kit;
+
+/**
+ * StrKit.
+ */
+public class StrKit {
+
+ /**
+ * 首字母变小写
+ */
+ public static String firstCharToLowerCase(String str) {
+ char firstChar = str.charAt(0);
+ if (firstChar >= 'A' && firstChar <= 'Z') {
+ char[] arr = str.toCharArray();
+ arr[0] += ('a' - 'A');
+ return new String(arr);
+ }
+ return str;
+ }
+
+ /**
+ * 首字母变大写
+ */
+ public static String firstCharToUpperCase(String str) {
+ char firstChar = str.charAt(0);
+ if (firstChar >= 'a' && firstChar <= 'z') {
+ char[] arr = str.toCharArray();
+ arr[0] -= ('a' - 'A');
+ return new String(arr);
+ }
+ return str;
+ }
+
+ /**
+ * 字符串为 null 或者内部字符全部为 ' ' '\t' '\n' '\r' 这四类字符时返回 true
+ */
+ public static boolean isBlank(String str) {
+ if (str == null) {
+ return true;
+ }
+ int len = str.length();
+ if (len == 0) {
+ return true;
+ }
+ for (int i = 0; i < len; i++) {
+ switch (str.charAt(i)) {
+ case ' ':
+ case '\t':
+ case '\n':
+ case '\r':
+ // case '\b':
+ // case '\f':
+ break;
+ default:
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static boolean notBlank(String str) {
+ return !isBlank(str);
+ }
+
+ public static boolean notBlank(String... strings) {
+ if (strings == null || strings.length == 0) {
+ return false;
+ }
+ for (String str : strings) {
+ if (isBlank(str)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static boolean notNull(Object... paras) {
+ if (paras == null) {
+ return false;
+ }
+ for (Object obj : paras) {
+ if (obj == null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static String toCamelCase(String stringWithUnderline) {
+ if (stringWithUnderline.indexOf('_') == -1) {
+ return stringWithUnderline;
+ }
+
+ stringWithUnderline = stringWithUnderline.toLowerCase();
+ char[] fromArray = stringWithUnderline.toCharArray();
+ char[] toArray = new char[fromArray.length];
+ int j = 0;
+ for (int i=0; i 0) {
+ sb.append(separator);
+ }
+ sb.append(stringArray[i]);
+ }
+ return sb.toString();
+ }
+
+ public static boolean slowEquals(String a, String b) {
+ byte[] aBytes = (a != null ? a.getBytes() : null);
+ byte[] bBytes = (b != null ? b.getBytes() : null);
+ return HashKit.slowEquals(aBytes, bBytes);
+ }
+
+ public static boolean equals(String a, String b) {
+ return a == null ? b == null : a.equals(b);
+ }
+
+ public static String getRandomUUID() {
+ return java.util.UUID.randomUUID().toString().replace("-", "");
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/Directive.java b/src/main/java/com/jfinal/template/Directive.java
new file mode 100644
index 0000000..8701462
--- /dev/null
+++ b/src/main/java/com/jfinal/template/Directive.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template;
+
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.stat.ast.Stat;
+
+/**
+ * Directive 供用户继承并扩展自定义指令,具体用法可以参考
+ * com.jfinal.template.ext.directive 包下面的例子
+ */
+public abstract class Directive extends Stat {
+
+ /**
+ * 传递给指令的表达式列表
+ * 1:表达式列表可通过 exprList.eval(scope) 以及 exprList.evalExprList(scope) 进行求值
+ * 2:使用赋值表达式可实现参数传递功能
+ *
+ *
+ * 例如:#render("_hot.html", title="热门新闻", list=newsList)
+ *
+ */
+ protected ExprList exprList;
+
+ /**
+ * 具有 #end 结束符的指令内部嵌套的所有内容,调用 stat.exec(env, scope, writer)
+ * 即可执行指令内部嵌入所有指令与表达式,如果指令没有 #end 结束符,该属性无效
+ */
+ protected Stat stat;
+
+ /**
+ * 指令被解析时注入指令参数表达式列表,继承类可以通过覆盖此方法对参数长度和参数类型进行校验
+ */
+ public void setExprList(ExprList exprList) {
+ this.exprList = exprList;
+ }
+
+ /**
+ * 指令被解析时注入指令 body 内容,仅对于具有 #end 结束符的指令有效
+ */
+ public void setStat(Stat stat) {
+ this.stat = stat;
+ }
+}
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/Engine.java b/src/main/java/com/jfinal/template/Engine.java
new file mode 100644
index 0000000..e52eee1
--- /dev/null
+++ b/src/main/java/com/jfinal/template/Engine.java
@@ -0,0 +1,480 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import com.jfinal.kit.HashKit;
+import com.jfinal.kit.StrKit;
+import com.jfinal.template.expr.ast.MethodKit;
+import com.jfinal.template.source.ISource;
+import com.jfinal.template.source.ISourceFactory;
+import com.jfinal.template.source.StringSource;
+import com.jfinal.template.stat.Parser;
+import com.jfinal.template.stat.ast.Stat;
+
+/**
+ * Engine
+ *
+ * Example:
+ * Engine.use().getTemplate(fileName).render(...);
+ * Engine.use().getTemplate(fileName).renderToString(...);
+ */
+public class Engine {
+
+ public static final String MAIN_ENGINE_NAME = "main";
+
+ private static Engine MAIN_ENGINE;
+ private static Map engineMap = new HashMap();
+
+ // Create main engine
+ static {
+ MAIN_ENGINE = new Engine(MAIN_ENGINE_NAME);
+ engineMap.put(MAIN_ENGINE_NAME, MAIN_ENGINE);
+ }
+
+ private String name;
+ private boolean devMode = false;
+ private EngineConfig config = new EngineConfig();
+ private ISourceFactory sourceFactory = config.getSourceFactory();
+
+ private Map templateCache = new HashMap();
+
+ /**
+ * Create engine without management of JFinal
+ */
+ public Engine() {
+ this.name = "NO_NAME";
+ }
+
+ /**
+ * Create engine by engineName without management of JFinal
+ */
+ public Engine(String engineName) {
+ this.name = engineName;
+ }
+
+ /**
+ * Using the main Engine
+ */
+ public static Engine use() {
+ return MAIN_ENGINE;
+ }
+
+ /**
+ * Using the engine with engine name
+ */
+ public static Engine use(String engineName) {
+ return engineMap.get(engineName);
+ }
+
+ /**
+ * Create engine with engine name managed by JFinal
+ */
+ public synchronized static Engine create(String engineName) {
+ if (StrKit.isBlank(engineName)) {
+ throw new IllegalArgumentException("Engine name can not be blank");
+ }
+ engineName = engineName.trim();
+ if (engineMap.containsKey(engineName)) {
+ throw new IllegalArgumentException("Engine already exists : " + engineName);
+ }
+ Engine newEngine = new Engine(engineName);
+ engineMap.put(engineName, newEngine);
+ return newEngine;
+ }
+
+ /**
+ * Remove engine with engine name managed by JFinal
+ */
+ public synchronized static Engine remove(String engineName) {
+ Engine removed = engineMap.remove(engineName);
+ if (removed != null && MAIN_ENGINE_NAME.equals(removed.name)) {
+ Engine.MAIN_ENGINE = null;
+ }
+ return removed;
+ }
+
+ /**
+ * Set main engine
+ */
+ public synchronized static void setMainEngine(Engine engine) {
+ if (engine == null) {
+ throw new IllegalArgumentException("Engine can not be null");
+ }
+ engine.name = Engine.MAIN_ENGINE_NAME;
+ engineMap.put(Engine.MAIN_ENGINE_NAME, engine);
+ Engine.MAIN_ENGINE = engine;
+ }
+
+ /**
+ * Get template with file name
+ */
+ public Template getTemplate(String fileName) {
+ if (fileName.charAt(0) != '/') {
+ char[] arr = new char[fileName.length() + 1];
+ fileName.getChars(0, fileName.length(), arr, 1);
+ arr[0] = '/';
+ fileName = new String(arr);
+ }
+
+ Template template = templateCache.get(fileName);
+ if (template == null) {
+ template = buildTemplateBySourceFactory(fileName);
+ templateCache.put(fileName, template);
+ } else if (devMode) {
+ if (template.isModified()) {
+ template = buildTemplateBySourceFactory(fileName);
+ templateCache.put(fileName, template);
+ }
+ }
+ return template;
+ }
+
+ private Template buildTemplateBySourceFactory(String fileName) {
+ // FileSource fileSource = new FileSource(config.getBaseTemplatePath(), fileName, config.getEncoding());
+ ISource source = sourceFactory.getSource(config.getBaseTemplatePath(), fileName, config.getEncoding());
+ Env env = new Env(config);
+ Parser parser = new Parser(env, source.getContent(), fileName);
+ if (devMode) {
+ env.addSource(source);
+ }
+ Stat stat = parser.parse();
+ Template template = new Template(env, stat);
+ return template;
+ }
+
+ /**
+ * Get template by string content and do not cache the template
+ *
+ * 重要:StringSource 中的 key = HashKit.md5(content),也即 key
+ * 与 content 有紧密的对应关系,当 content 发生变化时 key 值也相应变化
+ * 因此,原先 key 所对应的 Template 缓存对象已无法被获取,当 getTemplateByString(String)
+ * 的 String 参数的数量不确定时会引发内存泄漏
+ *
+ * 当 getTemplateByString(String, boolean) 中的 String 参数的
+ * 数量可控并且确定时,才可对其使用缓存
+ */
+ public Template getTemplateByString(String content) {
+ return getTemplateByString(content, false);
+ }
+
+ /**
+ * Get template by string content
+ * @param content 模板内容
+ * @param cache true 则缓存 Template,否则不缓存
+ */
+ public Template getTemplateByString(String content, boolean cache) {
+ if (!cache) {
+ return buildTemplateBySource(new StringSource(content, cache));
+ }
+
+ String key = HashKit.md5(content);
+ Template template = templateCache.get(key);
+ if (template == null) {
+ template = buildTemplateBySource(new StringSource(content, cache));
+ templateCache.put(key, template);
+ } else if (devMode) {
+ if (template.isModified()) {
+ template = buildTemplateBySource(new StringSource(content, cache));
+ templateCache.put(key, template);
+ }
+ }
+ return template;
+ }
+
+ /**
+ * Get template with implementation of ISource
+ */
+ public Template getTemplate(ISource source) {
+ String key = source.getKey();
+ if (key == null) { // key 为 null 则不缓存,详见 ISource.getKey() 注释
+ return buildTemplateBySource(source);
+ }
+
+ Template template = templateCache.get(key);
+ if (template == null) {
+ template = buildTemplateBySource(source);
+ templateCache.put(key, template);
+ } else if (devMode) {
+ if (template.isModified()) {
+ template = buildTemplateBySource(source);
+ templateCache.put(key, template);
+ }
+ }
+ return template;
+ }
+
+ private Template buildTemplateBySource(ISource source) {
+ Env env = new Env(config);
+ Parser parser = new Parser(env, source.getContent(), null);
+ if (devMode) {
+ env.addSource(source);
+ }
+ Stat stat = parser.parse();
+ Template template = new Template(env, stat);
+ return template;
+ }
+
+ /**
+ * Add shared function with file
+ */
+ public Engine addSharedFunction(String fileName) {
+ config.addSharedFunction(fileName);
+ return this;
+ }
+
+ /**
+ * Add shared function by ISource
+ */
+ public Engine addSharedFunction(ISource source) {
+ config.addSharedFunction(source);
+ return this;
+ }
+
+ /**
+ * Add shared function with files
+ */
+ public Engine addSharedFunction(String... fileNames) {
+ config.addSharedFunction(fileNames);
+ return this;
+ }
+
+ /**
+ * Add shared function by string content
+ */
+ public Engine addSharedFunctionByString(String content) {
+ config.addSharedFunctionByString(content);
+ return this;
+ }
+
+ /**
+ * Add shared object
+ */
+ public Engine addSharedObject(String name, Object object) {
+ config.addSharedObject(name, object);
+ return this;
+ }
+
+ /**
+ * Set output directive factory
+ */
+ public Engine setOutputDirectiveFactory(IOutputDirectiveFactory outputDirectiveFactory) {
+ config.setOutputDirectiveFactory(outputDirectiveFactory);
+ return this;
+ }
+
+ /**
+ * Add directive
+ */
+ public Engine addDirective(String directiveName, Directive directive) {
+ config.addDirective(directiveName, directive);
+ return this;
+ }
+
+ /**
+ * Remove directive
+ */
+ public Engine removeDirective(String directiveName) {
+ config.removeDirective(directiveName);
+ return this;
+ }
+
+ /**
+ * Add shared method from object
+ */
+ public Engine addSharedMethod(Object sharedMethodFromObject) {
+ config.addSharedMethod(sharedMethodFromObject);
+ return this;
+ }
+
+ /**
+ * Add shared method from class
+ */
+ public Engine addSharedMethod(Class> sharedMethodFromClass) {
+ config.addSharedMethod(sharedMethodFromClass);
+ return this;
+ }
+
+ /**
+ * Add shared static method of Class
+ */
+ public Engine addSharedStaticMethod(Class> sharedStaticMethodFromClass) {
+ config.addSharedStaticMethod(sharedStaticMethodFromClass);
+ return this;
+ }
+
+ /**
+ * Remove shared Method with method name
+ */
+ public Engine removeSharedMethod(String methodName) {
+ config.removeSharedMethod(methodName);
+ return this;
+ }
+
+ /**
+ * Remove shared Method of the Class
+ */
+ public Engine removeSharedMethod(Class> clazz) {
+ config.removeSharedMethod(clazz);
+ return this;
+ }
+
+ /**
+ * Remove shared Method
+ */
+ public Engine removeSharedMethod(Method method) {
+ config.removeSharedMethod(method);
+ return this;
+ }
+
+ /**
+ * Remove template cache with template key
+ */
+ public void removeTemplateCache(String templateKey) {
+ templateCache.remove(templateKey);
+ }
+
+ /**
+ * Remove all template cache
+ */
+ public void removeAllTemplateCache() {
+ templateCache.clear();
+ }
+
+ public int getTemplateCacheSize() {
+ return templateCache.size();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String toString() {
+ return "Template Engine: " + name;
+ }
+
+ // Engine config below ---------
+
+ public EngineConfig getEngineConfig() {
+ return config;
+ }
+
+ /**
+ * 设置 true 为开发模式,支持模板文件热加载
+ * 设置 false 为生产模式,不支持模板文件热加载,以达到更高的性能
+ */
+ public Engine setDevMode(boolean devMode) {
+ this.devMode = devMode;
+ this.config.setDevMode(devMode);
+ if (this.devMode) {
+ removeAllTemplateCache();
+ }
+ return this;
+ }
+
+ public boolean getDevMode() {
+ return devMode;
+ }
+
+ /**
+ * 设置 ISourceFactory 用于为 engine 切换不同的 ISource 实现类
+ * ISource 用于从不同的来源加载模板内容
+ *
+ *
+ * 配置为 ClassPathSourceFactory 时特别注意:
+ * 由于 JFinal 会在 configEngine(Engine me) 方法调用 “之前”,会默认调用一次如下方法:
+ * me.setBaseTemplatePath(PathKit.getWebRootPath())
+ *
+ * 而 ClassPathSourceFactory 在以上默认值下不能工作,所以需要通过如下方式清掉该值:
+ * me.setBaseTemplatePath(null)
+ *
+ * 或者配置具体要用的 baseTemplatePath 值,例如:
+ * me.setBaseTemplatePath("view");
+ *
+ */
+ public Engine setSourceFactory(ISourceFactory sourceFactory) {
+ this.config.setSourceFactory(sourceFactory); // 放第一行先进行参数验证
+ this.sourceFactory = sourceFactory;
+ return this;
+ }
+
+ public ISourceFactory getSourceFactory() {
+ return sourceFactory;
+ }
+
+ public Engine setBaseTemplatePath(String baseTemplatePath) {
+ config.setBaseTemplatePath(baseTemplatePath);
+ return this;
+ }
+
+ public String getBaseTemplatePath() {
+ return config.getBaseTemplatePath();
+ }
+
+ public Engine setDatePattern(String datePattern) {
+ config.setDatePattern(datePattern);
+ return this;
+ }
+
+ public String getDatePattern() {
+ return config.getDatePattern();
+ }
+
+ public Engine setEncoding(String encoding) {
+ config.setEncoding(encoding);
+ return this;
+ }
+
+ public String getEncoding() {
+ return config.getEncoding();
+ }
+
+ /**
+ * Engine 独立设置为 devMode 可以方便模板文件在修改后立即生效,
+ * 但如果在 devMode 之下并不希望对 addSharedFunction(...),
+ * 添加的模板进行是否被修改的检测可以通过此方法设置 false 参进去
+ *
+ * 注意:Engine 在生产环境下(devMode 为 false),该参数无效
+ */
+ public Engine setReloadModifiedSharedFunctionInDevMode(boolean reloadModifiedSharedFunctionInDevMode) {
+ config.setReloadModifiedSharedFunctionInDevMode(reloadModifiedSharedFunctionInDevMode);
+ return this;
+ }
+
+ public static void addExtensionMethod(Class> targetClass, Object objectOfExtensionClass) {
+ MethodKit.addExtensionMethod(targetClass, objectOfExtensionClass);
+ }
+
+ public static void addExtensionMethod(Class> targetClass, Class> extensionClass) {
+ MethodKit.addExtensionMethod(targetClass, extensionClass);
+ }
+
+ public static void removeExtensionMethod(Class> targetClass, Object objectOfExtensionClass) {
+ MethodKit.removeExtensionMethod(targetClass, objectOfExtensionClass);;
+ }
+
+ public static void removeExtensionMethod(Class> targetClass, Class> extensionClass) {
+ MethodKit.removeExtensionMethod(targetClass, extensionClass);
+ }
+}
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/EngineConfig.java b/src/main/java/com/jfinal/template/EngineConfig.java
new file mode 100644
index 0000000..dcb9a61
--- /dev/null
+++ b/src/main/java/com/jfinal/template/EngineConfig.java
@@ -0,0 +1,363 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import com.jfinal.kit.StrKit;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.expr.ast.SharedMethodKit;
+import com.jfinal.template.ext.directive.*;
+// import com.jfinal.template.ext.sharedmethod.Json;
+import com.jfinal.template.source.FileSource;
+import com.jfinal.template.source.FileSourceFactory;
+import com.jfinal.template.source.ISource;
+import com.jfinal.template.source.ISourceFactory;
+import com.jfinal.template.source.StringSource;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.Parser;
+import com.jfinal.template.stat.ast.Define;
+import com.jfinal.template.stat.ast.Output;
+import com.jfinal.template.stat.ast.Stat;
+
+/**
+ * EngineConfig
+ */
+public class EngineConfig {
+
+ public static final String DEFAULT_ENCODING = "UTF-8";
+
+ private Map sharedFunctionMap = new HashMap();
+ private List sharedFunctionSourceList = new ArrayList(); // for devMode only
+
+ Map sharedObjectMap = null;
+
+ private IOutputDirectiveFactory outputDirectiveFactory = OutputDirectiveFactory.me;
+ private ISourceFactory sourceFactory = new FileSourceFactory();
+ private Map directiveMap = new HashMap();
+ private SharedMethodKit sharedMethodKit = new SharedMethodKit();
+
+ private boolean devMode = false;
+ private boolean reloadModifiedSharedFunctionInDevMode = true;
+ private String baseTemplatePath = null;
+ private String encoding = DEFAULT_ENCODING;
+ private String datePattern = "yyyy-MM-dd HH:mm";
+
+ public EngineConfig() {
+ // Add official directive of Template Engine
+ addDirective("render", new RenderDirective());
+ addDirective("date", new DateDirective());
+ addDirective("escape", new EscapeDirective());
+ addDirective("string", new StringDirective());
+ addDirective("random", new RandomDirective());
+
+ // Add official shared method of Template Engine
+ // addSharedMethod(new Json());
+ }
+
+ /**
+ * Add shared function with file
+ */
+ public void addSharedFunction(String fileName) {
+ // FileSource fileSource = new FileSource(baseTemplatePath, fileName, encoding);
+ ISource source = sourceFactory.getSource(baseTemplatePath, fileName, encoding);
+ doAddSharedFunction(source, fileName);
+ }
+
+ private synchronized void doAddSharedFunction(ISource source, String fileName) {
+ Env env = new Env(this);
+ new Parser(env, source.getContent(), fileName).parse();
+ addToSharedFunctionMap(sharedFunctionMap, env);
+ if (devMode) {
+ sharedFunctionSourceList.add(source);
+ env.addSource(source);
+ }
+ }
+
+ /**
+ * Add shared function with files
+ */
+ public void addSharedFunction(String... fileNames) {
+ for (String fileName : fileNames) {
+ addSharedFunction(fileName);
+ }
+ }
+
+ /**
+ * Add shared function by string content
+ */
+ public void addSharedFunctionByString(String content) {
+ // content 中的内容被解析后会存放在 Env 之中,而 StringSource 所对应的
+ // Template 对象 isModified() 始终返回 false,所以没有必要对其缓存
+ StringSource stringSource = new StringSource(content, false);
+ doAddSharedFunction(stringSource, null);
+ }
+
+ /**
+ * Add shared function by ISource
+ */
+ public void addSharedFunction(ISource source) {
+ String fileName = source instanceof FileSource ? ((FileSource)source).getFileName() : null;
+ doAddSharedFunction(source, fileName);
+ }
+
+ private void addToSharedFunctionMap(Map sharedFunctionMap, Env env) {
+ Map funcMap = env.getFunctionMap();
+ for (Entry e : funcMap.entrySet()) {
+ if (sharedFunctionMap.containsKey(e.getKey())) {
+ throw new IllegalArgumentException("Template function already exists : " + e.getKey());
+ }
+ Define func = e.getValue();
+ if (devMode) {
+ func.setEnvForDevMode(env);
+ }
+ sharedFunctionMap.put(e.getKey(), func);
+ }
+ }
+
+ /**
+ * Get shared function by Env
+ */
+ Define getSharedFunction(String functionName) {
+ Define func = sharedFunctionMap.get(functionName);
+ if (func == null) {
+ /**
+ * 如果 func 最初未定义,但后续在共享模板文件中又被添加进来
+ * 此时在本 if 分支中无法被感知,仍然返回了 null
+ *
+ * 但共享模板文件会在后续其它的 func 调用时被感知修改并 reload
+ * 所以本 if 分支不考虑处理模板文件中追加 #define 的情况
+ *
+ * 如果要处理,只能是每次在 func 为 null 时,判断 sharedFunctionSourceList
+ * 中的模板是否被修改过,再重新加载,不优雅
+ */
+ return null;
+ }
+
+ if (devMode && reloadModifiedSharedFunctionInDevMode) {
+ if (func.isSourceModifiedForDevMode()) {
+ synchronized (this) {
+ func = sharedFunctionMap.get(functionName);
+ if (func.isSourceModifiedForDevMode()) {
+ reloadSharedFunctionSourceList();
+ func = sharedFunctionMap.get(functionName);
+ }
+ }
+ }
+ }
+ return func;
+ }
+
+ /**
+ * Reload shared function source list
+ *
+ * devMode 要照顾到 sharedFunctionFiles,所以暂不提供
+ * removeSharedFunction(String functionName) 功能
+ * 开发者可直接使用模板注释功能将不需要的 function 直接注释掉
+ */
+ private synchronized void reloadSharedFunctionSourceList() {
+ Map newMap = new HashMap();
+ for (int i = 0, size = sharedFunctionSourceList.size(); i < size; i++) {
+ ISource source = sharedFunctionSourceList.get(i);
+ String fileName = source instanceof FileSource ? ((FileSource)source).getFileName() : null;
+
+ Env env = new Env(this);
+ new Parser(env, source.getContent(), fileName).parse();
+ addToSharedFunctionMap(newMap, env);
+ if (devMode) {
+ env.addSource(source);
+ }
+ }
+ this.sharedFunctionMap = newMap;
+ }
+
+ public synchronized void addSharedObject(String name, Object object) {
+ if (sharedObjectMap == null) {
+ sharedObjectMap = new HashMap();
+ } else if (sharedObjectMap.containsKey(name)) {
+ throw new IllegalArgumentException("Shared object already exists: " + name);
+ }
+ sharedObjectMap.put(name, object);
+ }
+
+ Map getSharedObjectMap() {
+ return sharedObjectMap;
+ }
+
+ /**
+ * Set output directive factory
+ */
+ public void setOutputDirectiveFactory(IOutputDirectiveFactory outputDirectiveFactory) {
+ if (outputDirectiveFactory == null) {
+ throw new IllegalArgumentException("outputDirectiveFactory can not be null");
+ }
+ this.outputDirectiveFactory = outputDirectiveFactory;
+ }
+
+ public Output getOutputDirective(ExprList exprList, Location location) {
+ return outputDirectiveFactory.getOutputDirective(exprList, location);
+ }
+
+ /**
+ * Invoked by Engine only
+ */
+ void setDevMode(boolean devMode) {
+ this.devMode = devMode;
+ }
+
+ public boolean isDevMode() {
+ return devMode;
+ }
+
+ /**
+ * Invoked by Engine only
+ */
+ void setSourceFactory(ISourceFactory sourceFactory) {
+ if (sourceFactory == null) {
+ throw new IllegalArgumentException("sourceFactory can not be null");
+ }
+ this.sourceFactory = sourceFactory;
+ }
+
+ public ISourceFactory getSourceFactory() {
+ return sourceFactory;
+ }
+
+ public void setBaseTemplatePath(String baseTemplatePath) {
+ // 使用 ClassPathSourceFactory 时,允许 baseTemplatePath 为 null 值
+ if (baseTemplatePath == null) {
+ this.baseTemplatePath = null;
+ return ;
+ }
+ if (StrKit.isBlank(baseTemplatePath)) {
+ throw new IllegalArgumentException("baseTemplatePath can not be blank");
+ }
+ baseTemplatePath = baseTemplatePath.trim();
+ if (baseTemplatePath.length() > 1) {
+ if (baseTemplatePath.endsWith("/") || baseTemplatePath.endsWith("\\")) {
+ baseTemplatePath = baseTemplatePath.substring(0, baseTemplatePath.length() - 1);
+ }
+ }
+ this.baseTemplatePath = baseTemplatePath;
+ }
+
+ public String getBaseTemplatePath() {
+ return baseTemplatePath;
+ }
+
+ public void setEncoding(String encoding) {
+ if (StrKit.isBlank(encoding)) {
+ throw new IllegalArgumentException("encoding can not be blank");
+ }
+ this.encoding = encoding;
+ }
+
+ public String getEncoding() {
+ return encoding;
+ }
+
+ public void setDatePattern(String datePattern) {
+ if (StrKit.isBlank(datePattern)) {
+ throw new IllegalArgumentException("datePattern can not be blank");
+ }
+ this.datePattern = datePattern;
+ }
+
+ public String getDatePattern() {
+ return datePattern;
+ }
+
+ public void setReloadModifiedSharedFunctionInDevMode(boolean reloadModifiedSharedFunctionInDevMode) {
+ this.reloadModifiedSharedFunctionInDevMode = reloadModifiedSharedFunctionInDevMode;
+ }
+
+ public synchronized void addDirective(String directiveName, Directive directive) {
+ if (StrKit.isBlank(directiveName)) {
+ throw new IllegalArgumentException("directive name can not be blank");
+ }
+ if (directive == null) {
+ throw new IllegalArgumentException("directive can not be null");
+ }
+ if (directiveMap.containsKey(directiveName)) {
+ throw new IllegalArgumentException("directive already exists : " + directiveName);
+ }
+ directiveMap.put(directiveName, directive);
+ }
+
+ public Stat getDirective(String directiveName) {
+ return directiveMap.get(directiveName);
+ }
+
+ public void removeDirective(String directiveName) {
+ directiveMap.remove(directiveName);
+ }
+
+ /**
+ * Add shared method from object
+ */
+ public void addSharedMethod(Object sharedMethodFromObject) {
+ sharedMethodKit.addSharedMethod(sharedMethodFromObject);
+ }
+
+ /**
+ * Add shared method from class
+ */
+ public void addSharedMethod(Class> sharedMethodFromClass) {
+ sharedMethodKit.addSharedMethod(sharedMethodFromClass);
+ }
+
+ /**
+ * Add shared static method of Class
+ */
+ public void addSharedStaticMethod(Class> sharedStaticMethodFromClass) {
+ sharedMethodKit.addSharedStaticMethod(sharedStaticMethodFromClass);
+ }
+
+ /**
+ * Remove shared Method with method name
+ */
+ public void removeSharedMethod(String methodName) {
+ sharedMethodKit.removeSharedMethod(methodName);
+ }
+
+ /**
+ * Remove shared Method of the Class
+ */
+ public void removeSharedMethod(Class> sharedClass) {
+ sharedMethodKit.removeSharedMethod(sharedClass);
+ }
+
+ /**
+ * Remove shared Method
+ */
+ public void removeSharedMethod(Method method) {
+ sharedMethodKit.removeSharedMethod(method);
+ }
+
+ public SharedMethodKit getSharedMethodKit() {
+ return sharedMethodKit;
+ }
+}
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/Env.java b/src/main/java/com/jfinal/template/Env.java
new file mode 100644
index 0000000..a92c6eb
--- /dev/null
+++ b/src/main/java/com/jfinal/template/Env.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import com.jfinal.template.source.ISource;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.ast.Define;
+
+/**
+ * Env
+ *
+ * 1:解析时存放 #define 定义的模板函数
+ * 2:运行时提供 #define 定义的模板函数
+ * 3:每个 Template 对象持有一个 Env 对象
+ */
+public class Env {
+
+ protected EngineConfig engineConfig;
+ protected Map functionMap = new HashMap();
+
+ // 代替 Template 持有该属性,便于在 #include 指令中调用 Env.addSource()
+ protected List sourceList = null;
+
+ public Env(EngineConfig engineConfig) {
+ this.engineConfig = engineConfig;
+ }
+
+ public EngineConfig getEngineConfig() {
+ return engineConfig;
+ }
+
+ /**
+ * Add template function
+ */
+ public void addFunction(Define function) {
+ String fn = function.getFunctionName();
+ if (functionMap.containsKey(fn)) {
+ Define previous = functionMap.get(fn);
+ throw new ParseException(
+ "Template function \"" + fn + "\" already defined in " +
+ getAlreadyDefinedLocation(previous.getLocation()),
+ function.getLocation()
+ );
+ }
+ functionMap.put(fn, function);
+ }
+
+ private String getAlreadyDefinedLocation(Location loc) {
+ StringBuilder buf = new StringBuilder();
+ if (loc.getTemplateFile() != null) {
+ buf.append(loc.getTemplateFile()).append(", line ").append(loc.getRow());
+ } else {
+ buf.append("string template line ").append(loc.getRow());
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Get function of current template first, getting shared function if null before
+ */
+ public Define getFunction(String functionName) {
+ Define func = functionMap.get(functionName);
+ return func != null ? func : engineConfig.getSharedFunction(functionName);
+ }
+
+ /**
+ * For EngineConfig.addSharedFunction(...) only
+ */
+ Map getFunctionMap() {
+ return functionMap;
+ }
+
+ /**
+ * 本方法用于在 devMode 之下,判断当前 Template 以及其下 #include 指令
+ * 所涉及的所有 ISource 对象是否被修改,以便于在 devMode 下重新加载
+ *
+ * sourceList 属性用于存放主模板以及 #include 进来的模板所对应的
+ * ISource 对象
+ */
+ public boolean isSourceListModified() {
+ if (sourceList != null) {
+ for (int i = 0, size = sourceList.size(); i < size; i++) {
+ if (sourceList.get(i).isModified()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 添加本 Template 的 ISource,以及该 Template 使用 include 包含进来的所有 ISource
+ * 以便于在 devMode 之下判断该 Template 是否被 modified,进而 reload 该 Template
+ */
+ public void addSource(ISource source) {
+ if (sourceList == null) {
+ sourceList = new ArrayList();
+ }
+ sourceList.add(source);
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/FastStringWriter.java b/src/main/java/com/jfinal/template/FastStringWriter.java
new file mode 100644
index 0000000..b835b89
--- /dev/null
+++ b/src/main/java/com/jfinal/template/FastStringWriter.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * FastStringWriter
+ *
+ * 由 JDK 中 StringWriter 改造而成,将 StringBuffer 属性替换为
+ * StringBuilder,避免 StringBuffer 的 synchronized 操作
+ */
+public class FastStringWriter extends Writer {
+
+ private StringBuilder buf;
+
+ public FastStringWriter() {
+ buf = new StringBuilder();
+ }
+
+ public FastStringWriter(int initialSize) {
+ if (initialSize < 0) {
+ throw new IllegalArgumentException("Negative buffer size");
+ }
+ buf = new StringBuilder(initialSize);
+ }
+
+ public void write(int c) {
+ buf.append((char) c);
+ }
+
+ public void write(char cbuf[], int off, int len) {
+ if ((off < 0) || (off > cbuf.length) || (len < 0) ||
+ ((off + len) > cbuf.length) || ((off + len) < 0)) {
+ throw new IndexOutOfBoundsException();
+ } else if (len == 0) {
+ return;
+ }
+ buf.append(cbuf, off, len);
+ }
+
+ public void write(String str) {
+ buf.append(str);
+ }
+
+ public void write(String str, int off, int len) {
+ buf.append(str.substring(off, off + len));
+ }
+
+ public FastStringWriter append(CharSequence csq) {
+ if (csq == null) {
+ write("null");
+ } else {
+ write(csq.toString());
+ }
+ return this;
+ }
+
+ public FastStringWriter append(CharSequence csq, int start, int end) {
+ CharSequence cs = (csq == null ? "null" : csq);
+ write(cs.subSequence(start, end).toString());
+ return this;
+ }
+
+ public FastStringWriter append(char c) {
+ write(c);
+ return this;
+ }
+
+ public String toString() {
+ return buf.toString();
+ }
+
+ public StringBuilder getBuffer() {
+ return buf;
+ }
+
+ public void flush() {
+
+ }
+
+ public void close() throws IOException {
+
+ }
+}
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/IOutputDirectiveFactory.java b/src/main/java/com/jfinal/template/IOutputDirectiveFactory.java
new file mode 100644
index 0000000..9e9a153
--- /dev/null
+++ b/src/main/java/com/jfinal/template/IOutputDirectiveFactory.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template;
+
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ast.Output;
+
+/**
+ * IOutputDirectiveFactory
+ * 用于定制自定义输出指令,替换系统默认输出指令,满足个性化需求
+ *
+ * 用法:
+ * 1:定义 MyOutput
+ * public class MyOutput extends Output {
+ * public MyOutput(ExprList exprList) {
+ * super(exprList);
+ * }
+ *
+ * public void exec(Env env, Scope scope, Writer writer) {
+ * write(writer, exprList.eval(scope));
+ * }
+ * }
+ *
+ * 2:定义 MyOutputDirectiveFactory
+ * public class MyOutputDirectiveFactory implements IOutputDirectiveFactory {
+ * public Output getOutputDirective(ExprList exprList) {
+ * return new MyOutput(exprList);
+ * }
+ * }
+ *
+ * 3:配置
+ * engine.setOutputDirectiveFactory(new MyOutputDirectiveFactory())
+ */
+public interface IOutputDirectiveFactory {
+
+ public Output getOutputDirective(ExprList exprList, Location location);
+
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/OutputDirectiveFactory.java b/src/main/java/com/jfinal/template/OutputDirectiveFactory.java
new file mode 100644
index 0000000..6be28a6
--- /dev/null
+++ b/src/main/java/com/jfinal/template/OutputDirectiveFactory.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template;
+
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ast.Output;
+
+/**
+ * OutputDirectiveFactory
+ */
+public class OutputDirectiveFactory implements IOutputDirectiveFactory {
+
+ public static final OutputDirectiveFactory me = new OutputDirectiveFactory();
+
+ public Output getOutputDirective(ExprList exprList, Location location) {
+ return new Output(exprList, location);
+ }
+}
+
diff --git a/src/main/java/com/jfinal/template/Template.java b/src/main/java/com/jfinal/template/Template.java
new file mode 100644
index 0000000..9e1a9e1
--- /dev/null
+++ b/src/main/java/com/jfinal/template/Template.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template;
+
+import java.io.Writer;
+import java.util.Map;
+import com.jfinal.template.stat.Scope;
+import com.jfinal.template.stat.ast.Stat;
+
+/**
+ * Template
+ *
+ * 用法:
+ * Template template = Engine.use().getTemplate(...);
+ * template.render(data, writer);
+ * template.renderToString(data);
+ */
+public class Template {
+
+ private Env env;
+ private Stat ast;
+
+ public Template(Env env, Stat ast) {
+ if (env == null || ast == null) {
+ throw new IllegalArgumentException("env and ast can not be null");
+ }
+ this.env = env;
+ this.ast = ast;
+ }
+
+ /**
+ * 渲染到 Writer 中去
+ */
+ public void render(Map, ?> data, Writer writer) {
+ ast.exec(env, new Scope(data, env.engineConfig.sharedObjectMap), writer);
+ }
+
+ /**
+ * 支持无 data 参数,渲染到 Writer 中去
+ * 适用于数据在模板中通过表达式和语句直接计算得出等等应用场景
+ * 此外,其它所有 render 方法也支持传入 null 值 data 参数
+ */
+ public void render(Writer writer) {
+ ast.exec(env, new Scope(null, env.engineConfig.sharedObjectMap), writer);
+ }
+
+ /**
+ * 渲染到 FastStringWriter 中去
+ */
+ public void render(Map, ?> data, FastStringWriter fastStringWriter) {
+ ast.exec(env, new Scope(data, env.engineConfig.sharedObjectMap), fastStringWriter);
+ }
+
+ /**
+ * 渲染到 StringBuilder 中去
+ */
+ public StringBuilder renderToStringBuilder(Map, ?> data) {
+ FastStringWriter fsw = new FastStringWriter();
+ render(data, fsw);
+ return fsw.getBuffer();
+ }
+
+ /**
+ * 渲染到 String 中去
+ */
+ public String renderToString(Map, ?> data) {
+ return renderToStringBuilder(data).toString();
+ }
+
+ public boolean isModified() {
+ return env.isSourceListModified();
+ }
+}
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/TemplateException.java b/src/main/java/com/jfinal/template/TemplateException.java
new file mode 100644
index 0000000..756124b
--- /dev/null
+++ b/src/main/java/com/jfinal/template/TemplateException.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template;
+
+import com.jfinal.template.stat.Location;
+
+/**
+ * Template runtime exception
+ */
+@SuppressWarnings("serial")
+public class TemplateException extends RuntimeException {
+
+ public TemplateException(String msg, Location loc) {
+ super(loc != null ? msg + loc : msg);
+ }
+
+ public TemplateException(String msg, Location loc, Throwable t) {
+ super(loc != null ? msg + loc : msg, t);
+ }
+}
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ExprLexer.java b/src/main/java/com/jfinal/template/expr/ExprLexer.java
new file mode 100644
index 0000000..b840c19
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ExprLexer.java
@@ -0,0 +1,520 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import com.jfinal.kit.JavaKeyword;
+import com.jfinal.template.stat.CharTable;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParaToken;
+import com.jfinal.template.stat.ParseException;
+
+/**
+ * ExprLexer
+ */
+class ExprLexer {
+
+ static final char EOF = (char)-1;
+ static final JavaKeyword javaKeyword = new JavaKeyword();
+ static final Pattern DOUBLE_QUOTES_PATTERN = Pattern.compile("\\\\\"");
+ static final Pattern SINGLE_QUOTES_PATTERN = Pattern.compile("\\\\'");
+
+ char[] buf;
+ int state = 0;
+ int lexemeBegin = 0;
+ int forward = 0;
+ int beginRow = 1;
+ int forwardRow = 1;
+ List tokens = new ArrayList();
+ Location location;
+
+ public ExprLexer(ParaToken paraToken, Location location) {
+ this.location = location;
+ StringBuilder content = paraToken.getContent();
+ beginRow = paraToken.getRow();
+ forwardRow = beginRow;
+ if (content == null) {
+ buf = new char[]{EOF};
+ return ;
+ }
+ int len = content.length();
+ buf = new char[len + 1];
+ content.getChars(0, content.length(), buf, 0);
+ buf[len] = EOF;
+ }
+
+ public List scan() {
+ while (peek() != EOF) {
+ skipBlanks();
+ lexemeBegin = forward;
+ beginRow = forwardRow;
+ if (scanId()) {
+ continue ;
+ }
+ if (scanOperator()) {
+ continue ;
+ }
+ if (scanString()) {
+ continue ;
+ }
+ if (scanNumber()) {
+ continue ;
+ }
+
+ if (peek() != EOF) {
+ throw new ParseException("Expression not support the char: '" + peek() + "'", location);
+ }
+ }
+ return tokens;
+ }
+
+ /**
+ * 扫描 ID true false null
+ */
+ boolean scanId() {
+ if (state != 0) {
+ return false;
+ }
+
+ if (!CharTable.isLetter(peek())) {
+ return fail();
+ }
+
+ while (CharTable.isLetterOrDigit(next())) {
+ ;
+ }
+ String id = subBuf(lexemeBegin, forward - 1).toString();
+ if ("true".equals(id)) {
+ addToken(new Tok(Sym.TRUE, id, beginRow));
+ } else if ("false".equals(id)) {
+ addToken(new Tok(Sym.FALSE, id, beginRow));
+ } else if ("null".equals(id)) {
+ addToken(new Tok(Sym.NULL, id, beginRow));
+ } else if (CharTable.isBlankOrLineFeed(peek()) && javaKeyword.contains(id)) {
+ throw new ParseException("Identifier can not be java keyword : " + id, location);
+ } else {
+ addToken(new Tok(Sym.ID, id, beginRow));
+ }
+ return prepareNextScan();
+ }
+
+ /**
+ * + - * / % ++ --
+ * = == != < <= > >=
+ * ! && ||
+ * ? ?: ?!
+ * . .. : :: , ;
+ * ( ) [ ] { }
+ */
+ boolean scanOperator() {
+ if (state != 100) {
+ return false;
+ }
+
+ Tok tok;
+ switch (peek()) {
+ case '+': // + - * / % ++ --
+ if (next() == '+') {
+ tok = new Tok(Sym.INC, beginRow);
+ next();
+ } else {
+ tok = new Tok(Sym.ADD, beginRow);
+ }
+ return ok(tok);
+ case '-':
+ if (next() == '-') {
+ tok = new Tok(Sym.DEC, beginRow);
+ next();
+ } else {
+ tok = new Tok(Sym.SUB, beginRow);
+ }
+ return ok(tok);
+ case '*':
+ tok = new Tok(Sym.MUL, beginRow);
+ next();
+ return ok(tok);
+ case '/':
+ tok = new Tok(Sym.DIV, beginRow);
+ next();
+ return ok(tok);
+ case '%':
+ tok = new Tok(Sym.MOD, beginRow);
+ next();
+ return ok(tok);
+ case '=': // = == != < <= > >=
+ if (next() == '=') {
+ tok = new Tok(Sym.EQUAL, beginRow);
+ next();
+ } else {
+ tok = new Tok(Sym.ASSIGN, beginRow);
+ }
+ return ok(tok);
+ case '!':
+ if (next() == '=') {
+ tok = new Tok(Sym.NOTEQUAL, beginRow);
+ next();
+ } else {
+ tok = new Tok(Sym.NOT, beginRow);
+ }
+ return ok(tok);
+ case '<':
+ if (next() == '=') {
+ tok = new Tok(Sym.LE, beginRow);
+ next();
+ } else {
+ tok = new Tok(Sym.LT, beginRow);
+ }
+ return ok(tok);
+ case '>':
+ if (next() == '=') {
+ tok = new Tok(Sym.GE, beginRow);
+ next();
+ } else {
+ tok = new Tok(Sym.GT, beginRow);
+ }
+ return ok(tok);
+ case '&': // ! && ||
+ if (next() == '&') {
+ tok = new Tok(Sym.AND, beginRow);
+ next();
+ } else {
+ throw new ParseException("Unsupported operator: '&'", location);
+ }
+ return ok(tok);
+ case '|':
+ if (next() == '|') {
+ tok = new Tok(Sym.OR, beginRow);
+ next();
+ } else {
+ throw new ParseException("Unsupported operator: '|'", location);
+ }
+ return ok(tok);
+ case '?': // ? ??
+ if (next() == '?') {
+ tok = new Tok(Sym.NULL_SAFE, beginRow);
+ next();
+ } else {
+ tok = new Tok(Sym.QUESTION, beginRow);
+ }
+ return ok(tok);
+ case '.': // . .. : :: , ;
+ if (next() == '.') {
+ tok = new Tok(Sym.RANGE, beginRow);
+ next();
+ } else {
+ tok = new Tok(Sym.DOT, ".", beginRow);
+ }
+ return ok(tok);
+ case ':':
+ if (next() == ':') {
+ tok = new Tok(Sym.STATIC, beginRow);
+ next();
+ } else {
+ tok = new Tok(Sym.COLON, beginRow);
+ }
+ return ok(tok);
+ case ',':
+ tok = new Tok(Sym.COMMA, beginRow);
+ next();
+ return ok(tok);
+ case ';':
+ tok = new Tok(Sym.SEMICOLON, beginRow);
+ next();
+ return ok(tok);
+ case '(': // ( ) [ ] { }
+ tok = new Tok(Sym.LPAREN, beginRow);
+ next();
+ return ok(tok);
+ case ')':
+ tok = new Tok(Sym.RPAREN, beginRow);
+ next();
+ return ok(tok);
+ case '[':
+ tok = new Tok(Sym.LBRACK, beginRow);
+ next();
+ return ok(tok);
+ case ']':
+ tok = new Tok(Sym.RBRACK, beginRow);
+ next();
+ return ok(tok);
+ case '{':
+ tok = new Tok(Sym.LBRACE, beginRow);
+ next();
+ return ok(tok);
+ case '}':
+ tok = new Tok(Sym.RBRACE, beginRow);
+ next();
+ return ok(tok);
+ default :
+ return fail();
+ }
+ }
+
+ boolean ok(Tok tok) {
+ tokens.add(tok);
+ return prepareNextScan();
+ }
+
+ boolean scanString() {
+ if (state != 200) {
+ return false;
+ }
+
+ char quotes = peek();
+ if (quotes != '"' && quotes != '\'') {
+ return fail();
+ }
+
+ for (char c=next(); true; c=next()) {
+ if (c == quotes) {
+ if (buf[forward - 1] != '\\') { // 前一个字符不是转义字符
+ StringBuilder sb = subBuf(lexemeBegin + 1, forward -1);
+ String str;
+ if (sb != null) {
+ if (quotes == '"') {
+ str = DOUBLE_QUOTES_PATTERN.matcher(sb).replaceAll("\"");
+ } else {
+ str = SINGLE_QUOTES_PATTERN.matcher(sb).replaceAll("'");
+ }
+ } else {
+ str = "";
+ }
+
+ Tok tok = new Tok(Sym.STR, str, beginRow);
+ addToken(tok);
+ next();
+ return prepareNextScan();
+ } else {
+ continue ;
+ }
+ }
+
+ if (c == EOF) {
+ throw new ParseException("Expression error, the string not ending", location);
+ }
+ }
+ }
+
+ boolean scanNumber() {
+ if (state != 300) {
+ return false;
+ }
+
+ char c = peek();
+ if (!CharTable.isDigit(c)) {
+ return fail();
+ }
+
+ int numStart = lexemeBegin; // forward;
+ int radix = 10; // 10 进制
+ if (c == '0') {
+ c = next();
+ if (c == 'X' || c == 'x') {
+ radix = 16; // 16 进制
+ c = next();
+ numStart = numStart + 2;
+ } else {
+ radix = 8; // 8 进制
+ // numStart = numStart + 1; // 8 进制不用去掉前缀 0,可被正确转换,去除此行便于正确处理数字 0
+ }
+ }
+
+ c = skipDigit(radix);
+ Sym sym = null;
+ if (c == '.') { // 以 '.' 字符结尾是合法的浮点数
+ next();
+ if (peek() == '.' || // 处理 [0..9] 这样的表达式
+ CharTable.isLetter(peek())) { // 处理 123.toInt() 这样的表达式,1.2.toInt() 及 1D.toInt() 可正常处理
+ StringBuilder n = subBuf(numStart, forward - 2);
+ if (n == null /* && radix == 16 */) {
+ // 16 进制数格式错误,前缀 0x 后缺少 16 进制数字(16 进制时 numStart 已增加了 2, n 为 null 必是 16 进制解析出错)
+ throw new ParseException("Error hex format", location);
+ }
+ NumTok tok = new NumTok(Sym.INT, n.toString(), radix, false, location);
+ addToken(tok);
+ retract(1);
+ return prepareNextScan();
+ }
+
+ sym = Sym.DOUBLE; // 浮点型默认为 double
+ c = skipDigit(radix);
+ }
+
+ boolean isScientificNotation = false;
+ if (c == 'E' || c == 'e') { // scientific notation 科学计数法
+ c = next();
+ if (c == '+' || c == '-') {
+ c = next();
+ }
+ if (!CharTable.isDigit(c)) {
+ // 科学计数法后面缺少数字
+ throw new ParseException("Error scientific notation format", location);
+ }
+ isScientificNotation = true;
+ sym = Sym.DOUBLE; // 科学计数法默认类型为 double
+
+ c = skipDecimalDigit(); // 科学计数法的指数部分是十进制
+ }
+
+ StringBuilder num;
+ if (c == 'L' || c == 'l') {
+ if (sym == Sym.DOUBLE) {
+ // 浮点类型不能使用 'L' 或 'l' 后缀
+ throw new ParseException("Error float format", location);
+ }
+ sym = Sym.LONG;
+ next();
+ num = subBuf(numStart, forward - 2);
+ } else if (c == 'F' || c == 'f') {
+ sym = Sym.FLOAT;
+ next();
+ num = subBuf(numStart, forward - 2);
+ } else if (c == 'D' || c == 'd') {
+ sym = Sym.DOUBLE;
+ next();
+ num = subBuf(numStart, forward - 2);
+ } else {
+ if (sym == null) {
+ sym = Sym.INT;
+ }
+ num = subBuf(numStart, forward - 1);
+ }
+ if (errorFollow()) {
+ // "错误的表达式元素 : " + num + peek()
+ throw new ParseException("Error expression: " + num + peek(), location);
+ }
+ if (num == null /* && radix == 16 */) {
+ // 16 进制数格式错误,前缀 0x 后缺少 16 进制数字
+ throw new ParseException("Error hex format", location);
+ }
+
+ NumTok tok = new NumTok(sym, num.toString(), radix, isScientificNotation, location);
+ addToken(tok);
+ return prepareNextScan();
+ }
+
+ boolean errorFollow() {
+ char c = peek();
+ return CharTable.isLetterOrDigit(c) || c == '"' || c == '\'';
+ }
+
+ char skipDigit(int radix) {
+ if (radix == 10) {
+ return skipDecimalDigit();
+ } else if (radix == 16) {
+ return skipHexadecimalDigit();
+ } else {
+ return skipOctalDigit();
+ }
+ }
+
+ char skipDecimalDigit() {
+ char c = peek();
+ for (; CharTable.isDigit(c);) {
+ c = next();
+ }
+ return c;
+ }
+
+ char skipHexadecimalDigit() {
+ char c = peek();
+ for (; CharTable.isHexadecimalDigit(c);) {
+ c = next();
+ }
+ return c;
+ }
+
+ char skipOctalDigit() {
+ char c = peek();
+ for (; CharTable.isOctalDigit(c);) {
+ c = next();
+ }
+ return c;
+ }
+
+ boolean fail() {
+ forward = lexemeBegin;
+ forwardRow = beginRow;
+
+ if (state < 100) {
+ state = 100;
+ } else if (state < 200) {
+ state = 200;
+ } else if (state < 300) {
+ state = 300;
+ }
+ return false;
+ }
+
+ char next() {
+ if (buf[forward] == '\n') {
+ forwardRow++;
+ }
+ return buf[++forward];
+ }
+
+ char peek() {
+ return buf[forward];
+ }
+
+ /**
+ * 表达式词法分析需要跳过换行与回车
+ */
+ void skipBlanks() {
+ while(CharTable.isBlankOrLineFeed(buf[forward])) {
+ next();
+ }
+ }
+
+ StringBuilder subBuf(int start, int end) {
+ if (start > end) {
+ return null;
+ }
+ StringBuilder ret = new StringBuilder(end - start + 1);
+ for (int i=start; i<=end; i++) {
+ ret.append(buf[i]);
+ }
+ return ret;
+ }
+
+ boolean prepareNextScan() {
+ state = 0;
+ lexemeBegin = forward;
+ beginRow = forwardRow;
+ return true;
+ }
+
+ void addToken(Tok tok) {
+ tokens.add(tok);
+ }
+
+ void retract(int n) {
+ for (int i=0; i tokenList;
+ Location location;
+
+ ParaToken paraToken;
+ EngineConfig engineConfig;
+
+ public ExprParser(ParaToken paraToken, EngineConfig engineConfig, String fileName) {
+ this.paraToken = paraToken;
+ this.engineConfig = engineConfig;
+ this.location = new Location(fileName, paraToken.getRow());
+ }
+
+ void initPeek() {
+ peek = tokenList.get(forward);
+ }
+
+ Tok peek() {
+ return peek;
+ }
+
+ Tok move() {
+ peek = tokenList.get(++forward);
+ return peek;
+ }
+ void resetForward(int position) {
+ forward = position;
+ peek = tokenList.get(forward);
+ }
+
+ Tok match(Sym sym) {
+ Tok current = peek();
+ if (current.sym == sym) {
+ move();
+ return current;
+ }
+ throw new ParseException("Expression error: can not match the symbol \"" + sym.value() + "\"", location);
+ }
+
+ public ExprList parseExprList() {
+ return (ExprList)parse(true);
+ }
+
+ public ForCtrl parseForCtrl() {
+ Expr forCtrl = parse(false);
+ if (forCtrl instanceof ForCtrl) {
+ return (ForCtrl)forCtrl;
+ } else {
+ throw new ParseException("The expression of #for directive is error", location);
+ }
+ }
+
+ Expr parse(boolean isExprList) {
+ tokenList = new ExprLexer(paraToken, location).scan();
+ if (tokenList.size() == 0) {
+ return ExprList.NULL_EXPR_LIST;
+ }
+ tokenList.add(EOF);
+ initPeek();
+ Expr expr = isExprList ? exprList() : forCtrl();
+ if (peek() != EOF) {
+ throw new ParseException("Expression error: can not match \"" + peek().value() + "\"", location);
+ }
+ return expr;
+ }
+
+ /**
+ * exprList : expr (',' expr)*
+ */
+ Expr exprList() {
+ List exprList = new ArrayList();
+ while (true) {
+ Expr stat = expr();
+ if (stat != null) {
+ exprList.add(stat);
+ if (peek().sym == Sym.COMMA) {
+ move();
+ if (peek() == EOF) {
+ throw new ParseException("Expression error: can not match the char of comma ','", location);
+ }
+ continue ;
+ }
+ }
+ break ;
+ }
+ return new ExprList(exprList);
+ }
+
+ Expr expr() {
+ return assign();
+ }
+
+ /**
+ * assign : ID ( '[' expr ']' )? '=' expr
+ */
+ Expr assign() {
+ Tok idTok = peek();
+ if (idTok.sym != Sym.ID) {
+ return ternary();
+ }
+
+ int begin = forward;
+ // ID = expr
+ if (move().sym == Sym.ASSIGN) {
+ move();
+ return new Assign(idTok.value(), expr(), location);
+ }
+
+ // array、map 赋值:ID [ expr ] = expr
+ if (peek().sym == Sym.LBRACK) {
+ move();
+ Expr index = expr();
+ match(Sym.RBRACK);
+ if (peek().sym == Sym.ASSIGN) {
+ move();
+ return new Assign(idTok.value(), index, expr(), location); // 右结合无限连
+ }
+ }
+
+ resetForward(begin);
+ return ternary();
+ }
+
+ /**
+ * ternary : expr '?' expr ':' expr
+ */
+ Expr ternary() {
+ Expr cond = or();
+ if (peek().sym == Sym.QUESTION) {
+ move();
+ Expr exprOne = expr();
+ match(Sym.COLON);
+ return new Ternary(cond, exprOne, expr(), location);
+ }
+ return cond;
+ }
+
+ /**
+ * or : expr '||' expr
+ */
+ Expr or() {
+ Expr expr = and();
+ for (Tok tok=peek(); tok.sym==Sym.OR; tok=peek()) {
+ move();
+ expr = new Logic(Sym.OR, expr, and(), location);
+ }
+ return expr;
+ }
+
+ /**
+ * and : expr '&&' expr
+ */
+ Expr and() {
+ Expr expr = equalNotEqual();
+ for (Tok tok=peek(); tok.sym==Sym.AND; tok=peek()) {
+ move();
+ expr = new Logic(Sym.AND, expr, equalNotEqual(), location);
+ }
+ return expr;
+ }
+
+ /**
+ * equalNotEqual : expr ('==' | '!=') expr
+ */
+ Expr equalNotEqual() {
+ Expr expr = greaterLess();
+ for (Tok tok=peek(); tok.sym==Sym.EQUAL || tok.sym==Sym.NOTEQUAL; tok=peek()) {
+ move();
+ expr = new Compare(tok.sym, expr, greaterLess(), location);
+ }
+ return expr;
+ }
+
+ /**
+ * compare expr ('<=' | '>=' | '>' | '<') expr
+ * 不支持无限连: > >= < <=
+ */
+ Expr greaterLess() {
+ Expr expr = addSub();
+ Tok tok = peek();
+ if (tok.sym == Sym.LT || tok.sym == Sym.LE || tok.sym == Sym.GT || tok.sym == Sym.GE) {
+ move();
+ return new Compare(tok.sym, expr, addSub(), location);
+ }
+ return expr;
+ }
+
+ /**
+ * addSub : expr ('+'|'-') expr
+ */
+ Expr addSub() {
+ Expr expr = mulDivMod();
+ for (Tok tok=peek(); tok.sym==Sym.ADD || tok.sym==Sym.SUB; tok=peek()) {
+ move();
+ expr = new Arith(tok.sym, expr, mulDivMod(), location);
+ }
+ return expr;
+ }
+
+ /**
+ * mulDivMod : expr ('*'|'/'|'%') expr
+ */
+ Expr mulDivMod() {
+ Expr expr = nullSafe();
+ for (Tok tok=peek(); tok.sym==Sym.MUL || tok.sym==Sym.DIV || tok.sym==Sym.MOD; tok=peek()) {
+ move();
+ expr = new Arith(tok.sym, expr, nullSafe(), location);
+ }
+ return expr;
+ }
+
+ /**
+ * nullSafe : expr '??' expr
+ */
+ Expr nullSafe() {
+ Expr expr = unary();
+ for (Tok tok=peek(); tok.sym==Sym.NULL_SAFE; tok=peek()) {
+ move();
+ expr = new NullSafe(expr, unary(), location);
+ }
+ return expr;
+ }
+
+ /**
+ * unary : ('!' | '+' | '-'| '++' | '--') expr
+ */
+ Expr unary() {
+ Tok tok = peek();
+ switch (tok.sym) {
+ case NOT:
+ move();
+ return new Logic(tok.sym, unary(), location);
+ case ADD:
+ case SUB:
+ move();
+ return new Unary(tok.sym, unary(), location);
+ case INC:
+ case DEC:
+ move();
+ return new IncDec(tok.sym, false, incDec(), location);
+ default:
+ return incDec();
+ }
+ }
+
+ /**
+ * incDec : expr ('++' | '--')
+ */
+ Expr incDec() {
+ Expr expr = staticMember();
+ Tok tok = peek();
+ if (tok.sym == Sym.INC || tok.sym == Sym.DEC) {
+ move();
+ return new IncDec(tok.sym, true, expr, location);
+ }
+
+ return expr;
+ }
+
+ /**
+ * staticMember
+ * : ID_list '::' ID
+ * | ID_list '::' ID '(' exprList? ')'
+ */
+ Expr staticMember() {
+ if (peek().sym != Sym.ID) {
+ return sharedMethod();
+ }
+
+ int begin = forward;
+ while (move().sym == Sym.DOT && move().sym == Sym.ID) {
+ ;
+ }
+ // ID.ID.ID::
+ if (peek().sym != Sym.STATIC || tokenList.get(forward - 1).sym != Sym.ID) {
+ resetForward(begin);
+ return sharedMethod();
+ }
+
+ String clazz = getClazz(begin);
+ match(Sym.STATIC);
+ String memberName = match(Sym.ID).value();
+
+ // com.jfinal.kit.Str::isBlank(str)
+ if (peek().sym == Sym.LPAREN) {
+ move();
+ if (peek().sym == Sym.RPAREN) {
+ move();
+ return new StaticMethod(clazz, memberName, location);
+ }
+
+ ExprList exprList = (ExprList)exprList();
+ match(Sym.RPAREN);
+ return new StaticMethod(clazz, memberName, exprList, location);
+ }
+
+ // com.jfinal.core.Const::JFINAL_VERSION
+ return new StaticField(clazz, memberName, location);
+ }
+
+ String getClazz(int begin) {
+ StringBuilder clazz = new StringBuilder();
+ for (int i=begin; i mapEntry = new LinkedHashMap();
+ Map map = new Map(mapEntry);
+ move();
+ if (peek().sym == Sym.RBRACE) {
+ move();
+ return map;
+ }
+
+ buildMapEntry(mapEntry);
+ while (peek().sym == Sym.COMMA) {
+ move();
+ buildMapEntry(mapEntry);
+ }
+ match(Sym.RBRACE);
+ return map;
+ }
+
+ /**
+ * mapEntry : (ID | STR) ':' expr
+ */
+ void buildMapEntry(LinkedHashMap map) {
+ Tok tok = peek();
+ if (tok.sym == Sym.ID || tok.sym == Sym.STR) {
+ move();
+ match(Sym.COLON);
+ Expr value = expr();
+ if (value == null) {
+ throw new ParseException("Expression error: the value on the right side of map entry can not be blank", location);
+ }
+ map.put(tok.value(), value);
+ return ;
+ }
+ throw new ParseException("Expression error: the value of map key must be identifier or String", location);
+ }
+
+ /**
+ * array : '[' exprList ? | range ? ']'
+ * exprList : expr (',' expr)*
+ * range : expr .. expr
+ */
+ Expr array() {
+ if (peek().sym != Sym.LBRACK) {
+ return atom();
+ }
+
+ move();
+ if (peek().sym == Sym.RBRACK) {
+ move();
+ return new Array(ExprList.NULL_EXPR_ARRAY, location);
+ }
+ ExprList exprList = (ExprList)exprList();
+ if (exprList.length() == 1 && peek().sym == Sym.RANGE) {
+ move();
+ Expr end = expr();
+ match(Sym.RBRACK);
+ return new RangeArray(exprList.getExprArray()[0], end, location);
+ }
+
+ match(Sym.RBRACK);
+ return new Array(exprList.getExprArray(), location);
+ }
+
+ /**
+ * atom : '(' expr ')' | ID | STR | 'true' | 'false' | 'null'
+ * | INT | LONG | FLOAT | DOUBLE
+ */
+ Expr atom() {
+ Tok tok = peek();
+ switch (tok.sym) {
+ case LPAREN:
+ move();
+ Expr expr = expr();
+ match(Sym.RPAREN);
+ return expr;
+ case ID:
+ move();
+ return new Id(tok.value());
+ case STR:
+ case INT:
+ case LONG:
+ case FLOAT:
+ case DOUBLE:
+ move();
+ return new Const(tok.sym, tok.value());
+ case TRUE:
+ move();
+ return Const.TRUE;
+ case FALSE:
+ move();
+ return Const.FALSE;
+ case NULL:
+ move();
+ return Const.NULL;
+ case COMMA:
+ case SEMICOLON:
+ case QUESTION: // support "c ?? ? a : b"
+ case AND: case OR: case EQUAL: case NOTEQUAL: // support "a.b ?? && expr"
+ case RPAREN: // support "(a.b ??)"
+ case RBRACK: // support "[start .. end ??]"
+ case RBRACE: // support "{key : value ??}"
+ case RANGE: // support "[start ?? .. end]"
+ case COLON: // support "c ? a ?? : b"
+ case EOF:
+ return null;
+ default :
+ throw new ParseException("Expression error: can not match the symbol \"" + tok.value() + "\"", location);
+ }
+ }
+
+ /**
+ * forControl : ID : expr | exprList? ';' expr? ';' exprList?
+ */
+ Expr forCtrl() {
+ ExprList exprList = (ExprList)exprList();
+ if (peek().sym == Sym.SEMICOLON) {
+ move();
+ Expr cond = expr();
+ match(Sym.SEMICOLON);
+ Expr update = exprList();
+ return new ForCtrl(exprList, cond, update, location);
+ }
+
+ if (exprList.length() == 1) {
+ Expr expr = exprList.getExprArray()[0];
+ if (expr instanceof Id) {
+ match(Sym.COLON);
+ return new ForCtrl(((Id)expr), expr(), location);
+ }
+ }
+ throw new ParseException("The expression of #for directive is error", location);
+ }
+}
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/NumTok.java b/src/main/java/com/jfinal/template/expr/NumTok.java
new file mode 100644
index 0000000..cccfe8e
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/NumTok.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr;
+
+import java.math.BigDecimal;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+
+/**
+ * NumToken 封装所有数值类型,并进行类型转换,以便尽早抛出异常
+ *
+ * java 数值类型规则:
+ * 1:科学计数法默认为 double 类型,通过 Object v = 123E1; 测试可知
+ * 2:出现小数点的浮点数默认为 double 类型,无需指定 D/d 后缀。 而 float 类型必须指令 F/f 后缀
+ * 3:double、float (出现小数点即为浮点数) 只支持 10 进制:16 进制形式去书写直接报错,8 进制形式去书写被当成 10 进制
+ * 4:16 进制不支持科学计数法,因为 E/e 后缀会被当成是普通的 16 进制数字,而 +/- 号则被当成了加/减法运算
+ * 5: 8 进制在本质上不支持科学计数法,010E1 这样的科学计数写法会被当成 10 进制,去掉后面的 E1 变为 010 时才被当成 8 进制
+ * 6:所以 16 8 进制都不支持科学计数法,结论是对科学计数法的类型转换无需指定 radix 参数,而 BigDecimal 正好也不支持这个参数
+ *
+ * 概要:
+ * 1:16 8 进制不支持浮点数
+ * 前者直接报错,后者直接忽略前缀 0 并当作 10 进制处理
+ *
+ * 2:16 8 进制不支持科学计数法
+ * 虽然二者在书写方式上被允许写成 16 8 进制,但只将其当成 10 进制处理,前者将 E/e 当成16进制数字
+ * 后者忽略前缀 0 当成 10 进制处理,即看似 8 进制的科学计数法,实质是 10 进制科学计数法
+ *
+ * 3: 科学计数法在本质上是 double,所以总结为一点 ---> 16 8 进制只支持整型数据
+ */
+public class NumTok extends Tok {
+
+ private Object value;
+
+ NumTok(Sym sym, String s, int radix, boolean isScientificNotation, Location location) {
+ super(sym, location.getRow());
+ try {
+ typeConvert(sym, s, radix, isScientificNotation, location);
+ } catch (Exception e) {
+ throw new ParseException(e.getMessage(), location, e);
+ }
+ }
+
+ private void typeConvert(Sym sym, String s, int radix, boolean isScientificNotation, Location location) {
+ switch (sym) {
+ case INT:
+ if (isScientificNotation) {
+ value = new BigDecimal(s).intValue();
+ } else {
+ value = Integer.valueOf(s, radix); // 整型数据才支持 16 8 进制
+ }
+ break ;
+ case LONG:
+ if (isScientificNotation) {
+ value = new BigDecimal(s).longValue();
+ } else {
+ value = Long.valueOf(s, radix); // 整型数据才支持 16 8 进制
+ }
+ break ;
+ case FLOAT:
+ if (isScientificNotation) {
+ value = new BigDecimal(s).floatValue();
+ } else {
+ value = Float.valueOf(s); // 浮点数只支持 10 进制
+ }
+ break ;
+ case DOUBLE:
+ if (isScientificNotation) {
+ value = new BigDecimal(s).doubleValue();
+ } else {
+ value = Double.valueOf(s); // 浮点数只支持 10 进制
+ }
+ break ;
+ default :
+ throw new ParseException("Unsupported type: " + sym.value(), location);
+ }
+ }
+
+ public String value() {
+ return value.toString();
+ }
+
+ public Object getNumberValue() {
+ return value;
+ }
+
+ public String toString() {
+ return sym.value() + " : " + value;
+ }
+}
diff --git a/src/main/java/com/jfinal/template/expr/Sym.java b/src/main/java/com/jfinal/template/expr/Sym.java
new file mode 100644
index 0000000..4e22c2e
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/Sym.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr;
+
+/**
+ * Sym
+ */
+public enum Sym {
+
+ ASSIGN("="),
+
+ DOT("."), RANGE(".."), COLON(":"), STATIC("::"), COMMA(","), SEMICOLON(";"),
+ LPAREN("("), RPAREN(")"), LBRACK("["), RBRACK("]"), LBRACE("{"), RBRACE("}"),
+
+ ADD("+"), SUB("-"), INC("++"), DEC("--"),
+ MUL("*"), DIV("/"), MOD("%"),
+
+ EQUAL("=="), NOTEQUAL("!="), LT("<"), LE("<="), GT(">"), GE(">="),
+
+ NOT("!"), AND("&&"), OR("||"),
+
+ QUESTION("?"),
+ NULL_SAFE("??"),
+
+ ID("ID"),
+
+ STR("STR"), TRUE("TRUE"), FALSE("FALSE"), NULL("NULL"),
+ INT("INT"), LONG("LONG"), FLOAT("FLOAT"), DOUBLE("DOUBLE"),
+
+ EOF("EOF");
+
+ private final String value;
+
+ private Sym(String value) {
+ this.value = value;
+ }
+
+ public String value() {
+ return value;
+ }
+
+ public String toString() {
+ return value;
+ }
+}
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/Tok.java b/src/main/java/com/jfinal/template/expr/Tok.java
new file mode 100644
index 0000000..dcc8b6d
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/Tok.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr;
+
+/**
+ * Tok
+ */
+class Tok {
+
+ final Sym sym;
+ private final String value;
+ final int row;
+
+ Tok(Sym sym, int row) {
+ this(sym, sym.value(), row);
+ }
+
+ Tok(Sym exprSym, String value, int row) {
+ if (exprSym == null || value == null) {
+ throw new IllegalArgumentException("exprSym and value can not be null");
+ }
+ this.sym = exprSym;
+ this.value = value;
+ this.row = row;
+ }
+
+ String value() {
+ return value;
+ }
+
+ public String toString() {
+ return value;
+ }
+
+ void print() {
+ System.out.print("[");
+ System.out.print(row);
+ System.out.print(", ");
+ System.out.print(sym.value());
+ System.out.print(", ");
+ System.out.print(value());
+ System.out.println("]");
+ }
+}
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/Arith.java b/src/main/java/com/jfinal/template/expr/ast/Arith.java
new file mode 100644
index 0000000..5e07d18
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/Arith.java
@@ -0,0 +1,229 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.expr.Sym;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Arithmetic
+ * 1:支持 byte short int long float double BigDecimal 的 + - * / % 运算
+ * 2:支持字符串加法运算
+ */
+public class Arith extends Expr {
+
+ public static final int INT = 0; // byte、short 用 int 类型支持,java 表达式亦如此
+ public static final int LONG = 1;
+ public static final int FLOAT = 2;
+ public static final int DOUBLE = 3;
+ public static final int BIGDECIMAL = 4;
+
+ private Sym op;
+ private Expr left;
+ private Expr right;
+
+ public Arith(Sym op, Expr left, Expr right, Location location) {
+ if (left == null || right == null) {
+ throw new ParseException("The target of \"" + op.value() + "\" operator can not be blank", location);
+ }
+ this.op = op;
+ this.left = left;
+ this.right = right;
+ this.location = location;
+ }
+
+ public Object eval(Scope scope) {
+ try {
+ return doEval(scope);
+ } catch (TemplateException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+ }
+
+ private Object doEval(Scope scope) {
+ Object leftValue = left.eval(scope);
+ Object rightValue = right.eval(scope);
+
+ if (leftValue instanceof Number && rightValue instanceof Number) {
+ Number l = (Number)leftValue;
+ Number r = (Number)rightValue;
+ int maxType = getMaxType(l, r);
+
+ switch (op) {
+ case ADD:
+ return add(maxType, l, r);
+ case SUB:
+ return sub(maxType, l, r);
+ case MUL:
+ return mul(maxType, l, r);
+ case DIV:
+ return div(maxType, l, r);
+ case MOD:
+ return mod(maxType, l, r);
+ default :
+ throw new TemplateException("Unsupported operator: " + op.value(), location);
+ }
+ }
+
+ // 字符串加法运算
+ if (leftValue instanceof String || rightValue instanceof String) {
+ return String.valueOf(leftValue).concat(String.valueOf(rightValue));
+ }
+
+ String leftObj = leftValue != null ? leftValue.getClass().getName() : "null";
+ String rightObj = rightValue != null ? rightValue.getClass().getName() : "null";
+ throw new TemplateException("Unsupported operation type: " + leftObj + " " + op.value() + " " + rightObj, location);
+ }
+
+ private int getMaxType(Number obj1, Number obj2) {
+ int t1 = getType(obj1);
+ if (t1 == BIGDECIMAL) {
+ return BIGDECIMAL;
+ }
+ int t2 = getType(obj2);
+ return t1 > t2 ? t1 : t2;
+ }
+
+ /**
+ * 注意:调用此方法的前提是,其中有一个对象的类型已经确定是 BigDecimal
+ */
+ private BigDecimal[] toBigDecimals(Number left, Number right) {
+ BigDecimal[] ret = new BigDecimal[2];
+ if (left instanceof BigDecimal) {
+ ret[0] = (BigDecimal)left;
+ ret[1] = new BigDecimal(right.toString());
+ } else {
+ ret[0] = new BigDecimal(left.toString());
+ ret[1] = (BigDecimal)right;
+ }
+ return ret;
+ }
+
+ private int getType(Number obj) {
+ if (obj instanceof Integer) {
+ return INT;
+ } else if (obj instanceof Long) {
+ return LONG;
+ } else if (obj instanceof Float) {
+ return FLOAT;
+ } else if (obj instanceof Double) {
+ return DOUBLE;
+ } else if (obj instanceof BigDecimal) {
+ return BIGDECIMAL;
+ } else if (obj instanceof Short || obj instanceof Byte) {
+ return INT; // short byte 用 int 支持,java 表达式亦如此
+ }
+ throw new TemplateException("Unsupported data type: " + obj.getClass().getName(), location);
+ }
+
+ private Number add(int maxType, Number left, Number right) {
+ switch (maxType) {
+ case INT:
+ return Integer.valueOf(left.intValue() + right.intValue());
+ case LONG:
+ return Long.valueOf(left.longValue() + right.longValue());
+ case FLOAT:
+ return Float.valueOf(left.floatValue() + right.floatValue());
+ case DOUBLE:
+ return Double.valueOf(left.doubleValue() + right.doubleValue());
+ case BIGDECIMAL:
+ BigDecimal[] bd = toBigDecimals(left, right);
+ return (bd[0]).add(bd[1]);
+ }
+ throw new TemplateException("Unsupported data type", location);
+ }
+
+ private Number sub(int maxType, Number left, Number right) {
+ switch (maxType) {
+ case INT:
+ return Integer.valueOf(left.intValue() - right.intValue());
+ case LONG:
+ return Long.valueOf(left.longValue() - right.longValue());
+ case FLOAT:
+ return Float.valueOf(left.floatValue() - right.floatValue());
+ case DOUBLE:
+ return Double.valueOf(left.doubleValue() - right.doubleValue());
+ case BIGDECIMAL:
+ BigDecimal[] bd = toBigDecimals(left, right);
+ return (bd[0]).subtract(bd[1]);
+ }
+ throw new TemplateException("Unsupported data type", location);
+ }
+
+ private Number mul(int maxType, Number left, Number right) {
+ switch (maxType) {
+ case INT:
+ return Integer.valueOf(left.intValue() * right.intValue());
+ case LONG:
+ return Long.valueOf(left.longValue() * right.longValue());
+ case FLOAT:
+ return Float.valueOf(left.floatValue() * right.floatValue());
+ case DOUBLE:
+ return Double.valueOf(left.doubleValue() * right.doubleValue());
+ case BIGDECIMAL:
+ BigDecimal[] bd = toBigDecimals(left, right);
+ return (bd[0]).multiply(bd[1]);
+ }
+ throw new TemplateException("Unsupported data type", location);
+ }
+
+ private Number div(int maxType, Number left, Number right) {
+ switch (maxType) {
+ case INT:
+ return Integer.valueOf(left.intValue() / right.intValue());
+ case LONG:
+ return Long.valueOf(left.longValue() / right.longValue());
+ case FLOAT:
+ return Float.valueOf(left.floatValue() / right.floatValue());
+ case DOUBLE:
+ return Double.valueOf(left.doubleValue() / right.doubleValue());
+ case BIGDECIMAL:
+ BigDecimal[] bd = toBigDecimals(left, right);
+ // return (bd[0]).divide(bd[1]);
+ return (bd[0]).divide(bd[1], RoundingMode.HALF_EVEN); // 银行家舍入法
+ }
+ throw new TemplateException("Unsupported data type", location);
+ }
+
+ private Number mod(int maxType, Number left, Number right) {
+ switch (maxType) {
+ case INT:
+ return Integer.valueOf(left.intValue() % right.intValue());
+ case LONG:
+ return Long.valueOf(left.longValue() % right.longValue());
+ case FLOAT:
+ return Float.valueOf(left.floatValue() % right.floatValue());
+ case DOUBLE:
+ return Double.valueOf(left.doubleValue() % right.doubleValue());
+ case BIGDECIMAL:
+ BigDecimal[] bd = toBigDecimals(left, right);
+ return (bd[0]).divideAndRemainder(bd[1])[1];
+ }
+ throw new TemplateException("Unsupported data type", location);
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/Array.java b/src/main/java/com/jfinal/template/expr/ast/Array.java
new file mode 100644
index 0000000..3282327
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/Array.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.util.ArrayList;
+import java.util.List;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Array
+ *
+ * 用法:
+ * 1:[1, 2, 3]
+ * 2:["a", 1, "b", 2, false, 3.14]
+ */
+public class Array extends Expr {
+
+ private Expr[] exprList;
+
+ public Array(Expr[] exprList, Location location) {
+ if (exprList == null) {
+ throw new ParseException("exprList can not be null", location);
+ }
+ this.exprList = exprList;
+ }
+
+ public Object eval(Scope scope) {
+ List array = new ArrayListExt(exprList.length);
+ for (Expr expr : exprList) {
+ array.add(expr.eval(scope));
+ }
+ return array;
+ }
+
+ /**
+ * 支持 array.length 表达式
+ */
+ @SuppressWarnings("serial")
+ public static class ArrayListExt extends ArrayList {
+
+ public ArrayListExt(int initialCapacity) {
+ super(initialCapacity);
+ }
+
+ public Integer getLength() {
+ return size();
+ }
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/Assign.java b/src/main/java/com/jfinal/template/expr/ast/Assign.java
new file mode 100644
index 0000000..e777dc8
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/Assign.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.util.List;
+import java.util.Map;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Assign
+ *
+ * 支持三种赋值,其中第二种如果括号中是 ID 或 STR 则演变为第三种是对 map 赋值:
+ * 1:ID = expr
+ * 2:ID [ expr ] = expr
+ * 如果 expr 为 int 或 long 型,则是对 array 赋值
+ * 如果 expr 为 ID、STR 型,则是对 map 进行赋值
+ * 否则抛异常出来
+ * 3:ID [ ID ] = expr 或者 ID [ STR ] = expr
+ * 4:支持无限连:id = array[ i = 0 ] = array[1] = 123
+ */
+public class Assign extends Expr {
+
+ private String id;
+ private Expr index; // index 用于支持 ID [ expr ] = expr 这种形式
+ private Expr right;
+
+ /**
+ * 数组赋值表达式
+ */
+ public Assign(String id, Expr index, Expr right, Location location) {
+ if (index == null) {
+ throw new ParseException("The index expression of array assignment can not be null", location);
+ }
+ if (right == null) {
+ throw new ParseException("The expression on the right side of an assignment expression can not be null", location);
+ }
+ this.id = id;
+ this.index = index;
+ this.right = right;
+ this.location = location;
+ }
+
+ /**
+ * 普通赋值表达式
+ */
+ public Assign(String id, Expr right, Location location) {
+ if (right == null) {
+ throw new ParseException("The expression on the right side of an assignment expression can not be null", location);
+ }
+ this.id = id;
+ this.index = null;
+ this.right = right;
+ this.location = location;
+ }
+
+ /**
+ * 赋值语句有返回值,可以用于表达式计算
+ */
+ public Object eval(Scope scope) {
+ if (index == null) {
+ return assignVariable(scope);
+ } else {
+ return assignElement(scope);
+ }
+ }
+
+ Object assignVariable(Scope scope) {
+ Object rightValue = right.eval(scope);
+ if (scope.getCtrl().isWisdomAssignment()) {
+ scope.set(id, rightValue);
+ } else if (scope.getCtrl().isLocalAssignment()) {
+ scope.setLocal(id, rightValue);
+ } else {
+ scope.setGlobal(id, rightValue);
+ }
+
+ return rightValue;
+ }
+
+ /**
+ * 数组或 Map 赋值
+ */
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ Object assignElement(Scope scope) {
+ Object target = scope.get(id);
+ if (target == null) {
+ throw new TemplateException("The assigned targets \"" + id + "\" can not be null", location);
+ }
+ Object idx = index.eval(scope);
+ if (idx == null) {
+ throw new TemplateException("The index of list/array and the key of map can not be null", location);
+ }
+
+ Object value;
+ if (target instanceof Map) {
+ value = right.eval(scope);
+ ((Map)target).put(idx, value);
+ return value;
+ }
+
+ if ( !(idx instanceof Integer) ) {
+ throw new TemplateException("The index of list/array can only be integer", location);
+ }
+
+ if (target instanceof List) {
+ value = right.eval(scope);
+ ((List)target).set((Integer)idx, value);
+ return value;
+ }
+ if (target.getClass().isArray()) {
+ value = right.eval(scope);
+ java.lang.reflect.Array.set(target, (Integer)idx, value);
+ return value;
+ }
+
+ throw new TemplateException("Only the list array and map is supported by index assignment", location);
+ }
+}
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/Compare.java b/src/main/java/com/jfinal/template/expr/ast/Compare.java
new file mode 100644
index 0000000..7fbf9bb
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/Compare.java
@@ -0,0 +1,288 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.math.BigDecimal;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.expr.Sym;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Compare
+ *
+ * 1:支持 byte short int long float double BigDecimal 的 == != > >= < <= 操作
+ * 2:== != 作用于 string,调用其 equals 方法进行比较
+ * 3:> >= < <= 可以比较实现了 Comparable 接口的对象
+ *
+ * 注意:float double 浮点型数据在比较操作时,具有精度上的局限性,不建议对浮点数进行比较
+ */
+public class Compare extends Expr {
+
+ private Sym op;
+ private Expr left;
+ private Expr right;
+
+ public Compare(Sym op, Expr left, Expr right, Location location) {
+ if (left == null || right == null) {
+ throw new ParseException("The target of \"" + op.value() + "\" operator can not be blank", location);
+ }
+ this.op = op;
+ this.left = left;
+ this.right = right;
+ this.location = location;
+ }
+
+ public Object eval(Scope scope) {
+ Object leftValue = left.eval(scope);
+ Object rightValue = right.eval(scope);
+
+ switch(op) {
+ case EQUAL:
+ return equal(leftValue, rightValue);
+ case NOTEQUAL:
+ return ! equal(leftValue, rightValue);
+ case GT:
+ return gt(leftValue, rightValue);
+ case GE:
+ return ge(leftValue, rightValue);
+ case LT:
+ return lt(leftValue, rightValue);
+ case LE:
+ return le(leftValue, rightValue);
+ default:
+ String l = leftValue != null ? leftValue.getClass().getSimpleName() : "null";
+ String r = rightValue != null ? rightValue.getClass().getSimpleName() : "null";
+ throw new TemplateException("Unsupported operation: " + l + " \"" + op.value() + "\" " + r, location);
+ }
+ }
+
+ Boolean equal(Object leftValue, Object rightValue) {
+ if (leftValue == rightValue) {
+ return Boolean.TRUE;
+ }
+ if (leftValue == null || rightValue == null) {
+ return Boolean.FALSE;
+ }
+ if (leftValue.equals(rightValue)) {
+ return Boolean.TRUE;
+ }
+ if (leftValue instanceof Number && rightValue instanceof Number) {
+ Number l = (Number)leftValue;
+ Number r = (Number)rightValue;
+ int maxType = getMaxType(l, r);
+ switch (maxType) {
+ case Arith.INT:
+ return l.intValue() == r.intValue();
+ case Arith.LONG:
+ return l.longValue() == r.longValue();
+ case Arith.FLOAT:
+ // 此法仅适用于两个对象类型相同的情况,升级为 BigDecimal 后精度会再高几个数量级
+ // return Float.floatToIntBits(l.floatValue()) == Float.floatToIntBits(r.floatValue());
+ case Arith.DOUBLE:
+ // 此法仅适用于两个对象类型相同的情况,升级为 BigDecimal 后精度会再高几个数量级
+ // return Double.doubleToLongBits(l.doubleValue()) == Double.doubleToLongBits(r.doubleValue());
+ case Arith.BIGDECIMAL:
+ BigDecimal[] bd = toBigDecimals(l, r);
+ return (bd[0]).compareTo(bd[1]) == 0;
+ }
+ throw new TemplateException("Equal comparison support types of int long float double and BigDeciaml", location);
+ }
+
+ return Boolean.FALSE;
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ Boolean gt(Object leftValue, Object rightValue) {
+ if (leftValue instanceof Number && rightValue instanceof Number) {
+ Number l = (Number)leftValue;
+ Number r = (Number)rightValue;
+ int maxType = getMaxType(l, r);
+ switch (maxType) {
+ case Arith.INT:
+ return l.intValue() > r.intValue();
+ case Arith.LONG:
+ return l.longValue() > r.longValue();
+ case Arith.FLOAT:
+ // return Float.floatToIntBits(l.floatValue()) > Float.floatToIntBits(r.floatValue());
+ case Arith.DOUBLE:
+ // return Double.doubleToLongBits(l.doubleValue()) > Double.doubleToLongBits(r.doubleValue());
+ case Arith.BIGDECIMAL:
+ BigDecimal[] bd = toBigDecimals(l, r);
+ return (bd[0]).compareTo(bd[1]) > 0;
+ }
+ throw new TemplateException("Unsupported operation: " + l.getClass().getSimpleName() + " \">\" " + r.getClass().getSimpleName(), location);
+ }
+
+ if (leftValue instanceof Comparable &&
+ leftValue.getClass() == rightValue.getClass()) {
+ return ((Comparable)leftValue).compareTo((Comparable)rightValue) > 0;
+ }
+
+ return checkType(leftValue, rightValue);
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ Boolean ge(Object leftValue, Object rightValue) {
+ if (leftValue instanceof Number && rightValue instanceof Number) {
+ Number l = (Number)leftValue;
+ Number r = (Number)rightValue;
+ int maxType = getMaxType(l, r);
+ switch (maxType) {
+ case Arith.INT:
+ return l.intValue() >= r.intValue();
+ case Arith.LONG:
+ return l.longValue() >= r.longValue();
+ case Arith.FLOAT:
+ // return Float.floatToIntBits(l.floatValue()) >= Float.floatToIntBits(r.floatValue());
+ case Arith.DOUBLE:
+ // return Double.doubleToLongBits(l.doubleValue()) >= Double.doubleToLongBits(r.doubleValue());
+ case Arith.BIGDECIMAL:
+ BigDecimal[] bd = toBigDecimals(l, r);
+ return (bd[0]).compareTo(bd[1]) >= 0;
+ }
+ throw new TemplateException("Unsupported operation: " + l.getClass().getSimpleName() + " \">=\" " + r.getClass().getSimpleName(), location);
+ }
+
+ if (leftValue instanceof Comparable &&
+ leftValue.getClass() == rightValue.getClass()) {
+ return ((Comparable)leftValue).compareTo((Comparable)rightValue) >= 0;
+ }
+
+ return checkType(leftValue, rightValue);
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ Boolean lt(Object leftValue, Object rightValue) {
+ if (leftValue instanceof Number && rightValue instanceof Number) {
+ Number l = (Number)leftValue;
+ Number r = (Number)rightValue;
+ int maxType = getMaxType(l, r);
+ switch (maxType) {
+ case Arith.INT:
+ return l.intValue() < r.intValue();
+ case Arith.LONG:
+ return l.longValue() < r.longValue();
+ case Arith.FLOAT:
+ // return Float.floatToIntBits(l.floatValue()) < Float.floatToIntBits(r.floatValue());
+ case Arith.DOUBLE:
+ // return Double.doubleToLongBits(l.doubleValue()) < Double.doubleToLongBits(r.doubleValue());
+ case Arith.BIGDECIMAL:
+ BigDecimal[] bd = toBigDecimals(l, r);
+ return (bd[0]).compareTo(bd[1]) < 0;
+ }
+ throw new TemplateException("Unsupported operation: " + l.getClass().getSimpleName() + " \"<\" " + r.getClass().getSimpleName(), location);
+ }
+
+ if (leftValue instanceof Comparable &&
+ leftValue.getClass() == rightValue.getClass()) {
+ return ((Comparable)leftValue).compareTo((Comparable)rightValue) < 0;
+ }
+
+ return checkType(leftValue, rightValue);
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ Boolean le(Object leftValue, Object rightValue) {
+ if (leftValue instanceof Number && rightValue instanceof Number) {
+ Number l = (Number)leftValue;
+ Number r = (Number)rightValue;
+ int maxType = getMaxType(l, r);
+ switch (maxType) {
+ case Arith.INT:
+ return l.intValue() <= r.intValue();
+ case Arith.LONG:
+ return l.longValue() <= r.longValue();
+ case Arith.FLOAT:
+ // return Float.floatToIntBits(l.floatValue()) <= Float.floatToIntBits(r.floatValue());
+ case Arith.DOUBLE:
+ // return Double.doubleToLongBits(l.doubleValue()) <= Double.doubleToLongBits(r.doubleValue());
+ case Arith.BIGDECIMAL:
+ BigDecimal[] bd = toBigDecimals(l, r);
+ return (bd[0]).compareTo(bd[1]) <= 0;
+ }
+ throw new TemplateException("Unsupported operation: " + l.getClass().getSimpleName() + " \"<=\" " + r.getClass().getSimpleName(), location);
+ }
+
+ if (leftValue instanceof Comparable &&
+ leftValue.getClass() == rightValue.getClass()) {
+ return ((Comparable)leftValue).compareTo((Comparable)rightValue) <= 0;
+ }
+
+ return checkType(leftValue, rightValue);
+ }
+
+ private int getMaxType(Number obj1, Number obj2) {
+ int t1 = getType(obj1);
+ if (t1 == Arith.BIGDECIMAL) {
+ return Arith.BIGDECIMAL;
+ }
+ int t2 = getType(obj2);
+ return t1 > t2 ? t1 : t2;
+ }
+
+ private int getType(Number obj) {
+ if (obj instanceof Integer) {
+ return Arith.INT;
+ } else if (obj instanceof Long) {
+ return Arith.LONG;
+ } else if (obj instanceof Float) {
+ return Arith.FLOAT;
+ } else if (obj instanceof Double) {
+ return Arith.DOUBLE;
+ } else if (obj instanceof BigDecimal) {
+ return Arith.BIGDECIMAL;
+ } else if (obj instanceof Short || obj instanceof Byte) {
+ return Arith.INT; // short byte 用 int 支持,java 表达式亦如此
+ }
+ throw new TemplateException("Unsupported data type: " + obj.getClass().getName(), location);
+ }
+
+ BigDecimal[] toBigDecimals(Number left, Number right) {
+ BigDecimal[] ret = new BigDecimal[2];
+ ret[0] = (left instanceof BigDecimal ? (BigDecimal)left : new BigDecimal(left.toString()));
+ ret[1] = (right instanceof BigDecimal ? (BigDecimal)right : new BigDecimal(right.toString()));
+ return ret;
+ }
+
+ private Boolean checkType(Object leftValue, Object rightValue) {
+ if (leftValue == null) {
+ throw new TemplateException("The operation target on the left side of \"" + op.value() + "\" can not be null", location);
+ }
+ if (rightValue == null) {
+ throw new TemplateException("The operation target on the right side of \"" + op.value() + "\" can not be null", location);
+ }
+
+ throw new TemplateException(
+ "Unsupported operation: " +
+ leftValue.getClass().getSimpleName() +
+ " \"" + op.value() + "\" " +
+ rightValue.getClass().getSimpleName(),
+ location
+ );
+ }
+}
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/Const.java b/src/main/java/com/jfinal/template/expr/ast/Const.java
new file mode 100644
index 0000000..bd54ed6
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/Const.java
@@ -0,0 +1,150 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import com.jfinal.template.expr.Sym;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * STR INT LONG FLOAT DOUBLE true false null
+ */
+public class Const extends Expr {
+
+ public static final Const TRUE = new Const(Boolean.TRUE, Sym.TRUE);
+ public static final Const FALSE = new Const(Boolean.FALSE, Sym.FALSE);
+ public static final Const NULL = new Const(null, Sym.NULL);
+
+ private Sym type;
+ private Object value;
+
+ /**
+ * 构造 TRUE FALSE NULL 常量,无需对 value 进行转换
+ */
+ private Const(Object value, Sym type) {
+ this.type = type;
+ this.value = value;
+ }
+
+ public Const(Sym type, String value) {
+ this.type = type;
+ this.value = typeConvert(type, value);
+ }
+
+ private Object typeConvert(Sym type, String value) {
+ switch (type) {
+ case STR:
+ return value;
+ case INT:
+ return Integer.parseInt(value);
+ case LONG:
+ return Long.parseLong(value);
+ case FLOAT:
+ return Float.parseFloat(value);
+ case DOUBLE:
+ return Double.parseDouble(value);
+ /*
+ case TRUE:
+ case FALSE:
+ return Boolean.parseBoolean(value);
+ case NULL:
+ return null;
+ */
+ default:
+ throw new RuntimeException("never happend");
+ }
+ }
+
+ public Object eval(Scope scope) {
+ return value;
+ }
+
+ public String toString() {
+ return value.toString();
+ }
+
+ public boolean isStr() {
+ return type == Sym.STR;
+ }
+
+ public boolean isTrue() {
+ return type == Sym.TRUE;
+ }
+
+ public boolean isFalse() {
+ return type == Sym.FALSE;
+ }
+
+ public boolean isBoolean() {
+ return type == Sym.TRUE || type == Sym.FALSE;
+ }
+
+ public boolean isNull() {
+ return type == Sym.NULL;
+ }
+
+ public boolean isInt() {
+ return type == Sym.INT;
+ }
+
+ public boolean isLong() {
+ return type == Sym.LONG;
+ }
+
+ public boolean isFloat() {
+ return type == Sym.FLOAT;
+ }
+
+ public boolean isDouble() {
+ return type == Sym.DOUBLE;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+
+ public String getStr() {
+ return (String)value;
+ }
+
+ public Boolean getBoolean() {
+ return (Boolean)value;
+ }
+
+ public Integer getInt() {
+ return (Integer)value;
+ }
+
+ public Long getLong() {
+ return (Long)value;
+ }
+
+ public Float getFloat() {
+ return (Float)value;
+ }
+
+ public Double getDouble() {
+ return (Double)value;
+ }
+}
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/Expr.java b/src/main/java/com/jfinal/template/expr/ast/Expr.java
new file mode 100644
index 0000000..ee3ed22
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/Expr.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Expr
+ */
+public abstract class Expr {
+
+ protected Location location;
+
+ public abstract Object eval(Scope scope);
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/ExprList.java b/src/main/java/com/jfinal/template/expr/ast/ExprList.java
new file mode 100644
index 0000000..dc1ba13
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/ExprList.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.util.List;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * ExprList
+ */
+public class ExprList extends Expr {
+
+ public static final Expr[] NULL_EXPR_ARRAY = new Expr[0];
+ public static final Object[] NULL_OBJECT_ARRAY = new Object[0];
+ public static final ExprList NULL_EXPR_LIST = new ExprList();
+
+ private Expr[] exprArray;
+
+ private ExprList() {
+ this.exprArray = NULL_EXPR_ARRAY;
+ }
+
+ public ExprList(List exprList) {
+ if (exprList != null && exprList.size() > 0) {
+ exprArray = exprList.toArray(new Expr[exprList.size()]);
+ } else {
+ exprArray = NULL_EXPR_ARRAY;
+ }
+ }
+
+ public Expr[] getExprArray() {
+ return exprArray;
+ }
+
+ public Expr getExpr(int index) {
+ if (index < 0 || index >= exprArray.length) {
+ throw new TemplateException("Index out of bounds: index = " + index + ", length = " + exprArray.length, location);
+ }
+ return exprArray[index];
+ }
+
+ public int length() {
+ return exprArray.length;
+ }
+
+ /**
+ * 对所有表达式求值,只返回最后一个表达式的值
+ */
+ public Object eval(Scope scope) {
+ Object ret = null;
+ for (Expr expr : exprArray) {
+ ret = expr.eval(scope);
+ }
+ return ret;
+ }
+
+ /**
+ * 对所有表达式求值,并返回所有表达式的值
+ */
+ public Object[] evalExprList(Scope scope) {
+ if (exprArray.length == 0) {
+ return NULL_OBJECT_ARRAY;
+ }
+
+ Object[] ret = new Object[exprArray.length];
+ for (int i=0; i targetClass = target.getClass();
+ String key = FieldKit.getFieldKey(targetClass, getterName);
+ MethodInfo getter;
+ try {
+ getter = MethodKit.getGetterMethod(key, targetClass, getterName);
+ } catch (Exception e) {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+
+ try {
+ if (getter != null) {
+ return getter.invoke(target, ExprList.NULL_OBJECT_ARRAY);
+ }
+// if (target instanceof Model) {
+// return ((Model>)target).get(fieldName);
+// }
+// if (target instanceof Record) {
+// return ((Record)target).get(fieldName);
+// }
+ if (target instanceof java.util.Map) {
+ return ((java.util.Map, ?>)target).get(fieldName);
+ }
+ // if (target instanceof com.jfinal.kit.Ret) {
+ // return ((com.jfinal.kit.Ret)target).get(fieldName);
+ // }
+ java.lang.reflect.Field field = FieldKit.getField(key, targetClass, fieldName);
+ if (field != null) {
+ return field.get(target);
+ }
+
+ // 支持获取数组长度: array.length
+ if ("length".equals(fieldName) && target.getClass().isArray()) {
+ return Array.getLength(target);
+ }
+ } catch (Exception e) {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+
+ if (scope.getCtrl().isNullSafe()) {
+ return null;
+ }
+
+ if (expr instanceof Id) {
+ String id = ((Id)expr).getId();
+ throw new TemplateException("Field not found: \"" + id + "." + fieldName + "\" and getter method not found: \"" + id + "." + getterName + "()\"", location);
+ }
+ throw new TemplateException("Field not found: \"" + fieldName + "\" and getter method not found: \"" + getterName + "()\"", location);
+ }
+}
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/FieldKit.java b/src/main/java/com/jfinal/template/expr/ast/FieldKit.java
new file mode 100644
index 0000000..2c3c0fa
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/FieldKit.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * FieldKit
+ */
+public class FieldKit {
+
+ private static final ConcurrentHashMap fieldCache = new ConcurrentHashMap();
+
+ public static Field getField(String key, Class> targetClass, String fieldName) {
+ Object field = fieldCache.get(key);
+ if (field == null) {
+ field = doGetField(targetClass, fieldName);
+ if (field != null) {
+ fieldCache.putIfAbsent(key, field);
+ } else {
+ // 对于不存在的 Field,只进行一次获取操作,主要为了支持 null safe,未来需要考虑内存泄漏风险
+ fieldCache.put(key, Boolean.FALSE);
+ }
+ }
+ return field instanceof Field ? (Field)field : null;
+ }
+
+ private static Field doGetField(Class> targetClass, String fieldName) {
+ Field[] fs = targetClass.getFields();
+ for (Field f : fs) {
+ if (f.getName().equals(fieldName)) {
+ return f;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 获取 Field 用于缓存的 key
+ */
+ public static String getFieldKey(Class> targetClass, String getterName) {
+ return new StringBuilder(64).append(targetClass.getName())
+ .append('.').append(getterName).toString();
+ }
+}
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/ForCtrl.java b/src/main/java/com/jfinal/template/expr/ast/ForCtrl.java
new file mode 100644
index 0000000..fd695d9
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/ForCtrl.java
@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * forCtrl : ID : expression
+ * | exprList? ';' expr? ';' exprList?
+ *
+ * 两种用法
+ * 1:#for(id : list) #end
+ * #for(entry : map) #end
+ *
+ * 2:#for(init; cond; update) #end
+ */
+public class ForCtrl extends Expr {
+
+ private String id;
+ private Expr expr;
+
+ private Expr init;
+ private Expr cond;
+ private Expr update;
+
+ /**
+ * ID : expr
+ */
+ public ForCtrl(Id id, Expr expr, Location location) {
+ if (expr == null) {
+ throw new ParseException("The iterator target of #for statement can not be null", location);
+ }
+ this.id = id.getId();
+ this.expr = expr;
+ this.init = null;
+ this.cond = null;
+ this.update = null;
+ this.location = location;
+ }
+
+ /**
+ * exprList? ';' expr? ';' exprList?
+ */
+ public ForCtrl(Expr init, Expr cond, Expr update, Location location) {
+ this.init = init;
+ this.cond = cond;
+ this.update = update;
+ this.id = null;
+ this.expr = null;
+ this.location = location;
+ }
+
+ public boolean isIterator() {
+ return id != null;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public Expr getExpr() {
+ return expr;
+ }
+
+ public Expr getInit() {
+ return init;
+ }
+
+ public Expr getCond() {
+ return cond;
+ }
+
+ public Expr getUpdate() {
+ return update;
+ }
+
+ public Object eval(Scope scope) {
+ throw new TemplateException("The eval(Scope scope) method can not be invoked", location);
+ }
+}
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/Id.java b/src/main/java/com/jfinal/template/expr/ast/Id.java
new file mode 100644
index 0000000..a3f7738
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/Id.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Id
+ */
+public class Id extends Expr {
+
+ private String id;
+
+ public Id(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public Object eval(Scope scope) {
+ return scope.get(id);
+ }
+
+ /**
+ * Id.toString() 后续版本不能变动,已有部分第三方依赖此方法
+ */
+ public String toString() {
+ return id;
+ }
+}
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/IncDec.java b/src/main/java/com/jfinal/template/expr/ast/IncDec.java
new file mode 100644
index 0000000..cd4e99b
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/IncDec.java
@@ -0,0 +1,138 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Map;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.expr.Sym;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * 自增与自减
+ */
+public class IncDec extends Expr {
+
+ private Sym op;
+ private String id;
+ private boolean isPost; // 是否是后缀形式: i++ i--
+
+ public IncDec(Sym op, boolean isPost, Expr id, Location location) {
+ if (id == null) {
+ throw new ParseException(op.value() + " operator requires target to be operational", location);
+ }
+ if ( !(id instanceof Id) ) {
+ throw new ParseException(op.value() + " operator only supports identifiers", location);
+ }
+
+ this.op = op;
+ this.id = ((Id)id).getId();
+ this.isPost = isPost;
+ this.location = location;
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public Object eval(Scope scope) {
+ Map map = scope.getMapOfValue(id);
+ if (map == null) {
+ if (scope.getCtrl().isNullSafe()) {
+ return null;
+ }
+ throw new TemplateException("The target of " + op.value() + " operator can not be null", location);
+ }
+ Object value = map.get(id);
+ if ( !(value instanceof Number) ) {
+ throw new TemplateException(op.value() + " operator only support int long float double and BigDecimal type", location);
+ }
+
+ Number newValue;
+ switch (op) {
+ case INC:
+ newValue = inc((Number)value);
+ break ;
+ case DEC:
+ newValue = dec((Number)value);
+ break ;
+ default:
+ throw new TemplateException("Unsupported operator: " + op.value(), location);
+ }
+ map.put(id, newValue);
+ return isPost ? value : newValue;
+ }
+
+ private Number inc(Number num) {
+ if (num instanceof Integer) {
+ return Integer.valueOf(num.intValue() + 1);
+ }
+ if (num instanceof Long) {
+ return Long.valueOf(num.longValue() + 1L);
+ }
+ if (num instanceof Float) {
+ return Float.valueOf(num.floatValue() + 1F);
+ }
+ if (num instanceof Double) {
+ return Double.valueOf(num.doubleValue() + 1D);
+ }
+ if (num instanceof BigDecimal) {
+ return ((BigDecimal)num).add(BigDecimal.ONE);
+ }
+ if (num instanceof BigInteger) {
+ return ((BigInteger)num).add(BigInteger.ONE);
+ }
+ if (num instanceof Short) {
+ return (short)(((Short)num).shortValue() + 1);
+ }
+ if (num instanceof Byte) {
+ return (byte)(((Byte)num).byteValue() + 1);
+ }
+ return num.intValue() + 1;
+ }
+
+ private Number dec(Number num) {
+ if (num instanceof Integer) {
+ return Integer.valueOf(num.intValue() - 1);
+ }
+ if (num instanceof Long) {
+ return Long.valueOf(num.longValue() - 1L);
+ }
+ if (num instanceof Float) {
+ return Float.valueOf(num.floatValue() - 1F);
+ }
+ if (num instanceof Double) {
+ return Double.valueOf(num.doubleValue() - 1D);
+ }
+ if (num instanceof BigDecimal) {
+ return ((BigDecimal)num).subtract(BigDecimal.ONE);
+ }
+ if (num instanceof BigInteger) {
+ return ((BigInteger)num).subtract(BigInteger.ONE);
+ }
+ if (num instanceof Short) {
+ return (short)(((Short)num).shortValue() - 1);
+ }
+ if (num instanceof Byte) {
+ return (byte)(((Byte)num).byteValue() - 1);
+ }
+ return num.intValue() - 1;
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/Index.java b/src/main/java/com/jfinal/template/expr/ast/Index.java
new file mode 100644
index 0000000..c08e5ee
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/Index.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.util.List;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * index : expr '[' expr ']'
+ *
+ * 支持 a[i]、 a[b[i]]、a[i][j]、a[i][j]...[n]
+ */
+public class Index extends Expr {
+
+ private Expr expr;
+ private Expr index;
+
+ public Index(Expr expr, Expr index, Location location) {
+ if (expr == null || index == null) {
+ throw new ParseException("array/list/map and their index can not be null", location);
+ }
+ this.expr = expr;
+ this.index = index;
+ this.location = location;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public Object eval(Scope scope) {
+ Object array = expr.eval(scope);
+ if (array == null) {
+ if (scope.getCtrl().isNullSafe()) {
+ return null;
+ }
+ throw new TemplateException("The index access operation target can not be null", location);
+ }
+
+ Object idx = index.eval(scope);
+ if (idx == null) {
+ if (scope.getCtrl().isNullSafe()) {
+ return null;
+ }
+ throw new TemplateException("The index of list/array and the key of map can not be null", location);
+ }
+
+ if (array instanceof List) {
+ if (idx instanceof Integer) {
+ return ((List>)array).get((Integer)idx);
+ }
+ throw new TemplateException("The index of list can only be integer", location);
+ }
+
+ if (array instanceof java.util.Map) {
+ return ((java.util.Map)array).get(idx);
+ }
+
+ if (array.getClass().isArray()) {
+ if (idx instanceof Integer) {
+ return java.lang.reflect.Array.get(array, (Integer)idx);
+ }
+ throw new TemplateException("The index of array can only be integer", location);
+ }
+
+ throw new TemplateException("Only the list array and map is supported by index access", location);
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/Logic.java b/src/main/java/com/jfinal/template/expr/ast/Logic.java
new file mode 100644
index 0000000..dcbabb3
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/Logic.java
@@ -0,0 +1,145 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.lang.reflect.Array;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.expr.Sym;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Logic
+ *
+ * 支持逻辑运算: ! && ||
+ */
+public class Logic extends Expr {
+
+ private Sym op;
+ private Expr left; // ! 运算没有 left 参数
+ private Expr right;
+
+ /**
+ * 构造 || && 结点
+ */
+ public Logic(Sym op, Expr left, Expr right, Location location) {
+ if (left == null) {
+ throw new ParseException("The target of \"" + op.value() + "\" operator on the left side can not be blank", location);
+ }
+ if (right == null) {
+ throw new ParseException("The target of \"" + op.value() + "\" operator on the right side can not be blank", location);
+ }
+ this.op = op;
+ this.left = left;
+ this.right = right;
+ this.location = location;
+ }
+
+ /**
+ * 构造 ! 结点,left 为 null
+ */
+ public Logic(Sym op, Expr right, Location location) {
+ if (right == null) {
+ throw new ParseException("The target of \"" + op.value() + "\" operator on the right side can not be blank", location);
+ }
+ this.op = op;
+ this.left = null;
+ this.right = right;
+ this.location = location;
+ }
+
+ public Object eval(Scope scope) {
+ switch (op) {
+ case NOT:
+ return evalNot(scope);
+ case AND:
+ return evalAnd(scope);
+ case OR:
+ return evalOr(scope);
+ default:
+ throw new TemplateException("Unsupported operator: " + op.value(), location);
+ }
+ }
+
+ Object evalNot(Scope scope) {
+ return ! isTrue(right.eval(scope));
+ }
+
+ Object evalAnd(Scope scope) {
+ return isTrue(left.eval(scope)) && isTrue(right.eval(scope));
+ }
+
+ Object evalOr(Scope scope) {
+ return isTrue(left.eval(scope)) || isTrue(right.eval(scope));
+ }
+
+ /**
+ * 规则:
+ * 1:null 返回 false
+ * 2:boolean 类型,原值返回
+ * 3:Map、Connection(List被包括在内) 返回 size() > 0
+ * 4:数组,返回 length > 0
+ * 5:String、StringBuilder、StringBuffer 等继承自 CharSequence 类的对象,返回 length > 0
+ * 6:Number 类型,返回 value != 0
+ * 7:Iterator 返回 hasNext() 值
+ * 8:其它返回 true
+ */
+ public static boolean isTrue(Object v) {
+ if (v == null) {
+ return false;
+ }
+ if (v instanceof Boolean) {
+ return (Boolean)v;
+ }
+ if (v instanceof Collection) {
+ return ((Collection>)v).size() > 0;
+ }
+ if (v instanceof Map) {
+ return ((Map, ?>)v).size() > 0;
+ }
+ if (v.getClass().isArray()) {
+ return Array.getLength(v) > 0;
+ }
+ if (v instanceof CharSequence) {
+ return ((CharSequence)v).length() > 0;
+ }
+ if (v instanceof Number) {
+ if (v instanceof Double) {
+ return ((Number)v).doubleValue() != 0;
+ }
+ if (v instanceof Float) {
+ return ((Number)v).floatValue() != 0;
+ }
+ return ((Number)v).intValue() != 0;
+ }
+ if (v instanceof Iterator) {
+ return ((Iterator>)v).hasNext();
+ }
+ return true;
+ }
+
+ public static boolean isFalse(Object v) {
+ return !isTrue(v);
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/Map.java b/src/main/java/com/jfinal/template/expr/ast/Map.java
new file mode 100644
index 0000000..21dcd4e
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/Map.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.util.LinkedHashMap;
+import java.util.Map.Entry;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Map
+ *
+ * 1:定义 map 常量
+ * {k1:123, k2:"abc", 'k3':true, "k4":[1,2,3], k5:1+2}
+ * 如上所示,map定义的 key 可以为 String 或者 id 标识符,而右侧的 value 可以是任意的常量与表达式
+ *
+ * 2:取值
+ * 先将 Map 常量赋值给某个变量: #set(map = {...})
+ * map['k1']
+ * map["k1"]
+ * map[expr]
+ * map.get("k1")
+ * map.k1
+ *
+ * 如上所示,当以下标方式取值时,下标参数可以是 string 与 expr,而 expr 求值以后的值必须也为 string类型
+ * 当用 map.k1 这类 field 字段取值形式时,则是使用 id 标识符,而不是 string 形参数
+ *
+ * 注意:即便是定义的时候 key 用的是 id 标识符,但在取值时也要用 string 类型下标参数或 expr 求值后为 string
+ * 定义时 key 可以使用 id 标识符是为了书写方便,本质上仍然是 string
+ *
+ * 3:可创建空 map,如: #(map = {})
+ */
+public class Map extends Expr {
+
+ private LinkedHashMap map;
+
+ public Map(LinkedHashMap map) {
+ this.map = map;
+ }
+
+ public Object eval(Scope scope) {
+ LinkedHashMap valueMap = new LinkedHashMap(map.size());
+ for (Entry e : map.entrySet()) {
+ valueMap.put(e.getKey(), e.getValue().eval(scope));
+ }
+ return valueMap;
+ }
+}
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/Method.java b/src/main/java/com/jfinal/template/expr/ast/Method.java
new file mode 100644
index 0000000..6a8005a
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/Method.java
@@ -0,0 +1,129 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.lang.reflect.InvocationTargetException;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Method : expr '.' ID '(' exprList? ')'
+ */
+public class Method extends Expr {
+
+ private Expr expr;
+ private String methodName;
+ private ExprList exprList;
+
+ public Method(Expr expr, String methodName, ExprList exprList, Location location) {
+ if (exprList == null || exprList.length() == 0) {
+ throw new ParseException("The parameter of method can not be blank", location);
+ }
+ init(expr, methodName, exprList, location);
+ }
+
+ public Method(Expr expr, String methodName, Location location) {
+ init(expr, methodName, ExprList.NULL_EXPR_LIST, location);
+ }
+
+ private void init(Expr expr, String methodName, ExprList exprList, Location location) {
+ if (expr == null) {
+ throw new ParseException("The target for method invoking can not be blank", location);
+ }
+ if (MethodKit.isForbiddenMethod(methodName)) {
+ throw new ParseException("Forbidden method: " + methodName, location);
+ }
+ this.expr = expr;
+ this.methodName = methodName;
+ this.exprList = exprList;
+ this.location = location;
+ }
+
+ public Object eval(Scope scope) {
+ Object target = expr.eval(scope);
+ if (target == null) {
+ if (scope.getCtrl().isNullSafe()) {
+ return null;
+ }
+ throw new TemplateException("The target for method invoking can not be null, method name: " + methodName, location);
+ }
+
+ Object[] argValues = exprList.evalExprList(scope);
+ MethodInfo methodInfo;
+ try {
+ methodInfo = MethodKit.getMethod(target.getClass(), methodName, argValues);
+ } catch (Exception e) {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+ if (methodInfo == null) {
+ if (scope.getCtrl().isNullSafe()) {
+ return null;
+ }
+ throw new TemplateException(buildMethodNotFoundSignature("Method not found: " + target.getClass().getName() + ".", methodName, argValues), location);
+ }
+
+ try {
+ return methodInfo.invoke(target, argValues);
+ } catch (InvocationTargetException e) {
+ Throwable t = e.getTargetException();
+ if (t != null) {
+ throw new TemplateException(t.getMessage(), location, t);
+ } else {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+ } catch (Exception e) {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+ }
+
+ static String buildMethodNotFoundSignature(String preMsg, String methodName, Object[] argValues) {
+ StringBuilder ret = new StringBuilder().append(preMsg).append(methodName).append("(");
+ if (argValues != null) {
+ for (int i = 0; i < argValues.length; i++) {
+ if (i > 0) {
+ ret.append(", ");
+ }
+ ret.append(argValues[i] != null ? argValues[i].getClass().getName() : "null");
+ }
+ }
+ return ret.append(")").toString();
+ }
+
+ /*
+ public static Object invokeVarArgsMethod(java.lang.reflect.Method method, Object target, Object[] argValues) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+ Class>[] paraTypes = method.getParameterTypes();
+ Object[] finalArgValues = new Object[paraTypes.length];
+
+ int fixedParaLength = paraTypes.length - 1;
+ System.arraycopy(argValues, 0, finalArgValues, 0, fixedParaLength);
+ Class> varParaComponentType = paraTypes[paraTypes.length - 1].getComponentType();
+ Object varParaValues = Array.newInstance(varParaComponentType, argValues.length - fixedParaLength);
+ int p = 0;
+ for (int i=fixedParaLength; i clazz;
+ protected final Method method;
+
+ protected final boolean isVarArgs;
+ protected final Class>[] paraTypes;
+
+ public MethodInfo(String key, Class> clazz, Method method) {
+ this.key = key;
+ this.clazz = clazz;
+ this.method = method;
+ this.isVarArgs = method.isVarArgs();
+ this.paraTypes = method.getParameterTypes();
+ }
+
+ public Object invoke(Object target, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+ if (isVarArgs) {
+ return invokeVarArgsMethod(target, args);
+ } else {
+ return method.invoke(target, args);
+ }
+ }
+
+ protected Object invokeVarArgsMethod(Object target, Object[] argValues) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+ Object[] finalArgValues = new Object[paraTypes.length];
+
+ int fixedParaLength = paraTypes.length - 1;
+ System.arraycopy(argValues, 0, finalArgValues, 0, fixedParaLength);
+ Class> varParaComponentType = paraTypes[paraTypes.length - 1].getComponentType();
+ Object varParaValues = Array.newInstance(varParaComponentType, argValues.length - fixedParaLength);
+ int p = 0;
+ for (int i=fixedParaLength; i[] getParameterTypes() {
+ return paraTypes;
+ }
+
+ public String toString() {
+ StringBuilder ret = new StringBuilder(clazz.getName()).append(".").append(method.getName()).append("(");
+ for (int i=0; i 0) {
+ ret.append(", ");
+ }
+ ret.append(paraTypes[i].getName());
+ }
+ return ret.append(")").toString();
+ }
+}
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/MethodInfoExt.java b/src/main/java/com/jfinal/template/expr/ast/MethodInfoExt.java
new file mode 100644
index 0000000..b6dfcca
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/MethodInfoExt.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * MethodInfoExt 辅助实现 extension method 功能
+ */
+public class MethodInfoExt extends MethodInfo {
+
+ protected Object objectOfExtensionClass;
+
+ public MethodInfoExt(Object objectOfExtensionClass, String key, Class> clazz, Method method) {
+ super(key, clazz, method);
+ this.objectOfExtensionClass = objectOfExtensionClass;
+
+ // 将被 mixed 的类自身添加入参数类型数组的第一个位置
+ // Class>[] newParaTypes = new Class>[paraTypes.length + 1];
+ // newParaTypes[0] = clazz; // 第一个参数就是被 mixed 的类它自己
+ // System.arraycopy(paraTypes, 0, newParaTypes, 1, paraTypes.length);
+ // this.paraTypes = newParaTypes;
+ }
+
+ public Object invoke(Object target, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+ Object[] finalArgs = new Object[args.length + 1];
+ finalArgs[0] = target;
+
+ if (args.length > 0) {
+ System.arraycopy(args, 0, finalArgs, 1, args.length);
+ }
+
+ if (isVarArgs) {
+ return invokeVarArgsMethod(objectOfExtensionClass, finalArgs);
+ } else {
+ return method.invoke(objectOfExtensionClass, finalArgs);
+ }
+ }
+}
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/MethodKit.java b/src/main/java/com/jfinal/template/expr/ast/MethodKit.java
new file mode 100644
index 0000000..e8bf628
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/MethodKit.java
@@ -0,0 +1,393 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import com.jfinal.kit.HashKit;
+import com.jfinal.kit.ReflectKit;
+import com.jfinal.template.ext.extensionmethod.ByteExt;
+import com.jfinal.template.ext.extensionmethod.DoubleExt;
+import com.jfinal.template.ext.extensionmethod.FloatExt;
+import com.jfinal.template.ext.extensionmethod.IntegerExt;
+import com.jfinal.template.ext.extensionmethod.LongExt;
+import com.jfinal.template.ext.extensionmethod.ShortExt;
+import com.jfinal.template.ext.extensionmethod.StringExt;
+
+/**
+ * MethodKit
+ */
+public class MethodKit {
+
+ private static final Class>[] NULL_ARG_TYPES = new Class>[0];
+ private static final Set forbiddenMethods = new HashSet();
+ private static final Set> forbiddenClasses = new HashSet>();
+ private static final Map, Class>> primitiveMap = new HashMap, Class>>();
+ private static final ConcurrentHashMap methodCache = new ConcurrentHashMap();
+
+ // 初始化在模板中调用 method 时所在的被禁止使用类
+ static {
+ Class>[] cs = {
+ System.class, Runtime.class, Thread.class, Class.class, ClassLoader.class, File.class,
+ Compiler.class, InheritableThreadLocal.class, Package.class, Process.class,
+ RuntimePermission.class, SecurityManager.class, ThreadGroup.class, ThreadLocal.class
+ };
+ for (Class> c : cs) {
+ forbiddenClasses.add(c);
+ }
+ }
+
+ // 初始化在模板中被禁止使用的 method name
+ static {
+ String[] ms = {
+ "getClass", "getDeclaringClass", "forName", "newInstance", "getClassLoader",
+ "getMethod", "getMethods", "getField", "getFields",
+ "notify", "notifyAll", "wait",
+ "load", "exit", "loadLibrary", "halt",
+ "stop", "suspend", "resume", "setDaemon", "setPriority",
+ };
+ for (String m : ms) {
+ forbiddenMethods.add(m);
+ }
+ }
+
+ // 初始化 primitive type 与 boxed type 双向映射关系
+ static {
+ primitiveMap.put(byte.class, Byte.class);
+ primitiveMap.put(short.class, Short.class);
+ primitiveMap.put(int.class, Integer.class);
+ primitiveMap.put(long.class, Long.class);
+ primitiveMap.put(float.class, Float.class);
+ primitiveMap.put(double.class, Double.class);
+ primitiveMap.put(char.class, Character.class);
+ primitiveMap.put(boolean.class, Boolean.class);
+
+ primitiveMap.put(Byte.class, byte.class);
+ primitiveMap.put(Short.class, short.class);
+ primitiveMap.put(Integer.class, int.class);
+ primitiveMap.put(Long.class, long.class);
+ primitiveMap.put(Float.class, float.class);
+ primitiveMap.put(Double.class, double.class);
+ primitiveMap.put(Character.class, char.class);
+ primitiveMap.put(Boolean.class, boolean.class);
+ }
+
+ public static boolean isForbiddenClass(Class> clazz) {
+ return forbiddenClasses.contains(clazz);
+ }
+
+ public static boolean isForbiddenMethod(String methodName) {
+ return forbiddenMethods.contains(methodName);
+ }
+
+ public static void addForbiddenMethod(String methodName) {
+ forbiddenMethods.add(methodName);
+ }
+
+ public static MethodInfo getMethod(Class> targetClass, String methodName, Object[] argValues) {
+ Class>[] argTypes = getArgTypes(argValues);
+ String key = getMethodKey(targetClass, methodName, argTypes);
+ Object method = methodCache.get(key);
+ if (method == null) {
+ method = doGetMethod(key, targetClass, methodName, argTypes);
+ if (method != null) {
+ methodCache.putIfAbsent(key, method);
+ } else {
+ // 对于不存在的 Method,只进行一次获取操作,主要为了支持 null safe,未来需要考虑内存泄漏风险
+ methodCache.put(key, Boolean.FALSE);
+ }
+ }
+ return method instanceof MethodInfo ? (MethodInfo)method : null;
+ }
+
+ /**
+ * 获取 getter 方法
+ * 使用与 Field 相同的 key,避免生成两次 key值
+ */
+ public static MethodInfo getGetterMethod(String key, Class> targetClass, String methodName) {
+ Object getterMethod = methodCache.get(key);
+ if (getterMethod == null) {
+ getterMethod = doGetMethod(key, targetClass, methodName, NULL_ARG_TYPES);
+ if (getterMethod != null) {
+ methodCache.putIfAbsent(key, getterMethod);
+ } else {
+ methodCache.put(key, Boolean.FALSE);
+ }
+ }
+ return getterMethod instanceof MethodInfo ? (MethodInfo)getterMethod : null;
+ }
+
+ static Class>[] getArgTypes(Object[] argValues) {
+ if (argValues == null || argValues.length == 0) {
+ return NULL_ARG_TYPES;
+ }
+ Class>[] argTypes = new Class>[argValues.length];
+ for (int i=0; i targetClass, String methodName, Class>[] argTypes) {
+ if (forbiddenClasses.contains(targetClass)) {
+ throw new RuntimeException("Forbidden class: " + targetClass.getName());
+ }
+ // 仅开启 forbiddenClasses 检测
+ // if (forbiddenMethods.contains(methodName)) {
+ // throw new RuntimeException("Forbidden method: " + methodName);
+ // }
+
+ Method[] methodArray = targetClass.getMethods();
+ for (Method method : methodArray) {
+ if (method.getName().equals(methodName)) {
+ Class>[] paraTypes = method.getParameterTypes();
+ if (matchFixedArgTypes(paraTypes, argTypes)) { // 无条件优先匹配固定参数方法
+ return new MethodInfo(key, targetClass, method);
+ }
+ if (method.isVarArgs() && matchVarArgTypes(paraTypes, argTypes)) {
+ return new MethodInfo(key, targetClass, method);
+ }
+ }
+ }
+ return null;
+ }
+
+ static boolean matchFixedArgTypes(Class>[] paraTypes, Class>[] argTypes) {
+ if (paraTypes.length != argTypes.length) {
+ return false;
+ }
+ return matchRangeTypes(paraTypes, argTypes, paraTypes.length);
+ }
+
+ private static boolean matchRangeTypes(Class>[] paraTypes, Class>[] argTypes, int matchLength) {
+ for (int i=0; i[] paraTypes, Class>[] argTypes) {
+ int fixedParaLength = paraTypes.length - 1;
+ if (argTypes.length < fixedParaLength) {
+ return false;
+ }
+ if (!matchRangeTypes(paraTypes, argTypes, fixedParaLength)) {
+ return false;
+ }
+
+ Class> varArgType = paraTypes[paraTypes.length - 1].getComponentType();
+ for (int i=fixedParaLength; i targetClass, String methodName, Class>[] argTypes) {
+ StringBuilder key = new StringBuilder(96);
+ key.append(targetClass.getName());
+ key.append('.').append(methodName);
+ if (argTypes != null && argTypes.length > 0) {
+ createArgTypesDigest(argTypes, key);
+ }
+ return key.toString();
+ }
+
+ static void createArgTypesDigest(Class>[] argTypes, StringBuilder key) {
+ StringBuilder argTypesDigest = new StringBuilder(64);
+ for (int i=0; i type = argTypes[i];
+ argTypesDigest.append(type != null ? type.getName() : "null");
+ }
+ key.append(HashKit.md5(argTypesDigest.toString()));
+ }
+
+ // 以下代码实现 extension method 功能 --------------------
+
+ // 添加 jfinal 官方扩展方法 extension method
+ static {
+ addExtensionMethod(String.class, new StringExt());
+ addExtensionMethod(Integer.class, new IntegerExt());
+ addExtensionMethod(Long.class, new LongExt());
+ addExtensionMethod(Float.class, new FloatExt());
+ addExtensionMethod(Double.class, new DoubleExt());
+ addExtensionMethod(Short.class, new ShortExt());
+ addExtensionMethod(Byte.class, new ByteExt());
+ }
+
+ public synchronized static void addExtensionMethod(Class> targetClass, Object objectOfExtensionClass) {
+ Class> extensionClass = objectOfExtensionClass.getClass();
+ java.lang.reflect.Method[] methodArray = extensionClass.getMethods();
+ for (java.lang.reflect.Method method : methodArray) {
+ Class> decClass = method.getDeclaringClass();
+ if (decClass == Object.class) { // 考虑用于优化路由生成那段代码
+ continue ;
+ }
+
+ Class>[] extensionMethodParaTypes = method.getParameterTypes();
+ String methodName = method.getName();
+ if (extensionMethodParaTypes.length == 0) {
+ throw new RuntimeException(buildMethodSignatureForException("Extension method requires at least one argument: " + extensionClass.getName() + ".", methodName, extensionMethodParaTypes));
+ }
+
+ // Extension method 第一个参数必须与当前对象的类型一致,在调用时会将当前对象自身传给扩展方法的第一个参数
+ if (targetClass != extensionMethodParaTypes[0]) {
+ throw new RuntimeException(buildMethodSignatureForException("The first argument type of : " + extensionClass.getName() + ".", methodName, extensionMethodParaTypes) + " must be: " + targetClass.getName());
+ }
+
+ Class>[] targetParaTypes = new Class>[extensionMethodParaTypes.length - 1];
+ System.arraycopy(extensionMethodParaTypes, 1, targetParaTypes, 0, targetParaTypes.length);
+
+ try {
+ Method error = targetClass.getMethod(methodName, targetParaTypes);
+ if (error != null) {
+ throw new RuntimeException("Extension method \"" + methodName + "\" is already exists in class \"" + targetClass.getName() + "\"");
+ }
+ } catch (NoSuchMethodException e) { // Method 找不到才能添加该扩展方法
+ String key = MethodKit.getMethodKey(targetClass, methodName, toBoxedType(targetParaTypes));
+ if (methodCache.containsKey(key)) {
+ throw new RuntimeException(buildMethodSignatureForException("The extension method is already exists: " + extensionClass.getName() + ".", methodName, targetParaTypes));
+ }
+
+ MethodInfoExt mie = new MethodInfoExt(objectOfExtensionClass, key, extensionClass/* targetClass */, method);
+ methodCache.put(key, mie);
+ }
+ }
+ }
+
+ public static void addExtensionMethod(Class> targetClass, Class> extensionClass) {
+ addExtensionMethod(targetClass, ReflectKit.newInstance(extensionClass));
+ }
+
+ public static void removeExtensionMethod(Class> targetClass, Object objectOfExtensionClass) {
+ Class> extensionClass = objectOfExtensionClass.getClass();
+ java.lang.reflect.Method[] methodArray = extensionClass.getMethods();
+ for (java.lang.reflect.Method method : methodArray) {
+ Class> decClass = method.getDeclaringClass();
+ if (decClass == Object.class) { // 考虑用于优化路由生成那段代码
+ continue ;
+ }
+
+ Class>[] extensionMethodParaTypes = method.getParameterTypes();
+ String methodName = method.getName();
+ Class>[] targetParaTypes = new Class>[extensionMethodParaTypes.length - 1];
+ System.arraycopy(extensionMethodParaTypes, 1, targetParaTypes, 0, targetParaTypes.length);
+
+ String key = MethodKit.getMethodKey(targetClass, methodName, toBoxedType(targetParaTypes));
+ methodCache.remove(key);
+ }
+ }
+
+ private static final Map, Class>> primitiveToBoxedMap = new HashMap, Class>>();
+
+ // 初始化 primitive type 到 boxed type 的映射
+ static {
+ primitiveToBoxedMap.put(byte.class, Byte.class);
+ primitiveToBoxedMap.put(short.class, Short.class);
+ primitiveToBoxedMap.put(int.class, Integer.class);
+ primitiveToBoxedMap.put(long.class, Long.class);
+ primitiveToBoxedMap.put(float.class, Float.class);
+ primitiveToBoxedMap.put(double.class, Double.class);
+ primitiveToBoxedMap.put(char.class, Character.class);
+ primitiveToBoxedMap.put(boolean.class, Boolean.class);
+ }
+
+ /**
+ * 由于从在模板中传递的基本数据类型参数只可能是 boxed 类型,当 extension method 中的方法参数是
+ * primitive 类型时,在 getMethod(key) 时无法获取 addExtensionMethod(...) 注册的扩展方法
+ * 所以为扩展方法调用 getMethodKey(...) 生成 key 时一律转成 boxed 类型去生成方法的 key 值
+ *
+ * 注意:该值仅用于在获取方法是通过 key 能获取到 MethindInfoExt,而 MethindInfoExt.paraType 仍然
+ * 是原来的参数值
+ */
+ private static Class>[] toBoxedType(Class>[] targetParaTypes) {
+ int len = targetParaTypes.length;
+ if (len == 0) {
+ return targetParaTypes;
+ }
+
+ Class>[] ret = new Class>[len];
+ for (int i=0; i temp = primitiveToBoxedMap.get(targetParaTypes[i]);
+ if (temp != null) {
+ ret[i] = temp;
+ } else {
+ ret[i] = targetParaTypes[i];
+ }
+ }
+ return ret;
+ }
+
+ public static void removeExtensionMethod(Class> targetClass, Class> extensionClass) {
+ removeExtensionMethod(targetClass, ReflectKit.newInstance(extensionClass));
+ }
+
+ private static String buildMethodSignatureForException(String preMsg, String methodName, Class>[] argTypes) {
+ StringBuilder ret = new StringBuilder().append(preMsg).append(methodName).append("(");
+ if (argTypes != null) {
+ for (int i = 0; i < argTypes.length; i++) {
+ if (i > 0) {
+ ret.append(", ");
+ }
+ ret.append(argTypes[i] != null ? argTypes[i].getName() : "null");
+ }
+ }
+ return ret.append(")").toString();
+ }
+}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/NullSafe.java b/src/main/java/com/jfinal/template/expr/ast/NullSafe.java
new file mode 100644
index 0000000..4c9d05c
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/NullSafe.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import com.jfinal.template.stat.Ctrl;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * NullSafe
+ * 在原则上只支持具有动态特征的用法,例如:方法调用、字段取值、Map 与 List 取值
+ * 而不支持具有静态特征的用法,例如:static method 调用、shared method 调用
+ *
+ * 用法:
+ * #( seoTitle ?? "JFinal 极速开发社区" )
+ * 支持级联: #( a.b.c ?? "JFinal 极速开发社区" )
+ * 支持嵌套: #( a ?? b ?? c ?? d)
+ */
+public class NullSafe extends Expr {
+
+ private Expr left;
+ private Expr right;
+
+ public NullSafe(Expr left, Expr right, Location location) {
+ if (left == null) {
+ throw new ParseException("The expression on the left side of null coalescing and safe access operator \"??\" can not be blank", location);
+ }
+ this.left = left;
+ this.right = right;
+ this.location = location;
+ }
+
+ public Object eval(Scope scope) {
+ Ctrl ctrl = scope.getCtrl();
+ boolean oldNullSafeValue = ctrl.isNullSafe();
+
+ Object ret;
+ try {
+ ctrl.setNullSafe(true);
+ ret = left.eval(scope);
+ } finally {
+ ctrl.setNullSafe(oldNullSafeValue);
+ }
+
+ return ret == null && right != null ? right.eval(scope) : ret;
+ }
+}
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/RangeArray.java b/src/main/java/com/jfinal/template/expr/ast/RangeArray.java
new file mode 100644
index 0000000..3f77b84
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/RangeArray.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.util.AbstractList;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * RangeArray : [expr .. expr]
+ *
+ * 用法:
+ * 1:[1..3]
+ * 2:[3..1]
+ */
+public class RangeArray extends Expr {
+
+ private Expr start;
+ private Expr end;
+
+ /**
+ * array : '[' exprList ? | range ? ']'
+ * exprList : expr (',' expr)*
+ * range : expr .. expr
+ */
+ public RangeArray(Expr start, Expr end, Location location) {
+ if (start == null) {
+ throw new ParseException("The start value of range array can not be blank", location);
+ }
+ if (end == null) {
+ throw new ParseException("The end value of range array can not be blank", location);
+ }
+ this.start = start;
+ this.end = end;
+ this.location = location;
+ }
+
+ public Object eval(Scope scope) {
+ Object startValue = start.eval(scope);
+ if ( !(startValue instanceof Integer) ) {
+ throw new TemplateException("The start value of range array must be Integer", location);
+ }
+ Object endValue = end.eval(scope);
+ if ( !(endValue instanceof Integer) ) {
+ throw new TemplateException("The end value of range array must be Integer", location);
+ }
+
+ return new RangeList((Integer)startValue, (Integer)endValue, location);
+ }
+
+ public static class RangeList extends AbstractList {
+
+ final int start;
+ final int size;
+ final int increment;
+ final Location location;
+
+ public RangeList(int start, int end, Location location) {
+ this.start = start;
+ this.increment = start <= end ? 1 : -1;
+ this.size = Math.abs(end - start) + 1;
+ this.location = location;
+ }
+
+ public Integer get(int index) {
+ if (index < 0 || index >= size) {
+ throw new TemplateException("Index out of bounds. Index: " + index + ", Size: " + size, location);
+ }
+ return start + index * increment;
+ }
+
+ public int size() {
+ return size;
+ }
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/SharedMethod.java b/src/main/java/com/jfinal/template/expr/ast/SharedMethod.java
new file mode 100644
index 0000000..2b04b5a
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/SharedMethod.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.expr.ast.SharedMethodKit.SharedMethodInfo;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * SharedMethod
+ *
+ * 用法:
+ * engine.addSharedMethod(object);
+ * engine.addSharedStaticMethod(Xxx.class);
+ * #(method(para))
+ */
+public class SharedMethod extends Expr {
+
+ private SharedMethodKit sharedMethodKit;
+ private String methodName;
+ private ExprList exprList;
+
+ public SharedMethod(SharedMethodKit sharedMethodKit, String methodName, ExprList exprList, Location location) {
+ if (MethodKit.isForbiddenMethod(methodName)) {
+ throw new ParseException("Forbidden method: " + methodName, location);
+ }
+ this.sharedMethodKit = sharedMethodKit;
+ this.methodName = methodName;
+ this.exprList = exprList;
+ this.location = location;
+ }
+
+ public Object eval(Scope scope) {
+ Object[] argValues = exprList.evalExprList(scope);
+ SharedMethodInfo sharedMethodInfo = sharedMethodKit.getSharedMethodInfo(methodName, argValues);
+
+ // ShareMethod 相当于是固定的静态的方法,不支持 null safe,null safe 只支持具有动态特征的用法
+ if (sharedMethodInfo == null) {
+ throw new TemplateException(Method.buildMethodNotFoundSignature("Shared method not found: ", methodName, argValues), location);
+ }
+ try {
+ return sharedMethodInfo.invoke(argValues);
+ } catch (Exception e) {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/SharedMethodKit.java b/src/main/java/com/jfinal/template/expr/ast/SharedMethodKit.java
new file mode 100644
index 0000000..c2abb39
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/SharedMethodKit.java
@@ -0,0 +1,173 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import com.jfinal.kit.ReflectKit;
+
+/**
+ * SharedMethodKit
+ */
+public class SharedMethodKit {
+
+ private static final Set excludedMethodKey = new HashSet();
+
+ static {
+ Method[] methods = Object.class.getMethods();
+ for (Method method : methods) {
+ String key = getSharedMethodKey(method.getName(), method.getParameterTypes());
+ excludedMethodKey.add(key);
+ }
+ }
+
+ private final List sharedMethodList = new ArrayList();
+ private final ConcurrentHashMap methodCache = new ConcurrentHashMap();
+
+ public SharedMethodInfo getSharedMethodInfo(String methodName, Object[] argValues) {
+ Class>[] argTypes = MethodKit.getArgTypes(argValues);
+ String key = getSharedMethodKey(methodName, argTypes);
+ SharedMethodInfo method = methodCache.get(key);
+ if (method == null) {
+ method = doGetSharedMethodInfo(methodName, argTypes);
+ if (method != null) {
+ methodCache.putIfAbsent(key, method);
+ }
+ // shared method 不支持 null safe,不缓存: methodCache.put(key, Boolean.FALSE)
+ }
+ return method;
+ }
+
+ private SharedMethodInfo doGetSharedMethodInfo(String methodName, Class>[] argTypes) {
+ for (SharedMethodInfo smi : sharedMethodList) {
+ if (smi.getName().equals(methodName)) {
+ Class>[] paraTypes = smi.getParameterTypes();
+ if (MethodKit.matchFixedArgTypes(paraTypes, argTypes)) { // 无条件优先匹配固定参数方法
+ return smi;
+ }
+ if (smi.isVarArgs() && MethodKit.matchVarArgTypes(paraTypes, argTypes)) {
+ return smi;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void addSharedMethod(Object sharedMethodFromObject) {
+ addSharedMethod(sharedMethodFromObject.getClass(), sharedMethodFromObject);
+ }
+
+ public void addSharedMethod(Class> sharedMethodFromClass) {
+ addSharedMethod(sharedMethodFromClass, ReflectKit.newInstance(sharedMethodFromClass));
+ }
+
+ public void addSharedStaticMethod(Class> sharedStaticMethodFromClass) {
+ addSharedMethod(sharedStaticMethodFromClass, null);
+ }
+
+ public void removeSharedMethod(String methodName) {
+ Iterator it = sharedMethodList.iterator();
+ while(it.hasNext()) {
+ if (it.next().getName().equals(methodName)) {
+ it.remove();
+ }
+ }
+ }
+
+ public void removeSharedMethod(Class> sharedClass) {
+ Iterator it = sharedMethodList.iterator();
+ while(it.hasNext()) {
+ if (it.next().getClazz() == sharedClass) {
+ it.remove();
+ }
+ }
+ }
+
+ public void removeSharedMethod(Method method) {
+ Iterator it = sharedMethodList.iterator();
+ while(it.hasNext()) {
+ SharedMethodInfo current = it.next();
+ String methodName = method.getName();
+ if (current.getName().equals(methodName)) {
+ String key = getSharedMethodKey(methodName, method.getParameterTypes());
+ if (current.getKey().equals(key)) {
+ it.remove();
+ }
+ }
+ }
+ }
+
+ private synchronized void addSharedMethod(Class> sharedClass, Object target) {
+ if (MethodKit.isForbiddenClass(sharedClass)) {
+ throw new IllegalArgumentException("Forbidden class: " + sharedClass.getName());
+ }
+
+ Method[] methods = sharedClass.getMethods();
+ for (Method method : methods) {
+ String key = getSharedMethodKey(method.getName(), method.getParameterTypes());
+ if (excludedMethodKey.contains(key)) {
+ continue ;
+ }
+
+ for (SharedMethodInfo smi : sharedMethodList) {
+ if (smi.getKey().equals(key)) {
+ throw new RuntimeException("The shared method is already exists : " + smi.toString());
+ }
+ }
+
+ if (target != null) {
+ sharedMethodList.add(new SharedMethodInfo(key, sharedClass, method, target));
+ } else if (Modifier.isStatic(method.getModifiers())) { // target 为 null 时添加 static method
+ sharedMethodList.add(new SharedMethodInfo(key, sharedClass, method, null));
+ }
+ }
+ }
+
+ private static String getSharedMethodKey(String methodName, Class>[] argTypes) {
+ StringBuilder key = new StringBuilder(64);
+ key.append(methodName);
+ if (argTypes != null && argTypes.length > 0) {
+ MethodKit.createArgTypesDigest(argTypes, key);
+ }
+ return key.toString();
+ }
+
+ static class SharedMethodInfo extends MethodInfo {
+ final Object target;
+
+ private SharedMethodInfo(String key, Class> clazz, Method method, Object target) {
+ super(key, clazz, method);
+ this.target = target;
+ }
+
+ public Object invoke(Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+ return super.invoke(target, args);
+ }
+
+ Class> getClazz() {
+ return clazz;
+ }
+ }
+}
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/StaticField.java b/src/main/java/com/jfinal/template/expr/ast/StaticField.java
new file mode 100644
index 0000000..4968e78
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/StaticField.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+import java.lang.reflect.Field;
+
+/**
+ * StaticField : ID_list '::' ID
+ * 动态获取静态变量值,变量值改变时仍可正确获取
+ * 用法:com.jfinal.core.Const::JFINAL_VERSION
+ */
+public class StaticField extends Expr {
+
+ private Class> clazz;
+ private String fieldName;
+ private Field field;
+
+ public StaticField(String className, String fieldName, Location location) {
+ try {
+ this.clazz = Class.forName(className);
+ this.fieldName = fieldName;
+ this.field = clazz.getField(fieldName);
+ this.location = location;
+ } catch (Exception e) {
+ throw new ParseException(e.getMessage(), location, e);
+ }
+ }
+
+ public Object eval(Scope scope) {
+ try {
+ return field.get(null);
+ } catch (Exception e) {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+ }
+
+ public String toString() {
+ return clazz.getName() + "::" + fieldName;
+ }
+}
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/StaticMethod.java b/src/main/java/com/jfinal/template/expr/ast/StaticMethod.java
new file mode 100644
index 0000000..9938b02
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/StaticMethod.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * StaticMethod : ID_list : '::' ID '(' exprList? ')'
+ * 用法: com.jfinal.kit.Str::isBlank("abc")
+ */
+public class StaticMethod extends Expr {
+
+ private Class> clazz;
+ private String methodName;
+ private ExprList exprList;
+
+ public StaticMethod(String className, String methodName, Location location) {
+ init(className, methodName, ExprList.NULL_EXPR_LIST, location);
+ }
+
+ public StaticMethod(String className, String methodName, ExprList exprList, Location location) {
+ if (exprList == null || exprList.length() == 0) {
+ throw new ParseException("exprList can not be blank", location);
+ }
+ init(className, methodName, exprList, location);
+ }
+
+ private void init(String className, String methodName, ExprList exprList, Location location) {
+ try {
+ this.clazz = Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ throw new ParseException("Class not found: " + className, location, e);
+ } catch (Exception e) {
+ throw new ParseException(e.getMessage(), location, e);
+ }
+ this.methodName = methodName;
+ this.exprList = exprList;
+ this.location = location;
+ }
+
+ public Object eval(Scope scope) {
+ Object[] argValues = exprList.evalExprList(scope);
+ MethodInfo methodInfo;
+ try {
+ methodInfo = MethodKit.getMethod(clazz, methodName, argValues);
+ } catch (Exception e) {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+
+ // StaticMethod 是固定的存在,不支持 null safe,null safe 只支持具有动态特征的用法
+ if (methodInfo == null) {
+ throw new TemplateException(Method.buildMethodNotFoundSignature("public static method not found: " + clazz.getName() + "::", methodName, argValues), location);
+ }
+ if (!methodInfo.isStatic()) {
+ throw new TemplateException(Method.buildMethodNotFoundSignature("Not public static method: " + clazz.getName() + "::", methodName, argValues), location);
+ }
+
+ try {
+ return methodInfo.invoke(null, argValues);
+ } catch (Exception e) {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/Ternary.java b/src/main/java/com/jfinal/template/expr/ast/Ternary.java
new file mode 100644
index 0000000..f17afba
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/Ternary.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Ternary
+ */
+public class Ternary extends Expr {
+
+ private Expr cond;
+ private Expr exprOne;
+ private Expr exprTwo;
+
+ /**
+ * cond ? exprOne : exprTwo
+ */
+ public Ternary(Expr cond, Expr exprOne, Expr exprTwo, Location location) {
+ if (cond == null || exprOne == null || exprTwo == null) {
+ throw new ParseException("The parameter of ternary expression can not be blank", location);
+ }
+ this.cond = cond;
+ this.exprOne = exprOne;
+ this.exprTwo = exprTwo;
+ this.location = location;
+ }
+
+ public Object eval(Scope scope) {
+ return Logic.isTrue(cond.eval(scope)) ? exprOne.eval(scope) : exprTwo.eval(scope);
+ }
+}
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/expr/ast/Unary.java b/src/main/java/com/jfinal/template/expr/ast/Unary.java
new file mode 100644
index 0000000..2599fbe
--- /dev/null
+++ b/src/main/java/com/jfinal/template/expr/ast/Unary.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.expr.ast;
+
+import java.math.BigDecimal;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.expr.Sym;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * unary : ('!' | '+' | '-'| '++' | '--') expr
+ *
+ * 只支持 +expr 与 -expr
+ * !expr、 ++expr、 --expr 分别由 Logic、IncDec 支持
+ */
+public class Unary extends Expr {
+
+ private Sym op;
+ private Expr expr;
+
+ public Unary(Sym op, Expr expr, Location location) {
+ if (expr == null) {
+ throw new ParseException("The parameter of \"" + op.value() + "\" operator can not be blank", location);
+ }
+ this.op = op;
+ this.expr = expr;
+ this.location = location;
+ }
+
+ /**
+ * unary : ('!' | '+' | '-'| '++' | '--') expr
+ */
+ public Object eval(Scope scope) {
+ Object value = expr.eval(scope);
+ if (value == null) {
+ if (scope.getCtrl().isNullSafe()) {
+ return null;
+ }
+ throw new TemplateException("The parameter of \"" + op.value() + "\" operator can not be blank", location);
+ }
+ if (! (value instanceof Number) ) {
+ throw new TemplateException(op.value() + " operator only support int long float double BigDecimal type", location);
+ }
+
+ switch (op) {
+ case ADD:
+ return value;
+ case SUB:
+ Number n = (Number)value;
+ if (n instanceof Integer) {
+ return Integer.valueOf(-n.intValue());
+ }
+ if (n instanceof Long) {
+ return Long.valueOf(-n.longValue());
+ }
+ if (n instanceof Float) {
+ return Float.valueOf(-n.floatValue());
+ }
+ if (n instanceof Double) {
+ return Double.valueOf(-n.doubleValue());
+ }
+ if (n instanceof BigDecimal) {
+ return ((BigDecimal)n).negate();
+ }
+ throw new TemplateException("Unsupported data type: " + n.getClass().getName(), location);
+ default :
+ throw new TemplateException("Unsupported operator: " + op.value(), location);
+ }
+ }
+}
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/ext/directive/DateDirective.java b/src/main/java/com/jfinal/template/ext/directive/DateDirective.java
new file mode 100644
index 0000000..51ba3c6
--- /dev/null
+++ b/src/main/java/com/jfinal/template/ext/directive/DateDirective.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.ext.directive;
+
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import com.jfinal.template.Directive;
+import com.jfinal.template.Env;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.expr.ast.Expr;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * 不带参时,按默认 pattern 输出当前日期
+ *
+ * #date() 指令支持无参时获取当前指令,第一个参数 string 当成是 pattern
+ *
+ * 日期输出指令,第一个参数是被输出的 java.util.Date 对象或其子类对象
+ * 无第二个参数时按默认 patter 输出,第二个参数为 expr 表达式,表示 pattern
+ * 第二个为 date 时,表示当第一个为 null 时的默认值
+ */
+public class DateDirective extends Directive {
+
+ private Expr valueExpr;
+ private Expr datePatternExpr;
+ private int paraNum;
+
+ public void setExprList(ExprList exprList) {
+ this.paraNum = exprList.length();
+ if (paraNum > 2) {
+ throw new ParseException("Wrong number parameter of #date directive, two parameters allowed at most", location);
+ }
+
+ if (paraNum == 0) {
+ this.valueExpr = null;
+ this.datePatternExpr = null;
+ } else if (paraNum == 1) {
+ this.valueExpr = exprList.getExprArray()[0];
+ this.datePatternExpr = null;
+ } else if (paraNum == 2) {
+ this.valueExpr = exprList.getExprArray()[0];
+ this.datePatternExpr = exprList.getExprArray()[1];
+ }
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ if (paraNum == 0) {
+ outputToday(env, writer);
+ } else if (paraNum == 1) {
+ outputWithoutDatePattern(env, scope, writer);
+ } else if (paraNum == 2) {
+ outputWithDatePattern(env, scope, writer);
+ }
+ }
+
+ private void outputToday(Env env, Writer writer) {
+ Object value = format(new java.util.Date(), env.getEngineConfig().getDatePattern());
+ write(writer, value.toString());
+ }
+
+ private void outputWithoutDatePattern(Env env, Scope scope, Writer writer) {
+ Object value = valueExpr.eval(scope);
+ if (value != null) {
+ value = format(value, env.getEngineConfig().getDatePattern());
+ write(writer, value.toString());
+ }
+ }
+
+ private void outputWithDatePattern(Env env, Scope scope, Writer writer) {
+ Object value = valueExpr.eval(scope);
+ if (value == null) {
+ return ;
+ }
+
+ Object dp = this.datePatternExpr.eval(scope);
+ if ( !(dp instanceof String) ) {
+ throw new TemplateException("The sencond parameter dataPattern of #date directive must be String", location);
+ }
+ value = format(value, (String)dp);
+ write(writer, value.toString());
+ }
+
+ private String format(Object value, String datePattern) {
+ try {
+ return new SimpleDateFormat(datePattern).format(value);
+ } catch (Exception e) {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+ }
+}
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/ext/directive/EscapeDirective.java b/src/main/java/com/jfinal/template/ext/directive/EscapeDirective.java
new file mode 100644
index 0000000..38e17e0
--- /dev/null
+++ b/src/main/java/com/jfinal/template/ext/directive/EscapeDirective.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.ext.directive;
+
+import java.io.Writer;
+import com.jfinal.template.Directive;
+import com.jfinal.template.Env;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Escape 对字符串进行转义
+ * 用法:
+ * #escape(value)
+ */
+public class EscapeDirective extends Directive {
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ Object value = exprList.eval(scope);
+ if (value != null) {
+ write(writer, escape(value.toString()));
+ }
+ }
+
+ // TODO 挪到 StrKit 中
+ private String escape(String str) {
+ if (str == null || str.length() == 0) {
+ return str;
+ }
+
+ int len = str.length();
+ StringBuilder ret = new StringBuilder(len * 2);
+ for (int i = 0; i < len; i++) {
+ char cur = str.charAt(i);
+ switch (cur) {
+ case '<':
+ ret.append("<");
+ break;
+ case '>':
+ ret.append(">");
+ break;
+ case '\"':
+ ret.append(""");
+ break;
+ case '\'':
+ ret.append("'"); // IE 不支持 ' 考虑 '
+ break;
+ case '&':
+ ret.append("&");
+ break;
+ default:
+ ret.append(cur);
+ break;
+ }
+ }
+
+ return ret.toString();
+ }
+}
diff --git a/src/main/java/com/jfinal/template/ext/directive/NowDirective.java b/src/main/java/com/jfinal/template/ext/directive/NowDirective.java
new file mode 100644
index 0000000..7c4bddb
--- /dev/null
+++ b/src/main/java/com/jfinal/template/ext/directive/NowDirective.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.ext.directive;
+
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import com.jfinal.template.Directive;
+import com.jfinal.template.Env;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * 输出当前时间,默认考虑是输出时间,给 pattern 输出可能是 Date、DateTime、Timestamp
+ * 带 String 参数,表示 pattern
+ */
+public class NowDirective extends Directive {
+
+ public void setExrpList(ExprList exprList) {
+ if (exprList.length() > 1) {
+ throw new ParseException("#now directive support one parameter only", location);
+ }
+ super.setExprList(exprList);
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ String dataPattern;
+ if (exprList.length() == 0) {
+ dataPattern = env.getEngineConfig().getDatePattern();
+ } else {
+ Object dp = exprList.eval(scope);
+ if (dp instanceof String) {
+ dataPattern = (String)dp;
+ } else {
+ throw new TemplateException("The parameter of #new directive must be String", location);
+ }
+ }
+
+ try {
+ String value = new SimpleDateFormat(dataPattern).format(new java.util.Date());
+ write(writer, value);
+ } catch (Exception e) {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/ext/directive/RandomDirective.java b/src/main/java/com/jfinal/template/ext/directive/RandomDirective.java
new file mode 100644
index 0000000..c57f548
--- /dev/null
+++ b/src/main/java/com/jfinal/template/ext/directive/RandomDirective.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.ext.directive;
+
+import java.io.Writer;
+import com.jfinal.template.Directive;
+import com.jfinal.template.Env;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * 输出随机数
+ */
+public class RandomDirective extends Directive {
+
+ private java.util.Random random = new java.util.Random();
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ write(writer, String.valueOf(random.nextInt()));
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/ext/directive/RenderDirective.java b/src/main/java/com/jfinal/template/ext/directive/RenderDirective.java
new file mode 100644
index 0000000..58543fc
--- /dev/null
+++ b/src/main/java/com/jfinal/template/ext/directive/RenderDirective.java
@@ -0,0 +1,182 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.ext.directive;
+
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Map;
+import com.jfinal.template.Directive;
+import com.jfinal.template.EngineConfig;
+import com.jfinal.template.Env;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.expr.ast.Assign;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.source.ISource;
+import com.jfinal.template.stat.Ctrl;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Parser;
+import com.jfinal.template.stat.Scope;
+import com.jfinal.template.stat.ast.Define;
+import com.jfinal.template.stat.ast.Include;
+import com.jfinal.template.stat.ast.Stat;
+
+/**
+ * #render 指令用于动态渲染子模板,作为 include 指令的补充
+ *
+ *
+ * 两种用法:
+ * 1:只传入一个参数,参数可以是 String 常量,也可以是任意表达式
+ * #render("_hot.html")
+ * #render(subFile)
+ *
+ * 2:传入任意多个参数,除第一个参数以外的所有参数必须是赋值表达式,用于实现参数传递功能
+ * #render("_hot.html", title = "热门新闻", list = newsList)
+ *
+ * 上例中传递了 title、list 两个参数,可以代替父模板中的 #set 指令传参方式
+ * 并且此方式传入的参数只在子模板作用域有效,不会污染父模板作用域
+ *
+ * 这种传参方式有利于将子模板模块化,例如上例的调用改成如下的参数:
+ * #render("_hot.html", title = "热门项目", list = projectList)
+ * 通过这种传参方式在子模板 _hot.html 之中,完全不需要修改对于 title 与 list
+ * 这两个变量的处理代码,就实现了对 “热门项目” 数据的渲染
+ *
+ *
+ */
+public class RenderDirective extends Directive {
+
+ private String parentFileName;
+ private Map statInfoCache = new HashMap();
+
+ public void setExprList(ExprList exprList) {
+ int len = exprList.length();
+ if (len == 0) {
+ throw new ParseException("The parameter of #render directive can not be blank", location);
+ }
+ if (len > 1) {
+ for (int i = 1; i < len; i++) {
+ if (!(exprList.getExpr(i) instanceof Assign)) {
+ throw new ParseException("The " + i + "th parameter of #render directive must be an assignment expression", location);
+ }
+ }
+ }
+
+ /**
+ * 从 location 中获取父模板的 fileName,用于生成 subFileName
+ * 如果是孙子模板,那么 parentFileName 为最顶层的模板,而非直接上层的模板
+ */
+ this.parentFileName = location.getTemplateFile();
+ this.exprList = exprList;
+ }
+
+ /**
+ * 对 exprList 进行求值,并将第一个表达式的值作为模板名称返回,
+ * 开启 local assignment 保障 #render 指令参数表达式列表
+ * 中的赋值表达式在当前 scope 中进行,有利于模块化
+ */
+ private Object evalAssignExpressionAndGetFileName(Scope scope) {
+ Ctrl ctrl = scope.getCtrl();
+ try {
+ ctrl.setLocalAssignment();
+ return exprList.evalExprList(scope)[0];
+ } finally {
+ ctrl.setWisdomAssignment();
+ }
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ // 在 exprList.eval(scope) 之前创建,使赋值表达式在本作用域内进行
+ scope = new Scope(scope);
+
+ Object value = evalAssignExpressionAndGetFileName(scope);
+ if (!(value instanceof String)) {
+ throw new TemplateException("The parameter value of #render directive must be String", location);
+ }
+
+ String subFileName = Include.getSubFileName((String)value, parentFileName);
+ StatInfo statInfo = statInfoCache.get(subFileName);
+ if (statInfo == null) {
+ statInfo = parseStatInfo(env, subFileName);
+ statInfoCache.put(subFileName, statInfo);
+ } else if (env.getEngineConfig().isDevMode()) {
+ // statInfo.env.isSourceListModified() 逻辑可以支持 #render 子模板中的 #include 过来的子模板在 devMode 下在修改后可被重加载
+ if (statInfo.source.isModified() || statInfo.env.isSourceListModified()) {
+ statInfo = parseStatInfo(env, subFileName);
+ statInfoCache.put(subFileName, statInfo);
+ }
+ }
+
+ statInfo.stat.exec(statInfo.env, scope, writer);
+ scope.getCtrl().setJumpNone();
+ }
+
+ private StatInfo parseStatInfo(Env env, String subFileName) {
+ EngineConfig config = env.getEngineConfig();
+ // FileSource fileSource = new FileSource(config.getBaseTemplatePath(), subFileName, config.getEncoding());
+ ISource fileSource = config.getSourceFactory().getSource(config.getBaseTemplatePath(), subFileName, config.getEncoding());
+
+ try {
+ EnvSub envSub = new EnvSub(env);
+ Stat stat = new Parser(envSub, fileSource.getContent(), subFileName).parse();
+ return new StatInfo(envSub, stat, fileSource);
+ } catch (Exception e) {
+ throw new ParseException(e.getMessage(), location, e);
+ }
+ }
+
+ private static class StatInfo {
+ EnvSub env;
+ Stat stat;
+ ISource source;
+
+ StatInfo(EnvSub env, Stat stat, ISource source) {
+ this.env = env;
+ this.stat = stat;
+ this.source = source;
+ }
+ }
+
+ /**
+ * EnvSub 用于将子模板与父模板中的模板函数隔离开来,
+ * 否则在子模板被修改并被重新解析时会再次添加子模板中的
+ * 模板函数,从而抛出异常
+ *
+ * EnvSub 也可以使子模板中定义的模板函数不与上层产生冲突,
+ * 有利于动态型模板渲染的模块化
+ *
+ * 注意: #render 子模板中定义的模板函数无法被上层调用
+ */
+ private static class EnvSub extends Env {
+ Env parentEnv;
+
+ public EnvSub(Env parentEnv) {
+ super(parentEnv.getEngineConfig());
+ this.parentEnv = parentEnv;
+ }
+
+ /**
+ * 接管父类 getFunction(),先从子模板中找模板函数,找不到再去父模板中找
+ */
+ public Define getFunction(String functionName) {
+ Define func = functionMap.get(functionName);
+ return func != null ? func : parentEnv.getFunction(functionName);
+ }
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/ext/directive/StringDirective.java b/src/main/java/com/jfinal/template/ext/directive/StringDirective.java
new file mode 100644
index 0000000..72dbee2
--- /dev/null
+++ b/src/main/java/com/jfinal/template/ext/directive/StringDirective.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.ext.directive;
+
+import java.io.Writer;
+import com.jfinal.template.Directive;
+import com.jfinal.template.Env;
+import com.jfinal.template.FastStringWriter;
+import com.jfinal.template.expr.ast.Const;
+import com.jfinal.template.expr.ast.Expr;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.expr.ast.Id;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * #string 指令方便定义大量的多行文本变量,这个是 java 语言中极为需要的功能
+ *
+ * 定义:
+ * #string(name)
+ * 在此是大量的字符串
+ * #end
+ *
+ * 使用:
+ * #(name)
+ */
+public class StringDirective extends Directive {
+
+ private String name;
+ private boolean isLocalAssignment = false;
+
+ public void setExprList(ExprList exprList) {
+ Expr[] exprArray = exprList.getExprArray();
+ if (exprArray.length == 0) {
+ throw new ParseException("#string directive parameter cant not be null", location);
+ }
+ if (exprArray.length > 2) {
+ throw new ParseException("wrong number of #string directive parameter, two parameters allowed at most", location);
+ }
+
+ if (!(exprArray[0] instanceof Id)) {
+ throw new ParseException("#string first parameter must be identifier", location);
+ }
+ this.name = ((Id)exprArray[0]).getId();
+ if (exprArray.length == 2) {
+ if (exprArray[1] instanceof Const) {
+ if (((Const)exprArray[1]).isBoolean()) {
+ this.isLocalAssignment = ((Const)exprArray[1]).getBoolean();
+ } else {
+ throw new ParseException("#string sencond parameter must be boolean", location);
+ }
+ }
+ }
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ FastStringWriter fsw = new FastStringWriter();
+ stat.exec(env, scope, fsw);
+
+ if (this.isLocalAssignment) {
+ scope.setLocal(name, fsw.toString());
+ } else {
+ scope.set(name, fsw.toString());
+ }
+ }
+
+ /**
+ * hasEnd() 方法返回 true 时,表示该指令拥有指令体以及 #end 结束块
+ * 模板引擎在解析时会将 "指令体" 赋值到 stat 属性中,在 exec(...) 方法中
+ * 可通过 stat.exec(...) 执行 "指令体" 内部的所有指令
+ */
+ public boolean hasEnd() {
+ return true;
+ }
+}
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/ext/extensionmethod/ByteExt.java b/src/main/java/com/jfinal/template/ext/extensionmethod/ByteExt.java
new file mode 100644
index 0000000..0940ebf
--- /dev/null
+++ b/src/main/java/com/jfinal/template/ext/extensionmethod/ByteExt.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.ext.extensionmethod;
+
+/**
+ * 针对 java.lang.Byte 的扩展方法
+ *
+ * 用法:
+ * #if(value.toInt() == 123)
+ */
+public class ByteExt {
+
+ public Boolean toBoolean(Byte self) {
+ return self != 0;
+ }
+
+ public Integer toInt(Byte self) {
+ return self.intValue();
+ }
+
+ public Long toLong(Byte self) {
+ return self.longValue();
+ }
+
+ public Float toFloat(Byte self) {
+ return self.floatValue();
+ }
+
+ public Double toDouble(Byte self) {
+ return self.doubleValue();
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/ext/extensionmethod/DoubleExt.java b/src/main/java/com/jfinal/template/ext/extensionmethod/DoubleExt.java
new file mode 100644
index 0000000..f15707d
--- /dev/null
+++ b/src/main/java/com/jfinal/template/ext/extensionmethod/DoubleExt.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.ext.extensionmethod;
+
+/**
+ * 针对 java.lang.Double 的扩展方法
+ *
+ * 用法:
+ * #if(value.toInt() == 123)
+ */
+public class DoubleExt {
+
+ public Boolean toBoolean(Double self) {
+ return self != 0;
+ }
+
+ public Integer toInt(Double self) {
+ return self.intValue();
+ }
+
+ public Long toLong(Double self) {
+ return self.longValue();
+ }
+
+ public Float toFloat(Double self) {
+ return self.floatValue();
+ }
+
+ public Double toDouble(Double self) {
+ return self;
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/ext/extensionmethod/FloatExt.java b/src/main/java/com/jfinal/template/ext/extensionmethod/FloatExt.java
new file mode 100644
index 0000000..1380df8
--- /dev/null
+++ b/src/main/java/com/jfinal/template/ext/extensionmethod/FloatExt.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.ext.extensionmethod;
+
+/**
+ * 针对 java.lang.Float 的扩展方法
+ *
+ * 用法:
+ * #if(value.toInt() == 123)
+ */
+public class FloatExt {
+
+ public Boolean toBoolean(Float self) {
+ return self != 0;
+ }
+
+ public Integer toInt(Float self) {
+ return self.intValue();
+ }
+
+ public Long toLong(Float self) {
+ return self.longValue();
+ }
+
+ public Float toFloat(Float self) {
+ return self;
+ }
+
+ public Double toDouble(Float self) {
+ return self.doubleValue();
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/ext/extensionmethod/IntegerExt.java b/src/main/java/com/jfinal/template/ext/extensionmethod/IntegerExt.java
new file mode 100644
index 0000000..086790e
--- /dev/null
+++ b/src/main/java/com/jfinal/template/ext/extensionmethod/IntegerExt.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.ext.extensionmethod;
+
+/**
+ * 针对 java.lang.Integer 的扩展方法
+ *
+ * 重要用途:
+ * Controller.keepPara() 方法会将所有类型的数据当成 String 并传回到
+ * 到模板中,所以模板中的如下代码将无法工作:
+ * #if(age > 18)
+ * ....
+ * #end
+ *
+ * 以上代码,第一次渲染模板时,由于 age 为 int 类型,那么 if 语句中是正确的表达式,
+ * 当提交表单后在后端调用 keepPara() 以后 age 变成了 String 类型,表达式错误,
+ * 在有了扩展方法以后,解决办法如下:
+ * #if(age.toInt() > 18)
+ * ...
+ * #end
+ * 如上所示,无论 age 是 String 还是 int 型,调用其 toInt() 方法将一直确保
+ * age 为 int 类型
+ *
+ * 以上用法,必须针对 String 与 Integer 同时扩展一个 toInt() 方法,模板表达式中的
+ * 变量为 String 或为 Integer 时都存在 toInt() 方法可供调用
+ *
+ *
+ * 用法:
+ * #if(age.toInt() > 18)
+ */
+public class IntegerExt {
+
+ public Boolean toBoolean(Integer self) {
+ return self != 0;
+ }
+
+ public Integer toInt(Integer self) {
+ return self;
+ }
+
+ public Long toLong(Integer self) {
+ return self.longValue();
+ }
+
+ public Float toFloat(Integer self) {
+ return self.floatValue();
+ }
+
+ public Double toDouble(Integer self) {
+ return self.doubleValue();
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/ext/extensionmethod/LongExt.java b/src/main/java/com/jfinal/template/ext/extensionmethod/LongExt.java
new file mode 100644
index 0000000..3870090
--- /dev/null
+++ b/src/main/java/com/jfinal/template/ext/extensionmethod/LongExt.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.ext.extensionmethod;
+
+/**
+ * 针对 java.lang.Long 的扩展方法
+ *
+ * 用法:
+ * #if(value.toInt() == 123)
+ */
+public class LongExt {
+
+ public Boolean toBoolean(Long self) {
+ return self != 0;
+ }
+
+ public Integer toInt(Long self) {
+ return self.intValue();
+ }
+
+ public Long toLong(Long self) {
+ return self;
+ }
+
+ public Float toFloat(Long self) {
+ return self.floatValue();
+ }
+
+ public Double toDouble(Long self) {
+ return self.doubleValue();
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/ext/extensionmethod/ShortExt.java b/src/main/java/com/jfinal/template/ext/extensionmethod/ShortExt.java
new file mode 100644
index 0000000..0111b4c
--- /dev/null
+++ b/src/main/java/com/jfinal/template/ext/extensionmethod/ShortExt.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.ext.extensionmethod;
+
+/**
+ * 针对 java.lang.Short 的扩展方法
+ *
+ * 用法:
+ * #if(value.toInt() == 123)
+ */
+public class ShortExt {
+
+ public Boolean toBoolean(Short self) {
+ return self != 0;
+ }
+
+ public Integer toInt(Short self) {
+ return self.intValue();
+ }
+
+ public Long toLong(Short self) {
+ return self.longValue();
+ }
+
+ public Float toFloat(Short self) {
+ return self.floatValue();
+ }
+
+ public Double toDouble(Short self) {
+ return self.doubleValue();
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/ext/extensionmethod/StringExt.java b/src/main/java/com/jfinal/template/ext/extensionmethod/StringExt.java
new file mode 100644
index 0000000..23d0a49
--- /dev/null
+++ b/src/main/java/com/jfinal/template/ext/extensionmethod/StringExt.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.ext.extensionmethod;
+
+import com.jfinal.kit.StrKit;
+
+/**
+ * 针对 java.lang.String 的扩展方法
+ *
+ * 重要用途:
+ * Controller.keepPara() 方法会将所有类型的数据当成 String 并传回到
+ * 到模板中,所以模板中的如下代码将无法工作:
+ * #if(age > 18)
+ * ....
+ * #end
+ *
+ * 以上代码,第一次渲染模板时,由于 age 为 int 类型,那么 if 语句中是正确的表达式,
+ * 当提交表单后在后端调用 keepPara() 以后 age 变成了 String 类型,表达式错误,
+ * 在有了扩展方法以后,解决办法如下:
+ * #if(age.toInt() > 18)
+ * ...
+ * #end
+ * 如上所示,无论 age 是 String 还是 int 型,调用其 toInt() 方法将一直确保
+ * age 为 int 类型
+ *
+ * 以上用法,必须针对 String 与 Integer 同时扩展一个 toInt() 方法,模板表达式中的
+ * 变量为 String 或为 Integer 时都存在 toInt() 方法可供调用
+ *
+ * 用法:
+ * #if(age.toInt() > 18)
+ */
+public class StringExt {
+
+ /**
+ * StringExt.toBoolean() 是数据类型转换,所以与 Logic.isTrue(String)
+ * 中的逻辑不同,后者只要 String 值非 null 并且 length() > 0 即返回 true
+ */
+ public Boolean toBoolean(String self) {
+ if (StrKit.isBlank(self)) {
+ return null; // return Boolean.FALSE;
+ }
+
+ String value = self.trim().toLowerCase();
+ if ("true".equals(value) || "1".equals(value)) { // 未来考虑 "yes"、"on"
+ return Boolean.TRUE;
+ } else if ("false".equals(value) || "0".equals(value)) {
+ return Boolean.FALSE;
+ } else {
+ throw new RuntimeException("Can not parse to boolean type of value: \"" + self + "\"");
+ }
+ }
+
+ public Integer toInt(String self) {
+ return StrKit.isBlank(self) ? null : Integer.parseInt(self);
+ }
+
+ public Long toLong(String self) {
+ return StrKit.isBlank(self) ? null : Long.parseLong(self);
+ }
+
+ public Float toFloat(String self) {
+ return StrKit.isBlank(self) ? null : Float.parseFloat(self);
+ }
+
+ public Double toDouble(String self) {
+ return StrKit.isBlank(self) ? null : Double.parseDouble(self);
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/ext/spring/JFinalView.java b/src/main/java/com/jfinal/template/ext/spring/JFinalView.java
new file mode 100644
index 0000000..ae218b2
--- /dev/null
+++ b/src/main/java/com/jfinal/template/ext/spring/JFinalView.java
@@ -0,0 +1,166 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.ext.spring;
+
+import java.io.Writer;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import org.springframework.web.servlet.view.AbstractTemplateView;
+
+/**
+ * JFinalView
+ *
+ *
+ * 关键设置:
+ * 1:setContentType("text/html;charset=UTF-8") 设置 content type 字符集为 UTF-8
+ *
+ * 2:setExposeRequestAttributes(true) 设置将 request 中的属性值注入到 model 中去
+ * 便于在模板中使用 #(value) 访问 request.setAttribute(...) 进去的值
+ *
+ * 3: setExposeSessionAttributes(true) 设置将 session 中的属性值注入到 model 中去
+ * 使用在模板中使用 #(value) 访问 session.setAttribute(...) 进去的值
+ *
+ * 注意:JFinalViewResolver.setSessionInView(true) 中的配置与
+ * JFinalView.setExposeSessionAttributes(true) 可实现
+ * 相似的功能,区别在于前者访问方式为 #(session.value) 而后者为
+ * #(value),两种配置只选其一
+ *
+ */
+public class JFinalView extends AbstractTemplateView {
+
+ @Override
+ protected void renderMergedTemplateModel(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
+ if (JFinalViewResolver.sessionInView) {
+ HttpSession hs = request.getSession(JFinalViewResolver.createSession);
+ if (hs != null) {
+ model.put("session", new InnerSession(hs));
+ }
+ }
+
+ Writer writer = response.getWriter();
+ JFinalViewResolver.engine.getTemplate(getUrl()).render(model, writer);
+ writer.flush();
+ }
+}
+
+@SuppressWarnings({"unchecked", "rawtypes", "deprecation"})
+class InnerSession extends HashMap implements HttpSession {
+
+ private static final long serialVersionUID = -8679493647540628009L;
+ private HttpSession session;
+
+ public InnerSession(HttpSession session) {
+ this.session = session;
+ }
+
+ // HashMap 相关方法处理 ----------------------------------------------------
+ /**
+ * 覆盖 HashMap 的 put
+ */
+ public Object put(Object name, Object value) {
+ session.setAttribute((String)name, value);
+ return null;
+ }
+
+ /**
+ * 覆盖 HashMap 的 get
+ */
+ public Object get(Object name) {
+ return session.getAttribute((String)name);
+ }
+
+ // Session 相关方法处理 ----------------------------------------------------
+ public Object getAttribute(String key) {
+ return session.getAttribute(key);
+ }
+
+ public Enumeration getAttributeNames() {
+ return session.getAttributeNames();
+ }
+
+ public long getCreationTime() {
+ return session.getCreationTime();
+ }
+
+ public String getId() {
+ return session.getId();
+ }
+
+ public long getLastAccessedTime() {
+ return session.getLastAccessedTime();
+ }
+
+ public int getMaxInactiveInterval() {
+ return session.getMaxInactiveInterval();
+ }
+
+ public ServletContext getServletContext() {
+ return session.getServletContext();
+ }
+
+ public javax.servlet.http.HttpSessionContext getSessionContext() {
+ return session.getSessionContext();
+ }
+
+ public Object getValue(String key) {
+ return session.getValue(key);
+ }
+
+ public String[] getValueNames() {
+ return session.getValueNames();
+ }
+
+ public void invalidate() {
+ session.invalidate();
+ }
+
+ public boolean isNew() {
+ return session.isNew();
+ }
+
+ public void putValue(String key, Object value) {
+ session.putValue(key, value);
+ }
+
+ public void removeAttribute(String key) {
+ session.removeAttribute(key);
+ }
+
+ public void removeValue(String key) {
+ session.removeValue(key);
+ }
+
+ public void setAttribute(String key, Object value) {
+ session.setAttribute(key, value);
+ }
+
+ public void setMaxInactiveInterval(int maxInactiveInterval) {
+ session.setMaxInactiveInterval(maxInactiveInterval);
+ }
+}
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/ext/spring/JFinalViewResolver.java b/src/main/java/com/jfinal/template/ext/spring/JFinalViewResolver.java
new file mode 100644
index 0000000..00a99ad
--- /dev/null
+++ b/src/main/java/com/jfinal/template/ext/spring/JFinalViewResolver.java
@@ -0,0 +1,254 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.ext.spring;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.servlet.ServletContext;
+import org.springframework.web.servlet.view.AbstractTemplateViewResolver;
+import com.jfinal.kit.StrKit;
+import com.jfinal.template.Directive;
+import com.jfinal.template.Engine;
+import com.jfinal.template.source.ClassPathSourceFactory;
+import com.jfinal.template.source.ISourceFactory;
+
+/**
+ * JFinalViewResolver
+ *
+ *
+ * 关键配置:
+ * 1:setDevMode(true) 设置支持热加载模板文件
+ *
+ * 2:addSharedFunction(file) 添加共享函数文件
+ *
+ * 3:setSourceFactory(new ClassPathSourceFactory()),从 class path 与 jar 包中加载模板文件
+ * 一般用于 sprint boot
+ *
+ * 4:setSessionInView(true) 设置在模板中可通过 #(session.value) 访问 session 中的数据
+ *
+ * 5:setCreateSession(boolean) 用来设置 request.getSession(boolean) 调时的参数
+ *
+ * 6:setBaseTemplatePath(path) 设置模板文件所在的基础路径,通常用于 spring mvc
+ * 默认值为 web 根路径,一般不需要设置
+ *
+ */
+public class JFinalViewResolver extends AbstractTemplateViewResolver {
+
+ public static final Engine engine = new Engine();
+
+ static List sharedFunctionFiles = new ArrayList();
+ static boolean sessionInView = false;
+ static boolean createSession = true;
+
+ public Engine getEngine() {
+ return engine;
+ }
+
+ /**
+ * 设置开发模式,值为 true 时支持模板文件热加载
+ */
+ public void setDevMode(boolean devMode) {
+ engine.setDevMode(devMode);
+ }
+
+ /**
+ * 设置 shared function 文件,多个文件用逗号分隔
+ *
+ * 主要用于 Spring MVC 的 xml 配置方式
+ *
+ * Spring Boot 的代码配置方式可使用 addSharedFunction(...) 进行配置
+ */
+ public void setSharedFunction(String sharedFunctionFiles) {
+ if (StrKit.isBlank(sharedFunctionFiles)) {
+ throw new IllegalArgumentException("sharedFunctionFiles can not be blank");
+ }
+
+ String[] fileArray = sharedFunctionFiles.split(",");
+ for (String fileName : fileArray) {
+ JFinalViewResolver.sharedFunctionFiles.add(fileName);
+ }
+ }
+
+ /**
+ * 添加 shared function 文件,可调用多次添加多个文件
+ */
+ public void addSharedFunction(String fileName) {
+ // 等待 SourceFactory、baseTemplatePath 配置到位,利用 sharedFunctionFiles 实现延迟加载
+ sharedFunctionFiles.add(fileName);
+ }
+
+ /**
+ * 添加自定义指令
+ */
+ public void addDirective(String directiveName, Directive directive) {
+ engine.addDirective(directiveName, directive);
+ }
+
+ /**
+ * 添加共享对象
+ */
+ public void addSharedObject(String name, Object object) {
+ engine.addSharedObject(name, object);
+ }
+
+ /**
+ * 添加共享方法
+ */
+ public void addSharedMethod(Object sharedMethodFromObject) {
+ engine.addSharedMethod(sharedMethodFromObject);
+ }
+
+ /**
+ * 添加共享方法
+ */
+ public void addSharedMethod(Class> sharedMethodFromClass) {
+ engine.addSharedMethod(sharedMethodFromClass);
+ }
+
+ /**
+ * 添加扩展方法
+ */
+ public static void addExtensionMethod(Class> targetClass, Object objectOfExtensionClass) {
+ Engine.addExtensionMethod(targetClass, objectOfExtensionClass);
+ }
+
+ /**
+ * 添加扩展方法
+ */
+ public static void addExtensionMethod(Class> targetClass, Class> extensionClass) {
+ Engine.addExtensionMethod(targetClass, extensionClass);
+ }
+
+ /**
+ * 设置 ISourceFactory 用于为 engine 切换不同的 ISource 实现类
+ *
+ *
+ * 配置为 ClassPathSourceFactory 时特别注意:
+ * 由于在 initServletContext() 通过如下方法中已设置了 baseTemplatePath 值:
+ * setBaseTemplatePath(servletContext.getRealPath("/"))
+ *
+ * 而 ClassPathSourceFactory 在 initServletContext() 方法中设置的
+ * 值之下不能工作,所以在本方法中通过如下方法清掉了该值:
+ * setBaseTemplatePath(null)
+ *
+ * 这种处理方式适用于绝大部分场景,如果在使用 ClassPathSourceFactory 的同时
+ * 仍然需要设置 baseTemplatePath,则在调用该方法 “之后” 通过如下代码再次配置:
+ * setBaseTemplatePath(value)
+ *
+ */
+ public void setSourceFactory(ISourceFactory sourceFactory) {
+ if (sourceFactory instanceof ClassPathSourceFactory) {
+ engine.setBaseTemplatePath(null);
+ }
+ engine.setSourceFactory(sourceFactory);
+ }
+
+ /**
+ * 设置模板基础路径
+ */
+ public void setBaseTemplatePath(String baseTemplatePath) {
+ engine.setBaseTemplatePath(baseTemplatePath);
+ }
+
+ /**
+ * 设置为 true 时支持在模板中使用 #(session.value) 形式访问 session 中的数据
+ */
+ public void setSessionInView(boolean sessionInView) {
+ JFinalViewResolver.sessionInView = sessionInView;
+ }
+
+ /**
+ * 在使用 request.getSession(createSession) 时传入
+ * 用来指示 session 不存在时是否立即创建
+ */
+ public void setCreateSession(boolean createSession) {
+ JFinalViewResolver.createSession = createSession;
+ }
+
+ /**
+ * 设置 encoding
+ */
+ public void setEncoding(String encoding) {
+ engine.setEncoding(encoding);
+ }
+
+ /**
+ * 设置 #date(...) 指令,对于 Date、Timestamp、Time 的输出格式
+ */
+ public void setDatePattern(String datePattern) {
+ engine.setDatePattern(datePattern);
+ }
+
+ // ---------------------------------------------------------------
+
+ public JFinalViewResolver() {
+ setViewClass(requiredViewClass());
+ setOrder(0);
+ setContentType("text/html;charset=UTF-8");
+ // setPrefix("/view/");
+ // setSuffix(".html");
+ }
+
+ @Override
+ protected Class> requiredViewClass() {
+ return JFinalView.class;
+ }
+
+ /**
+ * spring 回调,利用 ServletContext 做必要的初始化工作
+ */
+ @Override
+ protected void initServletContext(ServletContext servletContext) {
+ super.initServletContext(servletContext);
+
+ initBaseTemplatePath(servletContext);
+ initSharedFunction();
+ }
+
+ /**
+ * 初始化 baseTemplatePath 值,启用 ClassPathSourceFactory 时
+ * 无需设置 baseTemplatePath 为 web 根路径
+ */
+ private void initBaseTemplatePath(ServletContext servletContext) {
+ if (engine.getSourceFactory() instanceof ClassPathSourceFactory) {
+ // do nothing
+ } else {
+ if (StrKit.isBlank(engine.getBaseTemplatePath())) {
+ String path = servletContext.getRealPath("/");
+ engine.setBaseTemplatePath(path);
+ }
+ }
+ }
+
+ /**
+ * 利用 sharedFunctionFiles 延迟调用 addSharedFunction
+ * 因为需要等待 baseTemplatePath 以及 ISourceFactory 设置完毕以后
+ * 才能正常工作
+ */
+ private void initSharedFunction() {
+ for (String file : sharedFunctionFiles) {
+ engine.addSharedFunction(file.trim());
+ }
+ }
+}
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/source/ClassPathSource.java b/src/main/java/com/jfinal/template/source/ClassPathSource.java
new file mode 100644
index 0000000..d8baf00
--- /dev/null
+++ b/src/main/java/com/jfinal/template/source/ClassPathSource.java
@@ -0,0 +1,207 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.source;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import com.jfinal.template.EngineConfig;
+
+/**
+ * ClassPathSource 用于从 class path 以及 jar 包之中加载模板内容
+ *
+ *
+ * 注意:
+ * 1:如果被加载的文件是 class path 中的普通文件,则该文件支持热加载
+ *
+ * 2:如果被加载的文件处于 jar 包之中,则该文件不支持热加载,jar 包之中的文件在运行时通常不会被修改
+ * 在极少数情况下如果需要对 jar 包之中的模板文件进行热加载,可以通过继承 ClassPathSource
+ * 的方式进行扩展
+ *
+ * 3:JFinal Template Engine 开启热加载需要配置 engine.setDevMode(true)
+ *
+ */
+public class ClassPathSource implements ISource {
+
+ protected String finalFileName;
+ protected String fileName;
+ protected String encoding;
+
+ protected boolean isInJar;
+ protected long lastModified;
+ protected ClassLoader classLoader;
+ protected URL url;
+
+ public ClassPathSource(String fileName) {
+ this(null, fileName, EngineConfig.DEFAULT_ENCODING);
+ }
+
+ public ClassPathSource(String baseTemplatePath, String fileName) {
+ this(baseTemplatePath, fileName, EngineConfig.DEFAULT_ENCODING);
+ }
+
+ public ClassPathSource(String baseTemplatePath, String fileName, String encoding) {
+ this.finalFileName = buildFinalFileName(baseTemplatePath, fileName);
+ this.fileName = fileName;
+ this.encoding= encoding;
+ this.classLoader = getClassLoader();
+ this.url = classLoader.getResource(finalFileName);
+ if (url == null) {
+ throw new IllegalArgumentException("File not found : \"" + finalFileName + "\"");
+ }
+
+ processIsInJarAndlastModified();
+ }
+
+ protected void processIsInJarAndlastModified() {
+ try {
+ URLConnection conn = url.openConnection();
+ if ("jar".equals(url.getProtocol()) || conn instanceof JarURLConnection) {
+ isInJar = true;
+ lastModified = -1;
+ } else {
+ isInJar = false;
+ lastModified = conn.getLastModified();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ protected ClassLoader getClassLoader() {
+ ClassLoader ret = Thread.currentThread().getContextClassLoader();
+ return ret != null ? ret : getClass().getClassLoader();
+ }
+
+ protected String buildFinalFileName(String baseTemplatePath, String fileName) {
+ String finalFileName;
+ if (baseTemplatePath != null) {
+ char firstChar = fileName.charAt(0);
+ if (firstChar == '/' || firstChar == '\\') {
+ finalFileName = baseTemplatePath + fileName;
+ } else {
+ finalFileName = baseTemplatePath + "/" + fileName;
+ }
+ } else {
+ finalFileName = fileName;
+ }
+
+ if (finalFileName.charAt(0) == '/') {
+ finalFileName = finalFileName.substring(1);
+ }
+
+ return finalFileName;
+ }
+
+ public String getKey() {
+ return fileName;
+ }
+
+ public String getEncoding() {
+ return encoding;
+ }
+
+ protected long getLastModified() {
+ try {
+ URLConnection conn = url.openConnection();
+ return conn.getLastModified();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 模板文件在 jar 包文件之内则不支持热加载
+ */
+ public boolean isModified() {
+ return isInJar ? false : lastModified != getLastModified();
+ }
+
+ public StringBuilder getContent() {
+ // 与 FileSorce 不同,ClassPathSource 在构造方法中已经初始化了 lastModified
+ // 下面的代码可以去掉,在此仅为了避免继承类忘了在构造中初始化 lastModified 的防卫式代码
+ if (!isInJar) { // 如果模板文件不在 jar 包文件之中,则需要更新 lastModified 值
+ lastModified = getLastModified();
+ }
+
+ InputStream inputStream = classLoader.getResourceAsStream(finalFileName);
+ if (inputStream == null) {
+ throw new RuntimeException("File not found : \"" + finalFileName + "\"");
+ }
+
+ return loadFile(inputStream, encoding);
+ }
+
+ public static StringBuilder loadFile(InputStream inputStream, String encoding) {
+ StringBuilder ret = new StringBuilder();
+ BufferedReader br = null;
+ try {
+ br = new BufferedReader(new InputStreamReader(inputStream, encoding));
+ // br = new BufferedReader(new FileReader(fileName));
+ String line = br.readLine();
+ if (line != null) {
+ ret.append(line);
+ } else {
+ return ret;
+ }
+
+ while ((line=br.readLine()) != null) {
+ ret.append('\n').append(line);
+ }
+ return ret;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ finally {
+ if (br != null) {
+ try {
+ br.close();
+ } catch (IOException e) {
+ // com.jfinal.kit.LogKit.error(e.getMessage(), e);
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("In Jar File: ").append(isInJar).append("\n");
+ sb.append("File name: ").append(fileName).append("\n");
+ sb.append("Final file name: ").append(finalFileName).append("\n");
+ sb.append("Last modified: ").append(lastModified).append("\n");
+ return sb.toString();
+ }
+}
+
+
+/*
+ protected File getFile(URL url) {
+ try {
+ // return new File(url.toURI().getSchemeSpecificPart());
+ return new File(url.toURI());
+ } catch (URISyntaxException ex) {
+ // Fallback for URLs that are not valid URIs (should hardly ever happen).
+ return new File(url.getFile());
+ }
+ }
+*/
+
diff --git a/src/main/java/com/jfinal/template/source/ClassPathSourceFactory.java b/src/main/java/com/jfinal/template/source/ClassPathSourceFactory.java
new file mode 100644
index 0000000..d72eaa6
--- /dev/null
+++ b/src/main/java/com/jfinal/template/source/ClassPathSourceFactory.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.source;
+
+/**
+ * ClassPathSourceFactory 用于配置 Engine 使用 ClassPathSource 加载模板文件
+ *
+ * 配置示例:
+ * engine.baseTemplatePath(null); // 清掉 base path
+ * engine.setSourceFactory(new ClassPathSourceFactory());
+ */
+public class ClassPathSourceFactory implements ISourceFactory {
+
+ public ISource getSource(String baseTemplatePath, String fileName, String encoding) {
+ return new ClassPathSource(baseTemplatePath, fileName, encoding);
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/source/FileSource.java b/src/main/java/com/jfinal/template/source/FileSource.java
new file mode 100644
index 0000000..8331242
--- /dev/null
+++ b/src/main/java/com/jfinal/template/source/FileSource.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.source;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import com.jfinal.template.EngineConfig;
+
+/**
+ * FileSource 用于从普通文件中加载模板内容
+ */
+public class FileSource implements ISource {
+
+ private String finalFileName;
+ private String fileName;
+ private String encoding;
+
+ private long lastModified;
+
+ public FileSource(String baseTemplatePath, String fileName, String encoding) {
+ this.finalFileName = buildFinalFileName(baseTemplatePath, fileName);
+ this.fileName = fileName;
+ this.encoding= encoding;
+ }
+
+ public FileSource(String baseTemplatePath, String fileName) {
+ this(baseTemplatePath, fileName, EngineConfig.DEFAULT_ENCODING);
+ }
+
+ public boolean isModified() {
+ return lastModified != new File(finalFileName).lastModified();
+ }
+
+ public String getKey() {
+ return fileName;
+ }
+
+ public String getEncoding() {
+ return encoding;
+ }
+
+ public String getFinalFileName() {
+ return finalFileName;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public StringBuilder getContent() {
+ File file = new File(finalFileName);
+ if (!file.exists()) {
+ throw new RuntimeException("File not found : " + finalFileName);
+ }
+
+ // 极为重要,否则在开发模式下 isModified() 一直返回 true,缓存一直失效(原因是 lastModified 默认值为 0)
+ this.lastModified = file.lastModified();
+
+ return loadFile(file, encoding);
+ }
+
+ private String buildFinalFileName(String baseTemplatePath, String fileName) {
+ char firstChar = fileName.charAt(0);
+ String finalFileName;
+ if (firstChar == '/' || firstChar == '\\') {
+ finalFileName = baseTemplatePath + fileName;
+ } else {
+ finalFileName = baseTemplatePath + File.separator + fileName;
+ }
+ return finalFileName;
+ }
+
+ public static StringBuilder loadFile(File file, String encoding) {
+ StringBuilder ret = new StringBuilder((int)file.length() + 3);
+ BufferedReader br = null;
+ try {
+ br = new BufferedReader(new InputStreamReader(new FileInputStream(file), encoding));
+ // br = new BufferedReader(new FileReader(fileName));
+ String line = br.readLine();
+ if (line != null) {
+ ret.append(line);
+ } else {
+ return ret;
+ }
+
+ while ((line=br.readLine()) != null) {
+ ret.append('\n').append(line);
+ }
+ return ret;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ finally {
+ if (br != null) {
+ try {
+ br.close();
+ } catch (IOException e) {
+ // com.jfinal.kit.LogKit.error(e.getMessage(), e);
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("File name: ").append(fileName).append("\n");
+ sb.append("Final file name: ").append(finalFileName).append("\n");
+ sb.append("Last modified: ").append(lastModified).append("\n");
+ return sb.toString();
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/source/FileSourceFactory.java b/src/main/java/com/jfinal/template/source/FileSourceFactory.java
new file mode 100644
index 0000000..a2806c3
--- /dev/null
+++ b/src/main/java/com/jfinal/template/source/FileSourceFactory.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.source;
+
+/**
+ * FileSourceFactory 用于配置 Engine 使用 FileSource 加载模板文件
+ *
+ * 注意:
+ * FileSourceFactory 为模板引擎默认配置
+ */
+public class FileSourceFactory implements ISourceFactory {
+
+ public ISource getSource(String baseTemplatePath, String fileName, String encoding) {
+ return new FileSource(baseTemplatePath, fileName, encoding);
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/source/ISource.java b/src/main/java/com/jfinal/template/source/ISource.java
new file mode 100644
index 0000000..ea38139
--- /dev/null
+++ b/src/main/java/com/jfinal/template/source/ISource.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.source;
+
+/**
+ * ISource 用于表示模板内容的来源
+ */
+public interface ISource {
+
+ /**
+ * reload template if modified on devMode
+ */
+ boolean isModified();
+
+ /**
+ * key used to cache, return null if do not cache the template
+ *
+ * 注意:如果不希望缓存从该 ISource 解析出来的 Template 对象
+ * 让 getKey() 返回 null 值即可
+ */
+ String getKey();
+
+ /**
+ * content of ISource
+ */
+ StringBuilder getContent();
+
+ /**
+ * encoding of content
+ */
+ String getEncoding();
+}
+
+
diff --git a/src/main/java/com/jfinal/template/source/ISourceFactory.java b/src/main/java/com/jfinal/template/source/ISourceFactory.java
new file mode 100644
index 0000000..1bd868e
--- /dev/null
+++ b/src/main/java/com/jfinal/template/source/ISourceFactory.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.source;
+
+/**
+ * ISourceFactory 用于为 engine 切换不同的 ISource 实现类
+ *
+ * FileSourceFactory 用于从指定的目录中加载模板文件
+ * ClassPathSourceFactory 用于从 class path 以及 jar 文件中加载模板文件
+ *
+ * 配置示例:
+ * engine.setSourceFactory(new ClassPathSourceFactory());
+ */
+public interface ISourceFactory {
+ ISource getSource(String baseTemplatePath, String fileName, String encoding);
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/source/StringSource.java b/src/main/java/com/jfinal/template/source/StringSource.java
new file mode 100644
index 0000000..c5af315
--- /dev/null
+++ b/src/main/java/com/jfinal/template/source/StringSource.java
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.source;
+
+import com.jfinal.kit.HashKit;
+import com.jfinal.kit.StrKit;
+import com.jfinal.template.EngineConfig;
+
+/**
+ * StringSource 用于从 String 变量中加载模板内容
+ */
+public class StringSource implements ISource {
+
+ private String key;
+ private StringBuilder content;
+
+ /**
+ * 构造 StringSource
+ * @param content 模板内容
+ * @param cache true 则缓存 Template,否则不缓存
+ */
+ public StringSource(String content, boolean cache) {
+ if (StrKit.isBlank(content)) {
+ throw new IllegalArgumentException("content can not be blank");
+ }
+ this.content = new StringBuilder(content);
+ this.key = cache ? HashKit.md5(content) : null; // 不缓存只要将 key 值赋为 null 即可
+ }
+
+ public StringSource(StringBuilder content, boolean cache) {
+ if (content == null || content.length() == 0) {
+ throw new IllegalArgumentException("content can not be blank");
+ }
+ this.content = content;
+ this.key = cache ? HashKit.md5(content.toString()) : null; // 不缓存只要将 key 值赋为 null 即可
+ }
+
+ public boolean isModified() {
+ return false;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public StringBuilder getContent() {
+ return content;
+ }
+
+ public String getEncoding() {
+ return EngineConfig.DEFAULT_ENCODING;
+ }
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Key : ").append(key).append("\n");
+ sb.append("Content : ").append(content).append("\n");
+ return sb.toString();
+ }
+}
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/CharTable.java b/src/main/java/com/jfinal/template/stat/CharTable.java
new file mode 100644
index 0000000..624c308
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/CharTable.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat;
+
+/**
+ * CharTable 空间换时间优化字符判断性能
+ * 负值参数强转 char 会自动变正值,无需判断负值数组下标
+ * isLetter(EOF) 不会下标越界
+ */
+public class CharTable {
+
+ private static final char[] letterChars = buildLetterChars();
+ private static final char[] letterOrDigitChars = buildLetterOrDigitChars();
+ private static final char[] exprChars = buildExprChars();
+ private static final char NULL = 0;
+ private static final char SIZE = 128;
+ private CharTable(){}
+
+ private static char[] createCharArray() {
+ char[] ret = new char[SIZE];
+ for (char i=0; i= '0' && c <= '9';
+ }
+
+ public static boolean isBlank(char c) {
+ return c == ' ' || c == '\t'; // \t\r\u000C
+ }
+
+ public static boolean isBlankOrLineFeed(char c) {
+ return c == ' ' || c == '\t' || c == '\r' || c == '\n'; // \t\r\n\u000C
+ }
+
+ public static boolean isHexadecimalDigit(char c) {
+ return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
+ }
+
+ public static boolean isOctalDigit(char c) {
+ return c >= '0' && c <= '7';
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/Ctrl.java b/src/main/java/com/jfinal/template/stat/Ctrl.java
new file mode 100644
index 0000000..0c1d2ea
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/Ctrl.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat;
+
+/**
+ * Ctrl
+ *
+ * 封装 AST 执行过程中的控制状态,避免使用 Scope.data 保存控制状态
+ * 从而污染用户空间数据,目前仅用于 nullSafe、break、continue、return 控制
+ * 未来可根据需求引入更多控制状态
+ */
+public class Ctrl {
+
+ private static final int JUMP_NONE = 0;
+ private static final int JUMP_BREAK = 1;
+ private static final int JUMP_CONTINUE = 2;
+ private static final int JUMP_RETURN = 3;
+
+ private static final int WISDOM_ASSIGNMENT = 0;
+ private static final int LOCAL_ASSIGNMENT = 1;
+ private static final int GLOBAL_ASSIGNMENT = 2;
+
+ private int jump = JUMP_NONE;
+ private int assignmentType = WISDOM_ASSIGNMENT;
+ private boolean nullSafe = false;
+
+ public boolean isJump() {
+ return jump != JUMP_NONE;
+ }
+
+ public boolean notJump() {
+ return jump == JUMP_NONE;
+ }
+
+ public boolean isBreak() {
+ return jump == JUMP_BREAK;
+ }
+
+ public void setBreak() {
+ jump = JUMP_BREAK;
+ }
+
+ public boolean isContinue() {
+ return jump == JUMP_CONTINUE;
+ }
+
+ public void setContinue() {
+ jump = JUMP_CONTINUE;
+ }
+
+ public boolean isReturn() {
+ return jump == JUMP_RETURN;
+ }
+
+ public void setReturn() {
+ jump = JUMP_RETURN;
+ }
+
+ public void setJumpNone() {
+ jump = JUMP_NONE;
+ }
+
+ public boolean isWisdomAssignment() {
+ return assignmentType == WISDOM_ASSIGNMENT;
+ }
+
+ public void setWisdomAssignment() {
+ assignmentType = WISDOM_ASSIGNMENT;
+ }
+
+ public boolean isLocalAssignment() {
+ return assignmentType == LOCAL_ASSIGNMENT;
+ }
+
+ public void setLocalAssignment() {
+ assignmentType = LOCAL_ASSIGNMENT;
+ }
+
+ public boolean isGlobalAssignment() {
+ return assignmentType == GLOBAL_ASSIGNMENT;
+ }
+
+ public void setGlobalAssignment() {
+ assignmentType = GLOBAL_ASSIGNMENT;
+ }
+
+ public boolean isNullSafe() {
+ return nullSafe;
+ }
+
+ public boolean notNullSafe() {
+ return !nullSafe;
+ }
+
+ public void setNullSafe(boolean nullSafe) {
+ this.nullSafe = nullSafe;
+ }
+}
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/Lexer.java b/src/main/java/com/jfinal/template/stat/Lexer.java
new file mode 100644
index 0000000..0a65251
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/Lexer.java
@@ -0,0 +1,526 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * DKFF(Dynamic Key Feature Forward) Lexer
+ */
+class Lexer {
+
+ static final char EOF = (char)-1;
+ static final int TEXT_STATE_DIAGRAM = 999;
+
+ char[] buf;
+ int state = 0;
+ int lexemeBegin = 0;
+ int forward = 0;
+ int beginRow = 1;
+ int forwardRow = 1;
+ TextToken previousTextToken = null;
+
+ List tokens = new ArrayList();
+ String fileName;
+
+ public Lexer(StringBuilder content, String fileName) {
+ int len = content.length();
+ buf = new char[len + 1];
+ content.getChars(0, content.length(), buf, 0);
+ buf[len] = EOF;
+ this.fileName = fileName;
+ }
+
+ /**
+ * 进入每个扫描方法之前 peek() 处于可用状态,不需要 next()
+ * 每个扫描方法内部是否要 next() 移动,取决定具体情况
+ * 每个扫描方法成功返回前,将 forward 置于下一次扫描需要处理的地方
+ * 让下个扫描方法不必 next()
+ * 紧靠 scanText() 之前的扫描方法在失败后必须保持住forward
+ * 这是 scanText() 可以一直向前的保障
+ */
+ public List scan() {
+ while (peek() != EOF) {
+ if (peek() == '#') {
+ if (scanDire()) {
+ continue ;
+ }
+ if (scanSingleLineComment()) {
+ continue ;
+ }
+ if (scanMultiLineComment()) {
+ continue ;
+ }
+ if (scanNoParse()) {
+ continue ;
+ }
+ }
+
+ scanText();
+ }
+ return tokens;
+ }
+
+ /**
+ * 指令模式与解析规则
+ * 1:指令 pattern
+ * #(p)
+ * #id(p)
+ * #define id(p)
+ * #@id(p) / #@id?(p)
+ * #else / #end
+ *
+ * 2:关键字类型指令在获取到关键字以后,必须要正确解析出后续内容,否则抛异常
+ *
+ * 3:非关键字类型指令只有在本行内出现 # id ( 三个序列以后,才要求正确解析出后续内容
+ * 否则当成普通文本
+ */
+ boolean scanDire() {
+ String id = null;
+ StringBuilder para = null;
+ Token idToken = null;
+ Token paraToken = null;
+ while(true) {
+ switch (state) {
+ case 0:
+ if (peek() == '#') { // #
+ next();
+ skipBlanks();
+ state = 1;
+ continue ;
+ }
+ return fail();
+ case 1:
+ if (peek() == '(') { // # (
+ para = scanPara("");
+ idToken = new Token(Symbol.OUTPUT, beginRow);
+ paraToken = new ParaToken(para, beginRow);
+ return addOutputToken(idToken, paraToken);
+ }
+ if (CharTable.isLetter(peek())) { // # id
+ state = 10;
+ continue ;
+ }
+ if (peek() == '@') { // # @
+ next();
+ skipBlanks();
+ if (CharTable.isLetter(peek())) { // # @ id
+ state = 20;
+ continue ;
+ }
+ }
+ return fail();
+ // -----------------------------------------------------
+ case 10: // # id
+ id = scanId();
+ Symbol symbol = Symbol.getKeywordSym(id);
+ // 非关键字指令
+ if (symbol == null) {
+ state = 11;
+ continue ;
+ }
+
+ // define 指令
+ if (symbol == Symbol.DEFINE) {
+ state = 12;
+ continue ;
+ }
+
+ // 无参关键字指令
+ if (symbol.noPara()) {
+ return addNoParaToken(new Token(symbol, id, beginRow));
+ }
+
+ // 有参关键字指令
+ skipBlanks();
+ if (peek() == '(') {
+ para = scanPara(id);
+ idToken = new Token(symbol, beginRow);
+ paraToken = new ParaToken(para, beginRow);
+ return addIdParaToken(idToken, paraToken);
+ }
+ throw new ParseException("#" + id + " directive requires parentheses \"()\"", new Location(fileName, beginRow));
+ case 11: // 用户自定义指令必须有参数
+ skipBlanks();
+ if (peek() == '(') {
+ para = scanPara(id);
+ idToken = new Token(Symbol.ID, id, beginRow);
+ paraToken = new ParaToken(para, beginRow);
+ return addIdParaToken(idToken, paraToken);
+ }
+ return fail(); // 用户自定义指令在没有左括号的情况下当作普通文本
+ case 12: // 处理 "# define id (para)" 指令
+ skipBlanks();
+ if (CharTable.isLetter(peek())) {
+ id = scanId(); // 模板函数名称
+ skipBlanks();
+ if (peek() == '(') {
+ para = scanPara("define " + id);
+ idToken = new Token(Symbol.DEFINE, id, beginRow);
+ paraToken = new ParaToken(para, beginRow);
+ return addIdParaToken(idToken, paraToken);
+ }
+ throw new ParseException("#define " + id + " : template function definition requires parentheses \"()\"", new Location(fileName, beginRow));
+ }
+ throw new ParseException("#define directive requires identifier as a function name", new Location(fileName, beginRow));
+ case 20: // # @ id
+ id = scanId();
+ skipBlanks();
+ boolean hasQuestionMark = peek() == '?';
+ if (hasQuestionMark) {
+ next();
+ skipBlanks();
+ }
+ if (peek() == '(') {
+ para = scanPara(hasQuestionMark ? "@" + id + "?" : "@" + id);
+ idToken = new Token(hasQuestionMark ? Symbol.CALL_IF_DEFINED : Symbol.CALL, id, beginRow);
+ paraToken = new ParaToken(para, beginRow);
+ return addIdParaToken(idToken, paraToken);
+ }
+ return fail();
+ default :
+ return fail();
+ }
+ }
+ }
+
+ /**
+ * 调用者已确定以字母或下划线开头,故一定可以获取到 id值
+ */
+ String scanId() {
+ int idStart = forward;
+ while (CharTable.isLetterOrDigit(next())) {
+ ;
+ }
+ return subBuf(idStart, forward - 1).toString();
+ }
+
+ /**
+ * 扫描指令参数,成功则返回,否则抛出词法分析异常
+ */
+ StringBuilder scanPara(String id) {
+ char quotes = '"';
+ int localState = 0;
+ int parenDepth = 1; // 指令后面参数的第一个 '(' 深度为 1
+ next();
+ int paraStart = forward;
+ while (true) {
+ switch (localState) {
+ case 0:
+ for (char c=peek(); true; c=next()) {
+ if (c == ')') {
+ parenDepth--;
+ if (parenDepth == 0) { // parenDepth 不可能小于0,因为初始值为 1
+ next();
+ return subBuf(paraStart, forward - 2);
+ }
+ continue ;
+ }
+
+ if (c == '(') {
+ parenDepth++;
+ continue ;
+ }
+
+ if (c == '"' || c == '\'') {
+ quotes = c;
+ localState = 1;
+ break ;
+ }
+
+ if (CharTable.isExprChar(c)) {
+ continue ;
+ }
+
+ if (c == EOF) {
+ throw new ParseException("#" + id + " parameter can not match the end char ')'", new Location(fileName, beginRow));
+ }
+
+ throw new ParseException("#" + id + " parameter exists illegal char: '" + c + "'", new Location(fileName, beginRow));
+ }
+ break ;
+ case 1:
+ for (char c=next(); true; c=next()) {
+ if (c == quotes) {
+ if (buf[forward - 1] != '\\') { // 前一个字符不是转义字符
+ next();
+ localState = 0;
+ break ;
+ } else {
+ continue ;
+ }
+ }
+
+ if (c == EOF) {
+ throw new ParseException("#" + id + " parameter error, the string parameter not ending", new Location(fileName, beginRow));
+ }
+ }
+ break ;
+ }
+ }
+ }
+
+ /**
+ * 单行注释,开始状态 100,关注换行与 EOF
+ */
+ boolean scanSingleLineComment() {
+ while (true) {
+ switch (state) {
+ case 100:
+ if (peek() == '#' && next() == '#' && next() == '#') {
+ state = 101;
+ continue ;
+ }
+ return fail();
+ case 101:
+ for (char c=next(); true; c=next()) {
+ if (c == '\n') {
+ if (deletePreviousTextTokenBlankTails()) {
+ return prepareNextScan(1);
+ } else {
+ return prepareNextScan(0);
+ }
+ }
+ if (c == EOF) {
+ deletePreviousTextTokenBlankTails();
+ return prepareNextScan(0);
+ }
+ }
+ default :
+ return fail();
+ }
+ }
+ }
+
+ /**
+ * 多行注释,开始状态 200,关注结尾标记与 EOF
+ */
+ boolean scanMultiLineComment() {
+ while (true) {
+ switch (state) {
+ case 200:
+ if (peek() == '#' && next() == '-' && next() == '-') {
+ state = 201;
+ continue ;
+ }
+ return fail();
+ case 201:
+ for (char c=next(); true; c=next()) {
+ if (c == '-' && buf[forward + 1] == '-' && buf[forward + 2] == '#') {
+ forward = forward + 3;
+ if (lookForwardLineFeedAndEof() && deletePreviousTextTokenBlankTails()) {
+ return prepareNextScan(peek() != EOF ? 1 : 0);
+ } else {
+ return prepareNextScan(0);
+ }
+ }
+ if (c == EOF) {
+ throw new ParseException("The multiline comment start block \"#--\" can not match the end block: \"--#\"", new Location(fileName, beginRow));
+ }
+ }
+ default :
+ return fail();
+ }
+ }
+ }
+
+ /**
+ * 非解析块,开始状态 300,关注结尾标记与 EOF
+ */
+ boolean scanNoParse() {
+ while (true) {
+ switch (state) {
+ case 300:
+ if (peek() == '#' && next() == '[' && next() == '[') {
+ state = 301;
+ continue ;
+ }
+ return fail();
+ case 301:
+ for (char c=next(); true; c=next()) {
+ if (c == ']' && buf[forward + 1] == ']' && buf[forward + 2] == '#') {
+ addTextToken(subBuf(lexemeBegin + 3, forward - 1)); // NoParse 块使用 TextToken
+ return prepareNextScan(3);
+ }
+ if (c == EOF) {
+ throw new ParseException("The \"no parse\" start block \"#[[\" can not match the end block: \"]]#\"", new Location(fileName, beginRow));
+ }
+ }
+ default :
+ return fail();
+ }
+ }
+ }
+
+ boolean scanText() {
+ for (char c=peek(); true; c=next()) {
+ if (c == '#' || c == EOF) {
+ addTextToken(subBuf(lexemeBegin, forward - 1));
+ return prepareNextScan(0);
+ }
+ }
+ }
+
+ boolean fail() {
+ if (state < 300) {
+ forward = lexemeBegin;
+ forwardRow = beginRow;
+ }
+ if (state < 100) {
+ state = 100;
+ } else if (state < 200) {
+ state = 200;
+ } else if (state < 300) {
+ state = 300;
+ } else {
+ state = TEXT_STATE_DIAGRAM;
+ }
+ return false;
+ }
+
+ char next() {
+ if (buf[forward] == '\n') {
+ forwardRow++;
+ }
+ return buf[++forward];
+ }
+
+ char peek() {
+ return buf[forward];
+ }
+
+ void skipBlanks() {
+ while(CharTable.isBlank(buf[forward])) {
+ next();
+ }
+ }
+
+ /**
+ * scanPara 与 scanNoParse 存在 start > end 的情况
+ */
+ StringBuilder subBuf(int start, int end) {
+ if (start > end) {
+ return null;
+ }
+ StringBuilder ret = new StringBuilder(end - start + 1);
+ for (int i=start; i<=end; i++) {
+ ret.append(buf[i]);
+ }
+ return ret;
+ }
+
+ boolean prepareNextScan(int moveForward) {
+ for (int i=0; i tokenList;
+ private StringBuilder content;
+ private String fileName;
+ private Env env;
+
+ public Parser(Env env, StringBuilder content, String fileName) {
+ this.env = env;
+ this.content = content;
+ this.fileName = fileName;
+ }
+
+ private Token peek() {
+ return tokenList.get(forward);
+ }
+
+ private Token move() {
+ return tokenList.get(++forward);
+ }
+
+ private Token matchPara(Token name) {
+ Token current = peek();
+ if (current.symbol == Symbol.PARA) {
+ move();
+ return current;
+ }
+ throw new ParseException("Can not match the parameter of directive #" + name.value(), getLocation(name.row));
+ }
+
+ private void matchEnd(Token name) {
+ if (peek().symbol == Symbol.END) {
+ move();
+ return ;
+ }
+ throw new ParseException("Can not match the #end of directive #" + name.value(), getLocation(name.row));
+ }
+
+ public Stat parse() {
+ tokenList = new Lexer(content, fileName).scan();
+ tokenList.add(EOF);
+ Stat statList = statList();
+ if (peek() != EOF) {
+ throw new ParseException("Syntax error: can not match " + peek().value(), getLocation(peek().row));
+ }
+ return statList;
+ }
+
+ private StatList statList() {
+ List statList = new ArrayList();
+ while (true) {
+ Stat stat = stat();
+ if (stat == null) {
+ break ;
+ }
+
+ if (stat instanceof Define) {
+ env.addFunction((Define)stat);
+ continue ;
+ }
+
+ // 过滤内容为空的 Text 节点,通常是处于两个指令之间的空白字符被移除以后的结果,详见 TextToken.deleteBlankTails()
+ if (stat instanceof Text && ((Text)stat).isEmpty()) {
+ continue ;
+ }
+
+ statList.add(stat);
+ }
+ return new StatList(statList);
+ }
+
+ private Stat stat() {
+ Token name = peek();
+ switch (name.symbol) {
+ case TEXT:
+ move();
+ return new Text(((TextToken)name).getContent()).setLocation(getLocation(name.row));
+ case OUTPUT:
+ move();
+ Token para = matchPara(name);
+ Location loc = getLocation(name.row);
+ return env.getEngineConfig().getOutputDirective(parseExprList(para), loc).setLocation(loc);
+ case INCLUDE:
+ move();
+ para = matchPara(name);
+ return new Include(env, parseExprList(para), fileName, getLocation(name.row));
+ case FOR:
+ move();
+ para = matchPara(name);
+ StatList statList = statList();
+ Stat _else = null;
+ if (peek().symbol == Symbol.ELSE) {
+ move();
+ StatList elseStats = statList();
+ _else = new Else(elseStats);
+ }
+ matchEnd(name);
+ return new For(parseForCtrl(para), statList, _else).setLocation(getLocation(name.row));
+ case IF:
+ move();
+ para = matchPara(name);
+ statList = statList();
+ Stat ret = new If(parseExprList(para), statList, getLocation(name.row));
+
+ Stat current = ret;
+ for (Token elseIfToken=peek(); elseIfToken.symbol == Symbol.ELSEIF; elseIfToken=peek()) {
+ move();
+ para = matchPara(elseIfToken);
+ statList = statList();
+ Stat elseIf = new ElseIf(parseExprList(para), statList, getLocation(elseIfToken.row));
+ current.setStat(elseIf);
+ current = elseIf;
+ }
+ if (peek().symbol == Symbol.ELSE) {
+ move();
+ statList = statList();
+ _else = new Else(statList);
+ current.setStat(_else);
+ }
+ matchEnd(name);
+ return ret;
+ case DEFINE:
+ String functionName = name.value();
+ move();
+ para = matchPara(name);
+ Stat stat = statList();
+ matchEnd(name);
+ return new Define(functionName, parseExprList(para), stat, getLocation(name.row));
+ case CALL:
+ functionName = name.value();
+ move();
+ para = matchPara(name);
+ return new Call(functionName, parseExprList(para), false).setLocation(getLocation(name.row));
+ case CALL_IF_DEFINED:
+ functionName = name.value();
+ move();
+ para = matchPara(name);
+ return new Call(functionName, parseExprList(para), true).setLocation(getLocation(name.row));
+ case SET:
+ move();
+ para = matchPara(name);
+ return new Set(parseExprList(para), getLocation(name.row));
+ case SET_LOCAL:
+ move();
+ para = matchPara(name);
+ return new SetLocal(parseExprList(para), getLocation(name.row));
+ case SET_GLOBAL:
+ move();
+ para = matchPara(name);
+ return new SetGlobal(parseExprList(para), getLocation(name.row));
+ case CONTINUE:
+ move();
+ return Continue.me;
+ case BREAK:
+ move();
+ return Break.me;
+ case RETURN:
+ move();
+ return Return.me;
+ case ID:
+ Stat dire = env.getEngineConfig().getDirective(name.value());
+ if (dire == null) {
+ throw new ParseException("Directive not found: #" + name.value(), getLocation(name.row));
+ }
+ ret = createDirective(dire, name).setLocation(getLocation(name.row));
+ move();
+ para = matchPara(name);
+ ret.setExprList(parseExprList(para));
+
+ if (dire.hasEnd()) {
+ statList = statList();
+ ret.setStat(statList);
+ matchEnd(name);
+ }
+ return ret;
+ case PARA:
+ case ELSEIF:
+ case ELSE:
+ case END:
+ case EOF:
+ return null;
+ default :
+ throw new ParseException("Syntax error: can not match the token: " + name.value(), getLocation(name.row));
+ }
+ }
+
+ private Location getLocation(int row) {
+ return new Location(fileName, row);
+ }
+
+ private Stat createDirective(Stat dire, Token name) {
+ try {
+ return dire.getClass().newInstance();
+ } catch (Exception e) {
+ throw new ParseException(e.getMessage(), getLocation(name.row), e);
+ }
+ }
+
+ private ExprList parseExprList(Token paraToken) {
+ return new ExprParser((ParaToken)paraToken, env.getEngineConfig(), fileName).parseExprList();
+ }
+
+ private ForCtrl parseForCtrl(Token paraToken) {
+ return new ExprParser((ParaToken)paraToken, env.getEngineConfig(), fileName).parseForCtrl();
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/Scope.java b/src/main/java/com/jfinal/template/stat/Scope.java
new file mode 100644
index 0000000..95650a6
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/Scope.java
@@ -0,0 +1,235 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Scope
+ * 1:顶层 scope.parent 为 null
+ * 2:scope.set(...) 自内向外查找赋值
+ * 3:scope.get(...) 自内向外查找获取
+ */
+@SuppressWarnings({"rawtypes", "unchecked"})
+public class Scope {
+
+ private final Scope parent;
+ private final Ctrl ctrl;
+ private Map data;
+ private Map sharedObjectMap;
+
+ /**
+ * 构建顶层 Scope, parent 为 null 是顶层 Scope 的标志
+ * @param data 用于在模板中使用的数据,data 支持 null 值
+ * @param sharedObjectMap 共享对象
+ */
+ public Scope(Map data, Map sharedObjectMap) {
+ this.parent = null;
+ this.ctrl = new Ctrl();
+ this.data = data;
+ this.sharedObjectMap = sharedObjectMap;
+ }
+
+ /**
+ * 构建 AST 执行过程中作用域栈
+ */
+ public Scope(Scope parent) {
+ if (parent == null) {
+ throw new IllegalArgumentException("parent can not be null.");
+ }
+ this.parent = parent;
+ this.ctrl = parent.ctrl;
+ this.data = null;
+ this.sharedObjectMap = parent.sharedObjectMap;
+ }
+
+ public Ctrl getCtrl() {
+ return ctrl;
+ }
+
+ /**
+ * 设置变量
+ * 自内向外在作用域栈中查找变量,如果找到则改写变量值,否则将变量存放到顶层 Scope
+ */
+ public void set(Object key, Object value) {
+ for (Scope cur=this; true; cur=cur.parent) {
+ // HashMap 允许有 null 值 value,必须要做 containsKey 判断
+ if (cur.data != null && cur.data.containsKey(key)) {
+ cur.data.put(key, value);
+ return ;
+ }
+
+ if (cur.parent == null) {
+ if (cur.data == null) { // 支持顶层 data 为 null 值
+ cur.data = new HashMap();
+ }
+ cur.data.put(key, value);
+ return ;
+ }
+ }
+ }
+
+ /**
+ * 获取变量
+ * 自内向外在作用域栈中查找变量,返回最先找到的变量
+ */
+ public Object get(Object key) {
+ for (Scope cur=this; cur!=null; cur=cur.parent) {
+ if (cur.data != null && cur.data.containsKey(key)) {
+ return cur.data.get(key);
+ }
+ }
+ // return null;
+ return sharedObjectMap != null ? sharedObjectMap.get(key) : null;
+ }
+
+ /**
+ * 移除变量
+ * 自内向外在作用域栈中查找变量,移除最先找到的变量
+ */
+ public void remove(Object key) {
+ for (Scope cur=this; cur!=null; cur=cur.parent) {
+ if (cur.data != null && cur.data.containsKey(key)) {
+ cur.data.remove(key);
+ return ;
+ }
+ }
+ }
+
+ /**
+ * 设置局部变量
+ */
+ public void setLocal(Object key, Object value) {
+ if (data == null) {
+ data = new HashMap();
+ }
+ data.put(key, value);
+ }
+
+ /**
+ * 获取局部变量
+ */
+ public Object getLocal(Object key) {
+ return data != null ? data.get(key) : null;
+ }
+
+ /**
+ * 移除局部变量
+ */
+ public void removeLocal(Object key) {
+ if (data != null) {
+ data.remove(key);
+ }
+ }
+
+ /**
+ * 设置全局变量
+ * 全局作用域是指本次请求的整个 template
+ */
+ public void setGlobal(Object key, Object value) {
+ for (Scope cur=this; true; cur=cur.parent) {
+ if (cur.parent == null) {
+ cur.data.put(key, value);
+ return ;
+ }
+ }
+ }
+
+ /**
+ * 获取全局变量
+ * 全局作用域是指本次请求的整个 template
+ */
+ public Object getGlobal(Object key) {
+ for (Scope cur=this; true; cur=cur.parent) {
+ if (cur.parent == null) {
+ return cur.data.get(key);
+ }
+ }
+ }
+
+ /**
+ * 移除全局变量
+ * 全局作用域是指本次请求的整个 template
+ */
+ public void removeGlobal(Object key) {
+ for (Scope cur=this; true; cur=cur.parent) {
+ if (cur.parent == null) {
+ cur.data.remove(key);
+ return ;
+ }
+ }
+ }
+
+ /**
+ * 自内向外在作用域栈中查找变量,获取变量所在的 Map,主要用于 IncDec
+ */
+ public Map getMapOfValue(Object key) {
+ for (Scope cur=this; cur!=null; cur=cur.parent) {
+ if (cur.data != null && cur.data.containsKey(key)) {
+ return cur.data;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 获取本层作用域 data,可能为 null 值
+ */
+ public Map getData() {
+ return data;
+ }
+
+ /**
+ * 设置/替换本层作用域 data,通常用于在扩展指令中使用现成可用的 Map 来存放数据,
+ * 从而避免 Scope 内部创建 data,节省时空
+ *
+ * 注意:本方法会替换掉已经存在的 data 对象
+ */
+ public void setData(Map data) {
+ this.data = data;
+ }
+
+ /**
+ * 获取顶层作用域 data,可能为 null 值
+ */
+ public Map getRootData() {
+ for (Scope cur=this; true; cur=cur.parent) {
+ if (cur.parent == null) {
+ return cur.data;
+ }
+ }
+ }
+
+ /**
+ * 设置/替换顶层作用域 data,可以在扩展指令之中通过此方法切换掉顶层作用域
+ * 实现作用域完全隔离的功能
+ *
+ * 注意:本方法会替换掉顶层已经存在的 data 对象
+ */
+ public void setRootData(Map data) {
+ for (Scope cur=this; true; cur=cur.parent) {
+ if (cur.parent == null) {
+ cur.data = data;
+ return ;
+ }
+ }
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/Symbol.java b/src/main/java/com/jfinal/template/stat/Symbol.java
new file mode 100644
index 0000000..972b460
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/Symbol.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Symbol
+ */
+enum Symbol {
+
+ TEXT("text", false),
+
+ OUTPUT("output", true),
+
+ DEFINE("define", true),
+ CALL("call", true),
+ CALL_IF_DEFINED("callIfDefined", true),
+ SET("set", true),
+ SET_LOCAL("setLocal", true),
+ SET_GLOBAL("setGlobal", true),
+ INCLUDE("include", true),
+
+ FOR("for", true),
+ IF("if", true),
+ ELSEIF("elseif", true),
+ ELSE("else", false),
+ END("end", false),
+ CONTINUE("continue", false),
+ BREAK("break", false),
+ RETURN("return", false),
+
+ ID("ID", false), // 标识符:下划线或字母开头 ^[A-Za-z_][A-Za-z0-9_]*$
+ PARA("PARA", false),
+
+ EOF("EOF", false);
+
+ private final String name;
+ private final boolean hasPara; // 是否有参
+
+ /**
+ * Lexer 中确定为系统指令以后,必须得到正确的后续 Token 序列,否则报异常
+ * 扩展指令在得到 # id ( 序列以后才要求得到正确的后续 Token 序列,否则仅仅 return fail()
+ */
+ @SuppressWarnings("serial")
+ private static final Map keywords = new HashMap() {{
+ put(Symbol.IF.getName(), IF);
+ put(Symbol.ELSEIF.getName(), ELSEIF);
+ put(Symbol.ELSE.getName(), ELSE);
+ put(Symbol.END.getName(), END);
+ put(Symbol.FOR.getName(), FOR);
+ put(Symbol.BREAK.getName(), BREAK);
+ put(Symbol.CONTINUE.getName(), CONTINUE);
+ put(Symbol.RETURN.getName(), RETURN);
+
+ put(Symbol.DEFINE.getName(), DEFINE);
+ put(Symbol.SET.getName(), SET);
+ put(Symbol.SET_LOCAL.getName(), SET_LOCAL);
+ put(Symbol.SET_GLOBAL.getName(), SET_GLOBAL);
+ put(Symbol.INCLUDE.getName(), INCLUDE);
+ }};
+
+ private Symbol(String name, boolean hasPara) {
+ this.name = name;
+ this.hasPara = hasPara;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String toString() {
+ return name;
+ }
+
+ boolean hasPara() {
+ return hasPara;
+ }
+
+ boolean noPara() {
+ return !hasPara;
+ }
+
+ public static Symbol getKeywordSym(String name) {
+ return keywords.get(name);
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/TextToken.java b/src/main/java/com/jfinal/template/stat/TextToken.java
new file mode 100644
index 0000000..bd07503
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/TextToken.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat;
+
+/**
+ * TextToken
+ * 词法分析时,合并相邻 TextToken
+ */
+class TextToken extends Token {
+
+ // 接管父类的 value
+ private StringBuilder text;
+
+ public TextToken(StringBuilder value, int row) {
+ super(Symbol.TEXT, row);
+ this.text = value;
+ }
+
+ public void append(StringBuilder content) {
+ if (content != null) {
+ text.append(content); // 不要使用 toString(),性能不如直接这样快
+ }
+ }
+
+ /**
+ * 1:当前指令"后方"全是空白字符并且以 '\n' 或 EOF 结尾,当前指令"前方"为 TextToken 时调用此方法
+ * 2:当前指令本行内前方为空白字符(必须遭遇 '\n'),则删掉前方的空白字符
+ * 3:当前指令前方全为空白字符(不含 '\n'),表明是两个指令之间全为空白字符的情况,
+ * 或者两指令不在同一行且第二个指令前方全是空白字符的情况,则删掉这两指令之间的全部空白字符
+ * 4:返回 true,告知调用方需要吃掉本指令行尾的 '\n'
+ *
+ * 简单描述:
+ * 1:当前指令独占一行,删除当前指令前方空白字符,并告知调用方吃掉行尾 '\n'
+ * 2:当前指令前方仍然是指令,两指令之间有空白字符,吃掉前方(即所有)的空白字符,并告知调用方吃掉行尾 '\n'
+ * 3:情况 2 时,相当于本 TextToken 内容变成了空字符串,后续的 Parser 将过滤掉这类节点
+ */
+ public boolean deleteBlankTails() {
+ for (int i = text.length() - 1; i >= 0; i--) {
+ if (CharTable.isBlank(text.charAt(i))) {
+ continue ;
+ }
+
+ if (text.charAt(i) == '\n') {
+ text.delete(i+1, text.length());
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // 两个指令之间全是空白字符, 设置其长度为 0,为 Parser 过滤内容为空的 Text 节点做准备
+ text.setLength(0);
+ return true; // 当两指令之间全为空白字符时,告知调用方需要吃掉行尾的 '\n'
+ }
+
+ public String value() {
+ return text.toString();
+ }
+
+ public StringBuilder getContent() {
+ return text;
+ }
+
+ public String toString() {
+ return text.toString();
+ }
+
+ public void print() {
+ System.out.print("[");
+ System.out.print(row);
+ System.out.print(", TEXT, ");
+ System.out.print(text.toString());
+ System.out.println("]");
+ }
+}
+
+
diff --git a/src/main/java/com/jfinal/template/stat/Token.java b/src/main/java/com/jfinal/template/stat/Token.java
new file mode 100644
index 0000000..da17a23
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/Token.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat;
+
+/**
+ * Token
+ */
+class Token {
+
+ final Symbol symbol;
+ final int row;
+ private final String value;
+
+ Token(Symbol symbol, String value, int row) {
+ if (symbol == null || value == null) {
+ throw new IllegalArgumentException("symbol and value can not be null");
+ }
+ this.symbol = symbol;
+ this.value = value;
+ this.row = row;
+ }
+
+ Token(Symbol symbol, int row) {
+ this(symbol, symbol.getName(), row);
+ }
+
+ boolean hasPara() {
+ return symbol.hasPara();
+ }
+
+ boolean noPara() {
+ return symbol.noPara();
+ }
+
+ public String value() {
+ return value;
+ }
+
+ public String toString() {
+ return value;
+ }
+
+ public int getRow() {
+ return row;
+ }
+
+ public void print() {
+ System.out.print("[");
+ System.out.print(row);
+ System.out.print(", ");
+ System.out.print(symbol.getName());
+ System.out.print(", ");
+ System.out.print(value());
+ System.out.println("]");
+ }
+}
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/Break.java b/src/main/java/com/jfinal/template/stat/ast/Break.java
new file mode 100644
index 0000000..c8cd24f
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/Break.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.Writer;
+import com.jfinal.template.Env;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Break
+ * java 中 break、continue 可出现在 for 中的最后一行,不一定要套在 if 中
+ */
+public class Break extends Stat {
+
+ public static final Break me = new Break();
+
+ private Break() {
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ scope.getCtrl().setBreak();
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/Call.java b/src/main/java/com/jfinal/template/stat/ast/Call.java
new file mode 100644
index 0000000..39af894
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/Call.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.Writer;
+import com.jfinal.template.Env;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Call 调用模板函数,两种用法:
+ * 1:常规调用
+ * #@funcName(p1, p2, ..., pn)
+ * 2:安全调用,函数被定义才调用,否则跳过
+ * #@funcName?(p1, p2, ..., pn)
+ *
+ * 注意:在函数名前面引入 '@' 字符是为了区分模板函数和指令
+ */
+public class Call extends Stat {
+
+ private String funcName;
+ private ExprList exprList;
+ private boolean callIfDefined;
+
+ public Call(String funcName, ExprList exprList, boolean callIfDefined) {
+ this.funcName = funcName;
+ this.exprList = exprList;
+ this.callIfDefined = callIfDefined;
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ Define function = env.getFunction(funcName);
+ if (function != null) {
+ function.call(env, scope, exprList, writer);
+ } else if (callIfDefined) {
+ return ;
+ } else {
+ throw new TemplateException("Template function not defined: " + funcName, location);
+ }
+ }
+}
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/Continue.java b/src/main/java/com/jfinal/template/stat/ast/Continue.java
new file mode 100644
index 0000000..713475c
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/Continue.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.Writer;
+import com.jfinal.template.Env;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Continue
+ */
+public class Continue extends Stat {
+
+ public static final Continue me = new Continue();
+
+ private Continue() {
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ scope.getCtrl().setContinue();
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/Define.java b/src/main/java/com/jfinal/template/stat/ast/Define.java
new file mode 100644
index 0000000..2f866d0
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/Define.java
@@ -0,0 +1,141 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.Writer;
+import com.jfinal.template.Env;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+import com.jfinal.template.expr.ast.Expr;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.expr.ast.Id;
+
+/**
+ * Define 定义模板函数:
+ * #define funcName(p1, p2, ..., pn)
+ * body
+ * #end
+ *
+ * 模板函数类型:
+ * 1:全局共享的模板函数
+ * 通过 engine.addSharedFunction(...) 添加,所有模板中可调用
+ * 2:模板中定义的局部模板函数
+ * 在模板中定义的模板函数,只在本模板中有效
+ *
+ * 高级用法:
+ * 1:局部模板函数可以与全局共享模板函数同名,调用时优先调用模板内模板数
+ * 2:模板内部不能定义同名的局部模板函数
+ */
+public class Define extends Stat {
+
+ private static final String[] NULL_PARAMETER_NAMES = new String[0];
+
+ private String functionName;
+ private String[] parameterNames;
+ private Stat stat;
+
+ public Define(String functionName, ExprList exprList, Stat stat, Location location) {
+ setLocation(location);
+ this.functionName = functionName;
+ this.stat = stat;
+
+ Expr[] exprArray = exprList.getExprArray();
+ if (exprArray.length == 0) {
+ this.parameterNames = NULL_PARAMETER_NAMES;
+ return ;
+ }
+
+ parameterNames = new String[exprArray.length];
+ for (int i=0; i 0) {
+ Object[] parameterValues = exprList.evalExprList(scope);
+ for (int i=0; i 0) {
+ ret.append(", ");
+ }
+ ret.append(parameterNames[i]);
+ }
+ return ret.append(")").toString();
+ }
+
+ // -----------------------------------------------------------------------
+ /**
+ * envForDevMode 属性性以及相关方法仅用于 devMode 判断当前 #define 指令所在资源是否被修改
+ * 仅用于 EngineConfig 中处理 shared function 的逻辑
+ */
+ private Env envForDevMode;
+
+ public void setEnvForDevMode(Env envForDevMode) {
+ this.envForDevMode = envForDevMode;
+ }
+
+ public boolean isSourceModifiedForDevMode() {
+ if (envForDevMode == null) {
+ throw new IllegalStateException("Check engine config: setDevMode(...) must be invoked before addSharedFunction(...)");
+ }
+ return envForDevMode.isSourceListModified();
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/Else.java b/src/main/java/com/jfinal/template/stat/ast/Else.java
new file mode 100644
index 0000000..e10e8b2
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/Else.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.Writer;
+import com.jfinal.template.Env;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Else
+ */
+public class Else extends Stat {
+
+ private Stat stat;
+
+ public Else(Stat stat) {
+ this.stat = stat;
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ stat.exec(env, scope, writer);
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/ElseIf.java b/src/main/java/com/jfinal/template/stat/ast/ElseIf.java
new file mode 100644
index 0000000..2a5db45
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/ElseIf.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.Writer;
+import com.jfinal.template.Env;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.expr.ast.Logic;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * ElseIf
+ */
+public class ElseIf extends Stat {
+
+ private ExprList cond;
+ private Stat stat;
+ private Stat elseIfOrElse;
+
+ public ElseIf(ExprList cond, Stat stat, Location location) {
+ if (cond.length() == 0) {
+ throw new ParseException("The condition expression of #elseif statement can not be blank", location);
+ }
+ this.cond = cond;
+ this.stat = stat;
+ }
+
+ /**
+ * take over setStat(...) method of super class
+ */
+ public void setStat(Stat elseIfOrElse) {
+ this.elseIfOrElse = elseIfOrElse;
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ if (Logic.isTrue(cond.eval(scope))) {
+ stat.exec(env, scope, writer);
+ } else if (elseIfOrElse != null) {
+ elseIfOrElse.exec(env, scope, writer);
+ }
+ }
+}
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/For.java b/src/main/java/com/jfinal/template/stat/ast/For.java
new file mode 100644
index 0000000..d2b425b
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/For.java
@@ -0,0 +1,140 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.Writer;
+import java.util.Iterator;
+import com.jfinal.template.Env;
+import com.jfinal.template.expr.ast.Expr;
+import com.jfinal.template.expr.ast.ForCtrl;
+import com.jfinal.template.expr.ast.Logic;
+import com.jfinal.template.stat.Ctrl;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * For 循环控制,支持 List、Map、数组、Collection、Iterator、Iterable
+ * Enumeration、null 以及任意单个对象的迭代,简单说是支持所有对象迭代
+ *
+ * 主要用法:
+ * 1:#for(item : list) #(item) #end
+ * 2:#for(item : list) #(item) #else content #end
+ * 3:#for(i=0; i<9; i++) #(item) #end
+ * 4:#for(i=0; i<9; i++) #(item) #else content #end
+ */
+public class For extends Stat {
+
+ private ForCtrl forCtrl;
+ private StatList statList;
+ private Stat _else;
+
+ public For(ForCtrl forCtrl, StatList statList, Stat _else) {
+ this.forCtrl = forCtrl;
+ this.statList = statList;
+ this._else = _else;
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ scope = new Scope(scope);
+ if (forCtrl.isIterator()) {
+ forIterator(env, scope, writer);
+ } else {
+ forLoop(env, scope, writer);
+ }
+ }
+
+ /**
+ * #for( id : expr)
+ */
+ private void forIterator(Env env, Scope scope, Writer writer) {
+ Ctrl ctrl = scope.getCtrl();
+ Object outer = scope.get("for");
+ ctrl.setLocalAssignment();
+ ForIteratorStatus forIteratorStatus = new ForIteratorStatus(outer, forCtrl.getExpr().eval(scope), location);
+ ctrl.setWisdomAssignment();
+ scope.setLocal("for", forIteratorStatus);
+
+ Iterator> it = forIteratorStatus.getIterator();
+ String itemName = forCtrl.getId();
+ while(it.hasNext()) {
+ scope.setLocal(itemName, it.next());
+ statList.exec(env, scope, writer);
+ forIteratorStatus.nextState();
+
+ if (ctrl.isJump()) {
+ if (ctrl.isBreak()) {
+ ctrl.setJumpNone();
+ break ;
+ } else if (ctrl.isContinue()) {
+ ctrl.setJumpNone();
+ continue ;
+ } else {
+ return ;
+ }
+ }
+ }
+
+ if (_else != null && forIteratorStatus.getIndex() == 0) {
+ _else.exec(env, scope, writer);
+ }
+ }
+
+ /**
+ * #for(exprList; cond; update)
+ */
+ private void forLoop(Env env, Scope scope, Writer writer) {
+ Ctrl ctrl = scope.getCtrl();
+ Object outer = scope.get("for");
+ ForLoopStatus forLoopStatus = new ForLoopStatus(outer);
+ scope.setLocal("for", forLoopStatus);
+
+ Expr init = forCtrl.getInit();
+ Expr cond = forCtrl.getCond();
+ Expr update = forCtrl.getUpdate();
+
+ ctrl.setLocalAssignment();
+ for (init.eval(scope); cond == null || Logic.isTrue(cond.eval(scope)); update.eval(scope)) {
+ ctrl.setWisdomAssignment();
+ statList.exec(env, scope, writer);
+ ctrl.setLocalAssignment();
+ forLoopStatus.nextState();
+
+ if (ctrl.isJump()) {
+ if (ctrl.isBreak()) {
+ ctrl.setJumpNone();
+ break ;
+ } else if (ctrl.isContinue()) {
+ ctrl.setJumpNone();
+ continue ;
+ } else {
+ ctrl.setWisdomAssignment();
+ return ;
+ }
+ }
+ }
+
+ ctrl.setWisdomAssignment();
+ if (_else != null && forLoopStatus.getIndex() == 0) {
+ _else.exec(env, scope, writer);
+ }
+ }
+}
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/ForEntry.java b/src/main/java/com/jfinal/template/stat/ast/ForEntry.java
new file mode 100644
index 0000000..a308d56
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/ForEntry.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.util.Map.Entry;
+
+/**
+ * ForEntry 包装 HashMap、LinkedHashMap 等 Map 类型的 Entry 对象
+ */
+public class ForEntry implements Entry {
+
+ private Entry entry;
+
+ public ForEntry(Entry entry) {
+ this.entry = entry;
+ }
+
+ public Object getKey() {
+ return entry.getKey();
+ }
+
+ public Object getValue() {
+ return entry.getValue();
+ }
+
+ public Object setValue(Object value) {
+ return entry.setValue(value);
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/ForIteratorStatus.java b/src/main/java/com/jfinal/template/stat/ast/ForIteratorStatus.java
new file mode 100644
index 0000000..50b2bc9
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/ForIteratorStatus.java
@@ -0,0 +1,244 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.NoSuchElementException;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.stat.Location;
+
+/**
+ * ForIteratorStatus
+ * 封装 #for( id : expr) 迭代语句状态,便于模板中获取
+ *
+ * 使用以下表达式可以模板中获取迭代状态:
+ * for.size 被迭代集合元素数量,不支持 Iterator 与 Iterable
+ * for.index 从 0 下始的下标
+ * for.count 从 1 开始的计数器
+ * for.first 是否第一个元素
+ * for.last 是否最后一个元素
+ * for.odd 是否第奇数个元素
+ * for.even 是否第偶数个元素
+ * for.outer 获取外层 for 对象,便于获取外层 for 循环状态
+ * 例如: for.outer.index
+ */
+public class ForIteratorStatus {
+
+ private Object outer;
+ private int index;
+ private int size;
+ private Iterator> iterator;
+ private Location location;
+
+ public ForIteratorStatus(Object outer, Object target, Location location) {
+ this.outer = outer;
+ this.index = 0;
+ this.location = location;
+ init(target);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void init(Object target) {
+ if (target == null) {
+ size = 0;
+ iterator = NullIterator.me;
+ return ;
+ }
+ if (target instanceof Collection) {
+ size = ((Collection>)target).size();
+ iterator = ((Collection>)target).iterator();
+ return ;
+ }
+ if (target instanceof Map, ?>) {
+ size = ((Map, ?>)target).size();
+ iterator = new MapIterator(((Map)target).entrySet().iterator());
+ return ;
+ }
+ if (target.getClass().isArray()) {
+ size = Array.getLength(target);
+ iterator = new ArrayIterator(target, size);
+ return ;
+ }
+ if (target instanceof Iterator) {
+ size = -1;
+ iterator = (Iterator>)target;
+ return ;
+ }
+ if (target instanceof Iterable) {
+ size = -1;
+ iterator = ((Iterable>)target).iterator();
+ return ;
+ }
+ if (target instanceof Enumeration) {
+ ArrayList> list = Collections.list((Enumeration>)target);
+ size = list.size();
+ iterator = list.iterator();
+ return ;
+ }
+
+ size = 1;
+ iterator = new SingleObjectIterator(target);
+ }
+
+ Iterator> getIterator() {
+ return iterator;
+ }
+
+ void nextState() {
+ index++;
+ }
+
+ public Object getOuter() {
+ return outer;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public int getCount() {
+ return index + 1;
+ }
+
+ public int getSize() {
+ if (size >= 0) {
+ return size;
+ }
+ throw new TemplateException("No such method getSize() of the iterator", location);
+ }
+
+ public boolean getFirst() {
+ return index == 0;
+ }
+
+ public boolean getLast() {
+ return !iterator.hasNext();
+ }
+
+ public boolean getOdd() {
+ return index % 2 == 0;
+ }
+
+ public boolean getEven() {
+ return index % 2 != 0;
+ }
+}
+
+class MapIterator implements Iterator> {
+
+ private Iterator> iterator;
+
+ public MapIterator(Iterator> iterator) {
+ this.iterator = iterator;
+ }
+
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ public Entry next() {
+ return new ForEntry((Entry)iterator.next());
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+}
+
+class ArrayIterator implements Iterator {
+
+ private Object array;
+ private int size;
+ private int index;
+
+ ArrayIterator(Object array, int size) {
+ this.array = array;
+ this.size = size;
+ this.index = 0;
+ }
+
+ public boolean hasNext() {
+ return index < size;
+ }
+
+ public Object next() {
+ return Array.get(array, index++);
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+}
+
+class SingleObjectIterator implements Iterator {
+
+ private Object target;
+ private boolean hasNext = true;
+
+ public SingleObjectIterator(Object target) {
+ this.target = target;
+ }
+
+ public boolean hasNext() {
+ return hasNext;
+ }
+
+ public Object next() {
+ if (hasNext) {
+ hasNext = false;
+ return target;
+ }
+ throw new NoSuchElementException();
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+}
+
+class NullIterator implements Iterator {
+
+ static final Iterator> me = new NullIterator();
+
+ private NullIterator() {
+ }
+
+ public boolean hasNext() {
+ return false;
+ }
+
+ public Object next() {
+ throw new NoSuchElementException();
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+}
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/ForLoopStatus.java b/src/main/java/com/jfinal/template/stat/ast/ForLoopStatus.java
new file mode 100644
index 0000000..9f20c64
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/ForLoopStatus.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+/**
+ * ForLoopStatus
+ * 封装 #for( init; cond; update) 循环的状态,便于模板中获取
+ *
+ * 如下表达式可从模板中获取循环状态:
+ * for.index 从 0 下始的下标
+ * for.count 从 1 开始的计数器
+ * for.first 是否第一个元素
+ * for.odd 是否第奇数个元素
+ * for.even 是否第偶数个元素
+ * for.outer 获取外层 for 对象,便于获取外层 for 循环状态
+ * 例如: for.outer.index
+ *
+ * 注意:比迭代型循环语句少支持两个状态取值表达式:for.size、for.last
+ */
+public class ForLoopStatus {
+
+ private Object outer;
+ private int index;
+
+ public ForLoopStatus(Object outer) {
+ this.outer = outer;
+ this.index = 0;
+ }
+
+ void nextState() {
+ index++;
+ }
+
+ public Object getOuter() {
+ return outer;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public int getCount() {
+ return index + 1;
+ }
+
+ public boolean getFirst() {
+ return index == 0;
+ }
+
+ public boolean getOdd() {
+ return index % 2 == 0;
+ }
+
+ public boolean getEven() {
+ return index % 2 != 0;
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/If.java b/src/main/java/com/jfinal/template/stat/ast/If.java
new file mode 100644
index 0000000..116fb7a
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/If.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.Writer;
+import com.jfinal.template.Env;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.expr.ast.Logic;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * If
+ */
+public class If extends Stat {
+
+ private ExprList cond;
+ private Stat stat;
+ private Stat elseIfOrElse;
+
+ public If(ExprList cond, Stat stat, Location location) {
+ if (cond.length() == 0) {
+ throw new ParseException("The condition expression of #if statement can not be blank", location);
+ }
+ this.cond = cond;
+ this.stat = stat;
+ }
+
+ /**
+ * take over setStat(...) method of super class
+ */
+ public void setStat(Stat elseIfOrElse) {
+ this.elseIfOrElse = elseIfOrElse;
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ if (Logic.isTrue(cond.eval(scope))) {
+ stat.exec(env, scope, writer);
+ } else if (elseIfOrElse != null) {
+ elseIfOrElse.exec(env, scope, writer);
+ }
+ }
+}
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/Include.java b/src/main/java/com/jfinal/template/stat/ast/Include.java
new file mode 100644
index 0000000..7b6599d
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/Include.java
@@ -0,0 +1,161 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.Writer;
+import com.jfinal.template.EngineConfig;
+import com.jfinal.template.Env;
+import com.jfinal.template.expr.ast.Assign;
+import com.jfinal.template.expr.ast.Const;
+import com.jfinal.template.expr.ast.Expr;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.source.ISource;
+import com.jfinal.template.stat.Ctrl;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Parser;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Include
+ *
+ * 1:父模板被缓存时,被 include 的模板会被间接缓存,无需关心缓存问题
+ * 2:同一个模板文件被多个父模板 include,所处的背景环境不同,例如各父模板中定义的模板函数不同
+ * 各父模板所处的相对路径不同,所以多个父模板不能共用一次 parse 出来的结果,而是在每个被include
+ * 的地方重新 parse
+ *
+ *
+ * 两种用法:
+ * 1:只传入一个参数,参数必须是 String 常量,如果希望第一个参数是变量可以使用 #render 指令去实现
+ * #include("_hot.html")
+ *
+ * 2:传入任意多个参数,除第一个参数以外的所有参数必须是赋值表达式,用于实现参数传递功能
+ * #include("_hot.html", title = "热门新闻", list = newsList)
+ *
+ * 上例中传递了 title、list 两个参数,可以代替父模板中的 #set 指令传参方式
+ * 并且此方式传入的参数只在子模板作用域有效,不会污染父模板作用域
+ *
+ * 这种传参方式有利于将子模板模块化,例如上例的调用改成如下的参数:
+ * #include("_hot.html", title = "热门项目", list = projectList)
+ * 通过这种传参方式在子模板 _hot.html 之中,完全不需要修改对于 title 与 list
+ * 这两个变量的处理代码,就实现了对 “热门项目” 数据的渲染
+ *
+ */
+public class Include extends Stat {
+
+ private Assign[] assignArray;
+ private Stat stat;
+
+ public Include(Env env, ExprList exprList, String parentFileName, Location location) {
+ int len = exprList.length();
+ if (len == 0) {
+ throw new ParseException("The parameter of #include directive can not be blank", location);
+ }
+ // 第一个参数必须为 String 类型
+ Expr expr = exprList.getExpr(0);
+ if (expr instanceof Const && ((Const)expr).isStr()) {
+ } else {
+ throw new ParseException("The first parameter of #include directive must be String", location);
+ }
+ // 其它参数必须为赋值表达式
+ if (len > 1) {
+ for (int i = 1; i < len; i++) {
+ if (!(exprList.getExpr(i) instanceof Assign)) {
+ throw new ParseException("The " + i + "th parameter of #include directive must be an assignment expression", location);
+ }
+ }
+ }
+
+ parseSubTemplate(env, ((Const)expr).getStr(), parentFileName, location);
+ getAssignExpression(exprList);
+ }
+
+ private void parseSubTemplate(Env env, String fileName, String parentFileName, Location location) {
+ String subFileName = getSubFileName(fileName, parentFileName);
+ EngineConfig config = env.getEngineConfig();
+ // FileSource fileSource = new FileSource(config.getBaseTemplatePath(), subFileName, config.getEncoding());
+ ISource fileSource = config.getSourceFactory().getSource(config.getBaseTemplatePath(), subFileName, config.getEncoding());
+ try {
+ Parser parser = new Parser(env, fileSource.getContent(), subFileName);
+ if (config.isDevMode()) {
+ env.addSource(fileSource);
+ }
+ this.stat = parser.parse();
+ } catch (Exception e) {
+ // 文件路径不正确抛出异常时添加 location 信息
+ throw new ParseException(e.getMessage(), location, e);
+ }
+ }
+
+ /**
+ * 获取在父模板之下子模板的最终文件名,子模板目录相对于父模板文件目录来确定
+ * 以 "/" 打头则以 baseTemplatePath 为根,否则以父文件所在路径为根
+ */
+ public static String getSubFileName(String fileName, String parentFileName) {
+ if (parentFileName == null) {
+ return fileName;
+ }
+ if (fileName.startsWith("/")) {
+ return fileName;
+ }
+ int index = parentFileName.lastIndexOf('/');
+ if (index == -1) {
+ return fileName;
+ }
+ return parentFileName.substring(0, index + 1) + fileName;
+ }
+
+ private void getAssignExpression(ExprList exprList) {
+ int len = exprList.length();
+ if (len > 1) {
+ assignArray = new Assign[len - 1];
+ for (int i = 0; i < assignArray.length; i++) {
+ assignArray[i] = (Assign)exprList.getExpr(i + 1);
+ }
+ } else {
+ assignArray = null;
+ }
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ scope = new Scope(scope);
+ if (assignArray != null) {
+ evalAssignExpression(scope);
+ }
+ stat.exec(env, scope, writer);
+ scope.getCtrl().setJumpNone();
+ }
+
+ private void evalAssignExpression(Scope scope) {
+ Ctrl ctrl = scope.getCtrl();
+ try {
+ ctrl.setLocalAssignment();
+ for (Assign assign : assignArray) {
+ assign.eval(scope);
+ }
+ } finally {
+ ctrl.setWisdomAssignment();
+ }
+ }
+}
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/Output.java b/src/main/java/com/jfinal/template/stat/ast/Output.java
new file mode 100644
index 0000000..585aa8f
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/Output.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.Writer;
+import com.jfinal.template.Env;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Output 输出指令
+ *
+ * 用法:
+ * 1:#(value)
+ * 2:#(x = 1, y = 2, x + y)
+ * 3:#(seoTitle ?? 'JFinal 极速开发社区')
+ */
+public class Output extends Stat {
+
+ private ExprList exprList;
+
+ public Output(ExprList exprList, Location location) {
+ if (exprList.length() == 0) {
+ throw new ParseException("The expression of output directive like #(expression) can not be blank", location);
+ }
+ this.exprList = exprList;
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ try {
+ Object value = exprList.eval(scope);
+ if (value != null) {
+ String str = value.toString();
+ writer.write(str, 0, str.length());
+ }
+ } catch(TemplateException e) {
+ throw e;
+ } catch(Exception e) {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+ }
+}
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/Return.java b/src/main/java/com/jfinal/template/stat/ast/Return.java
new file mode 100644
index 0000000..8d2df73
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/Return.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.Writer;
+import com.jfinal.template.Env;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Return
+ * 通常用于 #define 指令内部,不支持返回值
+ */
+public class Return extends Stat {
+
+ public static final Return me = new Return();
+
+ private Return() {
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ scope.getCtrl().setReturn();
+ }
+}
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/Set.java b/src/main/java/com/jfinal/template/stat/ast/Set.java
new file mode 100644
index 0000000..6b676d2
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/Set.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.Writer;
+import com.jfinal.template.Env;
+import com.jfinal.template.expr.ast.Assign;
+import com.jfinal.template.expr.ast.Expr;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Set 赋值,从内向外作用域查找变量,找到则替换变量值,否则在顶层作用域赋值
+ *
+ * 用法:
+ * 1:#set(k = v)
+ * 2:#set(k1 = v1, k2 = v2, ..., kn = vn)
+ * 3:#set(x = 1+2)
+ * 4:#set(x = 1+2, y = 3>4, ..., z = c ? a : b)
+ */
+public class Set extends Stat {
+
+ private ExprList exprList;
+
+ public Set(ExprList exprList, Location location) {
+ if (exprList.length() == 0) {
+ throw new ParseException("The parameter of #set directive can not be blank", location);
+ }
+
+ for (Expr expr : exprList.getExprArray()) {
+ if ( !(expr instanceof Assign) ) {
+ throw new ParseException("#set directive only supports assignment expressions", location);
+ }
+ }
+ this.exprList = exprList;
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ scope.getCtrl().setWisdomAssignment();
+ exprList.eval(scope);
+ }
+}
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/SetGlobal.java b/src/main/java/com/jfinal/template/stat/ast/SetGlobal.java
new file mode 100644
index 0000000..b332b63
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/SetGlobal.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.Writer;
+import com.jfinal.template.Env;
+import com.jfinal.template.expr.ast.Assign;
+import com.jfinal.template.expr.ast.Expr;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.stat.Ctrl;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * SetLocal 设置全局变量,全局作用域是指本次请求的整个 template
+ *
+ * 适用于极少数的在内层作用域中希望直接操作顶层作用域的场景
+ */
+public class SetGlobal extends Stat {
+
+ private ExprList exprList;
+
+ public SetGlobal(ExprList exprList, Location location) {
+ if (exprList.length() == 0) {
+ throw new ParseException("The parameter of #setGlobal directive can not be blank", location);
+ }
+
+ for (Expr expr : exprList.getExprArray()) {
+ if ( !(expr instanceof Assign) ) {
+ throw new ParseException("#setGlobal directive only supports assignment expressions", location);
+ }
+ }
+ this.exprList = exprList;
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ Ctrl ctrl = scope.getCtrl();
+ try {
+ ctrl.setGlobalAssignment();
+ exprList.eval(scope);
+ } finally {
+ ctrl.setWisdomAssignment();
+ }
+ }
+}
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/SetLocal.java b/src/main/java/com/jfinal/template/stat/ast/SetLocal.java
new file mode 100644
index 0000000..1cfedb4
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/SetLocal.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.Writer;
+import com.jfinal.template.Env;
+import com.jfinal.template.expr.ast.Assign;
+import com.jfinal.template.expr.ast.Expr;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.stat.Ctrl;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.ParseException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * SetLocal 设置局部变量
+ *
+ * 通常用于 #define #include 指令内部需要与外层作用域区分,以便于定义重用型模块的场景
+ * 也常用于 #for 循环内部的临时变量
+ */
+public class SetLocal extends Stat {
+
+ final ExprList exprList;
+
+ public SetLocal(ExprList exprList, Location location) {
+ if (exprList.length() == 0) {
+ throw new ParseException("The parameter of #setLocal directive can not be blank", location);
+ }
+
+ for (Expr expr : exprList.getExprArray()) {
+ if ( !(expr instanceof Assign) ) {
+ throw new ParseException("#setLocal directive only supports assignment expressions", location);
+ }
+ }
+ this.exprList = exprList;
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ Ctrl ctrl = scope.getCtrl();
+ try {
+ ctrl.setLocalAssignment();
+ exprList.eval(scope);
+ } finally {
+ ctrl.setWisdomAssignment();
+ }
+ }
+}
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/Stat.java b/src/main/java/com/jfinal/template/stat/ast/Stat.java
new file mode 100644
index 0000000..dff9a24
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/Stat.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.IOException;
+import java.io.Writer;
+import com.jfinal.template.Env;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.expr.ast.ExprList;
+import com.jfinal.template.stat.Location;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Stat
+ */
+public abstract class Stat {
+
+ protected Location location;
+
+ public Stat setLocation(Location location) {
+ this.location = location;
+ return this;
+ }
+
+ public Location getLocation() {
+ return location;
+ }
+
+ public void setExprList(ExprList exprList) {
+ }
+
+ public void setStat(Stat stat) {
+ }
+
+ public abstract void exec(Env env, Scope scope, Writer writer);
+
+ public boolean hasEnd() {
+ return false;
+ }
+
+ protected void write(Writer writer, String str) {
+ try {
+ writer.write(str, 0, str.length());
+ } catch (IOException e) {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+ }
+
+ protected void write(Writer writer, char[] chars) {
+ try {
+ writer.write(chars, 0, chars.length);
+ } catch (IOException e) {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+ }
+}
+
+
+
+
+
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/StatList.java b/src/main/java/com/jfinal/template/stat/ast/StatList.java
new file mode 100644
index 0000000..0fc26ef
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/StatList.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.Writer;
+import java.util.List;
+import com.jfinal.template.Env;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.stat.Ctrl;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * StatList
+ */
+public class StatList extends Stat {
+
+ public static final Stat[] NULL_STATS = new Stat[0];
+ private Stat[] statArray;
+
+ public StatList(List statList) {
+ if (statList.size() > 0) {
+ this.statArray = statList.toArray(new Stat[statList.size()]);
+ } else {
+ this.statArray = NULL_STATS;
+ }
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ Ctrl ctrl = scope.getCtrl();
+ for (Stat stat : statArray) {
+ if (ctrl.isJump()) {
+ break ;
+ }
+ stat.exec(env, scope, writer);
+ }
+ }
+
+ public int length() {
+ return statArray.length;
+ }
+
+ public Stat getStat(int index) {
+ if (index < 0 || index >= statArray.length) {
+ throw new TemplateException("Index out of bounds: index = " + index + ", length = " + statArray.length, location);
+ }
+ return statArray[index];
+ }
+}
+
+
diff --git a/src/main/java/com/jfinal/template/stat/ast/Text.java b/src/main/java/com/jfinal/template/stat/ast/Text.java
new file mode 100644
index 0000000..288aa3e
--- /dev/null
+++ b/src/main/java/com/jfinal/template/stat/ast/Text.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2011-2017, James Zhan 詹波 (jfinal@126.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jfinal.template.stat.ast;
+
+import java.io.IOException;
+import java.io.Writer;
+import com.jfinal.template.Env;
+import com.jfinal.template.TemplateException;
+import com.jfinal.template.stat.Scope;
+
+/**
+ * Text 输出纯文本块以及使用 "#[[" 与 "]]#" 指定的非解析块
+ */
+public class Text extends Stat {
+
+ private char[] text;
+
+ public Text(StringBuilder content) {
+ this.text = new char[content.length()];
+ content.getChars(0, content.length(), this.text, 0);
+ }
+
+ public void exec(Env env, Scope scope, Writer writer) {
+ try {
+ writer.write(text, 0, text.length);
+ } catch (IOException e) {
+ throw new TemplateException(e.getMessage(), location, e);
+ }
+ }
+
+ public boolean isEmpty() {
+ return text.length == 0;
+ }
+
+ public String getContent() {
+ return text != null ? new String(text) : null;
+ }
+
+ public String toString() {
+ return text != null ? new String(text) : "";
+ }
+}
+
+
+
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..80447f5
--- /dev/null
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/test/java/com/jfinal/template/EngineTest.java b/src/test/java/com/jfinal/template/EngineTest.java
new file mode 100644
index 0000000..8678b59
--- /dev/null
+++ b/src/test/java/com/jfinal/template/EngineTest.java
@@ -0,0 +1,11 @@
+package com.jfinal.template;
+
+import com.jfinal.kit.Kv;
+
+public class EngineTest {
+ public static void main(String[] args) {
+ Kv para = Kv.by("key", "value");
+ String result = Engine.use().getTemplateByString("#(key)").renderToString(para);
+ System.out.println(result);
+ }
+}
diff --git a/src/test/java/com/jfinal/template/SpringBootConfig.java b/src/test/java/com/jfinal/template/SpringBootConfig.java
new file mode 100644
index 0000000..bcb4883
--- /dev/null
+++ b/src/test/java/com/jfinal/template/SpringBootConfig.java
@@ -0,0 +1,34 @@
+package com.jfinal.template;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import com.jfinal.template.ext.spring.JFinalViewResolver;
+import com.jfinal.template.source.ClassPathSourceFactory;
+
+/**
+ * 整合 Spring Boot
+ */
+@Configuration
+public class SpringBootConfig {
+ @Bean(name = "jfinalViewResolver")
+ public JFinalViewResolver getJFinalViewResolver() {
+ JFinalViewResolver jfr = new JFinalViewResolver();
+ // setDevMode 配置放在最前面
+ jfr.setDevMode(true);
+
+ // 使用 ClassPathSourceFactory 从 class path 与 jar 包中加载模板文件
+ jfr.setSourceFactory( new ClassPathSourceFactory() );
+ jfr.setPrefix("/view/");
+ jfr.setSuffix(".html");
+ jfr.setContentType("text/html;charset=UTF-8");
+ jfr.setOrder(0);
+ jfr.addSharedFunction("/view/common/_layout.html");
+ jfr.addSharedFunction("/view/common/_paginate.html");
+ return jfr;
+ }
+}
+
+
+
+
+