From b82af8e21904379d975c3ba7ed1c34dbc92cce1c Mon Sep 17 00:00:00 2001 From: James Date: Mon, 31 Jul 2017 22:34:15 +0800 Subject: [PATCH] Enjoy 3.2 release ^_^ --- .gitignore | 52 ++ LICENSE | 191 ------ README.md | 43 +- pom.xml | 117 ++++ src/main/java/com/jfinal/kit/ElKit.java | 74 +++ src/main/java/com/jfinal/kit/HashKit.java | 105 ++++ src/main/java/com/jfinal/kit/JavaKeyword.java | 120 ++++ src/main/java/com/jfinal/kit/Kv.java | 197 ++++++ src/main/java/com/jfinal/kit/ReflectKit.java | 38 ++ src/main/java/com/jfinal/kit/StrKit.java | 165 +++++ .../java/com/jfinal/template/Directive.java | 63 ++ src/main/java/com/jfinal/template/Engine.java | 480 ++++++++++++++ .../com/jfinal/template/EngineConfig.java | 363 +++++++++++ src/main/java/com/jfinal/template/Env.java | 123 ++++ .../com/jfinal/template/FastStringWriter.java | 105 ++++ .../template/IOutputDirectiveFactory.java | 56 ++ .../template/OutputDirectiveFactory.java | 34 + .../java/com/jfinal/template/Template.java | 92 +++ .../jfinal/template/TemplateException.java | 36 ++ .../com/jfinal/template/expr/ExprLexer.java | 520 ++++++++++++++++ .../com/jfinal/template/expr/ExprParser.java | 588 ++++++++++++++++++ .../java/com/jfinal/template/expr/NumTok.java | 103 +++ .../java/com/jfinal/template/expr/Sym.java | 64 ++ .../java/com/jfinal/template/expr/Tok.java | 63 ++ .../com/jfinal/template/expr/ast/Arith.java | 229 +++++++ .../com/jfinal/template/expr/ast/Array.java | 68 ++ .../com/jfinal/template/expr/ast/Assign.java | 142 +++++ .../com/jfinal/template/expr/ast/Compare.java | 288 +++++++++ .../com/jfinal/template/expr/ast/Const.java | 150 +++++ .../com/jfinal/template/expr/ast/Expr.java | 34 + .../jfinal/template/expr/ast/ExprList.java | 89 +++ .../com/jfinal/template/expr/ast/Field.java | 121 ++++ .../jfinal/template/expr/ast/FieldKit.java | 65 ++ .../com/jfinal/template/expr/ast/ForCtrl.java | 99 +++ .../java/com/jfinal/template/expr/ast/Id.java | 48 ++ .../com/jfinal/template/expr/ast/IncDec.java | 138 ++++ .../com/jfinal/template/expr/ast/Index.java | 86 +++ .../com/jfinal/template/expr/ast/Logic.java | 145 +++++ .../com/jfinal/template/expr/ast/Map.java | 67 ++ .../com/jfinal/template/expr/ast/Method.java | 129 ++++ .../jfinal/template/expr/ast/MethodInfo.java | 99 +++ .../template/expr/ast/MethodInfoExt.java | 61 ++ .../jfinal/template/expr/ast/MethodKit.java | 393 ++++++++++++ .../jfinal/template/expr/ast/NullSafe.java | 68 ++ .../jfinal/template/expr/ast/RangeArray.java | 96 +++ .../template/expr/ast/SharedMethod.java | 67 ++ .../template/expr/ast/SharedMethodKit.java | 173 ++++++ .../jfinal/template/expr/ast/StaticField.java | 65 ++ .../template/expr/ast/StaticMethod.java | 85 +++ .../com/jfinal/template/expr/ast/Ternary.java | 56 ++ .../com/jfinal/template/expr/ast/Unary.java | 91 +++ .../template/ext/directive/DateDirective.java | 114 ++++ .../ext/directive/EscapeDirective.java | 72 +++ .../template/ext/directive/NowDirective.java | 64 ++ .../ext/directive/RandomDirective.java | 37 ++ .../ext/directive/RenderDirective.java | 182 ++++++ .../ext/directive/StringDirective.java | 97 +++ .../template/ext/extensionmethod/ByteExt.java | 49 ++ .../ext/extensionmethod/DoubleExt.java | 49 ++ .../ext/extensionmethod/FloatExt.java | 49 ++ .../ext/extensionmethod/IntegerExt.java | 69 ++ .../template/ext/extensionmethod/LongExt.java | 49 ++ .../ext/extensionmethod/ShortExt.java | 49 ++ .../ext/extensionmethod/StringExt.java | 86 +++ .../template/ext/spring/JFinalView.java | 166 +++++ .../ext/spring/JFinalViewResolver.java | 254 ++++++++ .../template/source/ClassPathSource.java | 207 ++++++ .../source/ClassPathSourceFactory.java | 34 + .../jfinal/template/source/FileSource.java | 133 ++++ .../template/source/FileSourceFactory.java | 34 + .../com/jfinal/template/source/ISource.java | 48 ++ .../template/source/ISourceFactory.java | 34 + .../jfinal/template/source/StringSource.java | 81 +++ .../com/jfinal/template/stat/CharTable.java | 114 ++++ .../java/com/jfinal/template/stat/Ctrl.java | 118 ++++ .../java/com/jfinal/template/stat/Lexer.java | 526 ++++++++++++++++ .../com/jfinal/template/stat/Location.java | 61 ++ .../com/jfinal/template/stat/ParaToken.java | 52 ++ .../jfinal/template/stat/ParseException.java | 34 + .../java/com/jfinal/template/stat/Parser.java | 258 ++++++++ .../java/com/jfinal/template/stat/Scope.java | 235 +++++++ .../java/com/jfinal/template/stat/Symbol.java | 106 ++++ .../com/jfinal/template/stat/TextToken.java | 91 +++ .../java/com/jfinal/template/stat/Token.java | 72 +++ .../com/jfinal/template/stat/ast/Break.java | 40 ++ .../com/jfinal/template/stat/ast/Call.java | 57 ++ .../jfinal/template/stat/ast/Continue.java | 40 ++ .../com/jfinal/template/stat/ast/Define.java | 141 +++++ .../com/jfinal/template/stat/ast/Else.java | 40 ++ .../com/jfinal/template/stat/ast/ElseIf.java | 63 ++ .../com/jfinal/template/stat/ast/For.java | 140 +++++ .../jfinal/template/stat/ast/ForEntry.java | 46 ++ .../template/stat/ast/ForIteratorStatus.java | 244 ++++++++ .../template/stat/ast/ForLoopStatus.java | 74 +++ .../java/com/jfinal/template/stat/ast/If.java | 61 ++ .../com/jfinal/template/stat/ast/Include.java | 161 +++++ .../com/jfinal/template/stat/ast/Output.java | 63 ++ .../com/jfinal/template/stat/ast/Return.java | 44 ++ .../com/jfinal/template/stat/ast/Set.java | 59 ++ .../jfinal/template/stat/ast/SetGlobal.java | 65 ++ .../jfinal/template/stat/ast/SetLocal.java | 66 ++ .../com/jfinal/template/stat/ast/Stat.java | 77 +++ .../jfinal/template/stat/ast/StatList.java | 64 ++ .../com/jfinal/template/stat/ast/Text.java | 59 ++ src/main/webapp/WEB-INF/web.xml | 4 + .../java/com/jfinal/template/EngineTest.java | 11 + .../com/jfinal/template/SpringBootConfig.java | 34 + 107 files changed, 12029 insertions(+), 192 deletions(-) create mode 100644 .gitignore delete mode 100644 LICENSE create mode 100644 pom.xml create mode 100644 src/main/java/com/jfinal/kit/ElKit.java create mode 100644 src/main/java/com/jfinal/kit/HashKit.java create mode 100644 src/main/java/com/jfinal/kit/JavaKeyword.java create mode 100644 src/main/java/com/jfinal/kit/Kv.java create mode 100644 src/main/java/com/jfinal/kit/ReflectKit.java create mode 100644 src/main/java/com/jfinal/kit/StrKit.java create mode 100644 src/main/java/com/jfinal/template/Directive.java create mode 100644 src/main/java/com/jfinal/template/Engine.java create mode 100644 src/main/java/com/jfinal/template/EngineConfig.java create mode 100644 src/main/java/com/jfinal/template/Env.java create mode 100644 src/main/java/com/jfinal/template/FastStringWriter.java create mode 100644 src/main/java/com/jfinal/template/IOutputDirectiveFactory.java create mode 100644 src/main/java/com/jfinal/template/OutputDirectiveFactory.java create mode 100644 src/main/java/com/jfinal/template/Template.java create mode 100644 src/main/java/com/jfinal/template/TemplateException.java create mode 100644 src/main/java/com/jfinal/template/expr/ExprLexer.java create mode 100644 src/main/java/com/jfinal/template/expr/ExprParser.java create mode 100644 src/main/java/com/jfinal/template/expr/NumTok.java create mode 100644 src/main/java/com/jfinal/template/expr/Sym.java create mode 100644 src/main/java/com/jfinal/template/expr/Tok.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/Arith.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/Array.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/Assign.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/Compare.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/Const.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/Expr.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/ExprList.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/Field.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/FieldKit.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/ForCtrl.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/Id.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/IncDec.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/Index.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/Logic.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/Map.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/Method.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/MethodInfo.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/MethodInfoExt.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/MethodKit.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/NullSafe.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/RangeArray.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/SharedMethod.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/SharedMethodKit.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/StaticField.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/StaticMethod.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/Ternary.java create mode 100644 src/main/java/com/jfinal/template/expr/ast/Unary.java create mode 100644 src/main/java/com/jfinal/template/ext/directive/DateDirective.java create mode 100644 src/main/java/com/jfinal/template/ext/directive/EscapeDirective.java create mode 100644 src/main/java/com/jfinal/template/ext/directive/NowDirective.java create mode 100644 src/main/java/com/jfinal/template/ext/directive/RandomDirective.java create mode 100644 src/main/java/com/jfinal/template/ext/directive/RenderDirective.java create mode 100644 src/main/java/com/jfinal/template/ext/directive/StringDirective.java create mode 100644 src/main/java/com/jfinal/template/ext/extensionmethod/ByteExt.java create mode 100644 src/main/java/com/jfinal/template/ext/extensionmethod/DoubleExt.java create mode 100644 src/main/java/com/jfinal/template/ext/extensionmethod/FloatExt.java create mode 100644 src/main/java/com/jfinal/template/ext/extensionmethod/IntegerExt.java create mode 100644 src/main/java/com/jfinal/template/ext/extensionmethod/LongExt.java create mode 100644 src/main/java/com/jfinal/template/ext/extensionmethod/ShortExt.java create mode 100644 src/main/java/com/jfinal/template/ext/extensionmethod/StringExt.java create mode 100644 src/main/java/com/jfinal/template/ext/spring/JFinalView.java create mode 100644 src/main/java/com/jfinal/template/ext/spring/JFinalViewResolver.java create mode 100644 src/main/java/com/jfinal/template/source/ClassPathSource.java create mode 100644 src/main/java/com/jfinal/template/source/ClassPathSourceFactory.java create mode 100644 src/main/java/com/jfinal/template/source/FileSource.java create mode 100644 src/main/java/com/jfinal/template/source/FileSourceFactory.java create mode 100644 src/main/java/com/jfinal/template/source/ISource.java create mode 100644 src/main/java/com/jfinal/template/source/ISourceFactory.java create mode 100644 src/main/java/com/jfinal/template/source/StringSource.java create mode 100644 src/main/java/com/jfinal/template/stat/CharTable.java create mode 100644 src/main/java/com/jfinal/template/stat/Ctrl.java create mode 100644 src/main/java/com/jfinal/template/stat/Lexer.java create mode 100644 src/main/java/com/jfinal/template/stat/Location.java create mode 100644 src/main/java/com/jfinal/template/stat/ParaToken.java create mode 100644 src/main/java/com/jfinal/template/stat/ParseException.java create mode 100644 src/main/java/com/jfinal/template/stat/Parser.java create mode 100644 src/main/java/com/jfinal/template/stat/Scope.java create mode 100644 src/main/java/com/jfinal/template/stat/Symbol.java create mode 100644 src/main/java/com/jfinal/template/stat/TextToken.java create mode 100644 src/main/java/com/jfinal/template/stat/Token.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/Break.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/Call.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/Continue.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/Define.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/Else.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/ElseIf.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/For.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/ForEntry.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/ForIteratorStatus.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/ForLoopStatus.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/If.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/Include.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/Output.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/Return.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/Set.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/SetGlobal.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/SetLocal.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/Stat.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/StatList.java create mode 100644 src/main/java/com/jfinal/template/stat/ast/Text.java create mode 100644 src/main/webapp/WEB-INF/web.xml create mode 100644 src/test/java/com/jfinal/template/EngineTest.java create mode 100644 src/test/java/com/jfinal/template/SpringBootConfig.java 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; + } +} + + + + +