From 29ca6499657998f57dbe4f474c465c007f15d276 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 3 Dec 2021 22:33:23 -0600 Subject: [PATCH] Add thymeleaf-spring6 --- thymeleaf-spring6/BUILD.txt | 34 + thymeleaf-spring6/ChangeLog.txt | 110 ++ thymeleaf-spring6/LICENSE.txt | 202 +++ thymeleaf-spring6/NOTICE.txt | 15 + thymeleaf-spring6/RELEASING.txt | 34 + thymeleaf-spring6/USAGE.txt | 20 + thymeleaf-spring6/pom.xml | 363 ++++++ .../spring5/ISpringTemplateEngine.java | 62 + .../spring5/ISpringWebFluxTemplateEngine.java | 98 ++ .../spring5/SpringTemplateEngine.java | 332 +++++ .../spring5/SpringWebFluxTemplateEngine.java | 917 ++++++++++++++ .../spring5/context/IThymeleafBindStatus.java | 58 + .../context/IThymeleafRequestContext.java | 94 ++ .../IThymeleafRequestDataValueProcessor.java | 46 + .../spring5/context/SpringContextUtils.java | 128 ++ .../IReactiveDataDriverContextVariable.java | 123 ++ ...IReactiveSSEDataDriverContextVariable.java | 117 ++ .../webflux/ISpringWebFluxContext.java | 92 ++ .../webflux/ReactiveContextVariableUtils.java | 110 ++ .../ReactiveDataDriverContextVariable.java | 321 +++++ .../context/webflux/SpringWebFluxContext.java | 145 +++ .../webflux/SpringWebFluxEngineContext.java | 749 +++++++++++ .../SpringWebFluxEngineContextFactory.java | 105 ++ .../SpringWebFluxExpressionContext.java | 95 ++ .../SpringWebFluxThymeleafBindStatus.java | 138 +++ .../SpringWebFluxThymeleafRequestContext.java | 239 ++++ ...luxThymeleafRequestDataValueProcessor.java | 89 ++ .../SpringWebMvcThymeleafBindStatus.java | 137 ++ .../SpringWebMvcThymeleafRequestContext.java | 239 ++++ ...MvcThymeleafRequestDataValueProcessor.java | 92 ++ .../dialect/SpringStandardDialect.java | 424 +++++++ .../thymeleaf/spring5/expression/Fields.java | 112 ++ .../IThymeleafEvaluationContext.java | 45 + .../org/thymeleaf/spring5/expression/Mvc.java | 155 +++ .../spring5/expression/RequestDataValues.java | 94 ++ .../expression/SPELContextMapWrapper.java | 190 +++ .../SPELContextPropertyAccessor.java | 188 +++ .../SPELVariableExpressionEvaluator.java | 435 +++++++ .../SpringStandardConversionService.java | 116 ++ ...SpringStandardExpressionObjectFactory.java | 112 ++ .../expression/SpringStandardExpressions.java | 82 ++ .../thymeleaf/spring5/expression/Themes.java | 75 ++ .../ThymeleafEvaluationContext.java | 149 +++ .../ThymeleafEvaluationContextWrapper.java | 201 +++ .../webflux/SpringWebFluxLinkBuilder.java | 121 ++ .../SpringMessageResolver.java | 178 +++ .../naming/SpringContextVariableNames.java | 50 + .../AbstractSpringFieldTagProcessor.java | 229 ++++ .../processor/SpringActionTagProcessor.java | 150 +++ .../SpringErrorClassTagProcessor.java | 192 +++ .../processor/SpringErrorsTagProcessor.java | 94 ++ .../processor/SpringHrefTagProcessor.java | 95 ++ .../SpringInputCheckboxFieldTagProcessor.java | 169 +++ .../SpringInputFileFieldTagProcessor.java | 70 ++ .../SpringInputGeneralFieldTagProcessor.java | 128 ++ .../SpringInputPasswordFieldTagProcessor.java | 76 ++ .../SpringInputRadioFieldTagProcessor.java | 92 ++ .../processor/SpringMethodTagProcessor.java | 154 +++ .../processor/SpringObjectTagProcessor.java | 97 ++ .../SpringOptionFieldTagProcessor.java | 84 ++ ...SpringOptionInSelectFieldTagProcessor.java | 90 ++ .../SpringSelectFieldTagProcessor.java | 117 ++ .../processor/SpringSrcTagProcessor.java | 88 ++ .../SpringTextareaFieldTagProcessor.java | 85 ++ .../processor/SpringUErrorsTagProcessor.java | 93 ++ .../processor/SpringValueTagProcessor.java | 116 ++ .../RequestDataValueProcessorUtils.java | 104 ++ .../SpringResourceTemplateResolver.java | 73 ++ .../SpringResourceTemplateResource.java | 211 ++++ .../thymeleaf/spring5/util/DetailedError.java | 87 ++ .../thymeleaf/spring5/util/FieldUtils.java | 423 +++++++ .../spring5/util/SpringContentTypeUtils.java | 100 ++ .../spring5/util/SpringRequestUtils.java | 115 ++ .../util/SpringSelectedValueComparator.java | 193 +++ .../util/SpringStandardExpressionUtils.java | 119 ++ .../spring5/util/SpringValueFormatter.java | 77 ++ .../spring5/util/SpringVersionUtils.java | 199 +++ .../spring5/view/AbstractThymeleafView.java | 563 +++++++++ .../thymeleaf/spring5/view/ThymeleafView.java | 378 ++++++ .../spring5/view/ThymeleafViewResolver.java | 891 +++++++++++++ .../view/reactive/ThymeleafReactiveView.java | 677 ++++++++++ .../ThymeleafReactiveViewResolver.java | 1099 +++++++++++++++++ .../Spring-Standard-Dialect.xml | 136 ++ thymeleaf-spring6/src/test/java/README.txt | 2 + .../src/test/resources/README.txt | 2 + 85 files changed, 15699 insertions(+) create mode 100755 thymeleaf-spring6/BUILD.txt create mode 100755 thymeleaf-spring6/ChangeLog.txt create mode 100644 thymeleaf-spring6/LICENSE.txt create mode 100755 thymeleaf-spring6/NOTICE.txt create mode 100644 thymeleaf-spring6/RELEASING.txt create mode 100755 thymeleaf-spring6/USAGE.txt create mode 100755 thymeleaf-spring6/pom.xml create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/ISpringTemplateEngine.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/ISpringWebFluxTemplateEngine.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/SpringTemplateEngine.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/SpringWebFluxTemplateEngine.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/IThymeleafBindStatus.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/IThymeleafRequestContext.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/IThymeleafRequestDataValueProcessor.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/SpringContextUtils.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/IReactiveDataDriverContextVariable.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/IReactiveSSEDataDriverContextVariable.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/ISpringWebFluxContext.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/ReactiveContextVariableUtils.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/ReactiveDataDriverContextVariable.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxContext.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxEngineContext.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxEngineContextFactory.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxExpressionContext.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxThymeleafBindStatus.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxThymeleafRequestContext.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxThymeleafRequestDataValueProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webmvc/SpringWebMvcThymeleafBindStatus.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webmvc/SpringWebMvcThymeleafRequestContext.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webmvc/SpringWebMvcThymeleafRequestDataValueProcessor.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/dialect/SpringStandardDialect.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/Fields.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/IThymeleafEvaluationContext.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/Mvc.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/RequestDataValues.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SPELContextMapWrapper.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SPELContextPropertyAccessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SPELVariableExpressionEvaluator.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SpringStandardConversionService.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SpringStandardExpressionObjectFactory.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SpringStandardExpressions.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/Themes.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/ThymeleafEvaluationContext.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/ThymeleafEvaluationContextWrapper.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/linkbuilder/webflux/SpringWebFluxLinkBuilder.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/messageresolver/SpringMessageResolver.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/naming/SpringContextVariableNames.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/AbstractSpringFieldTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringActionTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringErrorClassTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringErrorsTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringHrefTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputCheckboxFieldTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputFileFieldTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputGeneralFieldTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputPasswordFieldTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputRadioFieldTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringMethodTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringObjectTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringOptionFieldTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringOptionInSelectFieldTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringSelectFieldTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringSrcTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringTextareaFieldTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringUErrorsTagProcessor.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringValueTagProcessor.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/requestdata/RequestDataValueProcessorUtils.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/templateresolver/SpringResourceTemplateResolver.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/templateresource/SpringResourceTemplateResource.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/util/DetailedError.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/util/FieldUtils.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/util/SpringContentTypeUtils.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/util/SpringRequestUtils.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/util/SpringSelectedValueComparator.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/util/SpringStandardExpressionUtils.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/util/SpringValueFormatter.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/util/SpringVersionUtils.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/view/AbstractThymeleafView.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/view/ThymeleafView.java create mode 100755 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/view/ThymeleafViewResolver.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/view/reactive/ThymeleafReactiveView.java create mode 100644 thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/view/reactive/ThymeleafReactiveViewResolver.java create mode 100755 thymeleaf-spring6/src/main/resources/org/thymeleaf/xml/thymeleaf-spring5/Spring-Standard-Dialect.xml create mode 100644 thymeleaf-spring6/src/test/java/README.txt create mode 100644 thymeleaf-spring6/src/test/resources/README.txt diff --git a/thymeleaf-spring6/BUILD.txt b/thymeleaf-spring6/BUILD.txt new file mode 100755 index 00000000..80a50946 --- /dev/null +++ b/thymeleaf-spring6/BUILD.txt @@ -0,0 +1,34 @@ + + Building thymeleaf-spring5 + -------------------------- + + To build thymeleaf you will need Maven 3. You can get it at: + + http://maven.apache.org + + Build and install the project executing, from the thymeleaf + project root folder: + + mvn clean:clean install + + And you will get a fresh target/thymeleaf-spring5-{version}.jar file. + You will also get it installed in your local repository at: + + $M2_REPO/org/thymeleaf/thymeleaf-spring5/{version}/thymeleaf-spring5-{version}.jar + + + + Generating Javadoc for thymeleaf + -------------------------------- + + If you wish to generate the javadoc for thymeleaf, execute this from the + thymeleaf root folder: + + mvn javadoc:javadoc + + This will generate the javadoc documentation in HTML format in: + + target/site/apidocs + + + \ No newline at end of file diff --git a/thymeleaf-spring6/ChangeLog.txt b/thymeleaf-spring6/ChangeLog.txt new file mode 100755 index 00000000..bc5a27ae --- /dev/null +++ b/thymeleaf-spring6/ChangeLog.txt @@ -0,0 +1,110 @@ +3.0.13 +====== +- Fixed CVE-2021-43466: Specific scenarios in template injection may lead to remote code execution. +- Fixed incorrect double-unescaping of request parameters breaking processing of forms during restricted mode checks. +- Fixed SpringStandardDialect not allowing the use of a custom IStandardConversionService. + + +3.0.12 +====== +- Fixed memory leak at ThymeleafViewResolver when redirects were being performed to dynamically built URLs. +- Forbidden execution of view names as a fragment expressions if such view name is contained in the URL path + or query parameters. +- Added instantiation of new objects and calls to static classes as forbidden operations in restricted mode. +- Added encode() method to the #mvc expression utility object. +- Adapted Spring WebFlow support to Spring WebFlow 2.5 after changes in API (WebFlow 2.5.0+ is now required). + + +3.0.11 +====== +- Fixed non-blocking resolution of variables assuming a mutable model map and causing exception when adding + reactive variables for resolution using the WebFlux.Fn framework. +- Fixed JPMS "Automatic-Module-Name" entry in MANIFEST.MF: Was "thymeleaf-spring5" but hyphen is a forbidden + character. New value is "thymeleaf.spring5". + + +3.0.10 +====== +- Fixed th:field executing before th:remove and therefore potentially executing the ConversionService and the + RequestDataValueProcessor for values that are not meant to be displayed in the HTML result, which could lead + to unnecessary exceptions. +- Fixed bean validation path not correctly binding expressions with brackets ([...]) when BindStatus was being + obtained with optional = true. +- Added support for non-blocking resolution (before View rendering) of reactive variables specified by dialects + as execution attributes. +- Avoided blocking in the (reactive) resolution of the WebSession returned by the SeverWebExchange in WebFlux apps. +- Added a "renderHiddenMarkersBeforeCheckboxes" flag to SpringStandardDialect (also configurable from + SpringTemplateEngine) in order to instruct th:field to render the associated with + checkbox fields before the checkbox itself and not after, in order to improve compatibility with several CSS-based + frameworks. +- Added a "producePartialOutputWhileProcessing" flag (default: true) to ThymeleafView and ThymeleafViewResolver in + order to be able to specify whether Thymeleaf should start producing output as soon as possible during template + processing (as is default) or on the contrary it should only start sending results to the web server's + output buffers when processing of the template has completely finished and output is rendered in memory. +- Added "Automatic-Module-Name: thymeleaf-spring5" to MANIFEST.MF for Java 9+ JPMS. + + +3.0.9 +===== +- Fixed bean validation mechanism trying to create BindStatus for unbound objects because of an improper use of + a Spring 5 validation API. +- Fixed RequestDataValueProcessor not being applied in Spring WebFlux applications. +- Adapted Spring-based tag processors to new scenarios for restricted expression evaluation. + + +3.0.8 +===== +- Fixed RequestDataValueProcessor being called without a default "action" attribute (with GET value) for + forms that included th:action, which resulted in CSRF token being added as a request parameter to the URL + in some scenarios. +- Added "th:uerrors" attribute in order to easily output all selected errors without escaping the error messages. +- Added "fullModeViewNames" and "chunkedModeViewNames" properties to ThymeleafReactiveViewResolver so that + the way the "maxResponseChunkSize" property is applied to templates can be fine-tuned. Templates can now be forced + to be executed in FULL mode, or specific sets of templates can be selected to be the only ones executed as CHUNKED. +- Improved DATA-DRIVEN mode behaviour so that it now causes a flush of the server's output channels for every + output chunk produced (even if no max response chunk size has been set). +- Added the SpringWebFluxContext class as a base implementation of ISpringWebFluxContext, useful for calling the + SpringWebFluxTemplateEngine cleanly from outside a ThymeleafReactiveView. +- Added the capability to set a prefix to the names and IDs of SSE events, so that HTML events coming from different + executions of Thymeleaf in DATA-DRIVEN mode can be combined at the controller for UI composition in a single + event stream and then easily discriminated at the JavaScript side by the corresponding EventSource objects. +- Removed the ReactiveLazyContextVariable context variable wrapper in order to avoid confusion with the way + lazy evaluation of non-data-driven reactive variables worked. +- Set the default size of the buffers of elements generated by the data driver in DATA-DRIVEN mode to 10. +- Updated Spring Framework dependencies to 5.0.0.RELEASE. +- Updated SLF4j dependency to 1.7.25. + + +3.0.7.RC1 +========= +- Fixed exception being thrown when more than one ConversionService is available at the application context + (e.g. in scenarios where Spring Cloud Stream is present). + + +3.0.6.M4 +======== +- Added automatic negotiation of ContentType at the ViewResolver depending on view name (file extension). +- Improved instances of dynamic class loading by making references package-relative for better resilience + to package renaming. + + +3.0.5.M3 +======== +- (No changes) + + +3.0.4.M2 +======== +- Improved startup times at the template engine by means of lazy initialization of dialect artifacts. +- Improve behaviour of SpringContextUtils.getApplicationContext(): avoided cast exception when ApplicationContext + is not available. +- Added support for rendering Server-Sent Events (SSE) with content-type "text/event-stream" in Spring 5 WebFlux + when Thymeleaf is executing in reactive-friendly data-driven mode. +- Updated Spring Framework dependency to 5.0.0.M5. + + +3.0.3.M1 +======== +- First release of thymeleaf-spring5, includes + * Support for Spring 5 Web MVC + * Support for Spring 5 Web Reactive diff --git a/thymeleaf-spring6/LICENSE.txt b/thymeleaf-spring6/LICENSE.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/thymeleaf-spring6/LICENSE.txt @@ -0,0 +1,202 @@ + + 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: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) 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 + + (d) 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 [yyyy] [name of copyright owner] + + 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. diff --git a/thymeleaf-spring6/NOTICE.txt b/thymeleaf-spring6/NOTICE.txt new file mode 100755 index 00000000..52e4bc09 --- /dev/null +++ b/thymeleaf-spring6/NOTICE.txt @@ -0,0 +1,15 @@ + + Copyright (c) 2011-2016, The THYMELEAF team (http://www.thymeleaf.org) + + 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. + diff --git a/thymeleaf-spring6/RELEASING.txt b/thymeleaf-spring6/RELEASING.txt new file mode 100644 index 00000000..a5e688d1 --- /dev/null +++ b/thymeleaf-spring6/RELEASING.txt @@ -0,0 +1,34 @@ +In order to prepare a release, this steps have to be taken: + +1. In settings.xml, these entries must exist: + + + + releases + + [KEYNAME (EMAIL)] + [GPG PASSPHRASE] + + + + + + sonatype-nexus-snapshots + [USER IN SONATYPE NEXUS] + [PASSWORD IN SONATYPE NEXUS] + + + +2. Ensure all SVN URLs in pom.xml are using "https". A "502 Bad Gateway" error will be received if not. + +3. Deploy SNAPSHOT artifact to Sonatype NEXUS with "mvn deploy", and check in Nexus web interface. + +4. Execute a test "no modification" run of the release:prepare goal: "mvn -Preleases release:prepare -DdryRun=true -Dusername=[SVNUSER] -Dpassword=[SVNPASS]" + +5. Execute a real run of the release:prepare goal: "mvn -Preleases release:prepare -Dusername=[SVNUSER] -Dpassword=[SVNPASS]" + +6. Upload the release: "mvn -Preleases release:perform -Dusername=[SVNUSER] -Dpassword=[SVNPASS]" + +7. Follow instructions for Nexus at: http://nexus.sonatype.org/oss-repository-hosting.html + + diff --git a/thymeleaf-spring6/USAGE.txt b/thymeleaf-spring6/USAGE.txt new file mode 100755 index 00000000..f5e7ad06 --- /dev/null +++ b/thymeleaf-spring6/USAGE.txt @@ -0,0 +1,20 @@ + + Using thymeleaf from Maven 2 + ---------------------------- + + + To use it in your Maven 2 applications, you will need to add it as + a dependency with: + + + org.thymeleaf + thymeleaf-spring5 + {version} + compile + + + In order for this to work correctly, you need to have previously installed + thymeleaf in your local repository (as explained in BUILD.txt) or have a + working internet connection to let maven automatically download thymeleaf + binaries from Maven 2's central repositories. + \ No newline at end of file diff --git a/thymeleaf-spring6/pom.xml b/thymeleaf-spring6/pom.xml new file mode 100755 index 00000000..149ae20f --- /dev/null +++ b/thymeleaf-spring6/pom.xml @@ -0,0 +1,363 @@ + + + + + + + 4.0.0 + org.thymeleaf + thymeleaf-spring6 + jar + 3.1.0-SNAPSHOT + thymeleaf-spring6 + http://www.thymeleaf.org + + Modern server-side Java template engine for both web and standalone environments + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + The THYMELEAF team + http://www.thymeleaf.org + + + + scm:git:git@github.com:thymeleaf/thymeleaf-spring.git + scm:git:git@github.com:thymeleaf/thymeleaf-spring.git + scm:git:git@github.com:thymeleaf/thymeleaf-spring.git + HEAD + + + + + danielfernandez + Daniel Fernandez + daniel.fernandez AT 11thlabs DOT org + + Project Admin + Lead Developer + + + + jmiguelsamper + Jose Miguel Samper + jmiguelsamper AT users DOT sourceforge DOT net + + Developer + + + + ultraq + Emanuel Rabina + emanuelrabina AT gmail DOT com + + Developer + + + + + + + sonatype-nexus-snapshots + Sonatype Nexus Snapshots + https://oss.sonatype.org/content/repositories/snapshots + + + sonatype-nexus-snapshots + Sonatype Nexus Snapshots + https://oss.sonatype.org/service/local/staging/deploy/maven2 + + + + + + sonatype-nexus-snapshots + Sonatype Nexus Snapshots + https://oss.sonatype.org/content/repositories/snapshots + + true + + + + spring-snapshot + Spring Snapshot Repository + https://repo.spring.io/snapshot + + + + + + 17 + ${java.version} + ${java.version} + ${java.version} + US-ASCII + ISO-8859-1 + 5.0.0 + + + + thymeleaf.spring6 + 6.0.0-SNAPSHOT + 1.7.25 + + + + + + + + + src/main/resources + + + + . + META-INF + + LICENSE.txt + NOTICE.txt + + + + + + + + src/test/resources + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + + org.apache.maven.plugins + maven-resources-plugin + 3.2.0 + + ${project.build.sourceEncoding} + ${project.build.propertiesEncoding} + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + false + + true + true + + + + + + ${maven.compiler.source} + ${maven.compiler.target} + + thymeleaf + + + ${module.name} + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.1 + + protected + java.lang + ${maven.compiler.source} + ${maven.compiler.release} + ${basedir}/src/main/javadoc/overview.html + ${basedir}/src/main/javadoc + ${project.build.directory}/apidocs + org.springframework + + + + package + + jar + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + package + + jar + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + + + org.apache.maven.plugins + maven-release-plugin + 2.5.3 + + forked-path + + + + + + default + + perform + + + ${project.artifactId}/pom.xml + + + + + + + + + + + + + + + + + org.thymeleaf + thymeleaf + ${project.version} + compile + + + ognl + ognl + + + + + + jakarta.servlet + jakarta.servlet-api + ${jakarta-servlet-api.version} + provided + true + + + + org.springframework + spring-beans + ${springframework.version} + provided + true + + + + org.springframework + spring-web + ${springframework.version} + provided + true + + + + org.springframework + spring-webflux + ${springframework.version} + provided + true + + + + org.springframework + spring-webmvc + ${springframework.version} + provided + true + + + + org.springframework + spring-expression + ${springframework.version} + provided + true + + + + org.slf4j + slf4j-api + ${slf4j.version} + compile + + + + + + + diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/ISpringTemplateEngine.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/ISpringTemplateEngine.java new file mode 100644 index 00000000..e6fd389f --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/ISpringTemplateEngine.java @@ -0,0 +1,62 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5; + +import org.springframework.context.MessageSource; +import org.thymeleaf.ITemplateEngine; + + +/** + *

+ * Sub-interface of {@link ITemplateEngine} meant for Spring applications, meant to be + * using the {@link org.thymeleaf.spring5.dialect.SpringStandardDialect} and integrating with other + * Spring-bound infrastructure. + *

+ *

+ * The {@link SpringTemplateEngine} implementation of this interface (or a subclass) should be used + * in almost every case, but this interface improves testability of these artifacts. + *

+ * + * @see SpringTemplateEngine + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public interface ISpringTemplateEngine extends ITemplateEngine { + + + /** + *

+ * Sets the Spring {@link MessageSource} to be used for this template engine. + *

+ *

+ * Note that the {@link SpringTemplateEngine} implementation will allow this to be set + * automatically by implementing the {@link org.springframework.context.MessageSourceAware} + * interface, but in such case this method will allow to override this mechanism if needed. + *

+ * + * @param templateEngineMessageSource the message source to be used by the message resolver. + */ + public void setTemplateEngineMessageSource(final MessageSource templateEngineMessageSource); + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/ISpringWebFluxTemplateEngine.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/ISpringWebFluxTemplateEngine.java new file mode 100644 index 00000000..eda4174a --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/ISpringWebFluxTemplateEngine.java @@ -0,0 +1,98 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5; + +import java.nio.charset.Charset; +import java.util.Set; + +import org.reactivestreams.Processor; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.MediaType; +import org.thymeleaf.context.IContext; + + +/** + *

+ * Sub-interface of {@link ISpringTemplateEngine} meant for Spring WebFlux applications, adding + * methods specifically needed for the execution of templates in a reactive-friendly way. + *

+ *

+ * Template engines implementing this interface offer three possible processing modes (note Reactive + * Streams terminology is used in the explanation): + *

+ *
    + *
  1. Full mode: Output buffer size not limited and + * no data-driven execution (no context variable of type {@link Publisher} + * driving the template engine execution): In this case Thymeleaf will be executed unthrottled, + * computing the whole template in memory and sending all output to the output channels in a single + * {@link Subscriber#onNext(Object)} call, immediately followed by {@link Subscriber#onComplete()}. + *
  2. + *
  3. Chunked: Output buffers limited in size but no data-driven + * execution (no {@link Publisher} driving engine execution). All context variables are + * expected to be fully resolved (in a non-blocking fashion) by WebFlux before engine execution and the Thymeleaf + * engine will execute in throttled mode, performing a full-stop each time the output buffer reaches + * the specified size, sending it to the output channels with {@link Subscriber#onNext(Object)} and then + * waiting until these output channels make the engine resume its work with a new + * {@link Subscription#request(long)} back-pressure call. + *
  4. + *
  5. Data-Driven: one of the context variables is a reactive + * {@link Publisher} data stream wrapped inside an implementation + * of the {@link org.thymeleaf.spring5.context.webflux.IReactiveDataDriverContextVariable} interface. In + * this case, Thymeleaf will act as a {@link Subscriber} of this data stream and a + * {@link Publisher} of output buffers (the combination of which turns Thymeleaf into a {@link Processor} + * in Reactive Streams terminology). Thymeleaf will execute as a response to + * {@link Subscriber#onNext(Object)} events triggered by this data-driver + * {@link Publisher}. Thymeleaf will expect to find a {@code th:each} iteration on the data-driven variable + * inside the processed template, and will be executed in throttled mode for the published elements, sending + * the resulting output buffers to the output channels via {@link Subscriber#onNext(Object)} and stopping + * until the data-driver {@link Publisher} produces new data (normally after being requested to do + * so via back-pressure. When execution is data-driven, a limit in size can be optionally specified for + * the output buffers which will make Thymeleaf never send to the output channels a buffer bigger than that. + *
  6. + *
+ *

+ * The {@link SpringWebFluxTemplateEngine} implementation of this interface (or a subclass) should be used + * in almost every case, but this interface improves testability of these artifacts. + *

+ * + * @see SpringWebFluxTemplateEngine + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public interface ISpringWebFluxTemplateEngine extends ISpringTemplateEngine { + + + public Publisher processStream( + final String template, final Set markupSelectors, final IContext context, + final DataBufferFactory bufferFactory, final MediaType mediaType, final Charset charset); + + public Publisher processStream( + final String template, final Set markupSelectors, final IContext context, + final DataBufferFactory bufferFactory, final MediaType mediaType, final Charset charset, final int responseMaxChunkSizeBytes); + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/SpringTemplateEngine.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/SpringTemplateEngine.java new file mode 100755 index 00000000..cc588258 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/SpringTemplateEngine.java @@ -0,0 +1,332 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5; + +import java.util.Set; + +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.dialect.IDialect; +import org.thymeleaf.linkbuilder.JakartaLinkBuilder; +import org.thymeleaf.messageresolver.IMessageResolver; +import org.thymeleaf.messageresolver.StandardMessageResolver; +import org.thymeleaf.spring5.dialect.SpringStandardDialect; +import org.thymeleaf.spring5.messageresolver.SpringMessageResolver; + + +/** + *

+ * Implementation of {@link ISpringTemplateEngine} meant for Spring-enabled applications, + * that establishes by default an instance of {@link SpringStandardDialect} + * as a dialect (instead of an instance of {@link org.thymeleaf.standard.StandardDialect}. + *

+ *

+ * It also configures a {@link SpringMessageResolver} as message resolver, and + * implements the {@link MessageSourceAware} interface in order to let Spring + * automatically setting the {@link MessageSource} used at the application + * (bean needs to have id {@code "messageSource"}). If this Spring standard setting + * needs to be overridden, the {@link #setTemplateEngineMessageSource(MessageSource)} can + * be used. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public class SpringTemplateEngine + extends TemplateEngine + implements ISpringTemplateEngine, MessageSourceAware { + + + private static final SpringStandardDialect SPRINGSTANDARD_DIALECT = new SpringStandardDialect(); + + private MessageSource messageSource = null; + private MessageSource templateEngineMessageSource = null; + + + + + public SpringTemplateEngine() { + super(); + // This will set the SpringStandardDialect, overriding the Standard one set in the super constructor + super.setDialect(SPRINGSTANDARD_DIALECT); + this.setLinkBuilder(new JakartaLinkBuilder()); + } + + + + /** + *

+ * Implementation of the {@link MessageSourceAware#setMessageSource(MessageSource)} + * method at the {@link MessageSourceAware} interface, provided so that + * Spring is able to automatically set the currently configured {@link MessageSource} into + * this template engine. + *

+ *

+ * If several {@link MessageSource} implementation beans exist, Spring will inject here + * the one with id {@code "messageSource"}. + *

+ *

+ * This property should not be set manually in most scenarios (see + * {@link #setTemplateEngineMessageSource(MessageSource)} instead). + *

+ * + * @param messageSource the message source to be used by the message resolver + */ + @Override + public void setMessageSource(final MessageSource messageSource) { + this.messageSource = messageSource; + } + + + + /** + *

+ * Convenience method for setting the message source that will + * be used by this template engine, overriding the one automatically set by + * Spring at the {@link #setMessageSource(MessageSource)} method. + *

+ * + * @param templateEngineMessageSource the message source to be used by the message resolver + */ + @Override + public void setTemplateEngineMessageSource(final MessageSource templateEngineMessageSource) { + this.templateEngineMessageSource = templateEngineMessageSource; + } + + + + + /** + *

+ * Returns whether the SpringEL compiler should be enabled in SpringEL expressions or not. + *

+ *

+ * (This is just a convenience method, equivalent to calling + * {@link SpringStandardDialect#getEnableSpringELCompiler()} on the dialect instance itself. It is provided + * here in order to allow users to enable the SpEL compiler without + * having to directly create instances of the {@link SpringStandardDialect}) + *

+ *

+ * Expression compilation can significantly improve the performance of Spring EL expressions, but + * might not be adequate for every environment. Read + * the + * official Spring documentation for more detail. + *

+ *

+ * Also note that although Spring includes a SpEL compiler since Spring 4.1, most expressions + * in Thymeleaf templates will only be able to properly benefit from this compilation step when at least + * Spring Framework version 4.2.4 is used. + *

+ *

+ * This flag is set to {@code false} by default. + *

+ * + * @return {@code true} if SpEL expressions should be compiled if possible, {@code false} if not. + */ + public boolean getEnableSpringELCompiler() { + final Set dialects = getDialects(); + for (final IDialect dialect : dialects) { + if (dialect instanceof SpringStandardDialect) { + return ((SpringStandardDialect) dialect).getEnableSpringELCompiler(); + } + } + return false; + } + + + /** + *

+ * Sets whether the SpringEL compiler should be enabled in SpringEL expressions or not. + *

+ *

+ * (This is just a convenience method, equivalent to calling + * {@link SpringStandardDialect#setEnableSpringELCompiler(boolean)} on the dialect instance itself. It is provided + * here in order to allow users to enable the SpEL compiler without + * having to directly create instances of the {@link SpringStandardDialect}) + *

+ *

+ * Expression compilation can significantly improve the performance of Spring EL expressions, but + * might not be adequate for every environment. Read + * the + * official Spring documentation for more detail. + *

+ *

+ * Also note that although Spring includes a SpEL compiler since Spring 4.1, most expressions + * in Thymeleaf templates will only be able to properly benefit from this compilation step when at least + * Spring Framework version 4.2.4 is used. + *

+ *

+ * This flag is set to {@code false} by default. + *

+ * + * @param enableSpringELCompiler {@code true} if SpEL expressions should be compiled if possible, {@code false} if not. + */ + public void setEnableSpringELCompiler(final boolean enableSpringELCompiler) { + final Set dialects = getDialects(); + for (final IDialect dialect : dialects) { + if (dialect instanceof SpringStandardDialect) { + ((SpringStandardDialect) dialect).setEnableSpringELCompiler(enableSpringELCompiler); + } + } + } + + + + + /** + *

+ * Returns whether the {@code } marker tags rendered to signal the presence + * of checkboxes in forms when unchecked should be rendered before the checkbox tag itself, + * or after (default). + *

+ *

+ * (This is just a convenience method, equivalent to calling + * {@link SpringStandardDialect#getRenderHiddenMarkersBeforeCheckboxes()} on the dialect instance + * itself. It is provided here in order to allow users to modify this behaviour without + * having to directly create instances of the {@link SpringStandardDialect}) + *

+ *

+ * A number of CSS frameworks and style guides assume that the {@code

+ *

+ * Note this hidden field is introduced in order to signal the existence of the field in the form being sent, + * even if the checkbox is unchecked (no URL parameter is added for unchecked check boxes). + *

+ *

+ * This flag is set to {@code false} by default. + *

+ * + * @return {@code true} if hidden markers should be rendered before the checkboxes, {@code false} if not. + * + * @since 3.0.10 + */ + public boolean getRenderHiddenMarkersBeforeCheckboxes() { + final Set dialects = getDialects(); + for (final IDialect dialect : dialects) { + if (dialect instanceof SpringStandardDialect) { + return ((SpringStandardDialect) dialect).getRenderHiddenMarkersBeforeCheckboxes(); + } + } + return false; + } + + + /** + *

+ * Sets whether the {@code } marker tags rendered to signal the presence + * of checkboxes in forms when unchecked should be rendered before the checkbox tag itself, + * or after (default). + *

+ *

+ * (This is just a convenience method, equivalent to calling + * {@link SpringStandardDialect#setRenderHiddenMarkersBeforeCheckboxes(boolean)} on the dialect instance + * itself. It is provided here in order to allow users to modify this behaviour without + * having to directly create instances of the {@link SpringStandardDialect}) + *

+ *

+ * A number of CSS frameworks and style guides assume that the {@code

+ *

+ * Note this hidden field is introduced in order to signal the existence of the field in the form being sent, + * even if the checkbox is unchecked (no URL parameter is added for unchecked check boxes). + *

+ *

+ * This flag is set to {@code false} by default. + *

+ * + * @param renderHiddenMarkersBeforeCheckboxes {@code true} if hidden markers should be rendered + * before the checkboxes, {@code false} if not. + * + * @since 3.0.10 + */ + public void setRenderHiddenMarkersBeforeCheckboxes(final boolean renderHiddenMarkersBeforeCheckboxes) { + final Set dialects = getDialects(); + for (final IDialect dialect : dialects) { + if (dialect instanceof SpringStandardDialect) { + ((SpringStandardDialect) dialect).setRenderHiddenMarkersBeforeCheckboxes(renderHiddenMarkersBeforeCheckboxes); + } + } + } + + + + + @Override + protected final void initializeSpecific() { + + // First of all, give the opportunity to subclasses to apply their own configurations + initializeSpringSpecific(); + + // Once the subclasses have had their opportunity, compute configurations belonging to SpringTemplateEngine + super.initializeSpecific(); + + final MessageSource messageSource = + this.templateEngineMessageSource == null ? this.messageSource : this.templateEngineMessageSource; + + final IMessageResolver messageResolver; + if (messageSource != null) { + final SpringMessageResolver springMessageResolver = new SpringMessageResolver(); + springMessageResolver.setMessageSource(messageSource); + messageResolver = springMessageResolver; + } else { + messageResolver = new StandardMessageResolver(); + } + + super.setMessageResolver(messageResolver); + + } + + + + /** + *

+ * This method performs additional initializations required for a + * {@code SpringTemplateEngine} subclass instance. This method + * is called before the first execution of + * {@link TemplateEngine#process(String, org.thymeleaf.context.IContext)} + * or {@link TemplateEngine#processThrottled(String, org.thymeleaf.context.IContext)} + * in order to create all the structures required for a quick execution of + * templates. + *

+ *

+ * THIS METHOD IS INTERNAL AND SHOULD NEVER BE CALLED DIRECTLY. + *

+ *

+ * The implementation of this method does nothing, and it is designed + * for being overridden by subclasses of {@code SpringTemplateEngine}. + *

+ */ + protected void initializeSpringSpecific() { + // Nothing to be executed here. Meant for extension + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/SpringWebFluxTemplateEngine.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/SpringWebFluxTemplateEngine.java new file mode 100644 index 00000000..ad6754da --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/SpringWebFluxTemplateEngine.java @@ -0,0 +1,917 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5; + +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.logging.Level; + +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebSession; +import org.thymeleaf.IThrottledTemplateProcessor; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.TemplateSpec; +import org.thymeleaf.context.IContext; +import org.thymeleaf.context.IEngineContext; +import org.thymeleaf.engine.DataDrivenTemplateIterator; +import org.thymeleaf.engine.ISSEThrottledTemplateWriterControl; +import org.thymeleaf.engine.IThrottledTemplateWriterControl; +import org.thymeleaf.engine.ThrottledTemplateProcessor; +import org.thymeleaf.exceptions.TemplateProcessingException; +import org.thymeleaf.spring5.context.webflux.IReactiveDataDriverContextVariable; +import org.thymeleaf.spring5.context.webflux.IReactiveSSEDataDriverContextVariable; +import org.thymeleaf.spring5.context.webflux.ISpringWebFluxContext; +import org.thymeleaf.spring5.context.webflux.SpringWebFluxContext; +import org.thymeleaf.spring5.context.webflux.SpringWebFluxEngineContextFactory; +import org.thymeleaf.spring5.linkbuilder.webflux.SpringWebFluxLinkBuilder; +import org.thymeleaf.util.LoggingUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import static org.thymeleaf.spring5.SpringWebFluxTemplateEngine.DataDrivenFluxStep.FluxStepPhase.DATA_DRIVEN_PHASE_BUFFER; +import static org.thymeleaf.spring5.SpringWebFluxTemplateEngine.DataDrivenFluxStep.FluxStepPhase.DATA_DRIVEN_PHASE_HEAD; +import static org.thymeleaf.spring5.SpringWebFluxTemplateEngine.DataDrivenFluxStep.FluxStepPhase.DATA_DRIVEN_PHASE_TAIL; + + +/** + *

+ * Standard implementation of {@link ISpringWebFluxTemplateEngine}, and default + * template engine implementation to be used in Spring WebFlux environments. + *

+ * + * @see ISpringWebFluxTemplateEngine + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public class SpringWebFluxTemplateEngine + extends SpringTemplateEngine + implements ISpringWebFluxTemplateEngine { + + + private static final Logger logger = LoggerFactory.getLogger(SpringWebFluxTemplateEngine.class); + private static final String LOG_CATEGORY_FULL_OUTPUT = SpringWebFluxTemplateEngine.class.getName() + ".DOWNSTREAM.FULL"; + private static final String LOG_CATEGORY_CHUNKED_OUTPUT = SpringWebFluxTemplateEngine.class.getName() + ".DOWNSTREAM.CHUNKED"; + private static final String LOG_CATEGORY_DATADRIVEN_INPUT = SpringWebFluxTemplateEngine.class.getName() + ".UPSTREAM.DATA-DRIVEN"; + private static final String LOG_CATEGORY_DATADRIVEN_OUTPUT = SpringWebFluxTemplateEngine.class.getName() + ".DOWNSTREAM.DATA-DRIVEN"; + + + + + public SpringWebFluxTemplateEngine() { + + super(); + // In Spring WebFlux environments, we will need to use a special context factory in order to + // use an environment-tailored implementation of IEngineContext. + this.setEngineContextFactory(new SpringWebFluxEngineContextFactory()); + // In Spring WebFlux environments, we will need to use a special link builder able to adapt + // the creation of URLs as a result of @{...} expressions in a way that makes sense in this + // environment. + this.setLinkBuilder(new SpringWebFluxLinkBuilder()); + + } + + + + + @Override + public Publisher processStream( + final String template, final Set markupSelectors, final IContext context, + final DataBufferFactory bufferFactory, final MediaType mediaType, final Charset charset) { + return processStream(template, markupSelectors, context, bufferFactory, mediaType, charset, Integer.MAX_VALUE); + } + + + @Override + public Publisher processStream( + final String template, final Set markupSelectors, final IContext context, + final DataBufferFactory bufferFactory, final MediaType mediaType, final Charset charset, + final int responseMaxChunkSizeBytes) { + + /* + * PERFORM VALIDATIONS + */ + if (template == null) { + return Flux.error(new IllegalArgumentException("Template cannot be null")); + } + if (context == null) { + return Flux.error(new IllegalArgumentException("Context cannot be null")); + } + if (bufferFactory == null) { + return Flux.error(new IllegalArgumentException("Buffer Factory cannot be null")); + } + if (mediaType == null) { + return Flux.error(new IllegalArgumentException("Media Type cannot be null")); + } + if (charset == null) { + return Flux.error(new IllegalArgumentException("Charset cannot be null")); + } + + if (responseMaxChunkSizeBytes == 0) { + return Flux.error(new IllegalArgumentException("Max Chunk Size cannot be zero")); + } + + // Normalize the chunk size in bytes (MAX_VALUE == no limit) + final int chunkSizeBytes = (responseMaxChunkSizeBytes < 0? Integer.MAX_VALUE : responseMaxChunkSizeBytes); + + // Determine whether we have been asked to return data as SSE (Server-Sent Events) + final boolean sse = MediaType.TEXT_EVENT_STREAM.includes(mediaType); + + /* + * CHECK FOR DATA-DRIVEN EXECUTION + */ + try { + final String dataDriverVariableName = findDataDriverInModel(context); + if (dataDriverVariableName != null) { + // We should be executing in data-driven mode + return createDataDrivenStream( + template, markupSelectors, context, dataDriverVariableName, bufferFactory, charset, chunkSizeBytes, sse); + } + } catch (final Throwable t) { + return Flux.error(t); + } + + // Check if we need to fail here: If SSE has been requested, a data-driver variable is mandatory + if (sse) { + return Flux.error(new TemplateProcessingException( + "SSE mode has been requested ('Accept: text/event-stream') but no data-driver variable has been " + + "added to the model/context. In order to perform SSE rendering, a variable implementing the " + + IReactiveDataDriverContextVariable.class.getName() + " interface is required.")); + } + + /* + * IS THERE A LIMIT IN BUFFER SIZE? if not, given we are not data-driven, we should switch to FULL + */ + if (chunkSizeBytes == Integer.MAX_VALUE) { + // No limit on buffer size, so there is no reason to throttle: using FULL mode instead. + return createFullStream(template, markupSelectors, context, bufferFactory, charset); + } + + /* + * CREATE A CHUNKED STREAM + */ + return createChunkedStream( + template, markupSelectors, context, bufferFactory, charset, responseMaxChunkSizeBytes); + + } + + + + + private Mono createFullStream( + final String templateName, final Set markupSelectors, final IContext context, + final DataBufferFactory bufferFactory, final Charset charset) { + + final Mono stream = + Mono.create( + subscriber -> { + + if (logger.isTraceEnabled()) { + logger.trace("[THYMELEAF][{}] STARTING STREAM PROCESS (FULL MODE) OF TEMPLATE \"{}\" WITH LOCALE {}", + new Object[]{TemplateEngine.threadIndex(), LoggingUtils.loggifyTemplateName(templateName), context.getLocale()}); + } + + final DataBuffer dataBuffer = bufferFactory.allocateBuffer(); + // OutputStreamWriter object have an 8K buffer, but process(...) will flush it at the end + final OutputStreamWriter writer = new OutputStreamWriter(dataBuffer.asOutputStream(), charset); + + try { + + process(templateName, markupSelectors, context, writer); + + } catch (final Throwable t) { + logger.error( + String.format( + "[THYMELEAF][%s] Exception processing template \"%s\": %s", + new Object[] {TemplateEngine.threadIndex(), LoggingUtils.loggifyTemplateName(templateName), t.getMessage()}), + t); + subscriber.error(t); + return; + } + + final int bytesProduced = dataBuffer.readableByteCount(); + + if (logger.isTraceEnabled()) { + logger.trace( + "[THYMELEAF][{}] FINISHED STREAM PROCESS (FULL MODE) OF TEMPLATE \"{}\" WITH LOCALE {}. PRODUCED {} BYTES", + new Object[]{ + TemplateEngine.threadIndex(), LoggingUtils.loggifyTemplateName(templateName), + context.getLocale(), Integer.valueOf(bytesProduced)}); + } + + // This is a Mono, so no need to call "next()" or "complete()" + subscriber.success(dataBuffer); + + }); + + // Will add some logging to the data stream + return stream.log(LOG_CATEGORY_FULL_OUTPUT, Level.FINEST); + + } + + + + + private Flux createChunkedStream( + final String templateName, final Set markupSelectors, final IContext context, + final DataBufferFactory bufferFactory, final Charset charset, final int responseMaxChunkSizeBytes) { + + final Flux stream = Flux.generate( + + // Using the throttledProcessor as state in this Flux.generate allows us to delay the + // initialization of the throttled processor until the last moment, when output generation + // is really requested. + // NOTE 'sse' is specified as 'false' because SSE is only allowed in data-driven mode. Also, no + // data-driven iterator is available (we are in chunked mode). + () -> new StreamThrottledTemplateProcessor( + processThrottled(templateName, markupSelectors, context), null, null, 0L, false), + + // This stream will execute, in a one-by-one (non-interleaved) fashion, the following code + // for each back-pressure request coming from downstream. Each of these steps (chunks) will + // execute the throttled processor once and return its result as a DataBuffer object. + (throttledProcessor, emitter) -> { + + throttledProcessor.startChunk(); + + if (logger.isTraceEnabled()) { + logger.trace( + "[THYMELEAF][{}][{}] STARTING PARTIAL STREAM PROCESS (CHUNKED MODE, THROTTLER ID " + + "\"{}\", CHUNK {}) FOR TEMPLATE \"{}\" WITH LOCALE {}", + new Object[]{ + TemplateEngine.threadIndex(), throttledProcessor.getProcessorIdentifier(), + throttledProcessor.getProcessorIdentifier(), Integer.valueOf(throttledProcessor.getChunkCount()), + LoggingUtils.loggifyTemplateName(templateName), context.getLocale()}); + } + + final DataBuffer buffer = bufferFactory.allocateBuffer(responseMaxChunkSizeBytes); + + final int bytesProduced; + try { + bytesProduced = + throttledProcessor.process(responseMaxChunkSizeBytes, buffer.asOutputStream(), charset); + } catch (final Throwable t) { + emitter.error(t); + return null; + } + + if (logger.isTraceEnabled()) { + logger.trace( + "[THYMELEAF][{}][{}] FINISHED PARTIAL STREAM PROCESS (CHUNKED MODE, THROTTLER ID " + + "\"{}\", CHUNK {}) FOR TEMPLATE \"{}\" WITH LOCALE {}. PRODUCED {} BYTES", + new Object[]{ + TemplateEngine.threadIndex(), throttledProcessor.getProcessorIdentifier(), + throttledProcessor.getProcessorIdentifier(), Integer.valueOf(throttledProcessor.getChunkCount()), + LoggingUtils.loggifyTemplateName(templateName), context.getLocale(), Integer.valueOf(bytesProduced)}); + } + + emitter.next(buffer); + + if (throttledProcessor.isFinished()) { + + if (logger.isTraceEnabled()) { + logger.trace( + "[THYMELEAF][{}][{}] FINISHED ALL STREAM PROCESS (CHUNKED MODE, THROTTLER ID " + + "\"{}\") FOR TEMPLATE \"{}\" WITH LOCALE {}. PRODUCED A TOTAL OF {} BYTES IN {} CHUNKS", + new Object[]{ + TemplateEngine.threadIndex(), throttledProcessor.getProcessorIdentifier(), + throttledProcessor.getProcessorIdentifier(), + LoggingUtils.loggifyTemplateName(templateName), context.getLocale(), + Long.valueOf(throttledProcessor.getTotalBytesProduced()), + Integer.valueOf(throttledProcessor.getChunkCount() + 1)}); + } + + emitter.complete(); + + } + + return throttledProcessor; + + }); + + // Will add some logging to the data stream + return stream.log(LOG_CATEGORY_CHUNKED_OUTPUT, Level.FINEST); + + } + + + + + private Flux createDataDrivenStream( + final String templateName, final Set markupSelectors, final IContext context, + final String dataDriverVariableName, final DataBufferFactory bufferFactory, final Charset charset, + final int responseMaxChunkSizeBytes, final boolean sse) { + + // STEP 1: Obtain the data-driver variable and its metadata + final IReactiveDataDriverContextVariable dataDriver = + (IReactiveDataDriverContextVariable) context.getVariable(dataDriverVariableName); + final int bufferSizeElements = dataDriver.getBufferSizeElements(); + final String sseEventsPrefix = + (dataDriver instanceof IReactiveSSEDataDriverContextVariable? + ((IReactiveSSEDataDriverContextVariable) dataDriver).getSseEventsPrefix() : null); + final long sseEventsID = + (dataDriver instanceof IReactiveSSEDataDriverContextVariable? + ((IReactiveSSEDataDriverContextVariable) dataDriver).getSseEventsFirstID() : 0L); + final ReactiveAdapterRegistry reactiveAdapterRegistry = + (context instanceof SpringWebFluxContext ? + ((SpringWebFluxContext)context).getReactiveAdapterRegistry() : null); + + + // STEP 2: Replace the data driver variable with a DataDrivenTemplateIterator + final DataDrivenTemplateIterator dataDrivenIterator = new DataDrivenTemplateIterator(); + final IContext wrappedContext = applyDataDriverWrapper(context, dataDriverVariableName, dataDrivenIterator); + + + // STEP 3: Create the data stream buffers, plus add some logging in order to know how the stream is being used + final Flux> dataDrivenBufferedStream = + Flux.from(dataDriver.getDataStream(reactiveAdapterRegistry)) + .buffer(bufferSizeElements) + .log(LOG_CATEGORY_DATADRIVEN_INPUT, Level.FINEST); + + + // STEP 4: Initialize the (throttled) template engine for each subscriber (normally there will only be one) + final Flux dataDrivenWithContextStream = Flux.using( + + // Using the throttledProcessor as state in this Flux.using allows us to delay the + // initialization of the throttled processor until the last moment, when output generation + // is really requested. + () -> { + final String outputContentType = sse? "text/event-stream" : null; + final TemplateSpec templateSpec = + new TemplateSpec(templateName, markupSelectors, outputContentType, null); + return new StreamThrottledTemplateProcessor( + processThrottled(templateSpec, wrappedContext), dataDrivenIterator, sseEventsPrefix, sseEventsID, sse); + }, + + // This flux will be made by concatenating a phase for the head (template before data-driven + // iteration), another phase composed of most possibly several steps for the data-driven iteration, + // and finally a tail phase (template after data-driven iteration). + // + // But this concatenation will be done from a Flux created with Flux.generate, so that we have the + // opportunity to check if the processor has already signaled that it has finished, and in such + // case we might be able to avoid the subscription to the upstream data driver if its iteration is + // not needed at the template. + throttledProcessor -> Flux.concat(Flux.generate( + () -> DATA_DRIVEN_PHASE_HEAD, + (phase, emitter) -> { + + // Check if the processor has already signaled it has finished, in which case we + // might be able to avoid the BUFFER phase (if no iteration of the data-driver is present). + // + // *NOTE* we CANNOT GUARANTEE that this will stop the upstream data driver publisher from + // being subscribed to or even consumed, because there is no guarantee that this code + // will be executed for the BUFFER phase after the entire Flux generated downstream + // for the HEAD phase (see STEP 5 in the stream being built). Actually, it might even be + // executed concurrently to one of the steps of a Flux for the HEAD/BUFFER phases, which + // is why the IThrottledProcessor.isFinished() called here needs to be thread-safe. + if (throttledProcessor.isFinished()) { + // We can short-cut, and if we are lucky even avoid the BUFFER phase. + emitter.complete(); + return null; + } + + switch (phase) { + + case DATA_DRIVEN_PHASE_HEAD: + emitter.next(Mono.just(DataDrivenFluxStep.forHead(throttledProcessor))); + return DATA_DRIVEN_PHASE_BUFFER; + + case DATA_DRIVEN_PHASE_BUFFER: + emitter.next(dataDrivenBufferedStream.map(values -> DataDrivenFluxStep.forBuffer(throttledProcessor, values))); + return DATA_DRIVEN_PHASE_TAIL; + + case DATA_DRIVEN_PHASE_TAIL: + emitter.next(Mono.just(DataDrivenFluxStep.forTail(throttledProcessor))); + emitter.complete(); + + } + + return null; + + } + )), + + // No need to explicitly dispose the throttled template processor. + throttledProcessor -> { /* Nothing to be done here! */ }); + + + // STEP 5: React to each buffer of published data by creating one or many (concatMap) DataBuffers containing + // the result of processing only that buffer. + final Flux stream = dataDrivenWithContextStream.concatMap( + (step) -> Flux.generate( + + // We set initialize to TRUE as a state, so that the first step executed for this Flux + // performs the initialization of the dataDrivenIterator for the entire Flux. It is a need + // that this initialization is performed when the first step of this Flux is executed, + // because initialization actually consists of a lateral effect on a mutable variable + // (the dataDrivenIterator). And this way we are certain that it is executed in the + // right order, given concatMap guarantees to us that these Fluxes generated here will + // be consumed in the right order and executed one at a time (and the Reactor guarantees us + // that there will be no thread visibility issues between Flux steps). + () -> Boolean.TRUE, + + // The first time this is executed, initialize will be TRUE. From then on, it will be FALSE + // so that it is the first execution of this that initializes the (mutable) dataDrivenIterator. + (initialize, emitter) -> { + + final StreamThrottledTemplateProcessor throttledProcessor = step.getThrottledProcessor(); + final DataDrivenTemplateIterator dataDrivenTemplateIterator = throttledProcessor.getDataDrivenTemplateIterator(); + + // Let's check if we can short cut and simply finish execution. Maybe we can avoid consuming + // the data from the upstream data-driver publisher (e.g. if the data-driver variable is + // never actually iterated). + if (throttledProcessor.isFinished()) { + emitter.complete(); + return Boolean.FALSE; + } + + // Initialize the dataDrivenIterator. This is a lateral effect, this variable is mutable, + // so it is important to do it here so that we make sure it is executed in the right order. + if (initialize.booleanValue()) { + + if (step.isHead()) { + // Feed with no elements - we just want to output the part of the + // template that goes before the iteration of the data driver. + dataDrivenTemplateIterator.startHead(); + } else if (step.isDataBuffer()) { + // Value-based execution: we have values and we want to iterate them + dataDrivenTemplateIterator.feedBuffer(step.getValues()); + } else { // step.isTail() + // Signal feeding complete, indicating this is just meant to output the + // rest of the template after the iteration of the data driver. Note there + // is a case when this phase will still provoke the output of an iteration, + // and this is when the number of iterations is exactly ONE. In this case, + // it won't be possible to determine the iteration type (ZERO, ONE, MULTIPLE) + // until we close it with this 'feedingComplete()' + dataDrivenTemplateIterator.feedingComplete(); + dataDrivenTemplateIterator.startTail(); + } + + } + + // Signal the start of a new chunk (we are counting them for the logs) + throttledProcessor.startChunk(); + + if (logger.isTraceEnabled()) { + logger.trace( + "[THYMELEAF][{}][{}] STARTING PARTIAL STREAM PROCESS (DATA-DRIVEN MODE, THROTTLER ID " + + "\"{}\", CHUNK {}) FOR TEMPLATE \"{}\" WITH LOCALE {}", + new Object[]{ + TemplateEngine.threadIndex(), throttledProcessor.getProcessorIdentifier(), + throttledProcessor.getProcessorIdentifier(), Integer.valueOf(throttledProcessor.getChunkCount()), + LoggingUtils.loggifyTemplateName(templateName), context.getLocale()}); + } + + final DataBuffer buffer = + (responseMaxChunkSizeBytes != Integer.MAX_VALUE ? + bufferFactory.allocateBuffer(responseMaxChunkSizeBytes) : + bufferFactory.allocateBuffer()); + + final int bytesProduced; + try { + + bytesProduced = + throttledProcessor.process(responseMaxChunkSizeBytes, buffer.asOutputStream(), charset); + + } catch (final Throwable t) { + emitter.error(t); + return Boolean.FALSE; + } + + + if (logger.isTraceEnabled()) { + logger.trace( + "[THYMELEAF][{}][{}] FINISHED PARTIAL STREAM PROCESS (DATA-DRIVEN MODE, THROTTLER ID " + + "\"{}\", CHUNK {}) FOR TEMPLATE \"{}\" WITH LOCALE {}. PRODUCED {} BYTES", + new Object[]{ + TemplateEngine.threadIndex(), throttledProcessor.getProcessorIdentifier(), + throttledProcessor.getProcessorIdentifier(), Integer.valueOf(throttledProcessor.getChunkCount()), + LoggingUtils.loggifyTemplateName(templateName), context.getLocale(), Integer.valueOf(bytesProduced)}); + } + + + // If we produced no bytes, then let's avoid skipping an event number from the sequence + if (bytesProduced == 0) { + dataDrivenTemplateIterator.takeBackLastEventID(); + } + + + // Now it's time to determine if we should execute another time for the same + // data-driven step or rather we should consider we have done everything possible + // for this step (e.g. produced all markup for a data stream buffer) and just + // emit "complete" and go for the next step. + boolean phaseFinished = false; + if (throttledProcessor.isFinished()) { + + if (logger.isTraceEnabled()) { + logger.trace( + "[THYMELEAF][{}][{}] FINISHED ALL STREAM PROCESS (DATA-DRIVEN MODE, THROTTLER ID " + + "\"{}\") FOR TEMPLATE \"{}\" WITH LOCALE {}. PRODUCED A TOTAL OF {} BYTES IN {} CHUNKS", + new Object[]{ + TemplateEngine.threadIndex(), throttledProcessor.getProcessorIdentifier(), + throttledProcessor.getProcessorIdentifier(), + LoggingUtils.loggifyTemplateName(templateName), context.getLocale(), + Long.valueOf(throttledProcessor.getTotalBytesProduced()), + Integer.valueOf(throttledProcessor.getChunkCount() + 1)}); + } + + // We have finished executing the template, which can happen after + // finishing iterating all data driver values, or also if we are at the + // first execution and there was no need to use the data driver at all + phaseFinished = true; + dataDrivenTemplateIterator.finishStep(); + + } else { + + if (step.isHead() && dataDrivenTemplateIterator.hasBeenQueried()) { + + // We know everything before the data driven iteration has already been + // processed because the iterator has been used at least once (i.e. its + // 'hasNext()' or 'next()' method have been called at least once). This will + // mean we can switch to the buffer phase. + phaseFinished = true; + dataDrivenTemplateIterator.finishStep(); + + } else if (step.isDataBuffer() && !dataDrivenTemplateIterator.continueBufferExecution()) { + // We have finished executing this buffer of items and we can go for the + // next one or maybe the tail. + phaseFinished = true; + } + // fluxStep.isTail(): nothing to do, as the only reason we would have to emit + // 'complete' at the tail step would be throttledProcessor.isFinished(), which + // has been already checked. + + } + + // Compute if the output for this step has been already finished (i.e. not only the + // processing of the model's events, but also any existing overflows). This has to be + // queried BEFORE the buffer is emitted. + final boolean stepOutputFinished = dataDrivenTemplateIterator.isStepOutputFinished(); + + // Buffer has now everything it should, so send it to the output channels + emitter.next(buffer); + + + // If step finished, we have ot emit 'complete' now, giving the opportunity to execute + // again if processing has finished, but we still have some overflow to be flushed + if (phaseFinished && stepOutputFinished) { + emitter.complete(); + } + + + return Boolean.FALSE; + + })); + + + // Will add some logging to the data flow + return stream.log(LOG_CATEGORY_DATADRIVEN_OUTPUT, Level.FINEST); + + } + + + + + /* + * This method will apply a wrapper on the data driver variable so that a DataDrivenTemplateIterator takes + * the place of the original data-driver variable. This is done via a wrapper in order to not perform such a + * strong modification on the original context object. Even if context objects should not be reused among template + * engine executions, when a non-IEngineContext implementation is used we will let that degree of liberty to the + * user just in case. + */ + private static IContext applyDataDriverWrapper( + final IContext context, final String dataDriverVariableName, + final DataDrivenTemplateIterator dataDrivenTemplateIterator) { + + // This is an IEngineContext, a very internal, low-level context implementation, so let's simply modify it + if (context instanceof IEngineContext) { + ((IEngineContext)context).setVariable(dataDriverVariableName, dataDrivenTemplateIterator); + return context; + } + + // Not an IEngineContext, but might still be an ISpringWebFluxContext and we don't want to lose that info + if (context instanceof ISpringWebFluxContext) { + return new DataDrivenSpringWebFluxContextWrapper( + (ISpringWebFluxContext)context, dataDriverVariableName, dataDrivenTemplateIterator); + } + + // Not a recognized context interface: just use a default implementation + return new DataDrivenContextWrapper(context, dataDriverVariableName, dataDrivenTemplateIterator); + + + } + + + + + private static String findDataDriverInModel(final IContext context) { + + // In SpringWebFluxContext (used most of the times), variables are backed by a + // Map. So this iteration on all the names and many "getVariable()" calls + // shouldn't be an issue perf-wise. + + String dataDriverVariableName = null; + final Set contextVariableNames = context.getVariableNames(); + + for (final String contextVariableName : contextVariableNames) { + + final Object contextVariableValue = context.getVariable(contextVariableName); + if (contextVariableValue instanceof IReactiveDataDriverContextVariable) { + if (dataDriverVariableName != null) { + throw new TemplateProcessingException( + "Only one data-driver variable is allowed to be specified as a model attribute, but " + + "at least two have been identified: '" + dataDriverVariableName + "' " + + "and '" + contextVariableName + "'"); + } + dataDriverVariableName = contextVariableName; + } + + } + + return dataDriverVariableName; + + } + + + + + + /* + * This internal class is meant to be used in multi-step streams so that an account on the total + * number of bytes and steps/chunks can be kept, and also other aspects such as SSE event management can be offered. + * + * NOTE there is no need to synchronize these variables, even if different steps/chunks might be executed + * (non-concurrently) by different threads, because Reactive Streams implementations like Reactor should + * take care to establish the adequate thread synchronization/memory barriers at their asynchronous boundaries, + * thus avoiding thread visibility issues. + */ + static class StreamThrottledTemplateProcessor { + + private final IThrottledTemplateProcessor throttledProcessor; + private final DataDrivenTemplateIterator dataDrivenTemplateIterator; + private int chunkCount; + private long totalBytesProduced; + + StreamThrottledTemplateProcessor( + final IThrottledTemplateProcessor throttledProcessor, + final DataDrivenTemplateIterator dataDrivenTemplateIterator, + final String sseEventsPrefix, final long sseEventsFirstID, final boolean sse) { + + super(); + + this.throttledProcessor = throttledProcessor; + this.dataDrivenTemplateIterator = dataDrivenTemplateIterator; + + final IThrottledTemplateWriterControl writerControl; + if (this.throttledProcessor instanceof ThrottledTemplateProcessor) { + writerControl = ((ThrottledTemplateProcessor) this.throttledProcessor).getThrottledTemplateWriterControl(); + } else { + writerControl = null; + } + + if (sse) { + if (writerControl == null || !(writerControl instanceof ISSEThrottledTemplateWriterControl)) { + throw new TemplateProcessingException( + "Cannot process template in Server-Sent Events (SSE) mode: template writer is not SSE capable. " + + "Either SSE content type has not been declared at the " + TemplateSpec.class.getSimpleName() + " or " + + "an implementation of " + IThrottledTemplateProcessor.class.getName() + " other than " + + ThrottledTemplateProcessor.class.getName() + " is being used."); + } + if (this.dataDrivenTemplateIterator == null) { + throw new TemplateProcessingException( + "Cannot process template in Server-Sent Events (SSE) mode: a data-driven template iterator " + + "is required in context in order to apply SSE."); + } + } + + if (this.dataDrivenTemplateIterator != null) { + this.dataDrivenTemplateIterator.setWriterControl(writerControl); + this.dataDrivenTemplateIterator.setSseEventsPrefix(sseEventsPrefix); + this.dataDrivenTemplateIterator.setSseEventsFirstID(sseEventsFirstID); + } + + this.chunkCount = -1; // First chunk will be considered number 0 + this.totalBytesProduced = 0L; + + } + + int process(final int maxOutputInBytes, final OutputStream outputStream, final Charset charset) { + final int chunkBytes = this.throttledProcessor.process(maxOutputInBytes, outputStream, charset); + this.totalBytesProduced += chunkBytes; + return chunkBytes; + } + + String getProcessorIdentifier() { + return this.throttledProcessor.getProcessorIdentifier(); + } + + boolean isFinished() { + return this.throttledProcessor.isFinished(); + } + + void startChunk() { + this.chunkCount++; + } + + int getChunkCount() { + return this.chunkCount; + } + + long getTotalBytesProduced() { + return this.totalBytesProduced; + } + + DataDrivenTemplateIterator getDataDrivenTemplateIterator() { + return this.dataDrivenTemplateIterator; + } + + } + + + /* + * This internal class is used for keeping the accounting of the different phases in a data-driven stream: + * head (no value, template before the data-driven iteration), buffer (values, data-driven iteration), and + * tail (no value, template after the data-driven iteration). + * + * NOTE there is no need to synchronize these variables, even if different steps/chunks might be executed + * (non-concurrently) by different threads, because Reactive Streams implementations like Reactor should + * take care to establish the adequate thread synchronization/memory barriers at their asynchronous boundaries, + * thus avoiding thread visibility issues. + */ + static final class DataDrivenFluxStep { + + enum FluxStepPhase {DATA_DRIVEN_PHASE_HEAD, DATA_DRIVEN_PHASE_BUFFER, DATA_DRIVEN_PHASE_TAIL } + + private final StreamThrottledTemplateProcessor throttledProcessor; + private final List values; + private final FluxStepPhase phase; + + + static DataDrivenFluxStep forHead(final StreamThrottledTemplateProcessor throttledProcessor) { + return new DataDrivenFluxStep(throttledProcessor, null, DATA_DRIVEN_PHASE_HEAD); + } + + static DataDrivenFluxStep forBuffer(final StreamThrottledTemplateProcessor throttledProcessor, final List values) { + return new DataDrivenFluxStep(throttledProcessor, values, DATA_DRIVEN_PHASE_BUFFER); + } + + static DataDrivenFluxStep forTail(final StreamThrottledTemplateProcessor throttledProcessor) { + return new DataDrivenFluxStep(throttledProcessor, null, DATA_DRIVEN_PHASE_TAIL); + } + + private DataDrivenFluxStep( + final StreamThrottledTemplateProcessor throttledProcessor, final List values, + final FluxStepPhase phase) { + super(); + this.throttledProcessor = throttledProcessor; + this.values = values; + this.phase = phase; + } + + StreamThrottledTemplateProcessor getThrottledProcessor() { + return this.throttledProcessor; + } + + List getValues() { + return this.values; + } + + boolean isHead() { + return this.phase == DATA_DRIVEN_PHASE_HEAD; + } + + boolean isDataBuffer() { + return this.phase == DATA_DRIVEN_PHASE_BUFFER; + } + + boolean isTail() { + return this.phase == DATA_DRIVEN_PHASE_TAIL; + } + + } + + + + /* + * This wrapper of an ISpringWebFluxContext is meant to wrap the original context object sent to the + * template engine while hiding the data driver variable, returning a DataDrivenTemplateIterator in its place. + */ + static class DataDrivenSpringWebFluxContextWrapper + extends DataDrivenContextWrapper implements ISpringWebFluxContext { + + private final ISpringWebFluxContext context; + + DataDrivenSpringWebFluxContextWrapper( + final ISpringWebFluxContext context, final String dataDriverVariableName, + final DataDrivenTemplateIterator dataDrivenTemplateIterator) { + super(context, dataDriverVariableName, dataDrivenTemplateIterator); + this.context = context; + } + + @Override + public ServerHttpRequest getRequest() { + return this.context.getRequest(); + } + + @Override + public ServerHttpResponse getResponse() { + return this.context.getResponse(); + } + + @Override + public Mono getSession() { + return this.context.getSession(); + } + + @Override + public ServerWebExchange getExchange() { + return this.context.getExchange(); + } + + } + + + + /* + * This wrapper of an IContext (non-SpringWebFlux-specific) is meant to wrap the original context object sent + * to the template engine while hiding the data driver variable, returning a DataDrivenTemplateIterator in + * its place. + */ + static class DataDrivenContextWrapper implements IContext { + + private final IContext context; + private final String dataDriverVariableName; + private final DataDrivenTemplateIterator dataDrivenTemplateIterator; + + DataDrivenContextWrapper( + final IContext context, final String dataDriverVariableName, + final DataDrivenTemplateIterator dataDrivenTemplateIterator) { + super(); + this.context = context; + this.dataDriverVariableName = dataDriverVariableName; + this.dataDrivenTemplateIterator = dataDrivenTemplateIterator; + } + + public IContext getWrappedContext() { + return this.context; + } + + @Override + public Locale getLocale() { + return this.context.getLocale(); + } + + @Override + public boolean containsVariable(final String name) { + return this.context.containsVariable(name); + } + + @Override + public Set getVariableNames() { + return this.context.getVariableNames(); + } + + @Override + public Object getVariable(final String name) { + if (this.dataDriverVariableName.equals(name)) { + return this.dataDrivenTemplateIterator; + } + return this.context.getVariable(name); + } + + } + + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/IThymeleafBindStatus.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/IThymeleafBindStatus.java new file mode 100644 index 00000000..58f981c4 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/IThymeleafBindStatus.java @@ -0,0 +1,58 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context; + +import java.beans.PropertyEditor; + +import org.springframework.validation.Errors; + +/** + *

+ * This interface is meant to abstract a Spring {@code BindStatus}, without the client code + * needing to know if it is a Spring WebMVC or Spring WebFlux implementation of this + * {@code BindStatus}. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public interface IThymeleafBindStatus { + + public String getPath(); + public String getExpression(); + public Object getValue(); + public Class getValueType(); + public Object getActualValue(); + public String getDisplayValue(); + + public boolean isError(); + public String[] getErrorCodes(); + public String getErrorCode(); + public String[] getErrorMessages(); + public String getErrorMessage(); + public String getErrorMessagesAsString(String delimiter); + public Errors getErrors(); + + public PropertyEditor getEditor(); + public PropertyEditor findEditor(Class valueClass); + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/IThymeleafRequestContext.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/IThymeleafRequestContext.java new file mode 100644 index 00000000..bd6a65b9 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/IThymeleafRequestContext.java @@ -0,0 +1,94 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.TimeZone; + +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.ui.context.Theme; +import org.springframework.validation.Errors; +import org.thymeleaf.spring5.context.webflux.SpringWebFluxThymeleafRequestContext; + +/** + *

+ * This interface is meant to abstract a Spring {@code RequestContext}, without the client code + * needing to know if it is a Spring WebMVC or Spring WebFlux implementation of this + * {@code RequestContext}. + *

+ * + * @see org.thymeleaf.spring5.context.webmvc.SpringWebMvcThymeleafRequestContext + * @see SpringWebFluxThymeleafRequestContext + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public interface IThymeleafRequestContext { + + + public MessageSource getMessageSource(); + + public Map getModel(); + + public Locale getLocale(); + public TimeZone getTimeZone(); + public void changeLocale(Locale locale); + public void changeLocale(Locale locale, TimeZone timeZone); + + public void setDefaultHtmlEscape(boolean defaultHtmlEscape); + public boolean isDefaultHtmlEscape(); + public Boolean getDefaultHtmlEscape(); + + public String getContextPath(); + public String getContextUrl(String relativeUrl); + public String getContextUrl(String relativeUrl, Map params); + public String getRequestPath(); + public String getQueryString(); + + public String getMessage(String code, String defaultMessage); + public String getMessage(String code, Object[] args, String defaultMessage); + public String getMessage(String code, List args, String defaultMessage); + public String getMessage(String code, Object[] args, String defaultMessage, boolean htmlEscape); + public String getMessage(String code) throws NoSuchMessageException; + public String getMessage(String code, Object[] args) throws NoSuchMessageException; + public String getMessage(String code, List args) throws NoSuchMessageException; + public String getMessage(String code, Object[] args, boolean htmlEscape) throws NoSuchMessageException; + public String getMessage(MessageSourceResolvable resolvable) throws NoSuchMessageException; + public String getMessage(MessageSourceResolvable resolvable, boolean htmlEscape) throws NoSuchMessageException; + + public Optional getErrors(String name); + public Optional getErrors(String name, boolean htmlEscape); + + public Theme getTheme(); + + public IThymeleafRequestDataValueProcessor getRequestDataValueProcessor(); + + public IThymeleafBindStatus getBindStatus(String path) throws IllegalStateException; + public IThymeleafBindStatus getBindStatus(String path, boolean htmlEscape) throws IllegalStateException; + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/IThymeleafRequestDataValueProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/IThymeleafRequestDataValueProcessor.java new file mode 100644 index 00000000..55bf143b --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/IThymeleafRequestDataValueProcessor.java @@ -0,0 +1,46 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context; + +import java.util.Map; + +/** + *

+ * This interface is meant to abstract a Spring {@code RequestDataValueProcessor}, without the client code + * needing to know if it is a Spring WebMVC or Spring WebFlux implementation of this + * {@code RequestDataValueProcessor}. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public interface IThymeleafRequestDataValueProcessor { + + public String processAction(final String action, final String httpMethod); + + public String processFormFieldValue(final String name, final String value, final String type); + + public Map getExtraHiddenFields(); + + public String processUrl(final String url); + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/SpringContextUtils.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/SpringContextUtils.java new file mode 100755 index 00000000..16adfc13 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/SpringContextUtils.java @@ -0,0 +1,128 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context; + +import org.springframework.context.ApplicationContext; +import org.thymeleaf.context.IExpressionContext; +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.spring5.expression.IThymeleafEvaluationContext; +import org.thymeleaf.spring5.expression.ThymeleafEvaluationContext; +import org.thymeleaf.spring5.naming.SpringContextVariableNames; + +/** + *

+ * Utility class for easy access of information stored at the context in a Spring-enabled application + * (such as the Spring ApplicationContext). + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public class SpringContextUtils { + + + /** + *

+ * This is the name of the model attribute that will hold the (asychronously resolved) + * {@code WebSession} object in order to be used whenever needed, avoiding the need to block + * for obtaining it from the {@code ServerWebExchange}. + *

+ *

+ * Note resolving the {@code WebSession} from the reactive {@code Mono} stream does + * mean the creation of a {@code WebSession} instance, but not the real creation of a persisted session + * sent to the browser. + *

+ *

+ * Value: {@code "thymeleafWebSession"} + *

+ * + * @see org.springframework.web.server.WebSession + */ + public static final String WEB_SESSION_ATTRIBUTE_NAME = "thymeleafWebSession"; + + + /** + *

+ * Get the {@link ApplicationContext} from the Thymeleaf template context. + *

+ *

+ * Note that the application context might not be always accessible (and thus this method + * can return {@code null}). Application Context will be accessible when the template is being executed + * as a Spring View, or else when an object of class {@link ThymeleafEvaluationContext} has been + * explicitly set into the {@link ITemplateContext} {@code context} with variable name + * {@link ThymeleafEvaluationContext#THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME}. + *

+ * + * @param context the template context. + * @return the application context, or {@code null} if it could not be accessed. + */ + public static ApplicationContext getApplicationContext(final ITemplateContext context) { + if (context == null) { + return null; + } + // The ThymeleafEvaluationContext is set into the model by ThymeleafView (or wrapped by the SPEL evaluator) + final IThymeleafEvaluationContext evaluationContext = + (IThymeleafEvaluationContext) context.getVariable(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME); + if (evaluationContext == null || !(evaluationContext instanceof ThymeleafEvaluationContext)) { + return null; + } + // Only when the evaluation context is a ThymeleafEvaluationContext we can access the ApplicationContext. + // The reason is it could also be a wrapper on another EvaluationContext implementation, created at the + // SPELVariableExpressionEvaluator on-the-fly (where ApplicationContext is not available because there might + // even not exist one), instead of at ThymeleafView (where we are sure we are executing a Spring View and + // have an ApplicationContext available). + return ((ThymeleafEvaluationContext)evaluationContext).getApplicationContext(); + } + + + + /** + *

+ * Get the {@link IThymeleafRequestContext} from the Thymeleaf context. + *

+ *

+ * The returned object is a wrapper on the Spring request context that hides the fact of this request + * context corresponding to a Spring WebMVC or Spring WebFlux application. + *

+ *

+ * This will be done by looking for a context variable called + * {@link SpringContextVariableNames#THYMELEAF_REQUEST_CONTEXT}. + *

+ * + * @param context the context + * @return the thymeleaf request context + */ + public static IThymeleafRequestContext getRequestContext(final IExpressionContext context) { + if (context == null) { + return null; + } + return (IThymeleafRequestContext) context.getVariable(SpringContextVariableNames.THYMELEAF_REQUEST_CONTEXT); + } + + + + private SpringContextUtils() { + super(); + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/IReactiveDataDriverContextVariable.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/IReactiveDataDriverContextVariable.java new file mode 100644 index 00000000..3bffa14d --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/IReactiveDataDriverContextVariable.java @@ -0,0 +1,123 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context.webflux; + +import org.reactivestreams.Publisher; +import org.springframework.core.ReactiveAdapterRegistry; + +/** + *

+ * Interface to be implemented by context variables wrapping asynchronous objects in the form + * of reactive data streams which are meant to drive the reactive-friendly execution of a + * template. + *

+ *

+ * The presence of a variable of this type in the context sets the engine into data-driven + * mode, and only one of these variables is allowed to appear in the context for template execution. + *

+ *

+ * Using Reactive Streams terminology, this makes Thymeleaf act as a {@link org.reactivestreams.Processor}, given + * it will be a {@link org.reactivestreams.Subscriber} to the data-driver stream, and at the same time a + * {@link org.reactivestreams.Publisher} of output buffers (usually containing HTML markup). + *

+ *

+ * Templates executed in data-driven mode are expected to contain some kind iteration + * on the data-driver variable, normally by means of a {@code th:each} attribute. This iteration + * should be unique. Also note that, if this iteration is not present (or it doesn't end up being + * executed due to template logic), it is not guaranteed that the data-driven stream will not + * be consumed anyway -at least partially- depending on the use that the specific server implementation might make + * of the Reactor's back-pressure mechanism. + *

+ *

+ * Data-driver context variables are required to be multi-valued. + * Being multi-valued does not mean to necessarily return more than one value, + * but simply to have the capability to do so. E.g. a {@link reactor.core.publisher.Flux} object will + * be considered multi-valued even if it publishes none or just one result, whereas a + * {@link reactor.core.publisher.Mono} object will be considered single-valued. + *

+ *

+ * The {@link #getBufferSizeElements()} property describes the size (in elements) of the buffers that + * will be created from the data-driver stream before triggering the execution of the template (for each buffer). + * Normally there is no need to execute the template engine and generate output for each element of data + * published by the data stream, so this buffering prevents Thymeleaf from executing more times than actually needed. + *

+ *

+ * The {@link ReactiveDataDriverContextVariable} class contains a sensible implementation of this interface, + * directly usable in most scenarios. Example use: + *

+ *

+ * @RequestMapping("/something")
+ * public String doSomething(final Model model) {
+ *     final Publisher<Item> data = ...; // This has to be MULTI-VALUED (e.g. Flux)
+ *     model.addAttribute("data", new ReactiveDataDriverContextVariable(data, 100));
+ *     return "view";
+ * }
+ * 
+ *

+ * And then at the template: + *

+ *

+ * <table>
+ *   <tbody>
+ *     <tr th:each="item : ${data}">
+ *       <td th:text="${item}">some item...</td>
+ *     </tr>
+ *   </tbody>
+ * </table>
+ * 
+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public interface IReactiveDataDriverContextVariable { + + + /** + *

+ * Returns the reactive asynchronous object being wrapped, (perhaps) having been re-shaped into a + * {@link Publisher} stream. + *

+ * + * @param reactiveAdapterRegistry the Spring reactive adapter registry that should be used in order to transform + * other reactive implementations (RxJava, etc.) into + * {@link reactor.core.publisher.Flux}. Can be null (no adaptations will be + * available). + * @return the asynchronous object (as a {@link Publisher}). + */ + public Publisher getDataStream(final ReactiveAdapterRegistry reactiveAdapterRegistry); + + /** + *

+ * Returns the size (in elements) of the buffers that will be created from the data-driver stream + * before triggering the execution of the template (for each buffer). + *

+ *

+ * Normally there is no need to execute the template engine and generate output for each element of data + * published by the data stream, so this buffering prevents Thymeleaf from executing more times + * than actually needed. + *

+ * + * @return the size (in elements) of the buffers to be created for each (partial) execution of the engine. + */ + public int getBufferSizeElements(); + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/IReactiveSSEDataDriverContextVariable.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/IReactiveSSEDataDriverContextVariable.java new file mode 100644 index 00000000..0f49de0d --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/IReactiveSSEDataDriverContextVariable.java @@ -0,0 +1,117 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context.webflux; + +/** + *

+ * Interface to be implemented by context variables wrapping asynchronous objects in the form + * of reactive data streams which are meant to drive the reactive-friendly execution of a + * template in SSE (Server-Sent Event) mode. + *

+ *

+ * This interface adds to its parent {@link IReactiveDataDriverContextVariable} the possibility to specify a prefix + * to be applied to the names and IDs of events generated in SSE scenarios. This can be useful in scenarios such as + * UI composition, in which streams of markup events coming from different sources (e.g. different parts of a page) + * can be sent to the browser combined in a single {@code EventSource} SSE stream. That way client JavaScript + * code will be able to identify which part of the page the event belongs to by means of its prefix. Also, combining + * several (prefixed) SSE streams into one can also serve to overcome limitations in the amount of concurrent + * active {@code EventSource} allowed. + *

+ *

+ * This interface also allows the specification of the first ID value to be used in these SSE events. This is + * useful in SSE scenarios in which the browser requests the server to reconnect after a connection + * failure, specifying the HTTP {@code Last-Event-ID} header so that the + * application can start generating events again starting from the event following the last one successfully + * processed by the browser (note this resume operation has to be supported by whoever is in charge of + * creating the data stream that Thymeleaf subscribes to in {@code DATA-DRIVEN} mode, not by Thymeleaf itself + * which is only in charge of rendering the view layer). + *

+ *

+ * Returning SSE (Server-Sent Events) through Thymeleaf requires the presence of a variable implementing this + * interface in the context. Thymeleaf will generate three types of events during rendering: + *

+ *
    + *
  • Header ({@code event: head} or {@code event: {prefix}_head}), a single event containing all the + * markup previous to the iterated data (if any).
  • + *
  • Data message ({@code event: message} or {@code event: {prefix}_message})), a series of n events, one + * for each value produced by the data driver.
  • + *
  • Tail ({@code event: tail} or {@code event: {prefix}_tail})), a single event containing all the markup + * following the last iterated piece of data (if any).
  • + *
+ *

+ * Note that in the case of SSE, the value assigned to the {@link #getBufferSizeElements()} property does + * affect the immediacy of the generated (published) events being sent to the browser. If this buffer is set e.g. + * to 4, only when a total of 4 items of data are generated will be sent to the browser as SSE events. + *

+ *

+ * The {@link ReactiveDataDriverContextVariable} class contains a sensible implementation of this interface, + * directly usable in most scenarios. Example use: + *

+ *

+ * @RequestMapping("/something")
+ * public String doSomething(final Model model) {
+ *     final Publisher<Item> data = ...; // This has to be MULTI-VALUED (e.g. Flux)
+ *     model.addAttribute("data", new ReactiveDataDriverContextVariable(data, 100, 1L)); // firstEventID = 1L
+ *     return "view";
+ * }
+ * 
+ * + * @author Daniel Fernández + * + * @since 3.0.4 + * + */ +public interface IReactiveSSEDataDriverContextVariable extends IReactiveDataDriverContextVariable { + + + /** + *

+ * Returns the (optional) prefix to be used for SSE event names and IDs. + *

+ *

+ * Using a prefix for SSE events can be useful in scenarios such as UI composition, in which streams of + * markup events coming from different sources (e.g. different parts of a page) can be sent to the browser + * combined in a single {@code EventSource} SSE stream. That way client JavaScript + * code will be able to identify which part of the page the event belongs to by means of its + * prefix. Also, combining several (prefixed) SSE streams into one can also serve to overcome limitations + * in the amount of concurrent active {@code EventSource} allowed. + *

+ * + * @return the prefix to be applied to event names and IDs, or {@code null} if no prefix has been set. + * @since 3.0.8 + */ + public String getSseEventsPrefix(); + + + /** + *

+ * Returns the first value to be used as an {@code id} in the case this response is rendered as SSE + * (Server-Sent Events) with content type {@code text/event-stream}. + *

+ *

+ * After the first generated events, subsequent ones will be assigned an {@code id} by incrementing this + * first value. + *

+ * + * @return the first value to be used for returning the data driver variable values as SSE events. + */ + public long getSseEventsFirstID(); + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/ISpringWebFluxContext.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/ISpringWebFluxContext.java new file mode 100644 index 00000000..1ca463a1 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/ISpringWebFluxContext.java @@ -0,0 +1,92 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context.webflux; + +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebSession; +import org.thymeleaf.context.IContext; +import reactor.core.publisher.Mono; + +/** + *

+ * Specialization of the {@link IContext} interface to be implemented by contexts used for template + * processing in Spring WebFlux environments. + *

+ *

+ * Objects implementing this interface add to the usual {@link IContext} data the Spring WebFlux-related + * artifacts needed to perform functions such as URL rewriting or request/session access. + *

+ *

+ * A basic implementation of this interface is provided by {@link SpringWebFluxExpressionContext}, but + * there is normally no reason why users should use this interface (or its implementations) directly. + *

+ * + * @see SpringWebFluxExpressionContext + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public interface ISpringWebFluxContext extends IContext { + + /** + *

+ * Returns the {@link ServerHttpRequest} object associated with the template execution. + *

+ * + * @return the request object. + */ + public ServerHttpRequest getRequest(); + + /** + *

+ * Returns the {@link ServerHttpResponse} object associated with the template execution. + *

+ * + * @return the response object. + */ + public ServerHttpResponse getResponse(); + + /** + *

+ * Returns the {@link WebSession} object associated with the template execution. + *

+ *

+ * The returned {@link Mono} will always return an instance, either matching the client's session id + * or a new session. Note that calling this method does not create the session object itself. + *

+ * + * @return the session object. Might be null if no session has been created. + */ + public Mono getSession(); + + /** + *

+ * Returns the {@link ServerWebExchange} object associated with the template execution. + *

+ * + * @return the servlet context object. + */ + public ServerWebExchange getExchange(); + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/ReactiveContextVariableUtils.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/ReactiveContextVariableUtils.java new file mode 100644 index 00000000..37cf9f47 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/ReactiveContextVariableUtils.java @@ -0,0 +1,110 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context.webflux; + +import org.reactivestreams.Publisher; +import org.springframework.core.ReactiveAdapter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveView; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + *

+ * Utility class used by reactive context variable container/wrappers. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +class ReactiveContextVariableUtils { + + + + /** + *

+ * Lazily resolve the reactive asynchronous object into a {@link Publisher}. + *

+ *

+ * The main aim of this method is to mirror the mechanism used by Spring for resolving + * asynchronous variables at the model of views (see Spring's + * {@link org.springframework.web.reactive.result.view.ViewResolutionResultHandler}): + *

+ *
    + *
  • {@code Flux} or other multi-valued streams are resolved as + * {@code List} so that they are iterable.
  • + *
  • {@code Mono} or other single-valued streams are resolved as + * {@code T} so that they are directly referenceable just like any other object.
  • + *
+ * + * @param asyncObj the asynchronous object being wrapped by this lazy variable. + * @param reactiveAdapterRegistry the Spring {@link ReactiveAdapterRegistry}. + * @return the resolved {@link Publisher}. + */ + static Publisher computePublisherValue( + final Object asyncObj, final ReactiveAdapterRegistry reactiveAdapterRegistry) { + + if (asyncObj instanceof Flux || asyncObj instanceof Mono) { + // If the async object is a Flux or a Mono, we don't need the ReactiveAdapterRegistry (and we allow + // initialization to happen without the registry, which is not possible with other Publisher + // implementations. + return (Publisher) asyncObj; + } + + + if (reactiveAdapterRegistry == null) { + throw new IllegalArgumentException( + "Could not initialize lazy reactive context variable (data driver or explicitly-set " + + "reactive wrapper): Value is of class " + asyncObj.getClass().getName() +", but no " + + "ReactiveAdapterRegistry has been set. This can happen if this context variable is used " + + "for rendering a template without going through a " + + ThymeleafReactiveView.class.getSimpleName() + " or if there is no " + + "ReactiveAdapterRegistry bean registered at the application context. In such cases, it is " + + "required that the wrapped lazy variable values are instances of either " + + Flux.class.getName() + " or " + Mono.class.getName() + "."); + } + + final ReactiveAdapter adapter = reactiveAdapterRegistry.getAdapter(null, asyncObj); + if (adapter != null) { + final Publisher publisher = adapter.toPublisher(asyncObj); + if (adapter.isMultiValue()) { + return Flux.from(publisher); + } else { + return Mono.from(publisher); + } + } + + throw new IllegalArgumentException( + "Reactive context variable (data driver or explicitly-set reactive wrapper) is of " + + "class " + asyncObj.getClass().getName() +", but the ReactiveAdapterRegistry " + + "does not contain a valid adapter able to convert it into a supported reactive data stream."); + + } + + + + + private ReactiveContextVariableUtils() { + super(); + } + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/ReactiveDataDriverContextVariable.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/ReactiveDataDriverContextVariable.java new file mode 100644 index 00000000..0be1f931 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/ReactiveDataDriverContextVariable.java @@ -0,0 +1,321 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context.webflux; + +import org.reactivestreams.Publisher; +import org.springframework.core.ReactiveAdapterRegistry; +import org.thymeleaf.util.Validate; +import reactor.core.publisher.Flux; + +/** + *

+ * Basic implementation of the {@link IReactiveDataDriverContextVariable} interface, including also + * the extensions specified in {@link IReactiveSSEDataDriverContextVariable}. + *

+ *

+ * The reactive data stream wrapped by this class will usually have the shape of an implementation of the + * {@link Publisher} interface, such as {@link reactor.core.publisher.Flux}. But other types of reactive + * artifacts are supported thanks to Spring's {@link org.springframework.core.ReactiveAdapterRegistry} + * mechanism if such adapter registry has been set in the context + * (see {@link SpringWebFluxContext#getReactiveAdapterRegistry()}). + *

+ *

+ * Data-driver context variables are required to be multi-valued. + * Being multi-valued does not mean to necessarily return more than one value, + * but simply to have the capability to do so. E.g. a {@link reactor.core.publisher.Flux} object will + * be considered multi-valued even if it publishes none or just one result, whereas a + * {@link reactor.core.publisher.Mono} object will be considered single-valued. + *

+ *

+ * Example use: + *

+ *

+ * @RequestMapping("/something")
+ * public String doSomething(final Model model) {
+ *     final Publisher<Item> data = ...; // This has to be MULTI-VALUED (e.g. Flux)
+ *     model.addAttribute("data", new ReactiveDataDriverContextVariable(data, 100));
+ *     return "view";
+ * }
+ * 
+ *

+ * And then at the template: + *

+ *

+ * <table>
+ *   <tbody>
+ *     <tr th:each="item : ${data}">
+ *       <td th:text="${item}">some item...</td>
+ *     </tr>
+ *   </tbody>
+ * </table>
+ * 
+ *

+ * For more information on the way this class would work in SSE (Server-Sent Event) scenarios, see + * {@link IReactiveSSEDataDriverContextVariable}. + *

+ *

+ * This class is NOT thread-safe. Thread-safety is not a requirement for context variables. + *

+ * + * @see IReactiveDataDriverContextVariable + * @see IReactiveSSEDataDriverContextVariable + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public class ReactiveDataDriverContextVariable implements IReactiveSSEDataDriverContextVariable { + + /** + *

+ * Default buffer size to be applied if none is specified. Value = {@code 10}. + *

+ */ + public static final int DEFAULT_DATA_DRIVER_BUFFER_SIZE_ELEMENTS = 10; + + /** + *

+ * Default value for the first event ID (for SSE scenarios). Value = {@code 0}. + *

+ */ + public static final long DEFAULT_FIRST_EVENT_ID = 0L; + + + private final Object dataStream; + private final int dataStreamBufferSizeElements; + private final String sseEventsPrefix; + private final long sseEventsFirstID; + + + /** + *

+ * Creates a new lazy context variable, wrapping a reactive asynchronous data stream. + *

+ *

+ * Buffer size will be set to {@link #DEFAULT_DATA_DRIVER_BUFFER_SIZE_ELEMENTS}. + *

+ *

+ * The specified {@code dataStream} must be adaptable to a Reactive Stream's + * {@link Publisher} by means of Spring's {@link ReactiveAdapterRegistry} mechanism. If no + * adapter has been registered for the type of the asynchronous object, and exception will be + * thrown during lazy resolution. If no adapter registry has been set into the context + * (see {@link SpringWebFluxContext#getReactiveAdapterRegistry()}) this data stream must mandatorily + * be a {@link Flux}. + *

+ *

+ * Note the specified {@code dataStream} must be multi-valued. + *

+ *

+ * Examples of supported implementations are Reactor's {@link Flux} (but not + * {@link reactor.core.publisher.Mono}), and also RxJava's {@code Observable} + * (but not {@code Single}). + *

+ * + * @param dataStream the asynchronous object, which must be convertible to a multi-valued {@link Publisher} by + * means of Spring's {@link ReactiveAdapterRegistry}. + */ + public ReactiveDataDriverContextVariable(final Object dataStream) { + this(dataStream, DEFAULT_DATA_DRIVER_BUFFER_SIZE_ELEMENTS, null, DEFAULT_FIRST_EVENT_ID); + } + + + /** + *

+ * Creates a new lazy context variable, wrapping a reactive asynchronous data stream and specifying a + * buffer size. + *

+ *

+ * The specified {@code dataStream} must be adaptable to a Reactive Stream's + * {@link Publisher} by means of Spring's {@link ReactiveAdapterRegistry} mechanism. If no + * adapter has been registered for the type of the asynchronous object, and exception will be + * thrown during lazy resolution. If no adapter registry has been set into the context + * (see {@link SpringWebFluxContext#getReactiveAdapterRegistry()}) this data stream must mandatorily + * be a {@link Flux}. + *

+ *

+ * Note the specified {@code dataStream} must be multi-valued. + *

+ *

+ * Examples of supported implementations are Reactor's {@link Flux} (but not + * {@link reactor.core.publisher.Mono}), and also RxJava's {@code Observable} + * (but not {@code Single}). + *

+ * + * @param dataStream the asynchronous object, which must be convertible to a multi-valued {@link Publisher} by + * means of Spring's {@link ReactiveAdapterRegistry}. + * @param dataStreamBufferSizeElements the buffer size to be applied (in elements). + */ + public ReactiveDataDriverContextVariable(final Object dataStream, final int dataStreamBufferSizeElements) { + this(dataStream, dataStreamBufferSizeElements, null, DEFAULT_FIRST_EVENT_ID); + } + + + /** + *

+ * Creates a new lazy context variable, wrapping a reactive asynchronous data stream and specifying a + * buffer size and a prefix for all the names and IDs of events generated from a specific SSE stream. + *

+ *

+ * The specified {@code dataStream} must be adaptable to a Reactive Stream's + * {@link Publisher} by means of Spring's {@link ReactiveAdapterRegistry} mechanism. If no + * adapter has been registered for the type of the asynchronous object, and exception will be + * thrown during lazy resolution. If no adapter registry has been set into the context + * (see {@link SpringWebFluxContext#getReactiveAdapterRegistry()}) this data stream must mandatorily + * be a {@link Flux}. + *

+ *

+ * Note the specified {@code dataStream} must be multi-valued. + *

+ *

+ * Examples of supported implementations are Reactor's {@link Flux} (but not + * {@link reactor.core.publisher.Mono}), and also RxJava's {@code Observable} + * (but not {@code Single}). + *

+ * + * @param dataStream the asynchronous object, which must be convertible to a multi-valued {@link Publisher} by + * means of Spring's {@link ReactiveAdapterRegistry}. + * @param dataStreamBufferSizeElements the buffer size to be applied (in elements). + * @param sseEventsPrefix the prefix to be used for event names and IDs, so that events coming from a specific + * SSE stream can be identified (if applies). Can be null. + * + * @since 3.0.8 + */ + public ReactiveDataDriverContextVariable( + final Object dataStream, final int dataStreamBufferSizeElements, + final String sseEventsPrefix) { + this(dataStream, dataStreamBufferSizeElements, sseEventsPrefix, DEFAULT_FIRST_EVENT_ID); + } + + + /** + *

+ * Creates a new lazy context variable, wrapping a reactive asynchronous data stream and specifying a + * buffer size and a value for the ID of the first event generated in SSE scenarios. + *

+ *

+ * The specified {@code dataStream} must be adaptable to a Reactive Stream's + * {@link Publisher} by means of Spring's {@link ReactiveAdapterRegistry} mechanism. If no + * adapter has been registered for the type of the asynchronous object, and exception will be + * thrown during lazy resolution. If no adapter registry has been set into the context + * (see {@link SpringWebFluxContext#getReactiveAdapterRegistry()}) this data stream must mandatorily + * be a {@link Flux}. + *

+ *

+ * Note the specified {@code dataStream} must be multi-valued. + *

+ *

+ * Examples of supported implementations are Reactor's {@link Flux} (but not + * {@link reactor.core.publisher.Mono}), and also RxJava's {@code Observable} + * (but not {@code Single}). + *

+ * + * @param dataStream the asynchronous object, which must be convertible to a multi-valued {@link Publisher} by + * means of Spring's {@link ReactiveAdapterRegistry}. + * @param dataStreamBufferSizeElements the buffer size to be applied (in elements). + * @param sseEventsFirstID the first value to be used as event ID in SSE scenarios (if applies). + * + * @since 3.0.4 + */ + public ReactiveDataDriverContextVariable( + final Object dataStream, final int dataStreamBufferSizeElements, + final long sseEventsFirstID) { + this(dataStream, dataStreamBufferSizeElements, null, sseEventsFirstID); + } + + + /** + *

+ * Creates a new lazy context variable, wrapping a reactive asynchronous data stream and specifying a + * buffer size and a value for the ID of the first event generated in SSE scenarios and a prefix for all + * the names and IDs of events generated from a specific SSE stream. + *

+ *

+ * The specified {@code dataStream} must be adaptable to a Reactive Stream's + * {@link Publisher} by means of Spring's {@link ReactiveAdapterRegistry} mechanism. If no + * adapter has been registered for the type of the asynchronous object, and exception will be + * thrown during lazy resolution. If no adapter registry has been set into the context + * (see {@link SpringWebFluxContext#getReactiveAdapterRegistry()}) this data stream must mandatorily + * be a {@link Flux}. + *

+ *

+ * Note the specified {@code dataStream} must be multi-valued. + *

+ *

+ * Examples of supported implementations are Reactor's {@link Flux} (but not + * {@link reactor.core.publisher.Mono}), and also RxJava's {@code Observable} + * (but not {@code Single}). + *

+ * + * @param dataStream the asynchronous object, which must be convertible to a multi-valued {@link Publisher} by + * means of Spring's {@link ReactiveAdapterRegistry}. + * @param dataStreamBufferSizeElements the buffer size to be applied (in elements). + * @param sseEventsPrefix the prefix to be used for event names and IDs, so that events coming from a specific + * SSE stream can be identified (if applies). Can be null. + * @param sseEventsFirstID the first value to be used as event ID in SSE scenarios (if applies). + * + * @since 3.0.8 + */ + public ReactiveDataDriverContextVariable( + final Object dataStream, final int dataStreamBufferSizeElements, + final String sseEventsPrefix, final long sseEventsFirstID) { + super(); + Validate.notNull(dataStream, "Data stream cannot be null"); + Validate.isTrue(dataStreamBufferSizeElements > 0, "Data Buffer Size cannot be <= 0"); + // The prefix for SSE events CAN be null + Validate.isTrue(sseEventsFirstID >= 0L, "First Event ID cannot be < 0"); + this.dataStream = dataStream; + this.dataStreamBufferSizeElements = dataStreamBufferSizeElements; + this.sseEventsPrefix = sseEventsPrefix; + this.sseEventsFirstID = sseEventsFirstID; + } + + + @Override + public Publisher getDataStream(final ReactiveAdapterRegistry reactiveAdapterRegistry) { + final Publisher publisher = + ReactiveContextVariableUtils.computePublisherValue(this.dataStream, reactiveAdapterRegistry); + if (!(publisher instanceof Flux)) { + throw new IllegalArgumentException( + "Reactive Data Driver context variable was set single-valued asynchronous object. But data driver " + + "variables must wrap multi-valued data streams (so that they can be iterated at the template"); + } + return publisher; + } + + + @Override + public final int getBufferSizeElements() { + return this.dataStreamBufferSizeElements; + } + + + @Override + public final String getSseEventsPrefix() { + return this.sseEventsPrefix; + } + + + @Override + public final long getSseEventsFirstID() { + return this.sseEventsFirstID; + } + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxContext.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxContext.java new file mode 100644 index 00000000..cd39453b --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxContext.java @@ -0,0 +1,145 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context.webflux; + +import java.util.Locale; +import java.util.Map; + +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebSession; +import org.thymeleaf.context.AbstractContext; +import org.thymeleaf.util.Validate; +import reactor.core.publisher.Mono; + +/** + *

+ * Basic Spring WebFlux-oriented implementation of the {@link ISpringWebFluxContext} interfaces, + * easily usable as a context for calling the {@link org.thymeleaf.spring5.ISpringWebFluxTemplateEngine} + * from outside a {@link org.thymeleaf.spring5.view.reactive.ThymeleafReactiveView}. + *

+ *

+ * This class is not thread-safe, and should not be shared across executions of templates. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.8 + * + */ +public class SpringWebFluxContext extends AbstractContext implements ISpringWebFluxContext { + + + private final ServerWebExchange exchange; + private final ReactiveAdapterRegistry reactiveAdapterRegistry; // nullable + + + /** + *

+ * Build a new instance of this Spring WebFlux-specific context object. + *

+ * + * @param exchange the Spring WebFlux exchange object, containing request, response and session. Cannot be null. + */ + public SpringWebFluxContext(final ServerWebExchange exchange) { + this(exchange, null, null, null); + } + + /** + *

+ * Build a new instance of this Spring WebFlux-specific context object. + *

+ * + * @param exchange the Spring WebFlux exchange object, containing request, response and session. Cannot be null. + * @param locale the locale to be used for executing Thymeleaf. Can be null (Locale.getDefault() will be used). + */ + public SpringWebFluxContext(final ServerWebExchange exchange, final Locale locale) { + this(exchange, null, locale, null); + } + + /** + *

+ * Build a new instance of this Spring WebFlux-specific context object. + *

+ * + * @param exchange the Spring WebFlux exchange object, containing request, response and session. Cannot be null. + * @param locale the locale to be used for executing Thymeleaf. Can be null (Locale.getDefault() will be used). + * @param variables the variables to be used for executing Thymeleaf. Can be null. + */ + public SpringWebFluxContext( + final ServerWebExchange exchange, + final Locale locale, final Map variables) { + this(exchange, null, locale, variables); + } + + /** + *

+ * Build a new instance of this Spring WebFlux-specific context object. + *

+ * + * @param exchange the Spring WebFlux exchange object, containing request, response and session. Cannot be null. + * @param reactiveAdapterRegistry the Spring WebFlux reactive adapter object, used in cases when it is needed + * to turn non-Reactor reactive streams (RxJava, etc.) into Reactor equivalents + * (Flux, Mono) in order to be used by Thymeleaf as data drivers + * (see {@link IReactiveDataDriverContextVariable}). Can be null. + * @param locale the locale to be used for executing Thymeleaf. Can be null (Locale.getDefault() will be used). + * @param variables the variables to be used for executing Thymeleaf. Can be null. + */ + public SpringWebFluxContext( + final ServerWebExchange exchange, + final ReactiveAdapterRegistry reactiveAdapterRegistry, + final Locale locale, final Map variables) { + super(locale, variables); + Validate.notNull(exchange, "ServerWebExchange cannot be null in Spring WebFlux contexts"); + // reactiveAdapterRegistry CAN be null + this.exchange = exchange; + this.reactiveAdapterRegistry = reactiveAdapterRegistry; + } + + + // This method is not included in the interface as it is more an implementation detail usable for + // a specific scenario of Publisher-normalisation in WebFlux. + public ReactiveAdapterRegistry getReactiveAdapterRegistry() { + return this.reactiveAdapterRegistry; + } + + @Override + public ServerHttpRequest getRequest() { + return this.exchange.getRequest(); + } + + @Override + public Mono getSession() { + return this.exchange.getSession(); + } + + @Override + public ServerHttpResponse getResponse() { + return this.exchange.getResponse(); + } + + @Override + public ServerWebExchange getExchange() { + return this.exchange; + } + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxEngineContext.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxEngineContext.java new file mode 100644 index 00000000..5a826300 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxEngineContext.java @@ -0,0 +1,749 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context.webflux; + +import java.util.AbstractList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebSession; +import org.thymeleaf.IEngineConfiguration; +import org.thymeleaf.context.AbstractEngineContext; +import org.thymeleaf.context.Context; +import org.thymeleaf.context.EngineContext; +import org.thymeleaf.context.IContext; +import org.thymeleaf.context.IEngineContext; +import org.thymeleaf.context.ILazyContextVariable; +import org.thymeleaf.context.WebContext; +import org.thymeleaf.engine.TemplateData; +import org.thymeleaf.inline.IInliner; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.spring5.context.SpringContextUtils; +import org.thymeleaf.util.Validate; +import reactor.core.publisher.Mono; + +/** + *

+ * Basic web implementation of the {@link IEngineContext} interface, based on the Spring WebFlux + * infrastructure. + *

+ *

+ * This is the context implementation that will be used by default for template processing in Spring WebFlux + * environments. Note that this is an internal implementation, and there is no reason for users' code to + * directly reference or use it instead of its implemented interfaces. + *

+ *

+ * This class is NOT thread-safe. Thread-safety is not a requirement for context implementations. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public class SpringWebFluxEngineContext + extends AbstractEngineContext implements IEngineContext, ISpringWebFluxContext { + + + private static final String PARAM_VARIABLE_NAME = "param"; + private static final String SESSION_VARIABLE_NAME = "session"; + + private final ServerHttpRequest request; + private final ServerHttpResponse response; + private final ServerWebExchange exchange; + + private final WebExchangeAttributesVariablesMap webExchangeAttributesVariablesMap; + private final Map requestParametersVariablesMap; + private final Map sessionAttributesVariablesMap; + + + + + /** + *

+ * Creates a new instance of this {@link IEngineContext} implementation binding engine execution to + * the Spring WebFlux request handling mechanisms, mainly modelled by {@link ServerWebExchange}. + *

+ *

+ * Note that implementations of {@link IEngineContext} are not meant to be used in order to call + * the template engine (use implementations of {@link IContext} such as {@link Context} or {@link WebContext} + * instead). This is therefore mostly an internal implementation, and users should have no reason + * to ever call this constructor except in very specific integration/extension scenarios. + *

+ * + * @param configuration the configuration instance being used. + * @param templateData the template data for the template to be processed. + * @param templateResolutionAttributes the template resolution attributes. + * @param exchange the web exchange object being used for request handling. + * @param locale the locale. + * @param variables the context variables, probably coming from another {@link IContext} implementation. + */ + public SpringWebFluxEngineContext( + final IEngineConfiguration configuration, + final TemplateData templateData, + final Map templateResolutionAttributes, + final ServerWebExchange exchange, + final Locale locale, + final Map variables) { + + super(configuration, templateResolutionAttributes, locale); + + Validate.notNull(exchange, "Server Web Exchange cannot be null in web variables map"); + + this.exchange = exchange; + this.request = this.exchange.getRequest(); + this.response = this.exchange.getResponse(); + // No need to call "Mono this.exchange.getSession()" because the reactive variable it returns + // should have been already configured by SpringStandardDialect for async resolution before View execution + // (by means of a specially-named execution attribute), so we should be instead retrieving the WebSession + // (not he reactive Mono) from the context. + + this.webExchangeAttributesVariablesMap = + new WebExchangeAttributesVariablesMap(configuration, templateData, templateResolutionAttributes, this.exchange, locale, variables); + this.requestParametersVariablesMap = new RequestParametersMap(this.request); + this.sessionAttributesVariablesMap = + new SessionAttributesMap(this.webExchangeAttributesVariablesMap); + + } + + + @Override + public ServerHttpRequest getRequest() { + return this.request; + } + + + @Override + public ServerHttpResponse getResponse() { + return this.response; + } + + + @Override + public Mono getSession() { + return this.exchange.getSession(); + } + + + @Override + public ServerWebExchange getExchange() { + return this.exchange; + } + + + @Override + public boolean containsVariable(final String name) { + return SESSION_VARIABLE_NAME.equals(name) || + PARAM_VARIABLE_NAME.equals(name) || + this.webExchangeAttributesVariablesMap.containsVariable(name); + } + + + @Override + public Object getVariable(final String key) { + if (SESSION_VARIABLE_NAME.equals(key)) { + return this.sessionAttributesVariablesMap; + } + if (PARAM_VARIABLE_NAME.equals(key)) { + return this.requestParametersVariablesMap; + } + return this.webExchangeAttributesVariablesMap.getVariable(key); + } + + + @Override + public Set getVariableNames() { + // Note this set will NOT include 'param', 'session' or 'application', as they are considered special + // ways to access attributes/parameters in these Servlet API structures + return this.webExchangeAttributesVariablesMap.getVariableNames(); + } + + + @Override + public void setVariable(final String name, final Object value) { + if (SESSION_VARIABLE_NAME.equals(name) || PARAM_VARIABLE_NAME.equals(name)) { + throw new IllegalArgumentException( + "Cannot set variable called '" + name + "' into web variables map: such name is a reserved word"); + } + this.webExchangeAttributesVariablesMap.setVariable(name, value); + } + + + @Override + public void setVariables(final Map variables) { + if (variables == null || variables.isEmpty()) { + return; + } + // First perform reserved word check on every variable name to be inserted + for (final String name : variables.keySet()) { + if (SESSION_VARIABLE_NAME.equals(name) || PARAM_VARIABLE_NAME.equals(name)) { + throw new IllegalArgumentException( + "Cannot set variable called '" + name + "' into web variables map: such name is a reserved word"); + } + } + this.webExchangeAttributesVariablesMap.setVariables(variables); + } + + + @Override + public void removeVariable(final String name) { + if (SESSION_VARIABLE_NAME.equals(name) || PARAM_VARIABLE_NAME.equals(name)) { + throw new IllegalArgumentException( + "Cannot remove variable called '" + name + "' in web variables map: such name is a reserved word"); + } + this.webExchangeAttributesVariablesMap.removeVariable(name); + } + + + @Override + public boolean isVariableLocal(final String name) { + return this.webExchangeAttributesVariablesMap.isVariableLocal(name); + } + + + @Override + public boolean hasSelectionTarget() { + return this.webExchangeAttributesVariablesMap.hasSelectionTarget(); + } + + + @Override + public Object getSelectionTarget() { + return this.webExchangeAttributesVariablesMap.getSelectionTarget(); + } + + + @Override + public void setSelectionTarget(final Object selectionTarget) { + this.webExchangeAttributesVariablesMap.setSelectionTarget(selectionTarget); + } + + + + + @Override + public IInliner getInliner() { + return this.webExchangeAttributesVariablesMap.getInliner(); + } + + @Override + public void setInliner(final IInliner inliner) { + this.webExchangeAttributesVariablesMap.setInliner(inliner); + } + + + + + @Override + public TemplateData getTemplateData() { + return this.webExchangeAttributesVariablesMap.getTemplateData(); + } + + @Override + public void setTemplateData(final TemplateData templateData) { + this.webExchangeAttributesVariablesMap.setTemplateData(templateData); + } + + + @Override + public List getTemplateStack() { + return this.webExchangeAttributesVariablesMap.getTemplateStack(); + } + + + + + @Override + public void setElementTag(final IProcessableElementTag elementTag) { + this.webExchangeAttributesVariablesMap.setElementTag(elementTag); + } + + + + + @Override + public List getElementStack() { + return this.webExchangeAttributesVariablesMap.getElementStack(); + } + + + @Override + public List getElementStackAbove(final int contextLevel) { + return this.webExchangeAttributesVariablesMap.getElementStackAbove(contextLevel); + } + + + + + @Override + public int level() { + return this.webExchangeAttributesVariablesMap.level(); + } + + + @Override + public void increaseLevel() { + this.webExchangeAttributesVariablesMap.increaseLevel(); + } + + + @Override + public void decreaseLevel() { + this.webExchangeAttributesVariablesMap.decreaseLevel(); + } + + + + + public String getStringRepresentationByLevel() { + // Request parameters, session and servlet context can be safely ignored here + return this.webExchangeAttributesVariablesMap.getStringRepresentationByLevel(); + } + + + + + @Override + public String toString() { + // Request parameters, session and servlet context can be safely ignored here + return this.webExchangeAttributesVariablesMap.toString(); + } + + + + static Object resolveLazy(final Object variable) { + /* + * Check the possibility that this variable is a lazy one, in which case we should not return it directly + * but instead make sure it is initialized and return its value. + */ + if (variable != null && variable instanceof ILazyContextVariable) { + return ((ILazyContextVariable)variable).getValue(); + } + return variable; + } + + + + + private static final class SessionAttributesMap extends NoOpMapImpl { + + /* + * NOTE the WebSession is not actually obtained from the ServerWebExchange at this point, but instead + * from the context. The reason is ServerWebExchange#getSession() returns Mono, and in order + * to resolve this reactive variable in a non-blocking manner we have used the execution attributes + * mechanism in the dialect in order to specify that Mono should be resolved before + * template execution. + */ + + private final WebExchangeAttributesVariablesMap attrVars; + private WebSession session = null; + + + SessionAttributesMap(final WebExchangeAttributesVariablesMap attrVars) { + super(); + this.attrVars = attrVars; + } + + private WebSession getSession() { + if (this.session == null) { + this.session = (WebSession) this.attrVars.getVariable(SpringContextUtils.WEB_SESSION_ATTRIBUTE_NAME); + } + return this.session; + } + + @Override + public int size() { + final WebSession webSession = getSession(); + if (webSession == null) { + return 0; + } + return webSession.getAttributes().size(); + } + + @Override + public boolean isEmpty() { + final WebSession webSession = getSession(); + if (webSession == null) { + return true; + } + return webSession.getAttributes().isEmpty(); + } + + @Override + public boolean containsKey(final Object key) { + // Even if not completely correct to return 'true' for entries that might not exist, this is needed + // in order to avoid Spring's MapAccessor throwing an exception when trying to access an element + // that doesn't exist -- in the case of request parameters, session and servletContext attributes most + // developers would expect null to be returned in such case, and that's what this 'true' will cause. + return true; + } + + @Override + public boolean containsValue(final Object value) { + // It wouldn't be consistent to have an 'ad hoc' implementation of #containsKey() but a 100% correct + // implementation of #containsValue(), so we are leaving this as unsupported. + throw new UnsupportedOperationException("Map does not support #containsValue()"); + } + + @Override + public Object get(final Object key) { + final WebSession webSession = getSession(); + if (webSession == null) { + return null; + } + return resolveLazy(webSession.getAttributes().get(key != null? key.toString() : null)); + } + + @Override + public Set keySet() { + final WebSession webSession = getSession(); + if (webSession == null) { + return Collections.emptySet(); + } + return webSession.getAttributes().keySet(); + } + + @Override + public Collection values() { + final WebSession webSession = getSession(); + if (webSession == null) { + return Collections.emptyList(); + } + return webSession.getAttributes().values(); + } + + @Override + public Set> entrySet() { + final WebSession webSession = getSession(); + if (webSession == null) { + return Collections.emptySet(); + } + return webSession.getAttributes().entrySet(); + } + + } + + + + + private static final class RequestParametersMap extends NoOpMapImpl { + + private final ServerHttpRequest request; + + RequestParametersMap(final ServerHttpRequest request) { + super(); + this.request = request; + } + + + @Override + public int size() { + return this.request.getQueryParams().size(); + } + + @Override + public boolean isEmpty() { + return this.request.getQueryParams().isEmpty(); + } + + @Override + public boolean containsKey(final Object key) { + // Even if not completely correct to return 'true' for entries that might not exist, this is needed + // in order to avoid Spring's MapAccessor throwing an exception when trying to access an element + // that doesn't exist -- in the case of request parameters, session and servletContext attributes most + // developers would expect null to be returned in such case, and that's what this 'true' will cause. + return true; + } + + @Override + public boolean containsValue(final Object value) { + // It wouldn't be consistent to have an 'ad hoc' implementation of #containsKey() but a 100% correct + // implementation of #containsValue(), so we are leaving this as unsupported. + throw new UnsupportedOperationException("Map does not support #containsValue()"); + } + + @Override + public Object get(final Object key) { + final List parameterValues = this.request.getQueryParams().get(key != null? key.toString() : null); + if (parameterValues == null) { + return null; + } + return new RequestParameterValues(parameterValues); + } + + @Override + public Set keySet() { + return this.request.getQueryParams().keySet(); + } + + @Override + public Collection values() { + return (Collection) (Collection) this.request.getQueryParams().values(); + } + + @Override + public Set> entrySet() { + return (Set>) (Set) this.request.getQueryParams().entrySet(); + } + + } + + + + /* + * The variables map used for integration with Spring WebFlux's ServerWebExchange uses + * the attributes stored in this ServerWebExchange as a default, so that if a variable is not + * found in the maps managed by the extended EngineContext (which is where it would be if + * we had set it from the model or the template itself), the ServerWebExchange attributes will + * be queried. + */ + private static final class WebExchangeAttributesVariablesMap extends EngineContext { + + private final ServerWebExchange exchange; + + + WebExchangeAttributesVariablesMap( + final IEngineConfiguration configuration, + final TemplateData templateData, + final Map templateResolutionAttributes, + final ServerWebExchange exchange, + final Locale locale, + final Map variables) { + + super(configuration, templateData, templateResolutionAttributes, locale, variables); + this.exchange = exchange; + + } + + + @Override + public boolean containsVariable(final String name) { + if (super.containsVariable(name)) { + return true; + } + return this.exchange.getAttributes().containsKey(name); + } + + + @Override + public Object getVariable(final String key) { + final Object value = super.getVariable(key); + if (value != null) { + return value; + } + return this.exchange.getAttributes().get(key); + } + + + @Override + public Set getVariableNames() { + final Set variableNames = super.getVariableNames(); + variableNames.addAll(this.exchange.getAttributes().keySet()); + return variableNames; + } + + + @Override + public String getStringRepresentationByLevel() { + final StringBuilder strBuilder = new StringBuilder(super.getStringRepresentationByLevel()); + strBuilder.append("[[EXCHANGE: " + this.exchange.getAttributes() + "]]"); + return strBuilder.toString(); + } + + + + + @Override + public String toString() { + final StringBuilder strBuilder = new StringBuilder(super.toString()); + strBuilder.append("[[EXCHANGE: " + this.exchange.getAttributes() + "]]"); + return strBuilder.toString(); + } + + } + + + + + + private abstract static class NoOpMapImpl implements Map { + + protected NoOpMapImpl() { + super(); + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public boolean containsKey(final Object key) { + return false; + } + + @Override + public boolean containsValue(final Object value) { + return false; + } + + @Override + public Object get(final Object key) { + return null; + } + + @Override + public Object put(final String key, final Object value) { + throw new UnsupportedOperationException("Cannot add new entry: map is immutable"); + } + + @Override + public Object remove(final Object key) { + throw new UnsupportedOperationException("Cannot remove entry: map is immutable"); + } + + @Override + public void putAll(final Map m) { + throw new UnsupportedOperationException("Cannot add new entry: map is immutable"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Cannot clear: map is immutable"); + } + + @Override + public Set keySet() { + return Collections.emptySet(); + } + + @Override + public Collection values() { + return Collections.emptyList(); + } + + @Override + public Set> entrySet() { + return Collections.emptySet(); + } + + + static final class MapEntry implements Entry { + + private final String key; + private final Object value; + + MapEntry(final String key, final Object value) { + super(); + this.key = key; + this.value = value; + } + + @Override + public String getKey() { + return this.key; + } + + @Override + public Object getValue() { + return this.value; + } + + @Override + public Object setValue(final Object value) { + throw new UnsupportedOperationException("Cannot set value: map is immutable"); + } + + } + + + } + + + + private static final class RequestParameterValues extends AbstractList { + + private final List parameterValues; + + RequestParameterValues(final List parameterValues) { + this.parameterValues = parameterValues; + } + + @Override + public int size() { + return this.parameterValues.size(); + } + + @Override + public Object[] toArray() { + return this.parameterValues.toArray(); + } + + @Override + public T[] toArray(final T[] arr) { + return this.parameterValues.toArray(arr); + } + + @Override + public String get(final int index) { + return this.parameterValues.get(index); + } + + @Override + public int indexOf(final Object obj) { + return this.parameterValues.indexOf(obj); + } + + @Override + public boolean contains(final Object obj) { + return this.parameterValues.contains(obj); + } + + + @Override + public String toString() { + // This toString() method will be responsible of outputting non-indexed request parameters in the + // way most people expect, i.e. return parameterValues[0] when accessed without index and parameter is + // single-valued (${param.a}), returning ArrayList#toString() when accessed without index and parameter + // is multi-valued, and finally return the specific value when accessed with index (${param.a[0]}) + final int size = this.parameterValues.size(); + if (size == 0) { + return ""; + } + if (size == 1) { + return this.parameterValues.get(0); + } + return this.parameterValues.toString(); + } + } + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxEngineContextFactory.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxEngineContextFactory.java new file mode 100644 index 00000000..e6741c20 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxEngineContextFactory.java @@ -0,0 +1,105 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context.webflux; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.thymeleaf.IEngineConfiguration; +import org.thymeleaf.context.EngineContext; +import org.thymeleaf.context.IContext; +import org.thymeleaf.context.IEngineContext; +import org.thymeleaf.context.IEngineContextFactory; +import org.thymeleaf.engine.TemplateData; +import org.thymeleaf.spring5.SpringWebFluxTemplateEngine; +import org.thymeleaf.util.Validate; + +/** + *

+ * Spring WebFlux-based implementation of the {@link IEngineContextFactory} interface. + *

+ *

+ * This factory will examine the {@code context} being passed as a parameter and, depending on whether + * this context object implements the {@link ISpringWebFluxContext} interface or not (i.e. whether support + * for Spring WebFlux should be enabled or not), return a {@link SpringWebFluxEngineContext} or + * a simple {@link EngineContext} instance as a result. + *

+ *

+ * This is the default factory implementation used by {@link SpringWebFluxTemplateEngine}. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public class SpringWebFluxEngineContextFactory implements IEngineContextFactory { + + + + + public SpringWebFluxEngineContextFactory() { + super(); + } + + + + + public IEngineContext createEngineContext( + final IEngineConfiguration configuration, final TemplateData templateData, + final Map templateResolutionAttributes, final IContext context) { + + Validate.notNull(context, "Context object cannot be null"); + + final Set variableNames = context.getVariableNames(); + + if (variableNames == null || variableNames.isEmpty()) { + if (context instanceof ISpringWebFluxContext) { + final ISpringWebFluxContext srContext = (ISpringWebFluxContext)context; + return new SpringWebFluxEngineContext( + configuration, templateData, templateResolutionAttributes, + srContext.getExchange(), srContext.getLocale(), Collections.EMPTY_MAP); + } + return new EngineContext( + configuration, templateData, templateResolutionAttributes, + context.getLocale(), Collections.EMPTY_MAP); + } + + final Map variables = new LinkedHashMap<>(variableNames.size() + 1, 1.0f); + for (final String variableName : variableNames) { + variables.put(variableName, context.getVariable(variableName)); + } + if (context instanceof ISpringWebFluxContext) { + final ISpringWebFluxContext srContext = (ISpringWebFluxContext)context; + return new SpringWebFluxEngineContext( + configuration, templateData, templateResolutionAttributes, + srContext.getExchange(), srContext.getLocale(), variables); + } + + return new EngineContext( + configuration, templateData, templateResolutionAttributes, + context.getLocale(), variables); + + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxExpressionContext.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxExpressionContext.java new file mode 100644 index 00000000..f85afa6e --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxExpressionContext.java @@ -0,0 +1,95 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context.webflux; + +import java.util.Locale; +import java.util.Map; + +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.web.server.ServerWebExchange; +import org.thymeleaf.IEngineConfiguration; +import org.thymeleaf.context.IExpressionContext; +import org.thymeleaf.expression.ExpressionObjects; +import org.thymeleaf.expression.IExpressionObjects; + +/** + *

+ * Basic Spring WebFlux-oriented implementation of the {@link IExpressionContext} and + * {@link ISpringWebFluxContext} interfaces. + *

+ *

+ * This class is not thread-safe, and should not be shared across executions of templates. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public class SpringWebFluxExpressionContext extends SpringWebFluxContext implements IExpressionContext { + + private final IEngineConfiguration configuration; + private IExpressionObjects expressionObjects = null; + + + public SpringWebFluxExpressionContext( + final IEngineConfiguration configuration, final ServerWebExchange exchange) { + this(configuration, exchange, null, null, null); + } + + public SpringWebFluxExpressionContext( + final IEngineConfiguration configuration, final ServerWebExchange exchange, final Locale locale) { + this(configuration, exchange, null, locale, null); + } + + public SpringWebFluxExpressionContext( + final IEngineConfiguration configuration, + final ServerWebExchange exchange, + final Locale locale, final Map variables) { + this(configuration, exchange, null, locale, variables); + } + + public SpringWebFluxExpressionContext( + final IEngineConfiguration configuration, + final ServerWebExchange exchange, + final ReactiveAdapterRegistry reactiveAdapterRegistry, + final Locale locale, final Map variables) { + super(exchange, reactiveAdapterRegistry, locale, variables); + this.configuration = configuration; + } + + + + @Override + public IEngineConfiguration getConfiguration() { + return this.configuration; + } + + + @Override + public IExpressionObjects getExpressionObjects() { + // We delay creation of expression objects in case they are not needed at all + if (this.expressionObjects == null) { + this.expressionObjects = new ExpressionObjects(this, this.configuration.getExpressionObjectFactory()); + } + return this.expressionObjects; + } + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxThymeleafBindStatus.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxThymeleafBindStatus.java new file mode 100644 index 00000000..5fbbe4b4 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxThymeleafBindStatus.java @@ -0,0 +1,138 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context.webflux; + +import java.beans.PropertyEditor; + +import org.springframework.validation.Errors; +import org.springframework.web.reactive.result.view.BindStatus; +import org.thymeleaf.spring5.context.IThymeleafBindStatus; +import org.thymeleaf.util.Validate; + +/** + *

+ * Implementation of the {@link IThymeleafBindStatus} interface, meant to wrap a Spring + * {@link BindStatus} object. + *

+ * + * @see BindStatus + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +class SpringWebFluxThymeleafBindStatus implements IThymeleafBindStatus { + + private final BindStatus bindStatus; + + + SpringWebFluxThymeleafBindStatus(final BindStatus bindStatus) { + super(); + Validate.notNull(bindStatus, "BindStatus cannot be null"); + this.bindStatus = bindStatus; + } + + + @Override + public String getPath() { + return this.bindStatus.getPath(); + } + + @Override + public String getExpression() { + return this.bindStatus.getExpression(); + } + + @Override + public Object getValue() { + return this.bindStatus.getValue(); + } + + @Override + public Class getValueType() { + return this.bindStatus.getValueType(); + } + + @Override + public Object getActualValue() { + return this.bindStatus.getActualValue(); + } + + @Override + public String getDisplayValue() { + return this.bindStatus.getDisplayValue(); + } + + @Override + public boolean isError() { + return this.bindStatus.isError(); + } + + @Override + public String[] getErrorCodes() { + return this.bindStatus.getErrorCodes(); + } + + @Override + public String getErrorCode() { + return this.bindStatus.getErrorCode(); + } + + @Override + public String[] getErrorMessages() { + return this.bindStatus.getErrorMessages(); + } + + @Override + public String getErrorMessage() { + return this.bindStatus.getErrorMessage(); + } + + @Override + public String getErrorMessagesAsString(final String delimiter) { + return this.bindStatus.getErrorMessagesAsString(delimiter); + } + + @Override + public Errors getErrors() { + return this.bindStatus.getErrors(); + } + + @Override + public PropertyEditor getEditor() { + return this.bindStatus.getEditor(); + } + + @Override + public PropertyEditor findEditor(final Class valueClass) { + return this.bindStatus.findEditor(valueClass); + } + + + + + @Override + public String toString() { + return this.bindStatus.toString(); + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxThymeleafRequestContext.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxThymeleafRequestContext.java new file mode 100644 index 00000000..6420d900 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxThymeleafRequestContext.java @@ -0,0 +1,239 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context.webflux; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.TimeZone; + +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.ui.context.Theme; +import org.springframework.validation.Errors; +import org.springframework.web.reactive.result.view.RequestContext; +import org.springframework.web.server.ServerWebExchange; +import org.thymeleaf.exceptions.TemplateProcessingException; +import org.thymeleaf.spring5.context.IThymeleafBindStatus; +import org.thymeleaf.spring5.context.IThymeleafRequestContext; +import org.thymeleaf.spring5.context.IThymeleafRequestDataValueProcessor; +import org.thymeleaf.util.Validate; + +/** + *

+ * Implementation of the {@link IThymeleafRequestContext} interface, meant to wrap a Spring + * {@link RequestContext} object. + *

+ * + * @see RequestContext + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public class SpringWebFluxThymeleafRequestContext implements IThymeleafRequestContext { + + private final RequestContext requestContext; + private final ServerWebExchange serverWebExchange; + private final SpringWebFluxThymeleafRequestDataValueProcessor thymeleafRequestDataValueProcessor; + + + public SpringWebFluxThymeleafRequestContext( + final RequestContext requestContext, final ServerWebExchange serverWebExchange) { + super(); + Validate.notNull(requestContext, "Spring WebFlux RequestContext cannot be null"); + Validate.notNull(serverWebExchange, "Server Web Exchange cannot be null"); + this.requestContext = requestContext; + this.serverWebExchange = serverWebExchange; + this.thymeleafRequestDataValueProcessor = + new SpringWebFluxThymeleafRequestDataValueProcessor( + this.requestContext.getRequestDataValueProcessor(), this.serverWebExchange); + } + + + public ServerWebExchange getServerWebExchange() { + return this.serverWebExchange; + } + + + @Override + public MessageSource getMessageSource() { + return this.requestContext.getMessageSource(); + } + + @Override + public Map getModel() { + return this.requestContext.getModel(); + } + + @Override + public Locale getLocale() { + return this.requestContext.getLocale(); + } + + @Override + public TimeZone getTimeZone() { + return this.requestContext.getTimeZone(); + } + + @Override + public void changeLocale(final Locale locale) { + this.requestContext.changeLocale(locale); + } + + @Override + public void changeLocale(final Locale locale, final TimeZone timeZone) { + this.requestContext.changeLocale(locale, timeZone); + } + + @Override + public void setDefaultHtmlEscape(final boolean defaultHtmlEscape) { + this.requestContext.setDefaultHtmlEscape(defaultHtmlEscape); + } + + @Override + public boolean isDefaultHtmlEscape() { + return this.requestContext.isDefaultHtmlEscape(); + } + + @Override + public Boolean getDefaultHtmlEscape() { + return this.requestContext.getDefaultHtmlEscape(); + } + + @Override + public String getContextPath() { + return this.requestContext.getContextPath(); + } + + @Override + public String getContextUrl(final String relativeUrl) { + return this.requestContext.getContextUrl(relativeUrl); + } + + @Override + public String getContextUrl(final String relativeUrl, final Map params) { + return this.requestContext.getContextUrl(relativeUrl, params); + } + + @Override + public String getRequestPath() { + return this.requestContext.getRequestPath(); + } + + @Override + public String getQueryString() { + return this.requestContext.getQueryString(); + } + + @Override + public String getMessage(final String code, final String defaultMessage) { + return this.requestContext.getMessage(code, defaultMessage); + } + + @Override + public String getMessage(final String code, final Object[] args, final String defaultMessage) { + return this.requestContext.getMessage(code, args, defaultMessage); + } + + @Override + public String getMessage(final String code, final List args, final String defaultMessage) { + return this.requestContext.getMessage(code, args, defaultMessage); + } + + @Override + public String getMessage(final String code, final Object[] args, final String defaultMessage, final boolean htmlEscape) { + return this.requestContext.getMessage(code, args, defaultMessage, htmlEscape); + } + + @Override + public String getMessage(final String code) throws NoSuchMessageException { + return this.requestContext.getMessage(code); + } + + @Override + public String getMessage(final String code, final Object[] args) throws NoSuchMessageException { + return this.requestContext.getMessage(code, args); + } + + @Override + public String getMessage(final String code, final List args) throws NoSuchMessageException { + return this.requestContext.getMessage(code, args); + } + + @Override + public String getMessage(final String code, final Object[] args, final boolean htmlEscape) throws NoSuchMessageException { + return this.requestContext.getMessage(code, args, htmlEscape); + } + + @Override + public String getMessage(final MessageSourceResolvable resolvable) throws NoSuchMessageException { + return this.requestContext.getMessage(resolvable); + } + + @Override + public String getMessage(final MessageSourceResolvable resolvable, final boolean htmlEscape) throws NoSuchMessageException { + return this.requestContext.getMessage(resolvable, htmlEscape); + } + + @Override + public Optional getErrors(final String name) { + return Optional.ofNullable(this.requestContext.getErrors(name)); + } + + @Override + public Optional getErrors(final String name, final boolean htmlEscape) { + return Optional.ofNullable(this.requestContext.getErrors(name, htmlEscape)); + } + + @Override + public Theme getTheme() { + throw new TemplateProcessingException("Themes are not supported in Thymeleaf's Spring WebFlux integration"); + } + + + @Override + public IThymeleafRequestDataValueProcessor getRequestDataValueProcessor() { + return this.thymeleafRequestDataValueProcessor; + } + + @Override + public IThymeleafBindStatus getBindStatus(final String path) throws IllegalStateException { + return Optional.ofNullable(this.requestContext.getBindStatus(path)).map(SpringWebFluxThymeleafBindStatus::new).orElse(null); + } + + @Override + public IThymeleafBindStatus getBindStatus(final String path, final boolean htmlEscape) throws IllegalStateException { + return Optional.ofNullable(this.requestContext.getBindStatus(path, htmlEscape)).map(SpringWebFluxThymeleafBindStatus::new).orElse(null); + } + + + + + @Override + public String toString() { + return this.requestContext.toString(); + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxThymeleafRequestDataValueProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxThymeleafRequestDataValueProcessor.java new file mode 100644 index 00000000..46902965 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webflux/SpringWebFluxThymeleafRequestDataValueProcessor.java @@ -0,0 +1,89 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context.webflux; + +import java.util.Map; + +import org.springframework.web.reactive.result.view.RequestDataValueProcessor; +import org.springframework.web.server.ServerWebExchange; +import org.thymeleaf.spring5.context.IThymeleafRequestDataValueProcessor; + +/** + *

+ * Implementation of the {@link IThymeleafRequestDataValueProcessor} interface, meant to wrap a Spring + * {@link RequestDataValueProcessor} object. + *

+ * + * @see RequestDataValueProcessor + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +class SpringWebFluxThymeleafRequestDataValueProcessor implements IThymeleafRequestDataValueProcessor { + + private final RequestDataValueProcessor requestDataValueProcessor; + private final ServerWebExchange exchange; + + SpringWebFluxThymeleafRequestDataValueProcessor( + final RequestDataValueProcessor requestDataValueProcessor, final ServerWebExchange exchange) { + super(); + this.requestDataValueProcessor = requestDataValueProcessor; + this.exchange = exchange; + } + + @Override + public String processAction(final String action, final String httpMethod) { + if (this.requestDataValueProcessor == null) { + // The presence of a Request Data Value Processor is optional + return action; + } + return this.requestDataValueProcessor.processAction(this.exchange, action, httpMethod); + } + + @Override + public String processFormFieldValue(final String name, final String value, final String type) { + if (this.requestDataValueProcessor == null) { + // The presence of a Request Data Value Processor is optional + return value; + } + return this.requestDataValueProcessor.processFormFieldValue(this.exchange, name, value, type); + } + + @Override + public Map getExtraHiddenFields() { + if (this.requestDataValueProcessor == null) { + // The presence of a Request Data Value Processor is optional + return null; + } + return this.requestDataValueProcessor.getExtraHiddenFields(this.exchange); + } + + @Override + public String processUrl(final String url) { + if (this.requestDataValueProcessor == null) { + // The presence of a Request Data Value Processor is optional + return url; + } + return this.requestDataValueProcessor.processUrl(this.exchange, url); + } + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webmvc/SpringWebMvcThymeleafBindStatus.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webmvc/SpringWebMvcThymeleafBindStatus.java new file mode 100644 index 00000000..065f700e --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webmvc/SpringWebMvcThymeleafBindStatus.java @@ -0,0 +1,137 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context.webmvc; + +import java.beans.PropertyEditor; + +import org.springframework.validation.Errors; +import org.springframework.web.servlet.support.BindStatus; +import org.thymeleaf.spring5.context.IThymeleafBindStatus; +import org.thymeleaf.util.Validate; + +/** + *

+ * Implementation of the {@link IThymeleafBindStatus} interface, meant to wrap a Spring + * {@link BindStatus} object. + *

+ * + * @see BindStatus + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +class SpringWebMvcThymeleafBindStatus implements IThymeleafBindStatus { + + private final BindStatus bindStatus; + + + SpringWebMvcThymeleafBindStatus(final BindStatus bindStatus) { + super(); + Validate.notNull(bindStatus, "BindStatus cannot be null"); + this.bindStatus = bindStatus; + } + + + @Override + public String getPath() { + return this.bindStatus.getPath(); + } + + @Override + public String getExpression() { + return this.bindStatus.getExpression(); + } + + @Override + public Object getValue() { + return this.bindStatus.getValue(); + } + + @Override + public Class getValueType() { + return this.bindStatus.getValueType(); + } + + @Override + public Object getActualValue() { + return this.bindStatus.getActualValue(); + } + + @Override + public String getDisplayValue() { + return this.bindStatus.getDisplayValue(); + } + + @Override + public boolean isError() { + return this.bindStatus.isError(); + } + + @Override + public String[] getErrorCodes() { + return this.bindStatus.getErrorCodes(); + } + + @Override + public String getErrorCode() { + return this.bindStatus.getErrorCode(); + } + + @Override + public String[] getErrorMessages() { + return this.bindStatus.getErrorMessages(); + } + + @Override + public String getErrorMessage() { + return this.bindStatus.getErrorMessage(); + } + + @Override + public String getErrorMessagesAsString(final String delimiter) { + return this.bindStatus.getErrorMessagesAsString(delimiter); + } + + @Override + public Errors getErrors() { + return this.bindStatus.getErrors(); + } + + @Override + public PropertyEditor getEditor() { + return this.bindStatus.getEditor(); + } + + @Override + public PropertyEditor findEditor(final Class valueClass) { + return this.bindStatus.findEditor(valueClass); + } + + + + + @Override + public String toString() { + return this.bindStatus.toString(); + } + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webmvc/SpringWebMvcThymeleafRequestContext.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webmvc/SpringWebMvcThymeleafRequestContext.java new file mode 100644 index 00000000..f7f721a0 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webmvc/SpringWebMvcThymeleafRequestContext.java @@ -0,0 +1,239 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context.webmvc; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.TimeZone; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.ui.context.Theme; +import org.springframework.validation.Errors; +import org.springframework.web.servlet.support.RequestContext; +import org.thymeleaf.spring5.context.IThymeleafBindStatus; +import org.thymeleaf.spring5.context.IThymeleafRequestContext; +import org.thymeleaf.spring5.context.IThymeleafRequestDataValueProcessor; +import org.thymeleaf.util.Validate; + +/** + *

+ * Implementation of the {@link IThymeleafRequestContext} interface, meant to wrap a Spring + * {@link RequestContext} object. + *

+ * + * @see RequestContext + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public class SpringWebMvcThymeleafRequestContext implements IThymeleafRequestContext { + + private final RequestContext requestContext; + private final HttpServletRequest httpServletRequest; + private final SpringWebMvcThymeleafRequestDataValueProcessor thymeleafRequestDataValueProcessor; + + + public SpringWebMvcThymeleafRequestContext( + final RequestContext requestContext, final HttpServletRequest httpServletRequest) { + super(); + Validate.notNull(requestContext, "Spring WebMVC RequestContext cannot be null"); + Validate.notNull(httpServletRequest, "HttpServletRequest cannot be null"); + this.requestContext = requestContext; + this.httpServletRequest = httpServletRequest; + this.thymeleafRequestDataValueProcessor = + new SpringWebMvcThymeleafRequestDataValueProcessor( + this.requestContext.getRequestDataValueProcessor(), httpServletRequest); + } + + + public HttpServletRequest getHttpServletRequest() { + return this.httpServletRequest; + } + + + @Override + public MessageSource getMessageSource() { + return this.requestContext.getMessageSource(); + } + + @Override + public Map getModel() { + return this.requestContext.getModel(); + } + + @Override + public Locale getLocale() { + return this.requestContext.getLocale(); + } + + @Override + public TimeZone getTimeZone() { + return this.requestContext.getTimeZone(); + } + + @Override + public void changeLocale(final Locale locale) { + this.requestContext.changeLocale(locale); + } + + @Override + public void changeLocale(final Locale locale, final TimeZone timeZone) { + this.requestContext.changeLocale(locale, timeZone); + } + + @Override + public void setDefaultHtmlEscape(final boolean defaultHtmlEscape) { + this.requestContext.setDefaultHtmlEscape(defaultHtmlEscape); + } + + @Override + public boolean isDefaultHtmlEscape() { + return this.requestContext.isDefaultHtmlEscape(); + } + + @Override + public Boolean getDefaultHtmlEscape() { + return this.requestContext.getDefaultHtmlEscape(); + } + + @Override + public String getContextPath() { + return this.requestContext.getContextPath(); + } + + @Override + public String getContextUrl(final String relativeUrl) { + return this.requestContext.getContextUrl(relativeUrl); + } + + @Override + public String getContextUrl(final String relativeUrl, final Map params) { + return this.requestContext.getContextUrl(relativeUrl, params); + } + + @Override + public String getRequestPath() { + return this.requestContext.getRequestUri(); + } + + @Override + public String getQueryString() { + return this.requestContext.getQueryString(); + } + + @Override + public String getMessage(final String code, final String defaultMessage) { + return this.requestContext.getMessage(code, defaultMessage); + } + + @Override + public String getMessage(final String code, final Object[] args, final String defaultMessage) { + return this.requestContext.getMessage(code, args, defaultMessage); + } + + @Override + public String getMessage(final String code, final List args, final String defaultMessage) { + return this.requestContext.getMessage(code, args, defaultMessage); + } + + @Override + public String getMessage(final String code, final Object[] args, final String defaultMessage, final boolean htmlEscape) { + return this.requestContext.getMessage(code, args, defaultMessage, htmlEscape); + } + + @Override + public String getMessage(final String code) throws NoSuchMessageException { + return this.requestContext.getMessage(code); + } + + @Override + public String getMessage(final String code, final Object[] args) throws NoSuchMessageException { + return this.requestContext.getMessage(code, args); + } + + @Override + public String getMessage(final String code, final List args) throws NoSuchMessageException { + return this.requestContext.getMessage(code, args); + } + + @Override + public String getMessage(final String code, final Object[] args, final boolean htmlEscape) throws NoSuchMessageException { + return this.requestContext.getMessage(code, args, htmlEscape); + } + + @Override + public String getMessage(final MessageSourceResolvable resolvable) throws NoSuchMessageException { + return this.requestContext.getMessage(resolvable); + } + + @Override + public String getMessage(final MessageSourceResolvable resolvable, final boolean htmlEscape) throws NoSuchMessageException { + return this.requestContext.getMessage(resolvable, htmlEscape); + } + + @Override + public Optional getErrors(final String name) { + return Optional.ofNullable(this.requestContext.getErrors(name)); + } + + @Override + public Optional getErrors(final String name, final boolean htmlEscape) { + return Optional.ofNullable(this.requestContext.getErrors(name, htmlEscape)); + } + + @Override + public Theme getTheme() { + return this.requestContext.getTheme(); + } + + + @Override + public IThymeleafRequestDataValueProcessor getRequestDataValueProcessor() { + return this.thymeleafRequestDataValueProcessor; + } + + @Override + public IThymeleafBindStatus getBindStatus(final String path) throws IllegalStateException { + return Optional.ofNullable(this.requestContext.getBindStatus(path)).map(SpringWebMvcThymeleafBindStatus::new).orElse(null); + } + + @Override + public IThymeleafBindStatus getBindStatus(final String path, final boolean htmlEscape) throws IllegalStateException { + return Optional.ofNullable(this.requestContext.getBindStatus(path, htmlEscape)).map(SpringWebMvcThymeleafBindStatus::new).orElse(null); + } + + + + + @Override + public String toString() { + return this.requestContext.toString(); + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webmvc/SpringWebMvcThymeleafRequestDataValueProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webmvc/SpringWebMvcThymeleafRequestDataValueProcessor.java new file mode 100644 index 00000000..d0a80b24 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/context/webmvc/SpringWebMvcThymeleafRequestDataValueProcessor.java @@ -0,0 +1,92 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.context.webmvc; + +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.web.servlet.support.RequestDataValueProcessor; +import org.thymeleaf.spring5.context.IThymeleafRequestDataValueProcessor; + +/** + *

+ * Implementation of the {@link IThymeleafRequestDataValueProcessor} interface, meant to wrap a Spring + * {@link RequestDataValueProcessor} object. + *

+ * + * @see RequestDataValueProcessor + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +class SpringWebMvcThymeleafRequestDataValueProcessor implements IThymeleafRequestDataValueProcessor { + + + private final RequestDataValueProcessor requestDataValueProcessor; + private final HttpServletRequest httpServletRequest; + + + SpringWebMvcThymeleafRequestDataValueProcessor( + final RequestDataValueProcessor requestDataValueProcessor, final HttpServletRequest httpServletRequest) { + super(); + this.requestDataValueProcessor = requestDataValueProcessor; + this.httpServletRequest = httpServletRequest; + } + + @Override + public String processAction(final String action, final String httpMethod) { + if (this.requestDataValueProcessor == null) { + // The presence of a Request Data Value Processor is optional + return action; + } + return this.requestDataValueProcessor.processAction(this.httpServletRequest, action, httpMethod); + } + + @Override + public String processFormFieldValue(final String name, final String value, final String type) { + if (this.requestDataValueProcessor == null) { + // The presence of a Request Data Value Processor is optional + return value; + } + return this.requestDataValueProcessor.processFormFieldValue(this.httpServletRequest, name, value, type); + } + + @Override + public Map getExtraHiddenFields() { + if (this.requestDataValueProcessor == null) { + // The presence of a Request Data Value Processor is optional + return null; + } + return this.requestDataValueProcessor.getExtraHiddenFields(this.httpServletRequest); + } + + @Override + public String processUrl(final String url) { + if (this.requestDataValueProcessor == null) { + // The presence of a Request Data Value Processor is optional + return url; + } + return this.requestDataValueProcessor.processUrl(this.httpServletRequest, url); + } + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/dialect/SpringStandardDialect.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/dialect/SpringStandardDialect.java new file mode 100755 index 00000000..c16bfdab --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/dialect/SpringStandardDialect.java @@ -0,0 +1,424 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.dialect; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import org.springframework.web.server.ServerWebExchange; +import org.thymeleaf.expression.IExpressionObjectFactory; +import org.thymeleaf.processor.IProcessor; +import org.thymeleaf.spring5.SpringTemplateEngine; +import org.thymeleaf.spring5.context.SpringContextUtils; +import org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator; +import org.thymeleaf.spring5.expression.SpringStandardConversionService; +import org.thymeleaf.spring5.expression.SpringStandardExpressionObjectFactory; +import org.thymeleaf.spring5.expression.SpringStandardExpressions; +import org.thymeleaf.spring5.processor.SpringActionTagProcessor; +import org.thymeleaf.spring5.processor.SpringErrorClassTagProcessor; +import org.thymeleaf.spring5.processor.SpringErrorsTagProcessor; +import org.thymeleaf.spring5.processor.SpringHrefTagProcessor; +import org.thymeleaf.spring5.processor.SpringInputCheckboxFieldTagProcessor; +import org.thymeleaf.spring5.processor.SpringInputFileFieldTagProcessor; +import org.thymeleaf.spring5.processor.SpringInputGeneralFieldTagProcessor; +import org.thymeleaf.spring5.processor.SpringInputPasswordFieldTagProcessor; +import org.thymeleaf.spring5.processor.SpringInputRadioFieldTagProcessor; +import org.thymeleaf.spring5.processor.SpringMethodTagProcessor; +import org.thymeleaf.spring5.processor.SpringObjectTagProcessor; +import org.thymeleaf.spring5.processor.SpringOptionFieldTagProcessor; +import org.thymeleaf.spring5.processor.SpringOptionInSelectFieldTagProcessor; +import org.thymeleaf.spring5.processor.SpringSelectFieldTagProcessor; +import org.thymeleaf.spring5.processor.SpringSrcTagProcessor; +import org.thymeleaf.spring5.processor.SpringTextareaFieldTagProcessor; +import org.thymeleaf.spring5.processor.SpringUErrorsTagProcessor; +import org.thymeleaf.spring5.processor.SpringValueTagProcessor; +import org.thymeleaf.spring5.util.SpringVersionUtils; +import org.thymeleaf.standard.StandardDialect; +import org.thymeleaf.standard.expression.IStandardConversionService; +import org.thymeleaf.standard.expression.IStandardVariableExpressionEvaluator; +import org.thymeleaf.standard.processor.StandardActionTagProcessor; +import org.thymeleaf.standard.processor.StandardHrefTagProcessor; +import org.thymeleaf.standard.processor.StandardMethodTagProcessor; +import org.thymeleaf.standard.processor.StandardObjectTagProcessor; +import org.thymeleaf.standard.processor.StandardSrcTagProcessor; +import org.thymeleaf.standard.processor.StandardValueTagProcessor; +import org.thymeleaf.templatemode.TemplateMode; + +/** + *

+ * SpringStandard Dialect. This is the class containing the implementation of Thymeleaf Standard Dialect, including all + * {@code th:*} processors, expression objects, etc. for Spring-enabled environments. + *

+ *

+ * This dialect is valid both for Spring WebMVC and Spring WebFlux environments. + *

+ *

+ * Note this dialect uses SpringEL as an expression language and adds some Spring-specific + * features on top of {@link StandardDialect}, like {@code th:field} or Spring-related expression objects. + *

+ *

+ * The usual and recommended way of using this dialect is by instancing {@link SpringTemplateEngine} + * instead of {@link org.thymeleaf.TemplateEngine}. The former will automatically add this dialect and perform + * some specific configuration like e.g. Spring-integrated message resolution. + *

+ *

+ * Note a class with this name existed since 1.0, but it was completely reimplemented + * in Thymeleaf 3.0 + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public class SpringStandardDialect extends StandardDialect { + + public static final String NAME = "SpringStandard"; + public static final String PREFIX = "th"; + public static final int PROCESSOR_PRECEDENCE = 1000; + + public static final boolean DEFAULT_ENABLE_SPRING_EL_COMPILER = false; + public static final boolean DEFAULT_RENDER_HIDDEN_MARKERS_BEFORE_CHECKBOXES = false; + + private boolean enableSpringELCompiler = DEFAULT_ENABLE_SPRING_EL_COMPILER; + private boolean renderHiddenMarkersBeforeCheckboxes = DEFAULT_RENDER_HIDDEN_MARKERS_BEFORE_CHECKBOXES; + + private static final Map REACTIVE_MODEL_ADDITIONS_EXECUTION_ATTRIBUTES; + + // This execution attribute will force the asynchronous resolution of the WebSession (not the real creation of a + // persisted session) before view execution. This will avoid the need to block in order to obtain the WebSession + // from the ServerWebExchange during template execution. + // NOTE here we are not using the constant from the ReactiveThymeleafView class (instead we replicate its same + // value "ThymeleafReactiveModelAdditions:" so that we don't force the initialisation of that class in + // non-WebFlux environments. + private static final String WEB_SESSION_EXECUTION_ATTRIBUTE_NAME = + "ThymeleafReactiveModelAdditions:" + SpringContextUtils.WEB_SESSION_ATTRIBUTE_NAME; + + + + + + static { + + /* + * If this is a WebFlux application, we will use a special mechanism in ThymeleafReactiveView that allows + * the configuration of an execution attribute with a name with a certain prefix and type + * Function>, so that such function is called during View preparation and + * its result (Publisher) is put into the model so that Spring WebFlux asynchronously resolves it (without + * blocking) before asking the View to actually render. + * + * This way, by means of this execution attribute we will set the Mono returned by + * ServerWebExchange into the model and have Spring resolve it non-blockingly into the WebSession object we + * could need during template execution. + * + * NOTE that, per the definition of WebSession in Spring WebFlux, even if this operation does mean the creation + * of a WebSession instance, it does not mean the creation of a real (persisted) user session, cookie emission, + * etc. unless anything is actually put afterwards into the session or it is explicitly started. + */ + + if (!SpringVersionUtils.isSpringWebFluxPresent()) { + + REACTIVE_MODEL_ADDITIONS_EXECUTION_ATTRIBUTES = Collections.emptyMap(); + + } else { + + // Returns Mono, but we will specify Object in order not to bind this class to Mono at compile time + final Function webSessionInitializer = (exchange) -> exchange.getSession(); + + REACTIVE_MODEL_ADDITIONS_EXECUTION_ATTRIBUTES = + Collections.singletonMap(WEB_SESSION_EXECUTION_ATTRIBUTE_NAME, webSessionInitializer); + + } + + } + + + public SpringStandardDialect() { + super(NAME, PREFIX, PROCESSOR_PRECEDENCE); + } + + + + + /** + *

+ * Returns whether the SpringEL compiler should be enabled in SpringEL expressions or not. + *

+ *

+ * Expression compilation can significantly improve the performance of Spring EL expressions, but + * might not be adequate for every environment. Read + * the + * official Spring documentation for more detail. + *

+ *

+ * Also note that although Spring includes a SpEL compiler since Spring 4.1, most expressions + * in Thymeleaf templates will only be able to properly benefit from this compilation step when at least + * Spring Framework version 4.2.4 is used. + *

+ *

+ * This flag is set to {@code false} by default. + *

+ * + * @return {@code true} if SpEL expressions should be compiled if possible, {@code false} if not. + */ + public boolean getEnableSpringELCompiler() { + return enableSpringELCompiler; + } + + + /** + *

+ * Sets whether the SpringEL compiler should be enabled in SpringEL expressions or not. + *

+ *

+ * Expression compilation can significantly improve the performance of Spring EL expressions, but + * might not be adequate for every environment. Read + * the + * official Spring documentation for more detail. + *

+ *

+ * Also note that although Spring includes a SpEL compiler since Spring 4.1, most expressions + * in Thymeleaf templates will only be able to properly benefit from this compilation step when at least + * Spring Framework version 4.2.4 is used. + *

+ *

+ * This flag is set to {@code false} by default. + *

+ * + * @param enableSpringELCompiler {@code true} if SpEL expressions should be compiled if possible, {@code false} if not. + */ + public void setEnableSpringELCompiler(final boolean enableSpringELCompiler) { + this.enableSpringELCompiler = enableSpringELCompiler; + } + + + + + /** + *

+ * Returns whether the {@code } marker tags rendered to signal the presence + * of checkboxes in forms when unchecked should be rendered before the checkbox tag itself, + * or after (default). + *

+ *

+ * A number of CSS frameworks and style guides assume that the {@code

+ *

+ * Note this hidden field is introduced in order to signal the existence of the field in the form being sent, + * even if the checkbox is unchecked (no URL parameter is added for unchecked check boxes). + *

+ *

+ * This flag is set to {@code false} by default. + *

+ * + * @return {@code true} if hidden markers should be rendered before the checkboxes, {@code false} if not. + * + * @since 3.0.10 + */ + public boolean getRenderHiddenMarkersBeforeCheckboxes() { + return renderHiddenMarkersBeforeCheckboxes; + } + + + /** + *

+ * Sets whether the {@code } marker tags rendered to signal the presence + * of checkboxes in forms when unchecked should be rendered before the checkbox tag itself, + * or after (default). + *

+ *

+ * A number of CSS frameworks and style guides assume that the {@code

+ *

+ * Note this hidden field is introduced in order to signal the existence of the field in the form being sent, + * even if the checkbox is unchecked (no URL parameter is added for unchecked check boxes). + *

+ *

+ * This flag is set to {@code false} by default. + *

+ * + * @param renderHiddenMarkersBeforeCheckboxes {@code true} if hidden markers should be rendered + * before the checkboxes, {@code false} if not. + * + * @since 3.0.10 + */ + public void setRenderHiddenMarkersBeforeCheckboxes(final boolean renderHiddenMarkersBeforeCheckboxes) { + this.renderHiddenMarkersBeforeCheckboxes = renderHiddenMarkersBeforeCheckboxes; + } + + + + + @Override + public IStandardVariableExpressionEvaluator getVariableExpressionEvaluator() { + return SPELVariableExpressionEvaluator.INSTANCE; + } + + @Override + public void setVariableExpressionEvaluator(final IStandardVariableExpressionEvaluator variableExpressionEvaluator) { + throw new UnsupportedOperationException( + "Variable Expression Evaluator cannot be modified in SpringStandardDialect"); + } + + + + @Override + public IStandardConversionService getConversionService() { + if (this.conversionService == null) { + this.conversionService = new SpringStandardConversionService(); + } + return this.conversionService; + } + + + @Override + public IExpressionObjectFactory getExpressionObjectFactory() { + if (this.expressionObjectFactory == null) { + this.expressionObjectFactory = new SpringStandardExpressionObjectFactory(); + } + return this.expressionObjectFactory; + } + + + + + @Override + public Set getProcessors(final String dialectPrefix) { + return createSpringStandardProcessorsSet(dialectPrefix, this.renderHiddenMarkersBeforeCheckboxes); + } + + + + @Override + public Map getExecutionAttributes() { + + final Map executionAttributes = super.getExecutionAttributes(); + executionAttributes.putAll(REACTIVE_MODEL_ADDITIONS_EXECUTION_ATTRIBUTES); + executionAttributes.put( + SpringStandardExpressions.ENABLE_SPRING_EL_COMPILER_ATTRIBUTE_NAME, Boolean.valueOf(getEnableSpringELCompiler())); + + return executionAttributes; + + } + + + + + + /** + *

+ * Create a the set of SpringStandard processors, all of them freshly instanced. + *

+ * + * @param dialectPrefix the prefix established for the Standard Dialect, needed for initialization + * @return the set of SpringStandard processors. + */ + public static Set createSpringStandardProcessorsSet(final String dialectPrefix) { + return createSpringStandardProcessorsSet(dialectPrefix, DEFAULT_RENDER_HIDDEN_MARKERS_BEFORE_CHECKBOXES); + } + + + /** + *

+ * Create a the set of SpringStandard processors, all of them freshly instanced. + *

+ * + * @param dialectPrefix the prefix established for the Standard Dialect, needed for initialization + * @param renderHiddenMarkersBeforeCheckboxes {@code true} if hidden markers should be rendered + * before the checkboxes, {@code false} if not. + * + * @return the set of SpringStandard processors. + * + * @since 3.0.10 + */ + public static Set createSpringStandardProcessorsSet( + final String dialectPrefix, final boolean renderHiddenMarkersBeforeCheckboxes) { + /* + * It is important that we create new instances here because, if there are + * several dialects in the TemplateEngine that extend StandardDialect, they should + * not be returning the exact same instances for their processors in order + * to allow specific instances to be directly linked with their owner dialect. + */ + + final Set standardProcessors = StandardDialect.createStandardProcessorsSet(dialectPrefix); + + final Set processors = new LinkedHashSet(40); + + + /* + * REMOVE STANDARD PROCESSORS THAT WE WILL REPLACE + */ + for (final IProcessor standardProcessor : standardProcessors) { + // There are several processors we need to remove from the Standard Dialect set + if (!(standardProcessor instanceof StandardObjectTagProcessor) && + !(standardProcessor instanceof StandardActionTagProcessor) && + !(standardProcessor instanceof StandardHrefTagProcessor) && + !(standardProcessor instanceof StandardMethodTagProcessor) && + !(standardProcessor instanceof StandardSrcTagProcessor) && + !(standardProcessor instanceof StandardValueTagProcessor)) { + + processors.add(standardProcessor); + + } else if (standardProcessor.getTemplateMode() != TemplateMode.HTML) { + // We only want to remove from the StandardDialect the HTML versions of the attribute processors + processors.add(standardProcessor); + } + } + + + /* + * ATTRIBUTE TAG PROCESSORS + */ + processors.add(new SpringActionTagProcessor(dialectPrefix)); + processors.add(new SpringHrefTagProcessor(dialectPrefix)); + processors.add(new SpringMethodTagProcessor(dialectPrefix)); + processors.add(new SpringSrcTagProcessor(dialectPrefix)); + processors.add(new SpringValueTagProcessor(dialectPrefix)); + processors.add(new SpringObjectTagProcessor(dialectPrefix)); + processors.add(new SpringErrorsTagProcessor(dialectPrefix)); + processors.add(new SpringUErrorsTagProcessor(dialectPrefix)); + processors.add(new SpringInputGeneralFieldTagProcessor(dialectPrefix)); + processors.add(new SpringInputPasswordFieldTagProcessor(dialectPrefix)); + processors.add(new SpringInputCheckboxFieldTagProcessor(dialectPrefix, renderHiddenMarkersBeforeCheckboxes)); + processors.add(new SpringInputRadioFieldTagProcessor(dialectPrefix)); + processors.add(new SpringInputFileFieldTagProcessor(dialectPrefix)); + processors.add(new SpringSelectFieldTagProcessor(dialectPrefix)); + processors.add(new SpringOptionInSelectFieldTagProcessor(dialectPrefix)); + processors.add(new SpringOptionFieldTagProcessor(dialectPrefix)); + processors.add(new SpringTextareaFieldTagProcessor(dialectPrefix)); + processors.add(new SpringErrorClassTagProcessor(dialectPrefix)); + + return processors; + + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/Fields.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/Fields.java new file mode 100755 index 00000000..185db884 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/Fields.java @@ -0,0 +1,112 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.expression; + +import java.util.List; + +import org.thymeleaf.context.IExpressionContext; +import org.thymeleaf.spring5.util.DetailedError; +import org.thymeleaf.spring5.util.FieldUtils; + + +/** + *

+ * Expression Object for performing form-field-related operations inside Thymeleaf Standard Expressions in Spring + * environments. + *

+ *

+ * Note a class with this name existed since 1.0, but it was completely reimplemented + * in Thymeleaf 3.0 + *

+ * + * @author Daniel Fernández + * @author Tobias Gafner + * + * @since 3.0.3 + * + */ +public final class Fields { + + private final IExpressionContext context; + + public boolean hasAnyErrors() { + return FieldUtils.hasAnyErrors(this.context); + } + + public boolean hasErrors() { + return FieldUtils.hasAnyErrors(this.context); + } + + public boolean hasErrors(final String field) { + return FieldUtils.hasErrors(this.context, field); + } + + public boolean hasGlobalErrors() { + return FieldUtils.hasGlobalErrors(this.context); + } + + public List allErrors() { + return FieldUtils.errors(this.context); + } + + public List errors() { + return FieldUtils.errors(this.context); + } + + public List errors(final String field) { + return FieldUtils.errors(this.context, field); + } + + public List globalErrors() { + return FieldUtils.globalErrors(this.context); + } + + + public String idFromName(final String fieldName) { + return FieldUtils.idFromName(fieldName); + } + + + + public List allDetailedErrors() { + return FieldUtils.detailedErrors(this.context); + } + + public List detailedErrors() { + return FieldUtils.detailedErrors(this.context); + } + + public List detailedErrors(final String field) { + return FieldUtils.detailedErrors(this.context, field); + } + + public List globalDetailedErrors() { + return FieldUtils.globalDetailedErrors(this.context); + } + + + + public Fields(final IExpressionContext context) { + super(); + this.context = context; + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/IThymeleafEvaluationContext.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/IThymeleafEvaluationContext.java new file mode 100755 index 00000000..a75fc174 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/IThymeleafEvaluationContext.java @@ -0,0 +1,45 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.expression; + +import org.springframework.expression.EvaluationContext; +import org.thymeleaf.expression.IExpressionObjects; + +/** + *

+ * Interface for Thymeleaf-specific implementations of {@link EvaluationContext}. + *

+ * + * @see ThymeleafEvaluationContext + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public interface IThymeleafEvaluationContext extends EvaluationContext { + + public boolean isVariableAccessRestricted(); + public void setVariableAccessRestricted(final boolean restricted); + + public IExpressionObjects getExpressionObjects(); + public void setExpressionObjects(final IExpressionObjects expressionObjects); + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/Mvc.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/Mvc.java new file mode 100644 index 00000000..43ece749 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/Mvc.java @@ -0,0 +1,155 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.expression; + +import org.thymeleaf.exceptions.ConfigurationException; +import org.thymeleaf.spring5.util.SpringVersionUtils; +import org.thymeleaf.util.ClassLoaderUtils; + +/** + *

+ * Expression object in charge of the creation of URLs using the controller-based mechanism in Spring MVC 4.1. + *

+ *

+ * This mimics the {@code s:mvcUrl} behaviour explained at + * http://docs.spring.io/spring/docs/4.1.0.RELEASE/spring-framework-reference/html/mvc.html#mvc-links-to-controllers-from-views + * using the same method (function) names in the Spring JSP tag library. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public class Mvc { + + private static final MvcUriComponentsBuilderDelegate mvcUriComponentsBuilderDelegate; + private static final String SPRING41_MVC_URI_COMPONENTS_BUILDER_DELEGATE_CLASS_NAME = Mvc.class.getName() + "$Spring41MvcUriComponentsBuilderDelegate"; + private static final String NON_SPRING41_MVC_URI_COMPONENTS_BUILDER_DELEGATE_CLASS_NAME = Mvc.class.getName() + "$NonSpring41MvcUriComponentsBuilderDelegate"; + + + static { + + final String delegateClassName = + (SpringVersionUtils.isSpring41AtLeast()? + SPRING41_MVC_URI_COMPONENTS_BUILDER_DELEGATE_CLASS_NAME : + NON_SPRING41_MVC_URI_COMPONENTS_BUILDER_DELEGATE_CLASS_NAME); + + try { + final Class implClass = ClassLoaderUtils.loadClass(delegateClassName); + mvcUriComponentsBuilderDelegate = (MvcUriComponentsBuilderDelegate) implClass.newInstance(); + } catch (final Exception e) { + throw new ExceptionInInitializerError( + new ConfigurationException( + "Thymeleaf could not initialize a delegate of class \"" + delegateClassName + "\" for taking " + + "care of the " + SpringStandardExpressionObjectFactory.MVC_EXPRESSION_OBJECT_NAME + " expression utility object", e)); + } + + } + + + public MethodArgumentBuilderWrapper url(final String mappingName) { + return mvcUriComponentsBuilderDelegate.fromMappingName(mappingName); + } + + + + static interface MvcUriComponentsBuilderDelegate { + + public MethodArgumentBuilderWrapper fromMappingName(String mappingName); + + } + + + static class Spring41MvcUriComponentsBuilderDelegate implements MvcUriComponentsBuilderDelegate { + + Spring41MvcUriComponentsBuilderDelegate() { + super(); + } + + public MethodArgumentBuilderWrapper fromMappingName(final String mappingName) { + return new Spring41MethodArgumentBuilderWrapper(org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.fromMappingName(mappingName)); + } + + } + + + static class NonSpring41MvcUriComponentsBuilderDelegate implements MvcUriComponentsBuilderDelegate { + + NonSpring41MvcUriComponentsBuilderDelegate() { + super(); + } + + public MethodArgumentBuilderWrapper fromMappingName(final String mappingName) { + throw new UnsupportedOperationException( + "MVC URI component building is only supported in Spring versions 4.1 or newer"); + } + + } + + + + public static interface MethodArgumentBuilderWrapper { + + public MethodArgumentBuilderWrapper arg(int index, Object value); + public MethodArgumentBuilderWrapper encode(); + public String build(); + public String buildAndExpand(Object... uriVariables); + + } + + + static class Spring41MethodArgumentBuilderWrapper implements MethodArgumentBuilderWrapper { + + private final org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.MethodArgumentBuilder builder; + + + private Spring41MethodArgumentBuilderWrapper( + final org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.MethodArgumentBuilder builder) { + super(); + this.builder = builder; + } + + public MethodArgumentBuilderWrapper arg(final int index, final Object value) { + return new Spring41MethodArgumentBuilderWrapper(this.builder.arg(index, value)); + } + + public MethodArgumentBuilderWrapper encode() { + if (!SpringVersionUtils.isSpringAtLeast(5,0,8)) { + throw new IllegalStateException(String.format( + "At least Spring version 5.0.8.RELEASE is needed for executing " + + "MvcUriComponentsBuilder#encode() but detected Spring version is %s.", + SpringVersionUtils.getSpringVersion())); + } + return new Spring41MethodArgumentBuilderWrapper(this.builder.encode()); + } + + public String build() { + return this.builder.build(); + } + + public String buildAndExpand(final Object... uriVariables) { + return this.builder.buildAndExpand(uriVariables); + } + + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/RequestDataValues.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/RequestDataValues.java new file mode 100644 index 00000000..a8bf9625 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/RequestDataValues.java @@ -0,0 +1,94 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.expression; + +import java.util.Map; + +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.spring5.requestdata.RequestDataValueProcessorUtils; + +/** + *

+ * Expression object that offers the functionality of Spring MVC's {@code RequestDataValueProcessor} for + * performing last-moment modifications to request data values such as URLs or form contents in order to + * enable mechanism such as CSRF protection. + *

+ *

+ * Methods in this class correspond to the methods in Spring MVC's + * {@code org.springframework.web.servlet.support.RequestDataValueProcessor}. + *

+ *

+ * Using this expression object is not needed in most scenarios, as its functionality will be automatically + * applied by {@code th:href}, {@code th:src}, {@code th:action}, {@code th:value}, {@code th:method} + * and {@code th:field}. But sometimes there is a need to manually apply these behaviours when e.g. creating + * and using URLs outside attributes such as {@code th:href} or {@code th:src}. + *

+ *

+ * An example of such scenario would be using an URL as a parameter in a message expression: + *

+ * + * msg.knowmore=Click <a href="{0}">here</a> if you want to know more. + * + *

+ * And then in template code: + *

+ * + * <p th:with="morelink=@{/detail/more}" + * th:utext="#{msg.knowmore(${#requestdatavalues.url(morelink)})}"> + * + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public class RequestDataValues { + + private ITemplateContext context; + + + public RequestDataValues(final ITemplateContext context) { + super(); + this.context = context; + } + + + + public String action(final String action, final String httpMethod) { + return RequestDataValueProcessorUtils.processAction(this.context, action, httpMethod); + } + + + public String url(final String url) { + return RequestDataValueProcessorUtils.processUrl(this.context, url); + } + + + public String formFieldValue(final String name, final String value, final String type) { + return RequestDataValueProcessorUtils.processFormFieldValue(this.context, name, value, type); + } + + + public Map extraHiddenFields() { + return RequestDataValueProcessorUtils.getExtraHiddenFields(this.context); + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SPELContextMapWrapper.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SPELContextMapWrapper.java new file mode 100644 index 00000000..4effccf3 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SPELContextMapWrapper.java @@ -0,0 +1,190 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.expression; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import org.thymeleaf.context.IContext; +import org.thymeleaf.exceptions.TemplateProcessingException; + +/** + *

+ * Wrapper on {@link IContext} objects that makes them look like {@code java.util.Map} objects + * in order to be used at the root of SpEL expressions without the need to use custom property accessors + * in most scenarios. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class SPELContextMapWrapper implements Map { + + private static final String REQUEST_PARAMETERS_RESTRICTED_VARIABLE_NAME = "param"; + + + private final IContext context; + private final IThymeleafEvaluationContext evaluationContext; + + + + SPELContextMapWrapper(final IContext context, final IThymeleafEvaluationContext evaluationContext) { + super(); + this.context = context; + this.evaluationContext = evaluationContext; + } + + + + + + + + + + public int size() { + throw new TemplateProcessingException( + "Cannot call #size() on an " + IContext.class.getSimpleName() + " implementation"); + } + + + + + public boolean isEmpty() { + throw new TemplateProcessingException( + "Cannot call #isEmpty() on an " + IContext.class.getSimpleName() + " implementation"); + } + + + + + public boolean containsKey(final Object key) { + if (this.evaluationContext.isVariableAccessRestricted()) { + if (REQUEST_PARAMETERS_RESTRICTED_VARIABLE_NAME.equals(key)) { + throw new TemplateProcessingException( + "Access to variable \"" + key + "\" is forbidden in this context. Note some restrictions apply to " + + "variable access. For example, direct access to request parameters is forbidden in preprocessing and " + + "unescaped expressions, in TEXT template mode, in fragment insertion specifications and " + + "in some specific attribute processors."); + } + } + // We will be NOT calling this.context.containsVariable(key) as it could be very inefficient in web + // environments (based on HttpServletRequest#getAttributeName()), so we will just consider that every possible + // element exists in an IContext, and simply return null for those not found + return this.context != null; + } + + + + + public boolean containsValue(final Object value) { + throw new TemplateProcessingException( + "Cannot call #containsValue(value) on an " + IContext.class.getSimpleName() + " implementation"); + } + + + + + public Object get(final Object key) { + + if (this.context == null) { + throw new TemplateProcessingException("Cannot read property on null target"); + } + + /* + * NOTE we do not check here whether we are being asked for the 'locale', 'request', 'response', etc. + * because there already are specific expression objects for the most important of them, which should + * be used instead: #locale, #httpServletRequest, #httpSession, etc. + * The context should just be used as a map, without exposure of its more-internal methods... + */ + + // 'execInfo' translation from context variable to expression object - deprecated and to be removed in 3.1 + if ("execInfo".equals(key)) { // Quick check to avoid deprecated method call + final Object execInfoResult = SPELContextPropertyAccessor.checkExecInfo(key.toString(), this.evaluationContext); + if (execInfoResult != null) { + return execInfoResult; + } + } + + return this.context.getVariable(key == null? null : key.toString()); + + } + + + + + public Object put(final Object key, final Object value) { + throw new TemplateProcessingException( + "Cannot call #put(key,value) on an " + IContext.class.getSimpleName() + " implementation"); + } + + + + + public Object remove(final Object key) { + throw new TemplateProcessingException( + "Cannot call #remove(key) on an " + IContext.class.getSimpleName() + " implementation"); + } + + + + + public void putAll(final Map m) { + throw new TemplateProcessingException( + "Cannot call #putAll(m) on an " + IContext.class.getSimpleName() + " implementation"); + } + + + + + public void clear() { + throw new TemplateProcessingException( + "Cannot call #clear() on an " + IContext.class.getSimpleName() + " implementation"); + } + + + + + public Set keySet() { + throw new TemplateProcessingException( + "Cannot call #keySet() on an " + IContext.class.getSimpleName() + " implementation"); + } + + + + + public Collection values() { + throw new TemplateProcessingException( + "Cannot call #values() on an " + IContext.class.getSimpleName() + " implementation"); + } + + + + + public Set entrySet() { + throw new TemplateProcessingException( + "Cannot call #entrySet() on an " + IContext.class.getSimpleName() + " implementation"); + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SPELContextPropertyAccessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SPELContextPropertyAccessor.java new file mode 100644 index 00000000..d5dffed8 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SPELContextPropertyAccessor.java @@ -0,0 +1,188 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.expression; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.IContext; +import org.thymeleaf.exceptions.TemplateProcessingException; + +/** + *

+ * Property accessor used for allowing Spring EL expression evaluators + * treat {@link IContext} objects correctly (map keys will be accessible + * as object properties). + *

+ *

+ * Note that, even if {@link IContext} objects used as expression roots will be accessible as + * {@code java.util.Map}s thanks to {@link SPELContextMapWrapper}, this property accessor + * class is still needed in order to access nested context info like the {@code session} or + * {@code param} maps in web contexts. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class SPELContextPropertyAccessor implements PropertyAccessor { + + private static final Logger LOGGER = LoggerFactory.getLogger(SPELContextPropertyAccessor.class); + + static final SPELContextPropertyAccessor INSTANCE = new SPELContextPropertyAccessor(); + + private static final String REQUEST_PARAMETERS_RESTRICTED_VARIABLE_NAME = "param"; + private static final Class[] TARGET_CLASSES = new Class[] { IContext.class }; + + + + + SPELContextPropertyAccessor() { + super(); + } + + + public Class[] getSpecificTargetClasses() { + return TARGET_CLASSES; + } + + + + public boolean canRead(final EvaluationContext context, final Object target, final String name) + throws AccessException { + if (context instanceof IThymeleafEvaluationContext) { + if (((IThymeleafEvaluationContext) context).isVariableAccessRestricted()) { + if (REQUEST_PARAMETERS_RESTRICTED_VARIABLE_NAME.equals(name)) { + throw new AccessException( + "Access to variable \"" + name + "\" is forbidden in this context. Note some restrictions apply to " + + "variable access. For example, direct access to request parameters is forbidden in preprocessing and " + + "unescaped expressions, in TEXT template mode, in fragment insertion specifications and " + + "in some specific attribute processors."); + } + } + } + return target != null; + } + + + + public TypedValue read(final EvaluationContext evaluationContext, final Object target, final String name) + throws AccessException { + + if (target == null) { + throw new AccessException("Cannot read property of null target"); + } + + try { + + /* + * NOTE we do not check here whether we are being asked for the 'locale', 'request', 'response', etc. + * because there already are specific expression objects for the most important of them, which should + * be used instead: #locale, #httpServletRequest, #httpSession, etc. + * The context should just be used as a map, without exposure of its more-internal methods... + */ + + // 'execInfo' translation from context variable to expression object - deprecated and to be removed in 3.1 + if ("execInfo".equals(name)) { // Quick check to avoid deprecated method call + final Object execInfoResult = checkExecInfo(name, evaluationContext); + if (execInfoResult != null) { + return new TypedValue(execInfoResult); + } + } + + final IContext context = (IContext) target; + return new TypedValue(context.getVariable(name)); + + } catch (final ClassCastException e) { + // This can happen simply because we're applying the same + // AST tree on a different class (Spring internally caches property accessors). + // So this exception might be considered "normal" by Spring AST evaluator and + // just use it to refresh the property accessor cache. + throw new AccessException("Cannot read target of class " + target.getClass().getName()); + } + + } + + + + + /** + * Translation from 'execInfo' context variable (${execInfo}) to 'execInfo' expression object (${#execInfo}), needed + * since 3.0.0. + * + * Note this is expressed as a separate method in order to mark this as deprecated and make it easily locatable. + * + * @param propertyName the name of the property being accessed (we are looking for 'execInfo'). + * @param context the expression context, which should contain the expression objects. + * @deprecated created (and deprecated) in 3.0.0 in order to support automatic conversion of calls to the 'execInfo' + * context variable (${execInfo}) into the 'execInfo' expression object (${#execInfo}), which is its + * new only valid form. This method, along with the infrastructure for execInfo conversion in + * StandardExpressionUtils#mightNeedExpressionObjects(...) will be removed in 3.1. + */ + @Deprecated + static Object checkExecInfo(final String propertyName, final EvaluationContext context) { + if ("execInfo".equals(propertyName)) { + if (!(context instanceof IThymeleafEvaluationContext)) { + throw new TemplateProcessingException( + "Found Thymeleaf Standard Expression containing a call to the context variable " + + "\"execInfo\" (e.g. \"${execInfo.templateName}\"), which has been deprecated. The " + + "Execution Info should be now accessed as an expression object instead " + + "(e.g. \"${#execInfo.templateName}\"). Deprecated use is still allowed (will be removed " + + "in future versions of Thymeleaf) when the SpringEL EvaluationContext implements the " + + IThymeleafEvaluationContext.class + " interface, but the current evaluation context of " + + "class " + context.getClass().getName() + " DOES NOT implement such interface."); + } + LOGGER.warn( + "[THYMELEAF][{}] Found Thymeleaf Standard Expression containing a call to the context variable " + + "\"execInfo\" (e.g. \"${execInfo.templateName}\"), which has been deprecated. The " + + "Execution Info should be now accessed as an expression object instead " + + "(e.g. \"${#execInfo.templateName}\"). Deprecated use is still allowed, but will be removed " + + "in future versions of Thymeleaf.", + TemplateEngine.threadIndex()); + return ((IThymeleafEvaluationContext)context).getExpressionObjects().getObject("execInfo"); + } + return null; + } + + + + public boolean canWrite( + final EvaluationContext context, final Object target, final String name) + throws AccessException { + // There should never be a need to write on an IContext during a template execution + return false; + } + + + + public void write( + final EvaluationContext context, final Object target, final String name, final Object newValue) + throws AccessException { + // There should never be a need to write on an IContext during a template execution + throw new AccessException("Cannot write to " + IContext.class.getName()); + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SPELVariableExpressionEvaluator.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SPELVariableExpressionEvaluator.java new file mode 100644 index 00000000..2d1ee65b --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SPELVariableExpressionEvaluator.java @@ -0,0 +1,435 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.expression; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.SpelCompilerMode; +import org.springframework.expression.spel.SpelParserConfiguration; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.thymeleaf.IEngineConfiguration; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.cache.ExpressionCacheKey; +import org.thymeleaf.cache.ICache; +import org.thymeleaf.cache.ICacheManager; +import org.thymeleaf.context.IEngineContext; +import org.thymeleaf.context.IExpressionContext; +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.exceptions.TemplateProcessingException; +import org.thymeleaf.expression.IExpressionObjects; +import org.thymeleaf.spring5.context.IThymeleafBindStatus; +import org.thymeleaf.spring5.util.FieldUtils; +import org.thymeleaf.spring5.util.SpringStandardExpressionUtils; +import org.thymeleaf.spring5.util.SpringValueFormatter; +import org.thymeleaf.spring5.util.SpringVersionUtils; +import org.thymeleaf.standard.expression.IStandardConversionService; +import org.thymeleaf.standard.expression.IStandardVariableExpression; +import org.thymeleaf.standard.expression.IStandardVariableExpressionEvaluator; +import org.thymeleaf.standard.expression.SelectionVariableExpression; +import org.thymeleaf.standard.expression.StandardExpressionExecutionContext; +import org.thymeleaf.standard.expression.StandardExpressions; +import org.thymeleaf.standard.expression.VariableExpression; +import org.thymeleaf.standard.util.StandardExpressionUtils; +import org.thymeleaf.util.ClassLoaderUtils; + +/** + *

+ * Evaluator for variable expressions ({@code ${...}}) in Thymeleaf Standard Expressions, using the + * SpringEL expression language. + *

+ *

+ * Note a class with this name existed since 2.0.9, but it was completely reimplemented + * in Thymeleaf 3.0 + *

+ * + * @author Daniel Fernández + * @author Guven Demir + * + * @since 3.0.3 + * + */ +public class SPELVariableExpressionEvaluator + implements IStandardVariableExpressionEvaluator { + + + public static final SPELVariableExpressionEvaluator INSTANCE = new SPELVariableExpressionEvaluator(); + + private static final String EXPRESSION_CACHE_TYPE_SPEL = "spel"; + + + private static final Logger logger = LoggerFactory.getLogger(SPELVariableExpressionEvaluator.class); + + private static final SpelExpressionParser PARSER_WITHOUT_COMPILED_SPEL = new SpelExpressionParser(); + private static final SpelExpressionParser PARSER_WITH_COMPILED_SPEL; + + + /* + * INITIALIZATION OF THE Spring EL parser. + * Two parsers will be always initialized: one with expression compilation enabled (if the Spring version allows) + * and another one without. Then during template execution we will check which one should be used. + */ + static { + + SpelExpressionParser spelCompilerExpressionParser = null; + if (SpringVersionUtils.isSpring41AtLeast()) { + try { + // Enable the SpEL compiler, in MIXED mode (not IMMEDIATE) in order to avoid ClassCastExceptions + // when executing the same compiled expression against targets of different classes. + final SpelParserConfiguration spelParserConfiguration = + new SpelParserConfiguration( + SpelCompilerMode.MIXED, + ClassLoaderUtils.getClassLoader(SPELVariableExpressionEvaluator.class)); + spelCompilerExpressionParser = new SpelExpressionParser(spelParserConfiguration); + } catch (final Throwable t) { + if (logger.isDebugEnabled()) { + // We are issuing a WARN even if we checked for DEBUG, but in this case we will log the entire + // exception trace (if DEBUG is not available, we will avoid polluting the log). + logger.warn( + "An error happened during the initialization of the Spring EL expression compiler. " + + "However, initialization was completed anyway. Note that compilation of SpEL expressions " + + "will not be available even if you configure your Spring dialect to use them.", t); + } else { + logger.warn( + "An error happened during the initialization of the Spring EL expression compiler. " + + "However, initialization was completed anyway. Note that compilation of SpEL expressions " + + "will not be available even if you configure your Spring dialect to use them. For more " + + "info, set your log to at least DEBUG level: " + t.getMessage()); + } + } + } + + PARSER_WITH_COMPILED_SPEL = spelCompilerExpressionParser; + + } + + + protected SPELVariableExpressionEvaluator() { + super(); + } + + + + + public final Object evaluate( + final IExpressionContext context, + final IStandardVariableExpression expression, + final StandardExpressionExecutionContext expContext) { + + if (logger.isTraceEnabled()) { + logger.trace("[THYMELEAF][{}] SpringEL expression: evaluating expression \"{}\" on target", TemplateEngine.threadIndex(), expression.getExpression()); + } + + try { + + final String spelExpression = expression.getExpression(); + final boolean useSelectionAsRoot = expression.getUseSelectionAsRoot(); + + if (spelExpression == null) { + throw new TemplateProcessingException("Expression content is null, which is not allowed"); + } + + /* + * TRY TO DELEGATE EVALUATION TO SPRING IF EXPRESSION IS ON A BOUND OBJECT + */ + if (expContext.getPerformTypeConversion()) { + // This is a {{...}} expression, so we should use binding info (if available) for formatting. + + if (useSelectionAsRoot || !isLocalVariableOverriding(context, spelExpression)) { + // The "local variable override" check avoids scenarios where a locally defined variable + // (e.g. the iterated variable in a th:each) has the same name as a bound object (e.g. a + // form-backing bean). If this was not detected, the bound object value would be always used + // instead of the local variable's + + final IThymeleafBindStatus bindStatus = + FieldUtils.getBindStatusFromParsedExpression(context, true, useSelectionAsRoot, spelExpression); + + if (bindStatus != null) { + // The expression goes against a bound object! Let Spring do its magic for displaying it... + return SpringValueFormatter.getDisplayString(bindStatus.getValue(), bindStatus.getEditor(), false); + } + + } + + } + + final IEngineConfiguration configuration = context.getConfiguration(); + + + /* + * OBTAIN THE EXPRESSION (SpelExpression OBJECT) FROM THE CACHE, OR PARSE IT + */ + final ComputedSpelExpression exp = + obtainComputedSpelExpression(configuration, expression, spelExpression, expContext); + + + /* + * COMPUTE EXPRESSION OBJECTS AND ADDITIONAL CONTEXT VARIABLES MAP + * The IExpressionObjects implementation returned by processing contexts that include the Standard + * Dialects will be lazy in the creation of expression objects (i.e. they won't be created until really + * needed). + */ + final IExpressionObjects expressionObjects = + (exp.mightNeedExpressionObjects? context.getExpressionObjects() : null); + + + /* + * CREATE/OBTAIN THE SPEL EVALUATION CONTEXT OBJECT + */ + EvaluationContext evaluationContext = + (EvaluationContext) context. + getVariable(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME); + + if (evaluationContext == null) { + + // Using a StandardEvaluationContext one as base: we are losing bean resolution and conversion service!! + // + // The ideal scenario is that this is created before processing the page, e.g. at the ThymeleafView + // class, but it can happen that no ThymeleafView is ever called if we are using the Spring-integrated + // template engine on a standalone (non-web) scenario... + // + // Also, note Spring's EvaluationContexts are NOT THREAD-SAFE (in exchange for SpelExpressions being + // thread-safe). That's why we need to create a new EvaluationContext for each request / template + // execution, even if it is quite expensive to create because of requiring the initialization of + // several ConcurrentHashMaps. + evaluationContext = new ThymeleafEvaluationContextWrapper(new StandardEvaluationContext()); + + if (context instanceof IEngineContext) { + ((IEngineContext)context).setVariable( + ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME, evaluationContext); + } + + } else if (!(evaluationContext instanceof IThymeleafEvaluationContext)) { + + evaluationContext = new ThymeleafEvaluationContextWrapper(evaluationContext); + + if (context instanceof IEngineContext) { + ((IEngineContext)context).setVariable( + ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME, evaluationContext); + } + + } + + + /* + * AT THIS POINT, WE ARE SURE IT IS AN IThymeleafEvaluationContext + * + * This is needed in order to be sure we can modify the 'requestParametersRestricted' flag and also the + * expression objects. + */ + final IThymeleafEvaluationContext thymeleafEvaluationContext = (IThymeleafEvaluationContext) evaluationContext; + + + /* + * CONFIGURE THE IThymeleafEvaluationContext INSTANCE: expression objects and restrictions + * + * NOTE this is possible even if the evaluation context object is shared for the whole template execution + * because evaluation contexts are not thread-safe and are only used in a single template execution + */ + thymeleafEvaluationContext.setExpressionObjects(expressionObjects); + thymeleafEvaluationContext.setVariableAccessRestricted(expContext.getRestrictVariableAccess()); + + + /* + * RESOLVE THE EVALUATION ROOT + */ + final ITemplateContext templateContext = (context instanceof ITemplateContext ? (ITemplateContext) context : null); + final Object evaluationRoot = + (useSelectionAsRoot && templateContext != null && templateContext.hasSelectionTarget()? + templateContext.getSelectionTarget() : new SPELContextMapWrapper(context, thymeleafEvaluationContext)); + + + /* + * If no conversion is to be made, JUST RETURN + */ + if (!expContext.getPerformTypeConversion()) { + return exp.expression.getValue(thymeleafEvaluationContext, evaluationRoot); + } + + + /* + * If a conversion is to be made, OBTAIN THE CONVERSION SERVICE AND EXECUTE IT + */ + final IStandardConversionService conversionService = + StandardExpressions.getConversionService(configuration); + + if (conversionService instanceof SpringStandardConversionService) { + // The conversion service is a mere bridge with the Spring ConversionService, therefore + // this makes use of the complete Spring type conversion infrastructure, without needing + // to manually execute the conversion. + return exp.expression.getValue(thymeleafEvaluationContext, evaluationRoot, String.class); + } + + // We need type conversion, but conversion service is not a mere bridge to the Spring one, + // so we need manual execution. + final Object result = exp.expression.getValue(thymeleafEvaluationContext, evaluationRoot); + return conversionService.convert(context, result, String.class); + + + } catch (final TemplateProcessingException e) { + throw e; + } catch(final Exception e) { + throw new TemplateProcessingException( + "Exception evaluating SpringEL expression: \"" + expression.getExpression() + "\"", e); + } + + } + + + + + + + private static ComputedSpelExpression obtainComputedSpelExpression( + final IEngineConfiguration configuration, + final IStandardVariableExpression expression, final String spelExpression, + final StandardExpressionExecutionContext expContext) { + + if (expression instanceof VariableExpression) { + + final VariableExpression vexpression = (VariableExpression) expression; + + Object cachedExpression = vexpression.getCachedExpression(); + if (cachedExpression != null && cachedExpression instanceof ComputedSpelExpression) { + return (ComputedSpelExpression) cachedExpression; + } + cachedExpression = getExpression(configuration, spelExpression, expContext); + if (cachedExpression != null) { + vexpression.setCachedExpression(cachedExpression); + } + return (ComputedSpelExpression) cachedExpression; + + } + + if (expression instanceof SelectionVariableExpression) { + + final SelectionVariableExpression vexpression = (SelectionVariableExpression) expression; + + Object cachedExpression = vexpression.getCachedExpression(); + if (cachedExpression != null && cachedExpression instanceof ComputedSpelExpression) { + return (ComputedSpelExpression) cachedExpression; + } + cachedExpression = getExpression(configuration, spelExpression, expContext); + if (cachedExpression != null) { + vexpression.setCachedExpression(cachedExpression); + } + return (ComputedSpelExpression) cachedExpression; + + } + + return getExpression(configuration, spelExpression, expContext); + + } + + + private static ComputedSpelExpression getExpression( + final IEngineConfiguration configuration, + final String spelExpression, final StandardExpressionExecutionContext expContext) { + + ComputedSpelExpression exp = null; + ICache cache = null; + + final ICacheManager cacheManager = configuration.getCacheManager(); + if (cacheManager != null) { + cache = cacheManager.getExpressionCache(); + if (cache != null) { + exp = (ComputedSpelExpression) cache.get(new ExpressionCacheKey(EXPRESSION_CACHE_TYPE_SPEL,spelExpression)); + } + } + + if (exp == null) { + + // SELECT THE ADEQUATE SpEL EXPRESSION PARSER depending on whether SpEL compilation is enabled + final SpelExpressionParser spelExpressionParser = + PARSER_WITH_COMPILED_SPEL != null && SpringStandardExpressions.isSpringELCompilerEnabled(configuration)? + PARSER_WITH_COMPILED_SPEL : PARSER_WITHOUT_COMPILED_SPEL; + + if (expContext.getRestrictInstantiationAndStatic() + && SpringStandardExpressionUtils.containsSpELInstantiationOrStatic(spelExpression)) { + throw new TemplateProcessingException( + "Instantiation of new objects and access to static classes is forbidden in this context"); + } + + final boolean mightNeedExpressionObjects = StandardExpressionUtils.mightNeedExpressionObjects(spelExpression); + + final SpelExpression spelExpressionObject = (SpelExpression) spelExpressionParser.parseExpression(spelExpression); + + exp = new ComputedSpelExpression(spelExpressionObject, mightNeedExpressionObjects); + + if (cache != null && null != exp) { + cache.put(new ExpressionCacheKey(EXPRESSION_CACHE_TYPE_SPEL,spelExpression), exp); + } + + } + + return exp; + + } + + + + private static boolean isLocalVariableOverriding(final IExpressionContext context, final String expression) { + + if (!(context instanceof IEngineContext)) { + // We don't even have support for local variables! + return false; + } + + // NOTE this IEngineContext interface is internal and should not be used in users' code + final IEngineContext engineContext = (IEngineContext) context; + + final int dotPos = expression.indexOf('.'); + if (dotPos == -1) { + return false; + } + // Once we extract the first part of the expression, we check whether it is a local variable... + final String expressionFirstComponent = expression.substring(0, dotPos); + return engineContext.isVariableLocal(expressionFirstComponent); + + } + + + + + @Override + public String toString() { + return "SpringEL"; + } + + + + private static final class ComputedSpelExpression { + + final SpelExpression expression; + final boolean mightNeedExpressionObjects; + + ComputedSpelExpression(final SpelExpression expression, final boolean mightNeedExpressionObjects) { + super(); + this.expression = expression; + this.mightNeedExpressionObjects = mightNeedExpressionObjects; + } + + + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SpringStandardConversionService.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SpringStandardConversionService.java new file mode 100755 index 00000000..b2b113e8 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SpringStandardConversionService.java @@ -0,0 +1,116 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.expression; + + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.TypeConverter; +import org.thymeleaf.context.IExpressionContext; +import org.thymeleaf.standard.expression.AbstractStandardConversionService; + +/** + *

+ * Implementation of {@link org.thymeleaf.standard.expression.IStandardConversionService} that delegates + * to Spring's type conversion system. + *

+ *

+ * If there is a {@link org.springframework.core.convert.ConversionService} available at the application + * context, it will be used for conversion. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class SpringStandardConversionService extends AbstractStandardConversionService { + + + private static final TypeDescriptor TYPE_STRING = TypeDescriptor.valueOf(String.class); + + + + public SpringStandardConversionService() { + // Should only be instanced from SpringStandardDialect + super(); + } + + + + @Override + protected String convertToString( + final IExpressionContext context, + final Object object) { + + if (object == null) { + return null; + } + final TypeDescriptor objectTypeDescriptor = TypeDescriptor.forObject(object); + final TypeConverter typeConverter = getSpringConversionService(context); + if (typeConverter == null || !typeConverter.canConvert(objectTypeDescriptor, TYPE_STRING)) { + return super.convertToString(context, object); + } + return (String) typeConverter.convertValue(object, objectTypeDescriptor, TYPE_STRING); + + } + + + @Override + protected T convertOther( + final IExpressionContext context, + final Object object, final Class targetClass) { + + if (object == null) { + return null; + } + final TypeDescriptor objectTypeDescriptor = TypeDescriptor.forObject(object); + final TypeDescriptor targetTypeDescriptor = TypeDescriptor.valueOf(targetClass); + final TypeConverter typeConverter = getSpringConversionService(context); + if (typeConverter == null || !typeConverter.canConvert(objectTypeDescriptor, targetTypeDescriptor)) { + return super.convertOther(context, object, targetClass); + } + return (T) typeConverter.convertValue(object, objectTypeDescriptor, targetTypeDescriptor); + + } + + + + + + + + private static TypeConverter getSpringConversionService(final IExpressionContext context) { + + final EvaluationContext evaluationContext = + (EvaluationContext) context.getVariable( + ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME); + + if (evaluationContext != null) { + return evaluationContext.getTypeConverter(); + } + + return null; + + } + + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SpringStandardExpressionObjectFactory.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SpringStandardExpressionObjectFactory.java new file mode 100644 index 00000000..3a2cd841 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SpringStandardExpressionObjectFactory.java @@ -0,0 +1,112 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.expression; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.thymeleaf.context.IExpressionContext; +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.standard.expression.StandardExpressionObjectFactory; + +/** + *

+ * Builds the expression objects to be used by SpringStandard dialects. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public class SpringStandardExpressionObjectFactory extends StandardExpressionObjectFactory { + + /* + * Any new objects added here should also be added to the "ALL_EXPRESSION_OBJECT_NAMES" See below. + */ + public static final String FIELDS_EXPRESSION_OBJECT_NAME = "fields"; + public static final String THEMES_EXPRESSION_OBJECT_NAME = "themes"; + public static final String MVC_EXPRESSION_OBJECT_NAME = "mvc"; + public static final String REQUESTDATAVALUES_EXPRESSION_OBJECT_NAME = "requestdatavalues"; + + + public static final Set ALL_EXPRESSION_OBJECT_NAMES; + + private static final Mvc MVC_EXPRESSION_OBJECT = new Mvc(); + + + + + static { + + final Set allExpressionObjectNames = new LinkedHashSet(); + allExpressionObjectNames.addAll(StandardExpressionObjectFactory.ALL_EXPRESSION_OBJECT_NAMES); + allExpressionObjectNames.add(FIELDS_EXPRESSION_OBJECT_NAME); + allExpressionObjectNames.add(THEMES_EXPRESSION_OBJECT_NAME); + allExpressionObjectNames.add(MVC_EXPRESSION_OBJECT_NAME); + allExpressionObjectNames.add(REQUESTDATAVALUES_EXPRESSION_OBJECT_NAME); + + ALL_EXPRESSION_OBJECT_NAMES = Collections.unmodifiableSet(allExpressionObjectNames); + + } + + + + + public SpringStandardExpressionObjectFactory() { + super(); + } + + + + + + public Set getAllExpressionObjectNames() { + return ALL_EXPRESSION_OBJECT_NAMES; + } + + + + public Object buildObject(final IExpressionContext context, final String expressionObjectName) { + + + if (MVC_EXPRESSION_OBJECT_NAME.equals(expressionObjectName)) { + return MVC_EXPRESSION_OBJECT; + } + if (THEMES_EXPRESSION_OBJECT_NAME.equals(expressionObjectName)) { + return new Themes(context); + } + if (FIELDS_EXPRESSION_OBJECT_NAME.equals(expressionObjectName)) { + return new Fields(context); + } + if (REQUESTDATAVALUES_EXPRESSION_OBJECT_NAME.equals(expressionObjectName)) { + if (context instanceof ITemplateContext) { + return new RequestDataValues((ITemplateContext)context); + } + return null; + } + + return super.buildObject(context, expressionObjectName); + + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SpringStandardExpressions.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SpringStandardExpressions.java new file mode 100644 index 00000000..8ef52abd --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/SpringStandardExpressions.java @@ -0,0 +1,82 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.expression; + +import org.thymeleaf.IEngineConfiguration; +import org.thymeleaf.exceptions.TemplateProcessingException; +import org.thymeleaf.spring5.dialect.SpringStandardDialect; + + +/** + *

+ * Utility class for the easy obtention of objects relevant to the parsing and execution of Thymeleaf + * Spring-Standard Expressions (Thymeleaf Standard Expressions based using Spring EL as a base expression language). + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class SpringStandardExpressions { + + + /** + * Name used for registering whether Spring EL compilation should be enabled if available or not. + */ + public static final String ENABLE_SPRING_EL_COMPILER_ATTRIBUTE_NAME = "EnableSpringELCompiler"; + + + + + private SpringStandardExpressions() { + super(); + } + + + /** + *

+ * Check whether compilation of Spring EL expressions should be enabled or not. + *

+ *

+ * This is done through configuration methods at the {@link SpringStandardDialect} + * instance being used, and its value is offered to the engine as an execution attribute. + *

+ * + * @param configuration the configuration object for the current template execution environment. + * @return {@code true} if the SpEL compiler should be enabled if available, {@code false} if not. + */ + public static boolean isSpringELCompilerEnabled(final IEngineConfiguration configuration) { + final Object enableSpringELCompiler = + configuration.getExecutionAttributes().get(ENABLE_SPRING_EL_COMPILER_ATTRIBUTE_NAME); + if (enableSpringELCompiler == null) { + return false; + } + if (!(enableSpringELCompiler instanceof Boolean)) { + throw new TemplateProcessingException( + "A value for the \"" + ENABLE_SPRING_EL_COMPILER_ATTRIBUTE_NAME + "\" execution attribute " + + "has been specified, but it is not of the required type Boolean. " + + "(" + enableSpringELCompiler.getClass().getName() + ")"); + } + return ((Boolean) enableSpringELCompiler).booleanValue(); + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/Themes.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/Themes.java new file mode 100755 index 00000000..69e57966 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/Themes.java @@ -0,0 +1,75 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.expression; + +import java.util.Locale; + +import org.springframework.ui.context.Theme; +import org.thymeleaf.context.IExpressionContext; +import org.thymeleaf.exceptions.TemplateProcessingException; +import org.thymeleaf.spring5.context.IThymeleafRequestContext; +import org.thymeleaf.spring5.context.SpringContextUtils; + +/** + * A utility object, accessed in Thymeleaf templates by the {@code #themes} + * expression, that provides the same features as the Spring + * {@code } JSP tag. + * + * @author Emanuel Rabina + * @author Daniel Fernández + */ +public class Themes { + + private final Theme theme; + private final Locale locale; + + /** + * Constructor, obtains the current theme and locale from the processing + * context for code lookups later. + * + * @param context the processing context being used + */ + public Themes(final IExpressionContext context) { + + super(); + this.locale = context.getLocale(); + final IThymeleafRequestContext requestContext = SpringContextUtils.getRequestContext(context); + this.theme = requestContext != null ? requestContext.getTheme() : null; + } + + /** + * Looks up and returns the value of the given key in the properties file of + * the currently-selected theme. + * + * @param code Key to look up in the theme properties file. + * @return The value of the code in the current theme properties file, or an + * empty string if the code could not be resolved. + */ + public String code(final String code) { + if (this.theme == null) { + throw new TemplateProcessingException("Theme cannot be resolved because RequestContext was not found. " + + "Are you using a Context object without a RequestContext variable?"); + } + return this.theme.getMessageSource().getMessage(code, null, "", this.locale); + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/ThymeleafEvaluationContext.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/ThymeleafEvaluationContext.java new file mode 100755 index 00000000..8c99184c --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/ThymeleafEvaluationContext.java @@ -0,0 +1,149 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.expression; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.context.expression.MapAccessor; +import org.springframework.core.convert.ConversionService; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.StandardTypeConverter; +import org.thymeleaf.expression.IExpressionObjects; +import org.thymeleaf.spring5.view.ThymeleafView; +import org.thymeleaf.standard.expression.RestrictedRequestAccessUtils; +import org.thymeleaf.standard.expression.StandardExpressionObjectFactory; +import org.thymeleaf.util.Validate; + +/** + *

+ * Thymeleaf's basic implementation of the {@link IThymeleafEvaluationContext} interface, which in turn extends + * from Spring's {@link org.springframework.expression.EvaluationContext} interface. + *

+ *

+ * This implementation adds Thymeleaf's own property accessors + * (see {@link org.springframework.expression.PropertyAccessor}) for accessing + * the {@link org.thymeleaf.context.IContext} object in which variables are stored. + *

+ *

+ * Also, this evaluation context (which is usually instanced at the + * {@link ThymeleafView} initialization) links the execution of expressions + * with the available {@link BeanFactory} and {@link ConversionService} instances, used during evaluation. + *

+ *

+ * Before executing a Spring EL expression using this evaluation context, it should be enriched with the + * variables to be made accessible (like {@code #variableName}), using a + * {@link ThymeleafEvaluationContextWrapper} object. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class ThymeleafEvaluationContext + extends StandardEvaluationContext + implements IThymeleafEvaluationContext { + + public static final String THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME = "thymeleaf::EvaluationContext"; + + + private static final MapAccessor MAP_ACCESSOR_INSTANCE = new MapAccessor(); + + + private final ApplicationContext applicationContext; + + private IExpressionObjects expressionObjects = null; + private boolean variableAccessRestricted = false; + + + + + public ThymeleafEvaluationContext(final ApplicationContext applicationContext, final ConversionService conversionService) { + + super(); + + Validate.notNull(applicationContext, "Application Context cannot be null"); + // ConversionService CAN be null + + this.applicationContext = applicationContext; + this.setBeanResolver(new BeanFactoryResolver(applicationContext)); + if (conversionService != null) { + this.setTypeConverter(new StandardTypeConverter(conversionService)); + } + + this.addPropertyAccessor(SPELContextPropertyAccessor.INSTANCE); + this.addPropertyAccessor(MAP_ACCESSOR_INSTANCE); + + } + + + public ApplicationContext getApplicationContext() { + return this.applicationContext; + } + + + @Override + public Object lookupVariable(final String name) { + + if (this.expressionObjects != null && this.expressionObjects.containsObject(name)) { + + final Object result = this.expressionObjects.getObject(name); + if (result != null) { + + // We need to first check if we are in a restricted environment. If so, restrict access to the request. + if (this.variableAccessRestricted && + (StandardExpressionObjectFactory.REQUEST_EXPRESSION_OBJECT_NAME.equals(name) || + StandardExpressionObjectFactory.HTTP_SERVLET_REQUEST_EXPRESSION_OBJECT_NAME.equals(name))) { + return RestrictedRequestAccessUtils.wrapRequestObject(result); + } + + return result; + + } + + } + + // fall back to superclass + return super.lookupVariable(name); + + } + + + + + public boolean isVariableAccessRestricted() { + return this.variableAccessRestricted; + } + + public void setVariableAccessRestricted(final boolean restricted) { + this.variableAccessRestricted = restricted; + } + + public IExpressionObjects getExpressionObjects() { + return this.expressionObjects; + } + + public void setExpressionObjects(final IExpressionObjects expressionObjects) { + this.expressionObjects = expressionObjects; + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/ThymeleafEvaluationContextWrapper.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/ThymeleafEvaluationContextWrapper.java new file mode 100755 index 00000000..e40404f9 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/expression/ThymeleafEvaluationContextWrapper.java @@ -0,0 +1,201 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.expression; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.context.expression.MapAccessor; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.ConstructorResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.OperatorOverloader; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypeComparator; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.TypeLocator; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.thymeleaf.expression.IExpressionObjects; +import org.thymeleaf.standard.expression.RestrictedRequestAccessUtils; +import org.thymeleaf.standard.expression.StandardExpressionObjectFactory; +import org.thymeleaf.util.Validate; + +/** + *

+ * Implementation of Thymeleaf's {@link IThymeleafEvaluationContext} interface designed to wrap around a + * delegated implementation of {@link EvaluationContext}, adding the + * Thymeleaf-required {@link PropertyAccessor} implementations and (optionally) + * a series of variables to be accessed like {@code #variableName} during expression evaluation. + *

+ *

+ * Note a class with this name existed since 2.1.0, but it was completely reimplemented + * in Thymeleaf 3.0 + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class ThymeleafEvaluationContextWrapper implements IThymeleafEvaluationContext { + + + private static final MapAccessor MAP_ACCESSOR_INSTANCE = new MapAccessor(); + + + private final EvaluationContext delegate; + private final List propertyAccessors; // can be initialized to null if we can delegate + + private IExpressionObjects expressionObjects = null; + private boolean requestParametersRestricted = false; + private Map additionalVariables = null; + + + + + public ThymeleafEvaluationContextWrapper(final EvaluationContext delegate) { + + super(); + + Validate.notNull(delegate, "Evaluation context delegate cannot be null"); + + this.delegate = delegate; + + if ((this.delegate instanceof ThymeleafEvaluationContext)) { + + this.propertyAccessors = null; // No need to initialize our own list + + } else if (this.delegate instanceof StandardEvaluationContext) { + + ((StandardEvaluationContext) this.delegate).addPropertyAccessor(SPELContextPropertyAccessor.INSTANCE); + ((StandardEvaluationContext) this.delegate).addPropertyAccessor(MAP_ACCESSOR_INSTANCE); + this.propertyAccessors = null; // No need to initialize our own list + + } else { + + // We don't know the specific EvaluationContext implementation, so we need to initialize our own PA list + + this.propertyAccessors = new ArrayList(5); + this.propertyAccessors.addAll(this.delegate.getPropertyAccessors()); + this.propertyAccessors.add(SPELContextPropertyAccessor.INSTANCE); + this.propertyAccessors.add(MAP_ACCESSOR_INSTANCE); + + } + + } + + + public TypedValue getRootObject() { + return this.delegate.getRootObject(); + } + + public List getConstructorResolvers() { + return this.delegate.getConstructorResolvers(); + } + + public List getMethodResolvers() { + return this.delegate.getMethodResolvers(); + } + + public List getPropertyAccessors() { + return this.propertyAccessors == null ? this.delegate.getPropertyAccessors() : this.propertyAccessors; + } + + public TypeLocator getTypeLocator() { + return this.delegate.getTypeLocator(); + } + + public TypeConverter getTypeConverter() { + return this.delegate.getTypeConverter(); + } + + public TypeComparator getTypeComparator() { + return this.delegate.getTypeComparator(); + } + + public OperatorOverloader getOperatorOverloader() { + return this.delegate.getOperatorOverloader(); + } + + public BeanResolver getBeanResolver() { + return this.delegate.getBeanResolver(); + } + + public void setVariable(final String name, final Object value) { + if (this.additionalVariables == null) { + this.additionalVariables = new HashMap(5, 1.0f); + } + this.additionalVariables.put(name, value); + } + + public Object lookupVariable(final String name) { + + if (this.expressionObjects != null && this.expressionObjects.containsObject(name)) { + + final Object result = this.expressionObjects.getObject(name); + if (result != null) { + + // We need to first check if we are in a restricted environment. If so, restrict access to the request. + if (this.requestParametersRestricted && + (StandardExpressionObjectFactory.REQUEST_EXPRESSION_OBJECT_NAME.equals(name) || + StandardExpressionObjectFactory.HTTP_SERVLET_REQUEST_EXPRESSION_OBJECT_NAME.equals(name))) { + return RestrictedRequestAccessUtils.wrapRequestObject(result); + } + + return result; + } + + } + + if (this.additionalVariables != null && this.additionalVariables.containsKey(name)) { + final Object result = this.additionalVariables.get(name); + if (result != null) { + return result; + } + } + + // fall back to delegate + return this.delegate.lookupVariable(name); + + } + + + public boolean isVariableAccessRestricted() { + return this.requestParametersRestricted; + } + + public void setVariableAccessRestricted(final boolean restricted) { + this.requestParametersRestricted = restricted; + } + + public IExpressionObjects getExpressionObjects() { + return this.expressionObjects; + } + + public void setExpressionObjects(final IExpressionObjects expressionObjects) { + this.expressionObjects = expressionObjects; + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/linkbuilder/webflux/SpringWebFluxLinkBuilder.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/linkbuilder/webflux/SpringWebFluxLinkBuilder.java new file mode 100644 index 00000000..acf2f121 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/linkbuilder/webflux/SpringWebFluxLinkBuilder.java @@ -0,0 +1,121 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.linkbuilder.webflux; + +import java.util.Map; + +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.thymeleaf.context.IExpressionContext; +import org.thymeleaf.exceptions.TemplateProcessingException; +import org.thymeleaf.linkbuilder.ILinkBuilder; +import org.thymeleaf.linkbuilder.StandardLinkBuilder; +import org.thymeleaf.spring5.context.webflux.ISpringWebFluxContext; + + +/** + *

+ * Spring WebFlux-based implementation of {@link ILinkBuilder}. + *

+ *

+ * This class will build link URLs using the Spring WebFlux API and adapting to the needs of this + * type of application. + *

+ *

+ * This implementation will only return {@code null} at {@link #buildLink(IExpressionContext, String, Map)} + * if the specified {@code base} argument is {@code null}. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + */ +public class SpringWebFluxLinkBuilder extends StandardLinkBuilder { + + + + public SpringWebFluxLinkBuilder() { + super(); + } + + + + /** + *

+ * Compute the context path to be applied to URLs that have been determined to be context-relative (and therefore + * might need a context path to be inserted at their beginning). + *

+ *

+ * This method will obtain the context path from {@code ServerHttpRequest.getContextPath()}, + * throwing an exception if {@code context} is not an instance of {@code ISpringWebFluxContext} given + * context-relative URLs are (by default) only allowed in Spring WebFlux contexts. + *

+ *

+ * This method can be overridden by any subclasses that want to change this behaviour. + *

+ * + * @param context the execution context. + * @param base the URL base specified. + * @param parameters the URL parameters specified. + * @return the context path. + */ + @Override + protected String computeContextPath( + final IExpressionContext context, final String base, final Map parameters) { + + if (!(context instanceof ISpringWebFluxContext)) { + throw new TemplateProcessingException( + "Link base \"" + base + "\" cannot be context relative (/...) unless the context " + + "used for executing the engine implements the " + ISpringWebFluxContext.class.getName() + " interface"); + } + + // If it is context-relative, it has to be a Spring WebFlux-based context + final ServerHttpRequest request = ((ISpringWebFluxContext)context).getRequest(); + return request.getPath().contextPath().value(); + + } + + + /** + *

+ * Process an already-built URL just before returning it. + *

+ *

+ * This method can be overridden by any subclasses that want to change this behaviour. + *

+ * + * @param context the execution context. + * @param link the already-built URL. + * @return the processed URL, ready to be used. + */ + @Override + protected String processLink(final IExpressionContext context, final String link) { + + if (!(context instanceof ISpringWebFluxContext)) { + return link; + } + + final ServerWebExchange exchange = ((ISpringWebFluxContext)context).getExchange(); + return exchange.transformUrl(link); + + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/messageresolver/SpringMessageResolver.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/messageresolver/SpringMessageResolver.java new file mode 100755 index 00000000..e66318a7 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/messageresolver/SpringMessageResolver.java @@ -0,0 +1,178 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.messageresolver; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; +import org.springframework.context.NoSuchMessageException; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.exceptions.ConfigurationException; +import org.thymeleaf.messageresolver.AbstractMessageResolver; +import org.thymeleaf.messageresolver.IMessageResolver; +import org.thymeleaf.messageresolver.StandardMessageResolver; +import org.thymeleaf.util.Validate; + + + +/** + *

+ * Implementation of {@link IMessageResolver} that + * integrates the standard Spring way of resolving messages into Thymeleaf. + *

+ *

+ * Template-based resolution is done by means of using the available Spring-configured + * {@link MessageSource} objects. + *

+ *

+ * Origin-based resolution is done in exactly the same way as in {@link StandardMessageResolver}. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public class SpringMessageResolver + extends AbstractMessageResolver + implements MessageSourceAware { + + + private static final Logger logger = LoggerFactory.getLogger(SpringMessageResolver.class); + + + private final StandardMessageResolver standardMessageResolver; + private MessageSource messageSource; + + + public SpringMessageResolver() { + super(); + this.standardMessageResolver = new StandardMessageResolver(); + } + + + + + + /* + * Check the message source has been set. + */ + private void checkMessageSourceInitialized() { + if (this.messageSource == null) { + throw new ConfigurationException( + "Cannot initialize " + SpringMessageResolver.class.getSimpleName() + + ": MessageSource has not been set. Either define this object as " + + "a Spring bean (which will automatically set the MessageSource) or, " + + "if you instance it directly, set the MessageSource manually using its "+ + "corresponding setter method."); + } + } + + + + + + /** + *

+ * Returns the message source ({@link MessageSource}) to be + * used for message resolution. + *

+ * + * @return the message source + */ + public final MessageSource getMessageSource() { + return this.messageSource; + } + + + /** + *

+ * Sets the message source to be used for message resolution + *

+ * + * @param messageSource the message source + */ + public final void setMessageSource(final MessageSource messageSource) { + this.messageSource = messageSource; + } + + + + + + public final String resolveMessage( + final ITemplateContext context, final Class origin, final String key, final Object[] messageParameters) { + + Validate.notNull(context.getLocale(), "Locale in context cannot be null"); + Validate.notNull(key, "Message key cannot be null"); + + /* + * FIRST STEP: Look for the message using template-based resolution + */ + if (context != null) { + + checkMessageSourceInitialized(); + + if (logger.isTraceEnabled()) { + logger.trace( + "[THYMELEAF][{}] Resolving message with key \"{}\" for template \"{}\" and locale \"{}\". " + + "Messages will be retrieved from Spring's MessageSource infrastructure.", + new Object[]{TemplateEngine.threadIndex(), key, context.getTemplateData().getTemplate(), context.getLocale()}); + } + + try { + return this.messageSource.getMessage(key, messageParameters, context.getLocale()); + } catch (NoSuchMessageException e) { + // Try other methods + } + + } + + /* + * SECOND STEP: Look for the message using origin-based resolution, delegated to the StandardMessageResolver + */ + if (origin != null) { + // We will be disabling template-based resolution when delegating in order to use only origin-based + final String message = + this.standardMessageResolver.resolveMessage(context, origin, key, messageParameters, false, true, true); + if (message != null) { + return message; + } + } + + + /* + * NOT FOUND, return null + */ + return null; + + } + + + + + public String createAbsentMessageRepresentation( + final ITemplateContext context, final Class origin, final String key, final Object[] messageParameters) { + return this.standardMessageResolver.createAbsentMessageRepresentation(context, origin, key, messageParameters); + } + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/naming/SpringContextVariableNames.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/naming/SpringContextVariableNames.java new file mode 100755 index 00000000..06c85d51 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/naming/SpringContextVariableNames.java @@ -0,0 +1,50 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.naming; + + +/** + *

+ * Static class holding the names of context variables meant to have a special + * use inside the framework. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class SpringContextVariableNames { + + + public static final String SPRING_REQUEST_CONTEXT = "springRequestContext"; + public static final String THYMELEAF_REQUEST_CONTEXT = "thymeleafRequestContext"; + + public static final String THYMELEAF_FIELD_BIND_STATUS = "thymeleafFieldBindStatus"; + + public static final String SPRING_BOUND_OBJECT_EXPRESSION = "springBoundObjectExpression"; + + + + private SpringContextVariableNames() { + super(); + } + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/AbstractSpringFieldTagProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/AbstractSpringFieldTagProcessor.java new file mode 100644 index 00000000..31ff3a2b --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/AbstractSpringFieldTagProcessor.java @@ -0,0 +1,229 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.processor; + +import org.springframework.util.StringUtils; +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.engine.AttributeDefinition; +import org.thymeleaf.engine.AttributeDefinitions; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.engine.IAttributeDefinitionsAware; +import org.thymeleaf.exceptions.TemplateProcessingException; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.processor.element.AbstractAttributeTagProcessor; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.spring5.context.IThymeleafBindStatus; +import org.thymeleaf.spring5.naming.SpringContextVariableNames; +import org.thymeleaf.spring5.util.FieldUtils; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.util.Validate; + +/** + * Binds an input property with the value in the form's backing bean. + *

+ * Values for {@code th:field} attributes must be selection expressions + * {@code (*{...})}, as they will be evaluated on the form backing bean and not + * on the context variables (model attributes in Spring MVC jargon). + * + * @author Daniel Fernández + * @since 3.0.3 + */ +public abstract class AbstractSpringFieldTagProcessor + extends AbstractAttributeTagProcessor + implements IAttributeDefinitionsAware { + + + public static final int ATTR_PRECEDENCE = 1700; + public static final String ATTR_NAME = "field"; + + private static final TemplateMode TEMPLATE_MODE = TemplateMode.HTML; + + protected static final String INPUT_TAG_NAME = "input"; + protected static final String SELECT_TAG_NAME = "select"; + protected static final String OPTION_TAG_NAME = "option"; + protected static final String TEXTAREA_TAG_NAME = "textarea"; + + protected static final String ID_ATTR_NAME = "id"; + protected static final String TYPE_ATTR_NAME = "type"; + protected static final String NAME_ATTR_NAME = "name"; + protected static final String VALUE_ATTR_NAME = "value"; + protected static final String CHECKED_ATTR_NAME = "checked"; + protected static final String SELECTED_ATTR_NAME = "selected"; + protected static final String DISABLED_ATTR_NAME = "disabled"; + protected static final String MULTIPLE_ATTR_NAME = "multiple"; + + private AttributeDefinition discriminatorAttributeDefinition; + protected AttributeDefinition idAttributeDefinition; + protected AttributeDefinition typeAttributeDefinition; + protected AttributeDefinition nameAttributeDefinition; + protected AttributeDefinition valueAttributeDefinition; + protected AttributeDefinition checkedAttributeDefinition; + protected AttributeDefinition selectedAttributeDefinition; + protected AttributeDefinition disabledAttributeDefinition; + protected AttributeDefinition multipleAttributeDefinition; + + + + + + private final String discriminatorAttrName; + private final String[] discriminatorAttrValues; + private final boolean removeAttribute; + + + public AbstractSpringFieldTagProcessor( + final String dialectPrefix, final String elementName, + final String discriminatorAttrName, final String[] discriminatorAttrValues, + final boolean removeAttribute) { + super(TEMPLATE_MODE, dialectPrefix, elementName, false, ATTR_NAME, true, ATTR_PRECEDENCE, false); + this.discriminatorAttrName = discriminatorAttrName; + this.discriminatorAttrValues = discriminatorAttrValues; + this.removeAttribute = removeAttribute; + } + + + + + public void setAttributeDefinitions(final AttributeDefinitions attributeDefinitions) { + Validate.notNull(attributeDefinitions, "Attribute Definitions cannot be null"); + // We precompute the AttributeDefinitions in order to being able to use much + // faster methods for setting/replacing attributes on the ElementAttributes implementation + this.discriminatorAttributeDefinition = + (this.discriminatorAttrName != null? attributeDefinitions.forName(TEMPLATE_MODE, this.discriminatorAttrName) : null); + this.idAttributeDefinition = attributeDefinitions.forName(TEMPLATE_MODE, ID_ATTR_NAME); + this.typeAttributeDefinition = attributeDefinitions.forName(TEMPLATE_MODE, TYPE_ATTR_NAME); + this.nameAttributeDefinition = attributeDefinitions.forName(TEMPLATE_MODE, NAME_ATTR_NAME); + this.valueAttributeDefinition = attributeDefinitions.forName(TEMPLATE_MODE, VALUE_ATTR_NAME); + this.checkedAttributeDefinition = attributeDefinitions.forName(TEMPLATE_MODE, CHECKED_ATTR_NAME); + this.selectedAttributeDefinition = attributeDefinitions.forName(TEMPLATE_MODE, SELECTED_ATTR_NAME); + this.disabledAttributeDefinition = attributeDefinitions.forName(TEMPLATE_MODE, DISABLED_ATTR_NAME); + this.multipleAttributeDefinition = attributeDefinitions.forName(TEMPLATE_MODE, MULTIPLE_ATTR_NAME); + } + + + + + private boolean matchesDiscriminator(final IProcessableElementTag tag) { + + if (this.discriminatorAttrName == null) { + return true; + } + final boolean hasDiscriminatorAttr = tag.hasAttribute(this.discriminatorAttributeDefinition.getAttributeName()); + if (this.discriminatorAttrValues == null || this.discriminatorAttrValues.length == 0) { + return hasDiscriminatorAttr; + } + final String discriminatorTagValue = + (hasDiscriminatorAttr? tag.getAttributeValue(this.discriminatorAttributeDefinition.getAttributeName()) : null); + for (int i = 0; i < this.discriminatorAttrValues.length; i++) { + final String discriminatorAttrValue = this.discriminatorAttrValues[i]; + if (discriminatorAttrValue == null) { + if (!hasDiscriminatorAttr || discriminatorTagValue == null) { + return true; + } + } else if (discriminatorAttrValue.equals(discriminatorTagValue)) { + return true; + } + } + return false; + + } + + + + @Override + protected void doProcess( + final ITemplateContext context, + final IProcessableElementTag tag, + final AttributeName attributeName, final String attributeValue, + final IElementTagStructureHandler structureHandler) { + + /* + * First thing to check is whether this processor really matches, because so far we have asked the engine only + * to match per attribute (th:field) and host tag (input, select, option...) but we still don't know if the + * match is complete because we might still need to assess for example that the 'type' attribute has the + * correct value. For example, the same processor will not be executing on + * and on + */ + if (!matchesDiscriminator(tag)) { + // Note in this case we do not have to remove the th:field attribute because the correct processor is still + // to be executed! + return; + } + + if (this.removeAttribute) { + structureHandler.removeAttribute(attributeName); + } + + final IThymeleafBindStatus bindStatus = FieldUtils.getBindStatus(context, attributeValue); + + if (bindStatus == null) { + throw new TemplateProcessingException( + "Cannot process attribute '" + attributeName + "': no associated BindStatus could be found for " + + "the intended form binding operations. This can be due to the lack of a proper management of the " + + "Spring RequestContext, which is usually done through the ThymeleafView or ThymeleafReactiveView"); + } + + // We set the BindStatus into a local variable just in case we have more BindStatus-related processors to + // be applied for the same tag, like for example a th:errorclass + structureHandler.setLocalVariable(SpringContextVariableNames.THYMELEAF_FIELD_BIND_STATUS, bindStatus); + + doProcess(context, tag, attributeName, attributeValue, bindStatus, structureHandler); + + } + + + + + protected abstract void doProcess( + final ITemplateContext context, + final IProcessableElementTag tag, + final AttributeName attributeName, + final String attributeValue, + final IThymeleafBindStatus bindStatus, + final IElementTagStructureHandler structureHandler); + + + + + + // This method is designed to be called from the diverse subclasses + protected final String computeId( + final ITemplateContext context, + final IProcessableElementTag tag, + final String name, final boolean sequence) { + + String id = tag.getAttributeValue(this.idAttributeDefinition.getAttributeName()); + if (!org.thymeleaf.util.StringUtils.isEmptyOrWhitespace(id)) { + return (StringUtils.hasText(id) ? id : null); + } + + id = FieldUtils.idFromName(name); + if (sequence) { + final Integer count = context.getIdentifierSequences().getAndIncrementIDSeq(id); + return id + count.toString(); + } + return id; + + } + + + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringActionTagProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringActionTagProcessor.java new file mode 100644 index 00000000..d5738ecb --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringActionTagProcessor.java @@ -0,0 +1,150 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.processor; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.engine.AttributeDefinition; +import org.thymeleaf.engine.AttributeDefinitions; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.engine.IAttributeDefinitionsAware; +import org.thymeleaf.model.AttributeValueQuotes; +import org.thymeleaf.model.IModel; +import org.thymeleaf.model.IModelFactory; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.model.IStandaloneElementTag; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.spring5.requestdata.RequestDataValueProcessorUtils; +import org.thymeleaf.standard.processor.AbstractStandardExpressionAttributeTagProcessor; +import org.thymeleaf.standard.util.StandardProcessorUtils; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.util.Validate; +import org.unbescape.html.HtmlEscape; + + +/** + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class SpringActionTagProcessor + extends AbstractStandardExpressionAttributeTagProcessor + implements IAttributeDefinitionsAware { + + + public static final int ATTR_PRECEDENCE = 1000; + public static final String TARGET_ATTR_NAME = "action"; + + private static final TemplateMode TEMPLATE_MODE = TemplateMode.HTML; + + private static final String METHOD_ATTR_NAME = "method"; + private static final String TYPE_ATTR_NAME = "type"; + private static final String NAME_ATTR_NAME = "name"; + private static final String VALUE_ATTR_NAME = "value"; + private static final String METHOD_ATTR_DEFAULT_VALUE = "GET"; + + private AttributeDefinition targetAttributeDefinition; + private AttributeDefinition methodAttributeDefinition; + + + + + + public SpringActionTagProcessor(final String dialectPrefix) { + super(TEMPLATE_MODE, dialectPrefix, TARGET_ATTR_NAME, ATTR_PRECEDENCE, false, false); + } + + + + + public void setAttributeDefinitions(final AttributeDefinitions attributeDefinitions) { + Validate.notNull(attributeDefinitions, "Attribute Definitions cannot be null"); + // We precompute the AttributeDefinitions in order to being able to use much + // faster methods for setting/replacing attributes on the ElementAttributes implementation + this.targetAttributeDefinition = attributeDefinitions.forName(TEMPLATE_MODE, TARGET_ATTR_NAME); + this.methodAttributeDefinition = attributeDefinitions.forName(TEMPLATE_MODE, METHOD_ATTR_NAME); + } + + + + + @Override + protected final void doProcess( + final ITemplateContext context, + final IProcessableElementTag tag, + final AttributeName attributeName, final String attributeValue, + final Object expressionResult, + final IElementTagStructureHandler structureHandler) { + + String newAttributeValue = HtmlEscape.escapeHtml4Xml(expressionResult == null ? "" : expressionResult.toString()); + + // But before setting the 'action' attribute, we need to verify the 'method' attribute and let the + // RequestDataValueProcessor act on it. + final String methodAttributeValue = tag.getAttributeValue(this.methodAttributeDefinition.getAttributeName()); + final String httpMethod = methodAttributeValue == null ? METHOD_ATTR_DEFAULT_VALUE : methodAttributeValue; + + // Let RequestDataValueProcessor modify the attribute value if needed + newAttributeValue = RequestDataValueProcessorUtils.processAction(context, newAttributeValue, httpMethod); + + // Set the 'action' attribute + StandardProcessorUtils.replaceAttribute( + structureHandler, attributeName, this.targetAttributeDefinition, TARGET_ATTR_NAME, (newAttributeValue == null? "" : newAttributeValue)); + + // If this th:action is in a

tag, we might need to add a hidden field (depending on Spring configuration) + if ("form".equalsIgnoreCase(tag.getElementCompleteName())) { + + final Map extraHiddenFields = + RequestDataValueProcessorUtils.getExtraHiddenFields(context); + + if (extraHiddenFields != null && extraHiddenFields.size() > 0) { + + final IModelFactory modelFactory = context.getModelFactory(); + + final IModel extraHiddenElementTags = modelFactory.createModel(); + + for (final Map.Entry extraHiddenField : extraHiddenFields.entrySet()) { + + final Map extraHiddenAttributes = new LinkedHashMap(4,1.0f); + extraHiddenAttributes.put(TYPE_ATTR_NAME, "hidden"); + extraHiddenAttributes.put(NAME_ATTR_NAME, extraHiddenField.getKey()); + extraHiddenAttributes.put(VALUE_ATTR_NAME, extraHiddenField.getValue()); // no need to re-apply the processor here + + final IStandaloneElementTag extraHiddenElementTag = + modelFactory.createStandaloneElementTag("input", extraHiddenAttributes, AttributeValueQuotes.DOUBLE, false, true); + + extraHiddenElementTags.add(extraHiddenElementTag); + + } + + structureHandler.insertImmediatelyAfter(extraHiddenElementTags, false); + + } + + } + + } + + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringErrorClassTagProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringErrorClassTagProcessor.java new file mode 100644 index 00000000..d6d47d66 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringErrorClassTagProcessor.java @@ -0,0 +1,192 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.processor; + +import java.util.Arrays; + +import org.thymeleaf.IEngineConfiguration; +import org.thymeleaf.context.IExpressionContext; +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.engine.AttributeDefinition; +import org.thymeleaf.engine.AttributeDefinitions; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.engine.AttributeNames; +import org.thymeleaf.engine.IAttributeDefinitionsAware; +import org.thymeleaf.exceptions.TemplateProcessingException; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.processor.element.AbstractAttributeTagProcessor; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.spring5.context.IThymeleafBindStatus; +import org.thymeleaf.spring5.naming.SpringContextVariableNames; +import org.thymeleaf.spring5.util.FieldUtils; +import org.thymeleaf.standard.expression.IStandardExpression; +import org.thymeleaf.standard.expression.IStandardExpressionParser; +import org.thymeleaf.standard.expression.StandardExpressions; +import org.thymeleaf.standard.expression.VariableExpression; +import org.thymeleaf.standard.util.StandardProcessorUtils; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.util.StringUtils; +import org.thymeleaf.util.Validate; +import org.unbescape.html.HtmlEscape; + +/** + * Adds the given class to the field on which this attribute is applied, if that + * field contains errors. It's similar to a combination of {@code th:classappend} + * with a {@code ${#fields.hasErrors()}} expression. + * + * @author Daniel Fernández + * @since 3.0.3 + */ +public final class SpringErrorClassTagProcessor + extends AbstractAttributeTagProcessor + implements IAttributeDefinitionsAware { + + public static final int ATTR_PRECEDENCE = 1800; + public static final String ATTR_NAME = "errorclass"; + public static final String TARGET_ATTR_NAME = "class"; + + private static final TemplateMode TEMPLATE_MODE = TemplateMode.HTML; + + private AttributeDefinition targetAttributeDefinition; + + + + + public SpringErrorClassTagProcessor(final String dialectPrefix) { + super(TEMPLATE_MODE, dialectPrefix, null, false, ATTR_NAME, true,ATTR_PRECEDENCE, true); + } + + + + + public void setAttributeDefinitions(final AttributeDefinitions attributeDefinitions) { + Validate.notNull(attributeDefinitions, "Attribute Definitions cannot be null"); + // We precompute the AttributeDefinition of the target attribute in order to being able to use much + // faster methods for setting/replacing attributes on the ElementAttributes implementation + this.targetAttributeDefinition = attributeDefinitions.forName(TEMPLATE_MODE, TARGET_ATTR_NAME); + } + + + + + @Override + protected final void doProcess( + final ITemplateContext context, + final IProcessableElementTag tag, + final AttributeName attributeName, final String attributeValue, + final IElementTagStructureHandler structureHandler) { + + final IThymeleafBindStatus bindStatus = computeBindStatus(context, tag); + if (bindStatus == null) { + final AttributeName fieldAttributeName = + AttributeNames.forHTMLName(attributeName.getPrefix(), AbstractSpringFieldTagProcessor.ATTR_NAME); + throw new TemplateProcessingException( + "Cannot apply \"" + attributeName + "\": this attribute requires the existence of " + + "a \"name\" (or " + Arrays.asList(fieldAttributeName.getCompleteAttributeNames()) + ") attribute " + + "with non-empty value in the same host tag."); + } + + if (bindStatus.isError()) { + + final IEngineConfiguration configuration = context.getConfiguration(); + final IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration); + + final IStandardExpression expression = expressionParser.parseExpression(context, attributeValue); + final Object expressionResult = expression.execute(context); + + String newAttributeValue = HtmlEscape.escapeHtml4Xml(expressionResult == null ? null : expressionResult.toString()); + + // If we are not adding anything, we'll just leave it untouched + if (newAttributeValue != null && newAttributeValue.length() > 0) { + + final AttributeName targetAttributeName = this.targetAttributeDefinition.getAttributeName(); + + if (tag.hasAttribute(targetAttributeName)) { + final String currentValue = tag.getAttributeValue(targetAttributeName); + if (currentValue.length() > 0) { + newAttributeValue = currentValue + ' ' + newAttributeValue; + } + } + + StandardProcessorUtils.setAttribute(structureHandler, this.targetAttributeDefinition, TARGET_ATTR_NAME, newAttributeValue); + + } + + } + + } + + + + + /* + * There are two scenarios for a th:errorclass to appear in: one is in an element for which a th:field has already + * been executed, in which case we already have a BindStatus to check for errors; and the other one is an element + * for which a th:field has not been executed, but which should have a "name" attribute (either directly or as + * the result of executing a th:name) -- in this case, we'll have to build the BuildStatus ourselves. + */ + private static IThymeleafBindStatus computeBindStatus(final IExpressionContext context, final IProcessableElementTag tag) { + + /* + * First, try to obtain an already-existing BindStatus resulting from the execution of a th:field attribute + * in the same element. + */ + final IThymeleafBindStatus bindStatus = + (IThymeleafBindStatus) context.getVariable(SpringContextVariableNames.THYMELEAF_FIELD_BIND_STATUS); + if (bindStatus != null) { + return bindStatus; + } + + /* + * It seems no th:field was executed on the same element, so we must rely on the "name" attribute (probably + * specified by hand or by a th:name). No th:field was executed, so no BindStatus available -- we'll have to + * build it ourselves. + */ + final String fieldName = tag.getAttributeValue("name"); + if (StringUtils.isEmptyOrWhitespace(fieldName)) { + return null; + } + + final VariableExpression boundExpression = + (VariableExpression) context.getVariable(SpringContextVariableNames.SPRING_BOUND_OBJECT_EXPRESSION); + + if (boundExpression == null) { + // No bound expression, so just use the field name + return FieldUtils.getBindStatusFromParsedExpression(context, false, fieldName); + } + + // Bound object and field object names might intersect (e.g. th:object="a.b", name="b.c"), and we must compute + // the real 'bindable' name ("a.b.c") by only using the first token in the bound object name, appending the + // rest of the field name: "a" + "b.c" -> "a.b.c" + final String boundExpressionStr = boundExpression.getExpression(); + final String computedFieldName; + if (boundExpressionStr.indexOf('.') == -1) { + computedFieldName = boundExpressionStr + '.' + fieldName; // we append because we will use no form root afterwards + } else { + computedFieldName = boundExpressionStr.substring(0, boundExpressionStr.indexOf('.')) + '.' + fieldName; + } + + // We set "useRoot" to false because we have already computed that part + return FieldUtils.getBindStatusFromParsedExpression(context, false, computedFieldName); + + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringErrorsTagProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringErrorsTagProcessor.java new file mode 100644 index 00000000..efcbe5ba --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringErrorsTagProcessor.java @@ -0,0 +1,94 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.processor; + +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.processor.element.AbstractAttributeTagProcessor; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.spring5.context.IThymeleafBindStatus; +import org.thymeleaf.spring5.naming.SpringContextVariableNames; +import org.thymeleaf.spring5.util.FieldUtils; +import org.thymeleaf.spring5.util.SpringValueFormatter; +import org.thymeleaf.templatemode.TemplateMode; +import org.unbescape.html.HtmlEscape; + +/** + * Works in a similar way to #fields.errors(), but lists all errors for + * the given field name, separated by a <br/> + * + * @author Daniel Fernández + * @since 3.0.3 + */ +public final class SpringErrorsTagProcessor extends AbstractAttributeTagProcessor { + + private static final String ERROR_DELIMITER = "
"; + + public static final int ATTR_PRECEDENCE = 1700; + public static final String ATTR_NAME = "errors"; + + + + + + public SpringErrorsTagProcessor(final String dialectPrefix) { + super(TemplateMode.HTML, dialectPrefix, null, false, ATTR_NAME, true, ATTR_PRECEDENCE, true); + } + + + + + @Override + protected void doProcess( + final ITemplateContext context, final IProcessableElementTag tag, + final AttributeName attributeName, final String attributeValue, + final IElementTagStructureHandler structureHandler) { + + final IThymeleafBindStatus bindStatus = FieldUtils.getBindStatus(context, attributeValue); + + if (bindStatus.isError()) { + + final StringBuilder strBuilder = new StringBuilder(); + final String[] errorMsgs = bindStatus.getErrorMessages(); + + for (int i = 0; i < errorMsgs.length; i++) { + if (i > 0) { + strBuilder.append(ERROR_DELIMITER); + } + final String displayString = SpringValueFormatter.getDisplayString(errorMsgs[i], false); + strBuilder.append(HtmlEscape.escapeHtml4Xml(displayString)); + } + + structureHandler.setBody(strBuilder.toString(), false); + + // Just in case we also have a th:errorclass in this tag + structureHandler.setLocalVariable(SpringContextVariableNames.THYMELEAF_FIELD_BIND_STATUS, bindStatus); + + } else { + + structureHandler.removeElement(); + + } + + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringHrefTagProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringHrefTagProcessor.java new file mode 100644 index 00000000..b654dffe --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringHrefTagProcessor.java @@ -0,0 +1,95 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.processor; + +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.engine.AttributeDefinition; +import org.thymeleaf.engine.AttributeDefinitions; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.engine.IAttributeDefinitionsAware; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.spring5.requestdata.RequestDataValueProcessorUtils; +import org.thymeleaf.standard.processor.AbstractStandardExpressionAttributeTagProcessor; +import org.thymeleaf.standard.util.StandardProcessorUtils; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.util.Validate; +import org.unbescape.html.HtmlEscape; + + +/** + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class SpringHrefTagProcessor + extends AbstractStandardExpressionAttributeTagProcessor + implements IAttributeDefinitionsAware { + + + public static final int ATTR_PRECEDENCE = 1000; + public static final String ATTR_NAME = "href"; + + private static final TemplateMode TEMPLATE_MODE = TemplateMode.HTML; + + private AttributeDefinition targetAttributeDefinition; + + + + + public SpringHrefTagProcessor(final String dialectPrefix) { + super(TEMPLATE_MODE, dialectPrefix, ATTR_NAME, ATTR_PRECEDENCE, false, true); + } + + + + + public void setAttributeDefinitions(final AttributeDefinitions attributeDefinitions) { + Validate.notNull(attributeDefinitions, "Attribute Definitions cannot be null"); + // We precompute the AttributeDefinition of the target attribute in order to being able to use much + // faster methods for setting/replacing attributes on the ElementAttributes implementation + this.targetAttributeDefinition = attributeDefinitions.forName(TEMPLATE_MODE, ATTR_NAME); + } + + + + + @Override + protected final void doProcess( + final ITemplateContext context, + final IProcessableElementTag tag, + final AttributeName attributeName, final String attributeValue, + final Object expressionResult, + final IElementTagStructureHandler structureHandler) { + + String newAttributeValue = HtmlEscape.escapeHtml4Xml(expressionResult == null ? "" : expressionResult.toString()); + + // Let RequestDataValueProcessor modify the attribute value if needed + newAttributeValue = RequestDataValueProcessorUtils.processUrl(context, newAttributeValue); + + // Set the real, non prefixed attribute + StandardProcessorUtils.replaceAttribute(structureHandler, attributeName, this.targetAttributeDefinition, ATTR_NAME, (newAttributeValue == null ? "" : newAttributeValue)); + + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputCheckboxFieldTagProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputCheckboxFieldTagProcessor.java new file mode 100644 index 00000000..9713e9ab --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputCheckboxFieldTagProcessor.java @@ -0,0 +1,169 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.processor; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.web.bind.WebDataBinder; +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.exceptions.TemplateProcessingException; +import org.thymeleaf.model.AttributeValueQuotes; +import org.thymeleaf.model.IModel; +import org.thymeleaf.model.IModelFactory; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.model.IStandaloneElementTag; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.spring5.context.IThymeleafBindStatus; +import org.thymeleaf.spring5.dialect.SpringStandardDialect; +import org.thymeleaf.spring5.requestdata.RequestDataValueProcessorUtils; +import org.thymeleaf.spring5.util.SpringSelectedValueComparator; +import org.thymeleaf.standard.util.StandardProcessorUtils; +import org.unbescape.html.HtmlEscape; + + +/** + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class SpringInputCheckboxFieldTagProcessor + extends AbstractSpringFieldTagProcessor { + + + public static final String CHECKBOX_INPUT_TYPE_ATTR_VALUE = "checkbox"; + + private final boolean renderHiddenMarkersBeforeCheckboxes; + + + + + public SpringInputCheckboxFieldTagProcessor(final String dialectPrefix) { + this(dialectPrefix, SpringStandardDialect.DEFAULT_RENDER_HIDDEN_MARKERS_BEFORE_CHECKBOXES); + } + + + public SpringInputCheckboxFieldTagProcessor(final String dialectPrefix, final boolean renderHiddenMarkersBeforeCheckboxes) { + super(dialectPrefix, INPUT_TAG_NAME, TYPE_ATTR_NAME, new String[] { CHECKBOX_INPUT_TYPE_ATTR_VALUE }, true); + this.renderHiddenMarkersBeforeCheckboxes = renderHiddenMarkersBeforeCheckboxes; + } + + + + + @Override + protected void doProcess(final ITemplateContext context, + final IProcessableElementTag tag, + final AttributeName attributeName, final String attributeValue, + final IThymeleafBindStatus bindStatus, final IElementTagStructureHandler structureHandler) { + + String name = bindStatus.getExpression(); + name = (name == null? "" : name); + + final String id = computeId(context, tag, name, true); + + String value = null; + boolean checked = false; + + Object boundValue = bindStatus.getValue(); + final Class valueType = bindStatus.getValueType(); + + if (Boolean.class.equals(valueType) || boolean.class.equals(valueType)) { + + if (boundValue instanceof String) { + boundValue = Boolean.valueOf((String) boundValue); + } + final Boolean booleanValue = (boundValue != null ? (Boolean) boundValue : Boolean.FALSE); + value = "true"; + checked = booleanValue.booleanValue(); + + } else { + + value = tag.getAttributeValue(this.valueAttributeDefinition.getAttributeName()); + if (value == null) { + throw new TemplateProcessingException( + "Attribute \"value\" is required in \"input(checkbox)\" tags " + + "when binding to non-boolean values"); + } + + checked = SpringSelectedValueComparator.isSelected(bindStatus, HtmlEscape.unescapeHtml(value)); + + } + + StandardProcessorUtils.setAttribute(structureHandler, this.idAttributeDefinition, ID_ATTR_NAME, id); // No need to escape: this comes from an existing 'id' or from a token + StandardProcessorUtils.setAttribute(structureHandler, this.nameAttributeDefinition, NAME_ATTR_NAME, name); // No need to escape: this is a java-valid token + StandardProcessorUtils.setAttribute( + structureHandler, this.valueAttributeDefinition, VALUE_ATTR_NAME, RequestDataValueProcessorUtils.processFormFieldValue(context, name, value, "checkbox")); + if (checked) { + StandardProcessorUtils.setAttribute(structureHandler, this.checkedAttributeDefinition, CHECKED_ATTR_NAME, CHECKED_ATTR_NAME); + } else { + structureHandler.removeAttribute(this.checkedAttributeDefinition.getAttributeName()); + } + + + if (!isDisabled(tag)) { + + /* + * Non-disabled checkboxes need an additional in order to note their presence in + * the HTML document. Given unchecked checkboxes are not sent by browsers as a result of form submission, + * this is the only way to differentiate between a checkbox that is unchecked and a checkbox that was + * never displayed or is disabled. + */ + + final IModelFactory modelFactory = context.getModelFactory(); + + final IModel hiddenTagModel = modelFactory.createModel(); + + final String hiddenName = WebDataBinder.DEFAULT_FIELD_MARKER_PREFIX + name; + final String hiddenValue = "on"; + + final Map hiddenAttributes = new LinkedHashMap(4,1.0f); + hiddenAttributes.put(TYPE_ATTR_NAME, "hidden"); + hiddenAttributes.put(NAME_ATTR_NAME, hiddenName); + hiddenAttributes.put(VALUE_ATTR_NAME, RequestDataValueProcessorUtils.processFormFieldValue(context, hiddenName, hiddenValue, "hidden")); + + final IStandaloneElementTag hiddenTag = + modelFactory.createStandaloneElementTag(INPUT_TAG_NAME, hiddenAttributes, AttributeValueQuotes.DOUBLE, false, true); + + hiddenTagModel.add(hiddenTag); + + if (this.renderHiddenMarkersBeforeCheckboxes) { + structureHandler.insertBefore(hiddenTagModel); + } else { + structureHandler.insertImmediatelyAfter(hiddenTagModel, false); + } + + } + + + } + + + private final boolean isDisabled(final IProcessableElementTag tag) { + // Disabled = attribute "disabled" exists + return tag.hasAttribute(this.disabledAttributeDefinition.getAttributeName()); + } + + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputFileFieldTagProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputFileFieldTagProcessor.java new file mode 100644 index 00000000..a693b187 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputFileFieldTagProcessor.java @@ -0,0 +1,70 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.processor; + +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.spring5.context.IThymeleafBindStatus; +import org.thymeleaf.standard.util.StandardProcessorUtils; + + +/** + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class SpringInputFileFieldTagProcessor extends AbstractSpringFieldTagProcessor { + + + public static final String FILE_INPUT_TYPE_ATTR_VALUE = "file"; + + + + + public SpringInputFileFieldTagProcessor(final String dialectPrefix) { + super(dialectPrefix, INPUT_TAG_NAME, TYPE_ATTR_NAME, new String[] { FILE_INPUT_TYPE_ATTR_VALUE }, true); + } + + + + + @Override + protected void doProcess(final ITemplateContext context, + final IProcessableElementTag tag, + final AttributeName attributeName, final String attributeValue, + final IThymeleafBindStatus bindStatus, final IElementTagStructureHandler structureHandler) { + + String name = bindStatus.getExpression(); + name = (name == null? "" : name); + + final String id = computeId(context, tag, name, false); + + StandardProcessorUtils.setAttribute(structureHandler, this.idAttributeDefinition, ID_ATTR_NAME, id); // No need to escape: this comes from an existing 'id' or from a token + StandardProcessorUtils.setAttribute(structureHandler, this.nameAttributeDefinition, NAME_ATTR_NAME, name); // No need to escape: this is a java-valid token + + } + + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputGeneralFieldTagProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputGeneralFieldTagProcessor.java new file mode 100644 index 00000000..66797d69 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputGeneralFieldTagProcessor.java @@ -0,0 +1,128 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.processor; + +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.spring5.context.IThymeleafBindStatus; +import org.thymeleaf.spring5.requestdata.RequestDataValueProcessorUtils; +import org.thymeleaf.spring5.util.SpringValueFormatter; +import org.thymeleaf.standard.util.StandardProcessorUtils; + + +/** + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class SpringInputGeneralFieldTagProcessor + extends AbstractSpringFieldTagProcessor { + + + // HTML4 input types + public static final String TEXT_INPUT_TYPE_ATTR_VALUE = "text"; + public static final String HIDDEN_INPUT_TYPE_ATTR_VALUE = "hidden"; + // HTML5-specific input types + public static final String DATETIME_INPUT_TYPE_ATTR_VALUE = "datetime"; + public static final String DATETIMELOCAL_INPUT_TYPE_ATTR_VALUE = "datetime-local"; + public static final String DATE_INPUT_TYPE_ATTR_VALUE = "date"; + public static final String MONTH_INPUT_TYPE_ATTR_VALUE = "month"; + public static final String TIME_INPUT_TYPE_ATTR_VALUE = "time"; + public static final String WEEK_INPUT_TYPE_ATTR_VALUE = "week"; + public static final String NUMBER_INPUT_TYPE_ATTR_VALUE = "number"; + public static final String RANGE_INPUT_TYPE_ATTR_VALUE = "range"; + public static final String EMAIL_INPUT_TYPE_ATTR_VALUE = "email"; + public static final String URL_INPUT_TYPE_ATTR_VALUE = "url"; + public static final String SEARCH_INPUT_TYPE_ATTR_VALUE = "search"; + public static final String TEL_INPUT_TYPE_ATTR_VALUE = "tel"; + public static final String COLOR_INPUT_TYPE_ATTR_VALUE = "color"; + + + private static final String[] ALL_TYPE_ATTR_VALUES = + new String[] { + null, + TEXT_INPUT_TYPE_ATTR_VALUE, + HIDDEN_INPUT_TYPE_ATTR_VALUE, + DATETIME_INPUT_TYPE_ATTR_VALUE, + DATETIMELOCAL_INPUT_TYPE_ATTR_VALUE, + DATE_INPUT_TYPE_ATTR_VALUE, + MONTH_INPUT_TYPE_ATTR_VALUE, + TIME_INPUT_TYPE_ATTR_VALUE, + WEEK_INPUT_TYPE_ATTR_VALUE, + NUMBER_INPUT_TYPE_ATTR_VALUE, + RANGE_INPUT_TYPE_ATTR_VALUE, + EMAIL_INPUT_TYPE_ATTR_VALUE, + URL_INPUT_TYPE_ATTR_VALUE, + SEARCH_INPUT_TYPE_ATTR_VALUE, + TEL_INPUT_TYPE_ATTR_VALUE, + COLOR_INPUT_TYPE_ATTR_VALUE + }; + + + + + public SpringInputGeneralFieldTagProcessor(final String dialectPrefix) { + super(dialectPrefix, INPUT_TAG_NAME, TYPE_ATTR_NAME, ALL_TYPE_ATTR_VALUES, true); + } + + + + + @Override + protected void doProcess(final ITemplateContext context, + final IProcessableElementTag tag, + final AttributeName attributeName, final String attributeValue, + final IThymeleafBindStatus bindStatus, final IElementTagStructureHandler structureHandler) { + + String name = bindStatus.getExpression(); + name = (name == null? "" : name); + + final String id = computeId(context, tag, name, false); + + // Thanks to precedence, this should have already been computed + final String type = tag.getAttributeValue(this.typeAttributeDefinition.getAttributeName()); + + // Apply the conversions (editor), depending on type (no conversion for "number" and "range" + // Also, no escaping needed as attribute values are always escaped by default + final String value = + applyConversion(type)? + SpringValueFormatter.getDisplayString(bindStatus.getValue(), bindStatus.getEditor(), true) : + SpringValueFormatter.getDisplayString(bindStatus.getActualValue(), true); + + StandardProcessorUtils.setAttribute(structureHandler, this.idAttributeDefinition, ID_ATTR_NAME, id); // No need to escape: this comes from an existing 'id' or from a token + StandardProcessorUtils.setAttribute(structureHandler, this.nameAttributeDefinition, NAME_ATTR_NAME, name); // No need to escape: this is a java-valid token + + StandardProcessorUtils.setAttribute( + structureHandler, this.valueAttributeDefinition, VALUE_ATTR_NAME, RequestDataValueProcessorUtils.processFormFieldValue(context, name, value, type)); + + } + + + + private static boolean applyConversion(final String type) { + return !(type != null && ("number".equalsIgnoreCase(type) || "range".equalsIgnoreCase(type))); + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputPasswordFieldTagProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputPasswordFieldTagProcessor.java new file mode 100644 index 00000000..0fb0c422 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputPasswordFieldTagProcessor.java @@ -0,0 +1,76 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.processor; + +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.spring5.context.IThymeleafBindStatus; +import org.thymeleaf.spring5.requestdata.RequestDataValueProcessorUtils; +import org.thymeleaf.standard.util.StandardProcessorUtils; + + +/** + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class SpringInputPasswordFieldTagProcessor extends AbstractSpringFieldTagProcessor { + + + + public static final String PASSWORD_INPUT_TYPE_ATTR_VALUE = "password"; + + + + + + public SpringInputPasswordFieldTagProcessor(final String dialectPrefix) { + super(dialectPrefix, INPUT_TAG_NAME, TYPE_ATTR_NAME, new String[] { PASSWORD_INPUT_TYPE_ATTR_VALUE }, true); + } + + + + + @Override + protected void doProcess(final ITemplateContext context, + final IProcessableElementTag tag, + final AttributeName attributeName, final String attributeValue, + final IThymeleafBindStatus bindStatus, final IElementTagStructureHandler structureHandler) { + + String name = bindStatus.getExpression(); + name = (name == null? "" : name); + + final String id = computeId(context, tag, name, false); + + StandardProcessorUtils.setAttribute(structureHandler, this.idAttributeDefinition, ID_ATTR_NAME, id); // No need to escape: this comes from an existing 'id' or from a token + StandardProcessorUtils.setAttribute(structureHandler, this.nameAttributeDefinition, NAME_ATTR_NAME, name); // No need to escape: this is a java-valid token + + StandardProcessorUtils.setAttribute( + structureHandler, this.valueAttributeDefinition, VALUE_ATTR_NAME, RequestDataValueProcessorUtils.processFormFieldValue(context, name, "", "password")); + + } + + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputRadioFieldTagProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputRadioFieldTagProcessor.java new file mode 100644 index 00000000..667f5caf --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringInputRadioFieldTagProcessor.java @@ -0,0 +1,92 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.processor; + +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.exceptions.TemplateProcessingException; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.spring5.context.IThymeleafBindStatus; +import org.thymeleaf.spring5.requestdata.RequestDataValueProcessorUtils; +import org.thymeleaf.spring5.util.SpringSelectedValueComparator; +import org.thymeleaf.standard.util.StandardProcessorUtils; +import org.unbescape.html.HtmlEscape; + + +/** + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class SpringInputRadioFieldTagProcessor extends AbstractSpringFieldTagProcessor { + + + public static final String RADIO_INPUT_TYPE_ATTR_VALUE = "radio"; + + + + + public SpringInputRadioFieldTagProcessor(final String dialectPrefix) { + super(dialectPrefix, INPUT_TAG_NAME, TYPE_ATTR_NAME, new String[] { RADIO_INPUT_TYPE_ATTR_VALUE }, true); + } + + + + + @Override + protected void doProcess(final ITemplateContext context, + final IProcessableElementTag tag, + final AttributeName attributeName, final String attributeValue, + final IThymeleafBindStatus bindStatus, final IElementTagStructureHandler structureHandler) { + + String name = bindStatus.getExpression(); + name = (name == null? "" : name); + + final String id = computeId(context, tag, name, true); + + final String value = tag.getAttributeValue(this.valueAttributeDefinition.getAttributeName()); + if (value == null) { + throw new TemplateProcessingException( + "Attribute \"value\" is required in \"input(radio)\" tags"); + } + + final boolean checked = + SpringSelectedValueComparator.isSelected(bindStatus, HtmlEscape.unescapeHtml(value)); + + + StandardProcessorUtils.setAttribute(structureHandler, this.idAttributeDefinition, ID_ATTR_NAME, id); // No need to escape: this comes from an existing 'id' or from a token + StandardProcessorUtils.setAttribute(structureHandler, this.nameAttributeDefinition, NAME_ATTR_NAME, name); // No need to escape: this is a java-valid token + StandardProcessorUtils.setAttribute( + structureHandler, this.valueAttributeDefinition, VALUE_ATTR_NAME, RequestDataValueProcessorUtils.processFormFieldValue(context, name, value, "radio")); + + if (checked) { + StandardProcessorUtils.setAttribute(structureHandler, this.checkedAttributeDefinition, CHECKED_ATTR_NAME, CHECKED_ATTR_NAME); + } else { + structureHandler.removeAttribute(this.checkedAttributeDefinition.getAttributeName()); + } + + } + + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringMethodTagProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringMethodTagProcessor.java new file mode 100644 index 00000000..2152f8e7 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringMethodTagProcessor.java @@ -0,0 +1,154 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.processor; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.engine.AttributeDefinition; +import org.thymeleaf.engine.AttributeDefinitions; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.engine.IAttributeDefinitionsAware; +import org.thymeleaf.model.AttributeValueQuotes; +import org.thymeleaf.model.IModel; +import org.thymeleaf.model.IModelFactory; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.model.IStandaloneElementTag; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.spring5.requestdata.RequestDataValueProcessorUtils; +import org.thymeleaf.standard.processor.AbstractStandardExpressionAttributeTagProcessor; +import org.thymeleaf.standard.util.StandardProcessorUtils; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.util.Validate; +import org.unbescape.html.HtmlEscape; + + +/** + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class SpringMethodTagProcessor + extends AbstractStandardExpressionAttributeTagProcessor + implements IAttributeDefinitionsAware { + + + public static final int ATTR_PRECEDENCE = 990; + public static final String TARGET_ATTR_NAME = "method"; + + private static final TemplateMode TEMPLATE_MODE = TemplateMode.HTML; + + private static final String TYPE_ATTR_NAME = "type"; + private static final String NAME_ATTR_NAME = "name"; + private static final String VALUE_ATTR_NAME = "value"; + + private AttributeDefinition targetAttributeDefinition; + + + + + public SpringMethodTagProcessor(final String dialectPrefix) { + super(TEMPLATE_MODE, dialectPrefix, TARGET_ATTR_NAME, ATTR_PRECEDENCE, false, false); + } + + + + + public void setAttributeDefinitions(final AttributeDefinitions attributeDefinitions) { + Validate.notNull(attributeDefinitions, "Attribute Definitions cannot be null"); + // We precompute the AttributeDefinitions in order to being able to use much + // faster methods for setting/replacing attributes on the ElementAttributes implementation + this.targetAttributeDefinition = attributeDefinitions.forName(TEMPLATE_MODE, TARGET_ATTR_NAME); + } + + + + + @Override + protected final void doProcess( + final ITemplateContext context, + final IProcessableElementTag tag, + final AttributeName attributeName, final String attributeValue, + final Object expressionResult, + final IElementTagStructureHandler structureHandler) { + + final String newAttributeValue = HtmlEscape.escapeHtml4Xml(expressionResult == null ? null : expressionResult.toString()); + + // Set the 'method' attribute, or remove it if evaluated to null + if (newAttributeValue == null || newAttributeValue.length() == 0) { + structureHandler.removeAttribute(this.targetAttributeDefinition.getAttributeName()); + structureHandler.removeAttribute(attributeName); + } else { + StandardProcessorUtils.replaceAttribute(structureHandler, attributeName, this.targetAttributeDefinition, TARGET_ATTR_NAME, newAttributeValue); + } + + // If this th:action is in a tag, we might need to add a hidden field for non-supported HTTP methods + if (newAttributeValue != null && "form".equalsIgnoreCase(tag.getElementCompleteName())) { + + if (!isMethodBrowserSupported(newAttributeValue)) { + + // Browsers only support HTTP GET and POST. If a different method + // has been specified, then Spring MVC allows us to specify it + // using a hidden input with name '_method' and set 'post' for the + // tag. + + StandardProcessorUtils.setAttribute(structureHandler, this.targetAttributeDefinition, TARGET_ATTR_NAME, "post"); + + final IModelFactory modelFactory = context.getModelFactory(); + + final IModel hiddenMethodModel = modelFactory.createModel(); + + final String type = "hidden"; + final String name = "_method"; + final String value = RequestDataValueProcessorUtils.processFormFieldValue(context, name, newAttributeValue, type); + + final Map hiddenAttributes = new LinkedHashMap(4,1.0f); + hiddenAttributes.put(TYPE_ATTR_NAME, type); + hiddenAttributes.put(NAME_ATTR_NAME, name); + hiddenAttributes.put(VALUE_ATTR_NAME, value); // no need to escape + + final IStandaloneElementTag hiddenMethodElementTag = + modelFactory.createStandaloneElementTag("input", hiddenAttributes, AttributeValueQuotes.DOUBLE, false, true); + + hiddenMethodModel.add(hiddenMethodElementTag); + + structureHandler.insertImmediatelyAfter(hiddenMethodModel, false); + + } + + } + + } + + + + + /* + * Determine if the HTTP method is supported by browsers (i.e. GET or POST). + */ + protected boolean isMethodBrowserSupported(final String method) { + return ("get".equalsIgnoreCase(method) || "post".equalsIgnoreCase(method)); + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringObjectTagProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringObjectTagProcessor.java new file mode 100644 index 00000000..bf96eb85 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringObjectTagProcessor.java @@ -0,0 +1,97 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.processor; + +import java.util.Collections; +import java.util.Map; + +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.exceptions.TemplateProcessingException; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.spring5.naming.SpringContextVariableNames; +import org.thymeleaf.standard.expression.IStandardExpression; +import org.thymeleaf.standard.expression.VariableExpression; +import org.thymeleaf.standard.processor.AbstractStandardTargetSelectionTagProcessor; +import org.thymeleaf.templatemode.TemplateMode; + + +/** + * Specifies an object to use on a <form> + * + * @author Daniel Fernández + * @since 3.0.3 + */ +public final class SpringObjectTagProcessor extends AbstractStandardTargetSelectionTagProcessor { + + + public static final int ATTR_PRECEDENCE = 500; + public static final String ATTR_NAME = "object"; + + + + public SpringObjectTagProcessor(final String dialectPrefix) { + super(TemplateMode.HTML, dialectPrefix, ATTR_NAME, ATTR_PRECEDENCE); + } + + + + + + + + @Override + protected void validateSelectionValue(final ITemplateContext context, + final IProcessableElementTag tag, + final AttributeName attributeName, final String attributeValue, + final IStandardExpression expression) { + + if (expression == null || !(expression instanceof VariableExpression)) { + + throw new TemplateProcessingException( + "The expression used for object selection is " + expression + ", which is not valid: " + + "only variable expressions (${...}) are allowed in '" + attributeName + "' attributes in " + + "Spring-enabled environments."); + + } + + + } + + + + + + @Override + protected Map computeAdditionalLocalVariables( + final ITemplateContext context, + final IProcessableElementTag tag, + final AttributeName attributeName, final String attributeValue, + final IStandardExpression expression) { + + // We set the (parsed) expression itself as a local variable because we might use it at the expression evaluator + return Collections.singletonMap(SpringContextVariableNames.SPRING_BOUND_OBJECT_EXPRESSION, (Object)expression); + + } + + + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringOptionFieldTagProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringOptionFieldTagProcessor.java new file mode 100644 index 00000000..e2b41a28 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringOptionFieldTagProcessor.java @@ -0,0 +1,84 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.processor; + +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.exceptions.TemplateProcessingException; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.spring5.context.IThymeleafBindStatus; +import org.thymeleaf.spring5.requestdata.RequestDataValueProcessorUtils; +import org.thymeleaf.spring5.util.SpringSelectedValueComparator; +import org.thymeleaf.standard.util.StandardProcessorUtils; +import org.unbescape.html.HtmlEscape; + + +/** + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class SpringOptionFieldTagProcessor extends AbstractSpringFieldTagProcessor { + + + + + public SpringOptionFieldTagProcessor(final String dialectPrefix) { + super(dialectPrefix, OPTION_TAG_NAME, null, null, true); + } + + + + @Override + protected void doProcess(final ITemplateContext context, + final IProcessableElementTag tag, + final AttributeName attributeName, final String attributeValue, + final IThymeleafBindStatus bindStatus, final IElementTagStructureHandler structureHandler) { + + String name = bindStatus.getExpression(); + name = (name == null? "" : name); + + final String value = tag.getAttributeValue(this.valueAttributeDefinition.getAttributeName()); + if (value == null) { + throw new TemplateProcessingException( + "Attribute \"value\" is required in \"option\" tags"); + } + + final boolean selected = + SpringSelectedValueComparator.isSelected(bindStatus, HtmlEscape.unescapeHtml(value)); + + StandardProcessorUtils.setAttribute( + structureHandler, + this.valueAttributeDefinition, VALUE_ATTR_NAME, + RequestDataValueProcessorUtils.processFormFieldValue(context, name, value, "option")); + + if (selected) { + StandardProcessorUtils.setAttribute(structureHandler, this.selectedAttributeDefinition, SELECTED_ATTR_NAME, SELECTED_ATTR_NAME); + } else { + structureHandler.removeAttribute(this.selectedAttributeDefinition.getAttributeName()); + } + + } + + +} diff --git a/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringOptionInSelectFieldTagProcessor.java b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringOptionInSelectFieldTagProcessor.java new file mode 100644 index 00000000..43b7b6c6 --- /dev/null +++ b/thymeleaf-spring6/src/main/java/org/thymeleaf/spring5/processor/SpringOptionInSelectFieldTagProcessor.java @@ -0,0 +1,90 @@ +/* + * ============================================================================= + * + * Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org) + * + * 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 org.thymeleaf.spring5.processor; + +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.exceptions.TemplateProcessingException; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.processor.element.AbstractElementTagProcessor; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.templatemode.TemplateMode; + + +/** + * + * @author Daniel Fernández + * + * @since 3.0.3 + * + */ +public final class SpringOptionInSelectFieldTagProcessor extends AbstractElementTagProcessor { + + + // This is 1005 in order to make sure it is executed just before "value" (Spring's version) and especially "th:field" + public static final int ATTR_PRECEDENCE = 1005; + public static final String OPTION_TAG_NAME = "option"; + + + + public SpringOptionInSelectFieldTagProcessor(final String dialectPrefix) { + super(TemplateMode.HTML, dialectPrefix, OPTION_TAG_NAME, false, null, false, ATTR_PRECEDENCE); + } + + + + + @Override + protected void doProcess( + final ITemplateContext context, + final IProcessableElementTag tag, + final IElementTagStructureHandler structureHandler) { + + final AttributeName selectAttrNameToAdd = + (AttributeName) context.getVariable(SpringSelectFieldTagProcessor.OPTION_IN_SELECT_ATTR_NAME); + if (selectAttrNameToAdd == null) { + // Nothing to do + return; + } + + // It seems this