diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 000000000..d5c1bcbae --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,37 @@ +# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Java CI with Maven + +on: + push: + branches: [ "**" ] + pull_request: + branches: [ "**" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Set up JDK 11 on Temurin + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B package --file pom.xml + + # Commented this out because it breaks the build (for now) + # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive + #- name: Update dependency graph + # uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a2d1cf1e5..000000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: java - -jdk: - - oraclejdk7 - -script: mvn -Pwebtests verify diff --git a/dist/distribution-assembly.xml b/dist/distribution-assembly.xml index 9a5701cf9..5dc47639e 100644 --- a/dist/distribution-assembly.xml +++ b/dist/distribution-assembly.xml @@ -20,7 +20,7 @@ provided - servlets.com:cos + org.apache.commons.fileupload2:commons-fileupload2-jakarta diff --git a/dist/pom.xml b/dist/pom.xml index 90c07041d..710582aff 100644 --- a/dist/pom.xml +++ b/dist/pom.xml @@ -5,7 +5,7 @@ net.sourceforge.stripes stripes-parent - 1.6.0-SNAPSHOT + 1.6.0-JakartaEE10 .. stripes-dist diff --git a/dist/src/assembly/lib/cos.license b/dist/src/assembly/lib/cos.license deleted file mode 100644 index efc13dde9..000000000 --- a/dist/src/assembly/lib/cos.license +++ /dev/null @@ -1,78 +0,0 @@ -Copyright (C) 2001-2002 by Jason Hunter, jhunter_AT_servlets.com. -All rights reserved. -The source code, object code, and documentation in the com.oreilly.servlet -package is copyright and owned by Jason Hunter. - -LICENSE - -This license is granted on the binary form of the com.oreilly.servlet.* -packages that is distributed with the Stripes open source library. Source -code and documentation of the com.oreilly.servlet.* packages is available from -http://servlets.com/cos. You may, at your discretion, choose to use the -com.oreilly.servlet.* packages under the terms defined in this license, or the -terms defined in the license located at http://servlets.com/cos/license.html. -The licenses are mutally-exclusive and may not apply concurrently to any usage -of the com.oreilly.servlet.* packages. - -USE RIGHTS - -Permission is granted to use the com.oreilly.servlet.* packages in the -development of any commercial or non-commercial project, where "use" is -defined excluslively as "deployment of the com.oreilly.servlet.* packages in -unmodified binary form, solely for the purpose of satisfying the dependency of -the Stripes open source library". For this use you are granted a -non-exclusive, non-transferable limited license at no cost. - -This license does not confer upon the licensee the permission to extend or -modify the com.oreilly.servlets package, nor to create additional software -that directly uses or references the com.oreilly.servlets packages. - -REDISTRIBUTION RIGHTS - -Commercial redistribution rights of the com.oreilly.servlet.* packages are -available by writing jhunter_AT_servlets.com. - -Non-commercial redistribution is permitted provided that: - -1. You redistribute the package object code form only (as Java .class files or -a .jar file containing the .class files), as part of the Stripes open source -library, a derivative work of the Stripes open source library, or as part of -an application that uses and redistributes the Stripes open source library. - -2. The product containing the package is non-commercial in nature. - -3. The distribution includes copyright notice as follows: "The source code, -object code, and documentation in the com.oreilly.servlet package is copyright -and owned by Jason Hunter." in the documentation and/or other materials -provided with the distribution. - -4. You reproduce the above copyright notice, this list of conditions, and the -following disclaimer in the documentation and/or other materials provided with -the distribution. - -5. Licensor retains title to and ownership of the Software and all -enhancements, modifications, and updates to the Software. - -Note that the com.oreilly.servlet package is provided "as is" and the author -will not be liable for any damages suffered as a result of your use. -Furthermore, you understand the package comes without any technical support. - -You can always find the latest version of the com.oreilly.servlet package at -http://www.servlets.com. - - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS -OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -SUCH DAMAGE. - -Thanks, -Jason Hunter -jhunter_AT_servlets.com diff --git a/dist/src/assembly/upgrading.txt b/dist/src/assembly/upgrading.txt index 2b4215b21..9f4c5be10 100644 --- a/dist/src/assembly/upgrading.txt +++ b/dist/src/assembly/upgrading.txt @@ -2,9 +2,7 @@ Upgrading Stripes http://www.stripesframework.org/ This file documents the steps involved in upgrading an application from Stripes -1.4.x to Stripes 1.5. Since Stripes 1.0-1.4.3 have been backwards compatible it -should be possible to apple these steps to upgrade to Stripes 1.5 from earlier -versions also. +1.6.0 to Stripes 1.6.0-Jakarta. Contents -------- @@ -18,151 +16,26 @@ Contents of the distribution. -> Replace your existing commons-logging.jar with the new copy from the lib/ directory of the distribution - -> If you are using commons-fileupload download the latest version from the - apache website http://commons.apache.org/downloads/download_fileupload.cgi - -> Address backward compatibilities listed in section 3 as necessary - -2. Suggested Steps - -------------- - One significant new feature in Stripes 1.5 that you may wish to leverage at upgrade - time is the concept of "Extension Packages". Using this feature you can inform Stripes - of one or more extension packages, e.g.: - - Extension.Packages - com.myco.myapp.web.stripesext - - - If one or more packages are specified Stripes will search these packages for classes - that implement various interfaces and wire them up without requiring you to configure - them manually in web.xml. This includes custom: - -> ActionBeanContext classes - -> Interceptor classes - -> ConfigurableComponent classes (e.g. ActionResolvers, LocalePickers) - -> Formatter classes - -> TypeConverter classes - - To reduce your configuration it is recommended that you move any and all such - custom classes into a single package hierarchy and specify the root package. Once - done you can remove all web.xml configuration for these custom classes. - -3. Backwards Incompatibilities + -> For security reasons, Log4jLogger has been removed and replaced with SimpleJdk14Logger. + If you want to continue to use log4j, copy the source from the Stripes 1.6.0 branch + into your application source and use at your own risk. + -> If you are using cos.jar it has been removed for security reasons. Commons-fileupload2 is now + the default. Remove cos.jar and remove the CosMultipartWrapper from your web.xml if it is + referenced there. + +2. Backwards Incompatibilities --------------------------- - Stripes 1.5 is the first major release of Stripes that is not completely - backwards compatible with earlier versions of Stripes. However most of the + Stripes 1.6.0-Jakarta is not completely + backwards compatible with Stripes 1.6.0 (non-Jakarta). However, most of the incompatible change are quite small and require only minor edits to your - application (often achievable with search & replace functionality). This - section documents these incompatibilities. - - -> ActionResolver.PackageFilters has been replaced by ActionResolver.Packages - which is now a required configuration property. Changes in class scanning to - make it more efficient and robust across containers have changed the meaning - of this property and we have therefore changed the name to reflect this. If you - already specified ActionResolver.PackageFilters simply renaming it will do. - Otherwise you must specify ActionResolver.Packages to be the list of root - packages to scan for ActionBeans in your application - - -> Attribute name change in @Before and @After annotations. Previously the - lifecycle changes were specified using the 'value' attribute which - allowed the abbreviated form of '@Before(LifecycleStage.BindingAndValidation)'. - Due to the addition of the 'on' attribute to support selective execution this - is no longer desirable and the 'value' attribute has been renamed to 'stages'. - In most cases replacing '@Before(' with '@Before(stages=' (and the same for - @After) will be enough to ix this problem. - - -> The Validatable interface has been removed. Where it was used remove the - implemented clause from your ActionBean and annotate your validate() method - with the @ValidationMethod annotation - - -> The link-param tag has been removed in preference to the param tag. A search - and replace of 'link-param' to 'param' is sufficient to fix this. - - -> SpringAwareActionResolver has been removed. Please instead use the - SpringInterceptor introduced in Stripes 1.3. - - -> Stripes now performs stricter checking of various annotations. If you have - multiple methods annotated with @DefaultHandler or if you have @Validate - annotations in multiple locations (e.g. getter and setter) for a single - property Stripes will throw an exception instead of producing non-deterministic - behaviour. + application (mentioned above). This section documents these incompatibilities. - -> The _sourcePage parameter is no longer inserted into links (using the link and - url tags) by default (it is still submitted in forms). If the _sourcePage parameter - is relied on in certain places you can request Stripes insert it using the new - attribute addSourcePage="true". - - -> The _sourcePage parameter is always encrypted to avoid revealing potentially - sensitive information. The plaintext value can be easily accessed by calling - the new method ActionBeanContext.getSourcePage(). - - -> The way Interceptor classes are configured has changed. Two configuration - properties are supported: 'Interceptor.Classes' and 'CoreInterceptor.Classes'. - The first has the same name as before but the behaviour has changed. Now specifying - this property does NOT require you to specify the interceptors that Stripes - executes by default, only additional interceptors you would like to use. To - override the set of core interceptors used by Stripes you may now separately - specify the 'CoreInterceptor.Classes' property. When upgrading you should - remove referenced to the BeforeAfterMethodInterceptor from the - 'Interceptor.Classes' property. - - -> Behaviour of file uploads when using commons-fileupload has changed to match - the behaviour when using the cos implementation. When the user does not provide - a file a null FileBean will be produced instead of one containing a zero-length - file. In addition the filenames returned will never have path information - (previously path information would be included if the user was using IE). - - -> ActionClassCache has been removed. If you require a list of all ActionBeans - configured use ActionResolver.getActionBeanClasses() instead. - - -> All parameter values are now trimmed before validation, type conversion and - binding. If your app has any fields that allow whitespace as valid input or need - to retain leading and trailing whitespace during binding, you must use - @Validate(trim=false) on the field to override this behavior. - - -> The special parameter _eventName now overrides all request parameters that would - otherwise indicate the event name. E.g., given an ActionBean with events foo, bar - and blah and request parameters foo=foo, bar=bar and _eventName=blah, the event to - be executed will be blah because _eventName overrides the other two parameters. - Previous versions gave _eventName a lower precedence than other parameters. - - -> If multiple request parameters match event names, Stripes will throw an exception - reporting the conflict. In such a case, it cannot be determined which event should - execute. However, if the _eventName parameter is present, as noted in the previous - bullet, it overrides all others, even in case of such a conflict. The behavior of - previous versions of Stripes was indeterminate. - - -> When flashing an ActionBean and redirecting to that same ActionBean, as in: - - return new RedirectResolution(getClass(), "event").flash(this); - - the ActionBean receives a newly created ActionBeanContext on the ensuing request. - (In cases where the flashed bean is not the target of the redirect, the bean - retains its old ActionBeanContext.) One side-effect of this is that - ValidationErrors no longer carry over to the next request. It is recommended - that you use Messages instead of ValidationErrors when flashing and redirecting - to the same ActionBean. - - In addition to the above changes there have also been changes to several core classes. - In most cases these changes will be invisible to application developers, but where you - have implemented your own ConfigurableComponents from scratch (as opposed to extending - the default implementations) you may notice additional interface methods and changes - to signatures. Please refer to the javadoc for the 1.5 release in these cases. The - following is a non-exhaustive list of core classes with interface changes in 1.5: - -> Configuration (and implementations thereof) - -> ActionResolver (and implementations thereof) - -> BootstrapPropertyResolver - -> UrlBuilder - - -> The default PopulationStrategy is now BeanFirstPopulationStrategy instead of - DefaultPopulationStrategy. This may cause backward compatibility issues if - you were relying on the behavior of DefaultPopulationStrategy. You can revert - back to this behavior by configuring the population strategy in web.xml: - - - PopulationStrategy.Class - net.sourceforge.stripes.tag.DefaultPopulationStrategy - - - On the other hand, if you had the above configuration present in web.xml but with - BeanFirstPopulationStrategy, it is no longer required since it has become the - default. Leaving it there would be redundant, but would cause no harm. + -> Log4jLogger has been removed and the dependency on log4j has been removed. + -> cos.jar and CosMultipartWrapper have been removed. + -> commons-fileupload has been replaced with commons-fileupload2 + -> jakarta.mail dependency has been removed. +2. More Information + --------------------------- + For more information about the release, you can look at the release page on GitHub + located here: https://github.com/StripesFramework/stripes/releases/tag/1.6.0-Jakarta-beta diff --git a/eclipse/code-formatter-settings-eclipse-3.3.xml b/eclipse/code-formatter-settings-eclipse-3.3.xml deleted file mode 100644 index 4a0ff26c1..000000000 --- a/eclipse/code-formatter-settings-eclipse-3.3.xml +++ /dev/null @@ -1,264 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/pom.xml b/examples/pom.xml index 4820f5c73..b50545640 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -1,11 +1,11 @@ - + 4.0.0 net.sourceforge.stripes stripes-parent - 1.6.0-SNAPSHOT + 1.6.0-JakartaEE10 .. stripes-examples @@ -21,31 +21,32 @@ ${project.version} - javax.servlet - servlet-api + jakarta.servlet + jakarta.servlet-api + 6.0.0 + provided - javax.servlet.jsp - jsp-api - - - javax.servlet - jstl + jakarta.servlet.jsp.jstl + jakarta.servlet.jsp.jstl-api + 3.0.0 compile - taglibs - standard - compile + org.glassfish.web + jakarta.servlet.jsp.jstl + 3.0.1 - servlets.com - cos + commons-logging + commons-logging + 1.2 compile - javax.mail - mail + org.apache.commons + commons-fileupload2-jakarta + 2.0.0-M1 compile @@ -54,7 +55,7 @@ maven-antrun-plugin - 1.7 + 3.1.0 generate-resources @@ -74,10 +75,15 @@ org.apache.maven.plugins maven-war-plugin + 3.3.2 org.apache.maven.plugins maven-javadoc-plugin + 3.5.0 + + none + org.apache.maven.plugins @@ -98,6 +104,23 @@ true + + com.spotify.fmt + fmt-maven-plugin + 2.20 + + + + format + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.0 + diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/ajax/CalculatorActionBean.java b/examples/src/main/java/net/sourceforge/stripes/examples/ajax/CalculatorActionBean.java index 2b03f97e9..93526c0cb 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/ajax/CalculatorActionBean.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/ajax/CalculatorActionBean.java @@ -1,5 +1,7 @@ package net.sourceforge.stripes.examples.ajax; +import java.io.StringReader; +import java.util.List; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.action.DefaultHandler; @@ -11,58 +13,75 @@ import net.sourceforge.stripes.validation.ValidationErrorHandler; import net.sourceforge.stripes.validation.ValidationErrors; -import java.io.StringReader; -import java.util.List; - /** - * A very simple calculator action that is designed to work with an ajax front end. - * Handles 'add' and 'divide' events just like the non-ajax calculator. Each event - * calculates the result, and then "streams" it back to the browser. Implements the - * ValidationErrorHandler interface to intercept any validation errors, convert them - * to an HTML message and stream the back to the browser for display. + * A very simple calculator action that is designed to work with an ajax front end. Handles 'add' + * and 'divide' events just like the non-ajax calculator. Each event calculates the result, and then + * "streams" it back to the browser. Implements the ValidationErrorHandler interface to intercept + * any validation errors, convert them to an HTML message and stream the back to the browser for + * display. * * @author Tim Fennell */ @Public public class CalculatorActionBean implements ActionBean, ValidationErrorHandler { - private ActionBeanContext context; - @Validate(required=true) private double numberOne; - @Validate(required=true) private double numberTwo; + private ActionBeanContext context; - public ActionBeanContext getContext() { return context; } - public void setContext(ActionBeanContext context) { this.context = context; } + @Validate(required = true) + private double numberOne; - /** Converts errors to HTML and streams them back to the browser. */ - public Resolution handleValidationErrors(ValidationErrors errors) throws Exception { - StringBuilder message = new StringBuilder(); + @Validate(required = true) + private double numberTwo; - for (List fieldErrors : errors.values()) { - for (ValidationError error : fieldErrors) { - message.append("
"); - message.append(error.getMessage(getContext().getLocale())); - message.append("
"); - } - } + public ActionBeanContext getContext() { + return context; + } - return new StreamingResolution("text/html", new StringReader(message.toString())); - } + public void setContext(ActionBeanContext context) { + this.context = context; + } - /** Handles the 'add' event, adds the two numbers and returns the result. */ - @DefaultHandler public Resolution add() { - String result = String.valueOf(numberOne + numberTwo); - return new StreamingResolution("text", new StringReader(result)); - } + /** Converts errors to HTML and streams them back to the browser. */ + public Resolution handleValidationErrors(ValidationErrors errors) throws Exception { + StringBuilder message = new StringBuilder(); - /** Handles the 'divide' event, divides number two by oneand returns the result. */ - public Resolution divide() { - String result = String.valueOf(numberOne / numberTwo); - return new StreamingResolution("text", new StringReader(result)); + for (List fieldErrors : errors.values()) { + for (ValidationError error : fieldErrors) { + message.append("
"); + message.append(error.getMessage(getContext().getLocale())); + message.append("
"); + } } - // Standard getter and setter methods - public double getNumberOne() { return numberOne; } - public void setNumberOne(double numberOne) { this.numberOne = numberOne; } + return new StreamingResolution("text/html", new StringReader(message.toString())); + } + + /** Handles the 'add' event, adds the two numbers and returns the result. */ + @DefaultHandler + public Resolution add() { + String result = String.valueOf(numberOne + numberTwo); + return new StreamingResolution("text", new StringReader(result)); + } + + /** Handles the 'divide' event, divides number two by oneand returns the result. */ + public Resolution divide() { + String result = String.valueOf(numberOne / numberTwo); + return new StreamingResolution("text", new StringReader(result)); + } + + // Standard getter and setter methods + public double getNumberOne() { + return numberOne; + } + + public void setNumberOne(double numberOne) { + this.numberOne = numberOne; + } + + public double getNumberTwo() { + return numberTwo; + } - public double getNumberTwo() { return numberTwo; } - public void setNumberTwo(double numberTwo) { this.numberTwo = numberTwo; } + public void setNumberTwo(double numberTwo) { + this.numberTwo = numberTwo; + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/AdministerComponentsActionBean.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/AdministerComponentsActionBean.java index b377f728b..a4a988ee9 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/AdministerComponentsActionBean.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/AdministerComponentsActionBean.java @@ -1,7 +1,6 @@ package net.sourceforge.stripes.examples.bugzooky; import java.util.List; - import net.sourceforge.stripes.action.DefaultHandler; import net.sourceforge.stripes.action.DontBind; import net.sourceforge.stripes.action.ForwardResolution; @@ -13,60 +12,65 @@ import net.sourceforge.stripes.validation.ValidateNestedProperties; /** - * Manages the administration of Components, from the Administer Bugzooky page. Receives a List - * of Components, which may include a new component and persists the changes. Also receives an - * Array of IDs for components that are to be deleted, and deletes those. + * Manages the administration of Components, from the Administer Bugzooky page. Receives a List of + * Components, which may include a new component and persists the changes. Also receives an Array of + * IDs for components that are to be deleted, and deletes those. * * @author Tim Fennell */ public class AdministerComponentsActionBean extends BugzookyActionBean { - private int[] deleteIds; + private int[] deleteIds; - @ValidateNestedProperties ({ - @Validate(field="name", required=true, minlength=3, maxlength=25) - }) - private List components; + @ValidateNestedProperties({ + @Validate(field = "name", required = true, minlength = 3, maxlength = 25) + }) + private List components; - public int[] getDeleteIds() { return deleteIds; } - public void setDeleteIds(int[] deleteIds) { this.deleteIds = deleteIds; } + public int[] getDeleteIds() { + return deleteIds; + } - /** - * If no list of components is set and we're not handling the "save" event then populate the - * list of components and return it. - */ - public List getComponents() { - if (components == null && !"save".equals(getContext().getEventName())) { - components = new ComponentManager().getAllComponents(); - } + public void setDeleteIds(int[] deleteIds) { + this.deleteIds = deleteIds; + } - return components; + /** + * If no list of components is set and we're not handling the "save" event then populate the list + * of components and return it. + */ + public List getComponents() { + if (components == null && !"save".equals(getContext().getEventName())) { + components = new ComponentManager().getAllComponents(); } - public void setComponents(List components) { - this.components = components; - } + return components; + } - @DefaultHandler - @DontBind - public Resolution view() { - return new ForwardResolution("/bugzooky/AdministerBugzooky.jsp"); - } + public void setComponents(List components) { + this.components = components; + } - public Resolution save() { - ComponentManager cm = new ComponentManager(); + @DefaultHandler + @DontBind + public Resolution view() { + return new ForwardResolution("/bugzooky/AdministerBugzooky.jsp"); + } - // Save any changes to existing components (and create new ones) - for (Component component : components) { - cm.saveOrUpdate(component); - } + public Resolution save() { + ComponentManager cm = new ComponentManager(); - // Then, if the user checked anyone off to be deleted, delete them - if (deleteIds != null) { - for (int id : deleteIds) { - cm.deleteComponent(id); - } - } + // Save any changes to existing components (and create new ones) + for (Component component : components) { + cm.saveOrUpdate(component); + } - return new RedirectResolution(getClass()); + // Then, if the user checked anyone off to be deleted, delete them + if (deleteIds != null) { + for (int id : deleteIds) { + cm.deleteComponent(id); + } } + + return new RedirectResolution(getClass()); + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/AdministerPeopleActionBean.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/AdministerPeopleActionBean.java index a10176e7c..7c6cf2697 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/AdministerPeopleActionBean.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/AdministerPeopleActionBean.java @@ -1,7 +1,6 @@ package net.sourceforge.stripes.examples.bugzooky; import java.util.List; - import net.sourceforge.stripes.action.DefaultHandler; import net.sourceforge.stripes.action.DontBind; import net.sourceforge.stripes.action.ForwardResolution; @@ -15,65 +14,70 @@ import net.sourceforge.stripes.validation.ValidateNestedProperties; /** - * Manages the administration of People, from the Administer Bugzooky page. Receives a List - * of People, which may include a new person and persists the changes. Also receives an - * Array of IDs for people that are to be deleted, and deletes them. + * Manages the administration of People, from the Administer Bugzooky page. Receives a List of + * People, which may include a new person and persists the changes. Also receives an Array of IDs + * for people that are to be deleted, and deletes them. * * @author Tim Fennell */ public class AdministerPeopleActionBean extends BugzookyActionBean { - private int[] deleteIds; + private int[] deleteIds; - @ValidateNestedProperties ({ - @Validate(field="username", required=true, minlength=3, maxlength=15), - @Validate(field="password", minlength=6, maxlength=20), - @Validate(field="firstName", required=true, maxlength=25), - @Validate(field="lastName", required=true, maxlength=25), - @Validate(field="email", converter=EmailTypeConverter.class) - }) - private List people; + @ValidateNestedProperties({ + @Validate(field = "username", required = true, minlength = 3, maxlength = 15), + @Validate(field = "password", minlength = 6, maxlength = 20), + @Validate(field = "firstName", required = true, maxlength = 25), + @Validate(field = "lastName", required = true, maxlength = 25), + @Validate(field = "email", converter = EmailTypeConverter.class) + }) + private List people; - public int[] getDeleteIds() { return deleteIds; } - public void setDeleteIds(int[] deleteIds) { this.deleteIds = deleteIds; } + public int[] getDeleteIds() { + return deleteIds; + } - /** - * If no list of people is set and we're not handling the "save" event then populate the list of - * people and return it. - */ - public List getPeople() { - if (people == null && !"Save".equals(getContext().getEventName())) { - people = new PersonManager().getAllPeople(); - } + public void setDeleteIds(int[] deleteIds) { + this.deleteIds = deleteIds; + } - return people; + /** + * If no list of people is set and we're not handling the "save" event then populate the list of + * people and return it. + */ + public List getPeople() { + if (people == null && !"Save".equals(getContext().getEventName())) { + people = new PersonManager().getAllPeople(); } - public void setPeople(List people) { - this.people = people; - } + return people; + } - @DefaultHandler - @DontBind - public Resolution view() { - return new ForwardResolution("/bugzooky/AdministerBugzooky.jsp"); - } + public void setPeople(List people) { + this.people = people; + } - @HandlesEvent("Save") - public Resolution saveChanges() { - PersonManager pm = new PersonManager(); + @DefaultHandler + @DontBind + public Resolution view() { + return new ForwardResolution("/bugzooky/AdministerBugzooky.jsp"); + } - // Save any changes to existing people (and create new ones) - for (Person person : people) { - pm.saveOrUpdate(person); - } + @HandlesEvent("Save") + public Resolution saveChanges() { + PersonManager pm = new PersonManager(); - // Then, if the user checked anyone off to be deleted, delete them - if (deleteIds != null) { - for (int id : deleteIds) { - pm.deletePerson(id); - } - } + // Save any changes to existing people (and create new ones) + for (Person person : people) { + pm.saveOrUpdate(person); + } - return new RedirectResolution(getClass()); + // Then, if the user checked anyone off to be deleted, delete them + if (deleteIds != null) { + for (int id : deleteIds) { + pm.deletePerson(id); + } } + + return new RedirectResolution(getClass()); + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/BugListActionBean.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/BugListActionBean.java index ca38a52e8..20c395628 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/BugListActionBean.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/BugListActionBean.java @@ -21,13 +21,13 @@ /** * Forwards to the view and provides some help displaying the bug list. - * + * * @author Ben Gunter */ public class BugListActionBean extends BugzookyActionBean { - @DefaultHandler - @DontBind - public Resolution list() { - return new ForwardResolution("/bugzooky/BugList.jsp"); - } + @DefaultHandler + @DontBind + public Resolution list() { + return new ForwardResolution("/bugzooky/BugList.jsp"); + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/BugzookyActionBean.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/BugzookyActionBean.java index 2e2ec4402..35c695f2f 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/BugzookyActionBean.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/BugzookyActionBean.java @@ -5,20 +5,19 @@ import net.sourceforge.stripes.examples.bugzooky.ext.BugzookyActionBeanContext; /** - * Simple ActionBean implementation that all ActionBeans in the Bugzooky example - * will extend. + * Simple ActionBean implementation that all ActionBeans in the Bugzooky example will extend. * * @author Tim Fennell */ public abstract class BugzookyActionBean implements ActionBean { - private BugzookyActionBeanContext context; + private BugzookyActionBeanContext context; - public void setContext(ActionBeanContext context) { - this.context = (BugzookyActionBeanContext) context; - } + public void setContext(ActionBeanContext context) { + this.context = (BugzookyActionBeanContext) context; + } - /** Gets the ActionBeanContext set by Stripes during initialization. */ - public BugzookyActionBeanContext getContext() { - return this.context; - } -} \ No newline at end of file + /** Gets the ActionBeanContext set by Stripes during initialization. */ + public BugzookyActionBeanContext getContext() { + return this.context; + } +} diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/DownloadAttachmentActionBean.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/DownloadAttachmentActionBean.java index 59fbedd85..e6a10357d 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/DownloadAttachmentActionBean.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/DownloadAttachmentActionBean.java @@ -1,7 +1,6 @@ package net.sourceforge.stripes.examples.bugzooky; import java.io.ByteArrayInputStream; - import net.sourceforge.stripes.action.DefaultHandler; import net.sourceforge.stripes.action.Resolution; import net.sourceforge.stripes.action.StreamingResolution; @@ -12,28 +11,39 @@ /** * Action that responds to a user's request to download an attachment to a bug. This ActionBean * demonstrates the use of clean URLs. - * + * * @author Tim Fennell */ @UrlBinding("/attachment/{bug}/{attachmentIndex}") public class DownloadAttachmentActionBean extends BugzookyActionBean { - private Bug bug; - private Integer attachmentIndex; + private Bug bug; + private Integer attachmentIndex; + + public Bug getBug() { + return bug; + } + + public void setBug(Bug bugId) { + this.bug = bugId; + } - public Bug getBug() { return bug; } - public void setBug(Bug bugId) { this.bug = bugId; } + public Integer getAttachmentIndex() { + return attachmentIndex; + } - public Integer getAttachmentIndex() { return attachmentIndex; } - public void setAttachmentIndex(Integer attachmentIndex) { this.attachmentIndex = attachmentIndex; } + public void setAttachmentIndex(Integer attachmentIndex) { + this.attachmentIndex = attachmentIndex; + } - @DefaultHandler - public Resolution getAttachment() { - Attachment attachment = getBug().getAttachments().get(this.attachmentIndex); + @DefaultHandler + public Resolution getAttachment() { + Attachment attachment = getBug().getAttachments().get(this.attachmentIndex); - // Uses a StreamingResolution to send the file contents back to the user. - // Note the use of the chained .setFilename() method, which causes the - // browser to [prompt to] save the "file" instead of displaying it in browser - return new StreamingResolution(attachment.getContentType(), - new ByteArrayInputStream(attachment.getData())).setFilename(attachment.getName()); - } + // Uses a StreamingResolution to send the file contents back to the user. + // Note the use of the chained .setFilename() method, which causes the + // browser to [prompt to] save the "file" instead of displaying it in browser + return new StreamingResolution( + attachment.getContentType(), new ByteArrayInputStream(attachment.getData())) + .setFilename(attachment.getName()); + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/LoginActionBean.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/LoginActionBean.java index 4cf0a2d93..8ec56aa2e 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/LoginActionBean.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/LoginActionBean.java @@ -13,68 +13,77 @@ import net.sourceforge.stripes.validation.ValidationError; /** - * An example of an ActionBean that uses validation annotations on fields instead of - * on methods. Logs the user in using a conventional username/password combo and - * validates the password in the action method. + * An example of an ActionBean that uses validation annotations on fields instead of on methods. + * Logs the user in using a conventional username/password combo and validates the password in the + * action method. * * @author Tim Fennell */ @Public public class LoginActionBean extends BugzookyActionBean { - @Validate(required=true) - private String username; + @Validate(required = true) + private String username; - @Validate(required=true) - private String password; + @Validate(required = true) + private String password; - private String targetUrl; + private String targetUrl; - /** The username of the user trying to log in. */ - public void setUsername(String username) { this.username = username; } + /** The username of the user trying to log in. */ + public void setUsername(String username) { + this.username = username; + } - /** The username of the user trying to log in. */ - public String getUsername() { return username; } + /** The username of the user trying to log in. */ + public String getUsername() { + return username; + } - /** The password of the user trying to log in. */ - public void setPassword(String password) { this.password = password; } + /** The password of the user trying to log in. */ + public void setPassword(String password) { + this.password = password; + } - /** The password of the user trying to log in. */ - public String getPassword() { return password; } + /** The password of the user trying to log in. */ + public String getPassword() { + return password; + } - /** The URL the user was trying to access (null if the login page was accessed directly). */ - public String getTargetUrl() { return targetUrl; } + /** The URL the user was trying to access (null if the login page was accessed directly). */ + public String getTargetUrl() { + return targetUrl; + } - /** The URL the user was trying to access (null if the login page was accessed directly). */ - public void setTargetUrl(String targetUrl) { this.targetUrl = targetUrl; } + /** The URL the user was trying to access (null if the login page was accessed directly). */ + public void setTargetUrl(String targetUrl) { + this.targetUrl = targetUrl; + } - @DefaultHandler - @DontValidate - public Resolution view() { - return new ForwardResolution("/bugzooky/Login.jsp"); - } + @DefaultHandler + @DontValidate + public Resolution view() { + return new ForwardResolution("/bugzooky/Login.jsp"); + } - public Resolution login() { - PersonManager pm = new PersonManager(); - Person person = pm.getPerson(this.username); + public Resolution login() { + PersonManager pm = new PersonManager(); + Person person = pm.getPerson(this.username); - if (person == null) { - ValidationError error = new LocalizableError("usernameDoesNotExist"); - getContext().getValidationErrors().add("username", error); - return getContext().getSourcePageResolution(); - } - else if (!person.getPassword().equals(password)) { - ValidationError error = new LocalizableError("incorrectPassword"); - getContext().getValidationErrors().add("password", error); - return getContext().getSourcePageResolution(); - } - else { - getContext().setUser(person); - if (this.targetUrl != null) { - return new RedirectResolution(this.targetUrl); - } - else { - return new RedirectResolution(BugListActionBean.class); - } - } + if (person == null) { + ValidationError error = new LocalizableError("usernameDoesNotExist"); + getContext().getValidationErrors().add("username", error); + return getContext().getSourcePageResolution(); + } else if (!person.getPassword().equals(password)) { + ValidationError error = new LocalizableError("incorrectPassword"); + getContext().getValidationErrors().add("password", error); + return getContext().getSourcePageResolution(); + } else { + getContext().setUser(person); + if (this.targetUrl != null) { + return new RedirectResolution(this.targetUrl); + } else { + return new RedirectResolution(BugListActionBean.class); + } } + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/LogoutActionBean.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/LogoutActionBean.java index c2ed1c196..ab63b5539 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/LogoutActionBean.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/LogoutActionBean.java @@ -6,12 +6,13 @@ /** * Straightforward logout action that logs the user out and then sends to an exit page. + * * @author Tim Fennell */ @Public public class LogoutActionBean extends BugzookyActionBean { - public Resolution logout() throws Exception { - getContext().logout(); - return new ForwardResolution("/bugzooky/Exit.jsp"); - } + public Resolution logout() throws Exception { + getContext().logout(); + return new ForwardResolution("/bugzooky/Exit.jsp"); + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/MultiBugActionBean.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/MultiBugActionBean.java index 9c783e6de..50219e1fe 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/MultiBugActionBean.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/MultiBugActionBean.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.List; - import net.sourceforge.stripes.action.DefaultHandler; import net.sourceforge.stripes.action.DontValidate; import net.sourceforge.stripes.action.ForwardResolution; @@ -16,57 +15,58 @@ import net.sourceforge.stripes.validation.ValidateNestedProperties; /** - * ActionBean that deals with setting up and saving edits to multiple bugs at once. Can also - * deal with adding multiple new bugs at once. + * ActionBean that deals with setting up and saving edits to multiple bugs at once. Can also deal + * with adding multiple new bugs at once. * * @author Tim Fennell */ public class MultiBugActionBean extends BugzookyActionBean { - /** Populated during bulk add/edit operations. */ - @ValidateNestedProperties({ - @Validate(field="shortDescription", required=true, maxlength=75), - @Validate(field="longDescription", required=true, minlength=25), - @Validate(field="component", required=true), - @Validate(field="owner", required=true), - @Validate(field="priority", required=true) - }) - private List bugs = new ArrayList(); + /** Populated during bulk add/edit operations. */ + @ValidateNestedProperties({ + @Validate(field = "shortDescription", required = true, maxlength = 75), + @Validate(field = "longDescription", required = true, minlength = 25), + @Validate(field = "component", required = true), + @Validate(field = "owner", required = true), + @Validate(field = "priority", required = true) + }) + private List bugs = new ArrayList(); - /** - * Simple getter that returns the List of Bugs. Note the use of generics syntax - this is - * necessary to let Stripes know what type of object to create and insert into the list. - */ - public List getBugs() { - return bugs; - } + /** + * Simple getter that returns the List of Bugs. Note the use of generics syntax - this is + * necessary to let Stripes know what type of object to create and insert into the list. + */ + public List getBugs() { + return bugs; + } - /** Setter for the list of bugs. */ - public void setBugs(List bugs) { - this.bugs = bugs; - } + /** Setter for the list of bugs. */ + public void setBugs(List bugs) { + this.bugs = bugs; + } - @DefaultHandler - @DontValidate - public Resolution view() { - // Check for the "view" parameter. It will be there if we got here by a form submission. - BugzookyActionBeanContext context = getContext(); - boolean fromForm = context.getRequest().getParameter("view") != null; - if (fromForm && (getBugs() == null || getBugs().isEmpty())) { - context.getValidationErrors().addGlobalError( - new SimpleError("You must select at least one bug to edit.")); - return context.getSourcePageResolution(); - } - - return new ForwardResolution("/bugzooky/BulkAddEditBugs.jsp"); + @DefaultHandler + @DontValidate + public Resolution view() { + // Check for the "view" parameter. It will be there if we got here by a form submission. + BugzookyActionBeanContext context = getContext(); + boolean fromForm = context.getRequest().getParameter("view") != null; + if (fromForm && (getBugs() == null || getBugs().isEmpty())) { + context + .getValidationErrors() + .addGlobalError(new SimpleError("You must select at least one bug to edit.")); + return context.getSourcePageResolution(); } - public Resolution save() { - BugManager bm = new BugManager(); + return new ForwardResolution("/bugzooky/BulkAddEditBugs.jsp"); + } - for (Bug bug : bugs) { - bm.saveOrUpdate(bug); - } + public Resolution save() { + BugManager bm = new BugManager(); - return new RedirectResolution(BugListActionBean.class); + for (Bug bug : bugs) { + bm.saveOrUpdate(bug); } + + return new RedirectResolution(BugListActionBean.class); + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/RegisterActionBean.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/RegisterActionBean.java index bb7e81396..7ec33b308 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/RegisterActionBean.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/RegisterActionBean.java @@ -24,64 +24,71 @@ @Public @Wizard(startEvents = "start") public class RegisterActionBean extends BugzookyActionBean { - @ValidateNestedProperties({ - @Validate(field="username", required=true, minlength=5, maxlength=20), - @Validate(field="password", required=true, minlength=5, maxlength=20), - @Validate(field="firstName", required=true, maxlength=50), - @Validate(field="lastName", required=true, maxlength=50) - }) - private Person user; + @ValidateNestedProperties({ + @Validate(field = "username", required = true, minlength = 5, maxlength = 20), + @Validate(field = "password", required = true, minlength = 5, maxlength = 20), + @Validate(field = "firstName", required = true, maxlength = 50), + @Validate(field = "lastName", required = true, maxlength = 50) + }) + private Person user; - @Validate(required=true, minlength=5, maxlength=20, expression="this == user.password") - private String confirmPassword; + @Validate(required = true, minlength = 5, maxlength = 20, expression = "self == user.password") + private String confirmPassword; - /** The user being registered. */ - public void setUser(Person user) { this.user = user; } + /** The user being registered. */ + public void setUser(Person user) { + this.user = user; + } - /** The user being registered. */ - public Person getUser() { return user; } + /** The user being registered. */ + public Person getUser() { + return user; + } - /** The 2nd/confirmation password entered by the user. */ - public void setConfirmPassword(String confirmPassword) { - this.confirmPassword = confirmPassword; - } + /** The 2nd/confirmation password entered by the user. */ + public void setConfirmPassword(String confirmPassword) { + this.confirmPassword = confirmPassword; + } - /** The 2nd/confirmation password entered by the user. */ - public String getConfirmPassword() { return confirmPassword; } + /** The 2nd/confirmation password entered by the user. */ + public String getConfirmPassword() { + return confirmPassword; + } - /** - * Validates that the two passwords entered match each other, and that the - * username entered is not already taken in the system. - */ - @ValidationMethod - public void validate(ValidationErrors errors) { - if ( new PersonManager().getPerson(this.user.getUsername()) != null ) { - errors.add("user.username", new LocalizableError("usernameTaken")); - } + /** + * Validates that the two passwords entered match each other, and that the username entered is not + * already taken in the system. + */ + @ValidationMethod + public void validate(ValidationErrors errors) { + if (new PersonManager().getPerson(this.user.getUsername()) != null) { + errors.add("user.username", new LocalizableError("usernameTaken")); } + } - @DontBind - @DefaultHandler - public Resolution start() { - return new ForwardResolution("/bugzooky/Register.jsp"); - } + @DontBind + @DefaultHandler + public Resolution start() { + return new ForwardResolution("/bugzooky/Register.jsp"); + } - public Resolution gotoStep2() throws Exception { - return new ForwardResolution("/bugzooky/Register2.jsp"); - } + public Resolution gotoStep2() throws Exception { + return new ForwardResolution("/bugzooky/Register2.jsp"); + } - /** - * Registers a new user, logs them in, and redirects them to the bug list page. - */ - public Resolution register() { - PersonManager pm = new PersonManager(); - pm.saveOrUpdate(this.user); - getContext().setUser(this.user); - getContext().getMessages().add( - new LocalizableMessage(getClass().getName() + ".successMessage", - this.user.getFirstName(), - this.user.getUsername())); + /** Registers a new user, logs them in, and redirects them to the bug list page. */ + public Resolution register() { + PersonManager pm = new PersonManager(); + pm.saveOrUpdate(this.user); + getContext().setUser(this.user); + getContext() + .getMessages() + .add( + new LocalizableMessage( + getClass().getName() + ".successMessage", + this.user.getFirstName(), + this.user.getUsername())); - return new RedirectResolution(BugListActionBean.class); - } + return new RedirectResolution(BugListActionBean.class); + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/SingleBugActionBean.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/SingleBugActionBean.java index f983e7fa3..53578fb18 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/SingleBugActionBean.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/SingleBugActionBean.java @@ -3,7 +3,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.Date; - import net.sourceforge.stripes.action.DefaultHandler; import net.sourceforge.stripes.action.DontValidate; import net.sourceforge.stripes.action.FileBean; @@ -18,66 +17,79 @@ import net.sourceforge.stripes.validation.ValidateNestedProperties; /** - * ActionBean that provides method for editing a single bug in detail. Includes an - * event for pre-populating the ActionBean on the way in to an edit screen, and a - * single event for saving an existing or new bug. Uses a FileBean property to - * support the uploading of a File concurrent with other edits. + * ActionBean that provides method for editing a single bug in detail. Includes an event for + * pre-populating the ActionBean on the way in to an edit screen, and a single event for saving an + * existing or new bug. Uses a FileBean property to support the uploading of a File concurrent with + * other edits. * * @author Tim Fennell */ public class SingleBugActionBean extends BugzookyActionBean { - @ValidateNestedProperties({ - @Validate(field="shortDescription", required=true), - @Validate(field="longDescription", required=true), - @Validate(field="percentComplete", minvalue=0, maxvalue=1, - converter=PercentageTypeConverter.class) - }) - private Bug bug; - private FileBean newAttachment; + @ValidateNestedProperties({ + @Validate(field = "shortDescription", required = true), + @Validate(field = "longDescription", required = true), + @Validate( + field = "percentComplete", + minvalue = 0, + maxvalue = 1, + converter = PercentageTypeConverter.class) + }) + private Bug bug; - public Bug getBug() { return bug; } - public void setBug(Bug bug) { this.bug = bug; } + private FileBean newAttachment; - public FileBean getNewAttachment() { return newAttachment; } - public void setNewAttachment(FileBean newAttachment) { this.newAttachment = newAttachment; } + public Bug getBug() { + return bug; + } - /** Loads a bug on to the form ready for editing. */ - @DefaultHandler - @DontValidate - public Resolution view() { - return new ForwardResolution("/bugzooky/AddEditBug.jsp"); - } + public void setBug(Bug bug) { + this.bug = bug; + } - /** Saves (or updates) a bug, and then returns the user to the bug list. */ - public Resolution save() throws IOException { - BugManager bm = new BugManager(); + public FileBean getNewAttachment() { + return newAttachment; + } - Bug bug = getBug(); - if (this.newAttachment != null) { - Attachment attachment = new Attachment(); - attachment.setName(this.newAttachment.getFileName()); - attachment.setSize(this.newAttachment.getSize()); - attachment.setContentType(this.newAttachment.getContentType()); + public void setNewAttachment(FileBean newAttachment) { + this.newAttachment = newAttachment; + } - byte[] data = new byte[(int) this.newAttachment.getSize()]; - InputStream in = this.newAttachment.getInputStream(); - in.read(data); - attachment.setData(data); - bug.addAttachment(attachment); - } + /** Loads a bug on to the form ready for editing. */ + @DefaultHandler + @DontValidate + public Resolution view() { + return new ForwardResolution("/bugzooky/AddEditBug.jsp"); + } - // Set the open date for new bugs - if (bug.getOpenDate() == null) - bug.setOpenDate(new Date()); + /** Saves (or updates) a bug, and then returns the user to the bug list. */ + public Resolution save() throws IOException { + BugManager bm = new BugManager(); - bm.saveOrUpdate(bug); + Bug bug = getBug(); + if (this.newAttachment != null) { + Attachment attachment = new Attachment(); + attachment.setName(this.newAttachment.getFileName()); + attachment.setSize(this.newAttachment.getSize()); + attachment.setContentType(this.newAttachment.getContentType()); - return new RedirectResolution(BugListActionBean.class); + byte[] data = new byte[(int) this.newAttachment.getSize()]; + InputStream in = this.newAttachment.getInputStream(); + in.read(data); + attachment.setData(data); + bug.addAttachment(attachment); } - /** Saves or updates a bug, and then returns to the edit page to add another just like it. */ - public Resolution saveAndAgain() throws IOException { - save(); - return new RedirectResolution(getClass()).addParameter("bug", getBug()); - } + // Set the open date for new bugs + if (bug.getOpenDate() == null) bug.setOpenDate(new Date()); + + bm.saveOrUpdate(bug); + + return new RedirectResolution(BugListActionBean.class); + } + + /** Saves or updates a bug, and then returns to the edit page to add another just like it. */ + public Resolution saveAndAgain() throws IOException { + save(); + return new RedirectResolution(getClass()).addParameter("bug", getBug()); + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ViewResourceActionBean.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ViewResourceActionBean.java index 2815f1025..1e912c596 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ViewResourceActionBean.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ViewResourceActionBean.java @@ -1,5 +1,8 @@ package net.sourceforge.stripes.examples.bugzooky; +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; @@ -8,11 +11,6 @@ import java.util.Iterator; import java.util.SortedSet; import java.util.TreeSet; - -import javax.servlet.ServletContext; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import net.sourceforge.stripes.action.Resolution; import net.sourceforge.stripes.action.UrlBinding; import net.sourceforge.stripes.util.HtmlUtil; @@ -25,76 +23,84 @@ * ActionBean that is used to display source files from the bugzooky web application to the user. * This ActionBean demonstrates the use of clean URLs by embedding the "resource" parameter in the * binding. - * + * * @author Tim Fennell */ @UrlBinding("/bugzooky/view{resource}") public class ViewResourceActionBean extends BugzookyActionBean { - @Validate(required=true) - private String resource; + @Validate(required = true) + private String resource; - /** Sets the name resource to be viewed. */ - public void setResource(String resource) { this.resource = resource; } + /** Sets the name resource to be viewed. */ + public void setResource(String resource) { + this.resource = resource; + } - /** Gets the name of the resource to be viewed. */ - public String getResource() { return resource; } + /** Gets the name of the resource to be viewed. */ + public String getResource() { + return resource; + } - /** Validates that only resources in the allowed places are asked for. */ - @ValidationMethod - public void validate(ValidationErrors errors) { - if (resource.startsWith("/WEB-INF") && !resource.startsWith("/WEB-INF/src")) { - errors.add("resource", - new SimpleError("Naughty, naughty. We mustn't hack the URL now.")); - } + /** Validates that only resources in the allowed places are asked for. */ + @ValidationMethod + public void validate(ValidationErrors errors) { + if (resource.startsWith("/WEB-INF") && !resource.startsWith("/WEB-INF/src")) { + errors.add("resource", new SimpleError("Naughty, naughty. We mustn't hack the URL now.")); } + } - /** - * Handler method which will handle a request for a resource in the web application - * and stream it back to the client inside of an HTML preformatted section. - */ - public Resolution view() { - final InputStream stream = getContext().getRequest().getSession() - .getServletContext().getResourceAsStream(this.resource); - final BufferedReader reader = new BufferedReader( new InputStreamReader(stream) ); - - return new Resolution() { - public void execute(HttpServletRequest request, HttpServletResponse response) throws Exception { - PrintWriter writer = response.getWriter(); - writer.write(""); - writer.write(resource); - writer.write("
");
+  /**
+   * Handler method which will handle a request for a resource in the web application and stream it
+   * back to the client inside of an HTML preformatted section.
+   */
+  public Resolution view() {
+    final InputStream stream =
+        getContext()
+            .getRequest()
+            .getSession()
+            .getServletContext()
+            .getResourceAsStream(this.resource);
+    final BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
 
-                String line;
-                while ( (line = reader.readLine()) != null ) {
-                    writer.write(HtmlUtil.encode(line));
-                    writer.write("\n");
-                }
+    return new Resolution() {
+      public void execute(HttpServletRequest request, HttpServletResponse response)
+          throws Exception {
+        PrintWriter writer = response.getWriter();
+        writer.write("");
+        writer.write(resource);
+        writer.write("
");
 
-                writer.write("
"); - } - }; - } + String line; + while ((line = reader.readLine()) != null) { + writer.write(HtmlUtil.encode(line)); + writer.write("\n"); + } - /** - * Method used when this ActionBean is used as a view helper. Returns a listing of all the - * JSPs and ActionBeans available for viewing. - */ - @SuppressWarnings("unchecked") - public Collection getAvailableResources() { - ServletContext ctx = getContext().getRequest().getSession().getServletContext(); - SortedSet resources = new TreeSet(); - resources.addAll( ctx.getResourcePaths("/bugzooky/")); - resources.addAll( ctx.getResourcePaths("/bugzooky/layout/")); - resources.addAll( ctx.getResourcePaths("/WEB-INF/src/")); + writer.write("
"); + } + }; + } - Iterator iterator = resources.iterator(); - while (iterator.hasNext()) { - String file = iterator.next(); - if (!file.endsWith(".jsp") && !file.endsWith(".java")) { - iterator.remove(); - } - } + /** + * Method used when this ActionBean is used as a view helper. Returns a listing of all the JSPs + * and ActionBeans available for viewing. + */ + @SuppressWarnings("unchecked") + public Collection getAvailableResources() { + ServletContext ctx = getContext().getRequest().getSession().getServletContext(); + SortedSet resources = new TreeSet(); + resources.addAll(ctx.getResourcePaths("/bugzooky/")); + resources.addAll(ctx.getResourcePaths("/bugzooky/layout/")); + resources.addAll(ctx.getResourcePaths("/WEB-INF/src/")); - return resources; + Iterator iterator = resources.iterator(); + while (iterator.hasNext()) { + String file = iterator.next(); + if (!file.endsWith(".jsp") && !file.endsWith(".java")) { + iterator.remove(); + } } + + return resources; + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Attachment.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Attachment.java index c76fe0e84..a9de344d1 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Attachment.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Attachment.java @@ -1,36 +1,55 @@ package net.sourceforge.stripes.examples.bugzooky.biz; /** - * Very simple wrapper for file attachments uploaded for bugs. Assumes that the attachment - * contains some type of textual data. + * Very simple wrapper for file attachments uploaded for bugs. Assumes that the attachment contains + * some type of textual data. * * @author Tim Fennell */ public class Attachment { - private String name; - private long size; - private byte[] data; - private String contentType; - - public String getName() { return name; } - public void setName(String name) { this.name = name; } - - public long getSize() { return size; } - public void setSize(long size) { this.size = size; } - - public byte[] getData() { return data; } - public void setData(byte[] data) { this.data = data; } - - public String getContentType() { return contentType; } - public void setContentType(String contentType) { this.contentType = contentType; } - - public String getPreview() { - if (contentType.startsWith("text")) { - int amount = Math.min(data.length, 30); - return new String(data, 0, amount); - } - else { - return "[Binary File]"; - } + private String name; + private long size; + private byte[] data; + private String contentType; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getPreview() { + if (contentType.startsWith("text")) { + int amount = Math.min(data.length, 30); + return new String(data, 0, amount); + } else { + return "[Binary File]"; } + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Bug.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Bug.java index 6e407bea9..00c797ae2 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Bug.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Bug.java @@ -1,8 +1,8 @@ package net.sourceforge.stripes.examples.bugzooky.biz; +import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.util.ArrayList; /** * Represents a bug in the bug database. @@ -10,111 +10,111 @@ * @author Tim Fennell */ public class Bug { - private Integer id; - private Date openDate; - private String shortDescription; - private String longDescription; - private Component component; - private Priority priority; - private Status status; - private Person owner; - private Date dueDate; - private Float percentComplete; - private List attachments; - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public Date getOpenDate() { - return openDate; - } - - public void setOpenDate(Date openDate) { - this.openDate = openDate; - } - - public String getShortDescription() { - return shortDescription; - } - - public void setShortDescription(String shortDescription) { - this.shortDescription = shortDescription; - } - - public String getLongDescription() { - return longDescription; - } - - public void setLongDescription(String longDescription) { - this.longDescription = longDescription; - } - - public Component getComponent() { - return component; - } - - public void setComponent(Component component) { - this.component = component; - } + private Integer id; + private Date openDate; + private String shortDescription; + private String longDescription; + private Component component; + private Priority priority; + private Status status; + private Person owner; + private Date dueDate; + private Float percentComplete; + private List attachments; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Date getOpenDate() { + return openDate; + } + + public void setOpenDate(Date openDate) { + this.openDate = openDate; + } + + public String getShortDescription() { + return shortDescription; + } + + public void setShortDescription(String shortDescription) { + this.shortDescription = shortDescription; + } + + public String getLongDescription() { + return longDescription; + } + + public void setLongDescription(String longDescription) { + this.longDescription = longDescription; + } + + public Component getComponent() { + return component; + } + + public void setComponent(Component component) { + this.component = component; + } + + public Priority getPriority() { + return priority; + } + + public void setPriority(Priority priority) { + this.priority = priority; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public Person getOwner() { + return owner; + } + + public void setOwner(Person owner) { + this.owner = owner; + } + + public Date getDueDate() { + return dueDate; + } + + public void setDueDate(Date dueDate) { + this.dueDate = dueDate; + } + + public Float getPercentComplete() { + return percentComplete; + } + + public void setPercentComplete(Float percentComplete) { + this.percentComplete = percentComplete; + } + + public List getAttachments() { + return attachments; + } + + public void setAttachments(List attachments) { + this.attachments = attachments; + } + + public void addAttachment(Attachment attachment) { + if (this.attachments == null) { + this.attachments = new ArrayList(); + } - public Priority getPriority() { - return priority; - } - - public void setPriority(Priority priority) { - this.priority = priority; - } - - public Status getStatus() { - return status; - } - - public void setStatus(Status status) { - this.status = status; - } - - public Person getOwner() { - return owner; - } - - public void setOwner(Person owner) { - this.owner = owner; - } - - public Date getDueDate() { - return dueDate; - } - - public void setDueDate(Date dueDate) { - this.dueDate = dueDate; - } - - public Float getPercentComplete() { - return percentComplete; - } - - public void setPercentComplete(Float percentComplete) { - this.percentComplete = percentComplete; - } - - public List getAttachments() { - return attachments; - } - - public void setAttachments(List attachments) { - this.attachments = attachments; - } - - public void addAttachment(Attachment attachment) { - if (this.attachments == null) { - this.attachments = new ArrayList(); - } - - this.attachments.add(attachment); - } + this.attachments.add(attachment); + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/BugManager.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/BugManager.java index 37abb5903..01f6a414a 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/BugManager.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/BugManager.java @@ -1,11 +1,11 @@ package net.sourceforge.stripes.examples.bugzooky.biz; +import java.util.ArrayList; +import java.util.Collections; import java.util.Date; +import java.util.List; import java.util.Map; import java.util.TreeMap; -import java.util.List; -import java.util.Collections; -import java.util.ArrayList; /** * Maintains an in memory list of bugs in the system. @@ -13,88 +13,88 @@ * @author Tim Fennell */ public class BugManager { - /** Sequence number to use for generating IDs. */ - private static int idSequence = 0; + /** Sequence number to use for generating IDs. */ + private static int idSequence = 0; - /** Storage for all known bugs. */ - private static Map bugs = new TreeMap(); + /** Storage for all known bugs. */ + private static Map bugs = new TreeMap(); - static { - ComponentManager cm = new ComponentManager(); - PersonManager pm = new PersonManager(); + static { + ComponentManager cm = new ComponentManager(); + PersonManager pm = new PersonManager(); - Bug bug = new Bug(); - bug.setShortDescription("First ever bug in the system."); - bug.setLongDescription("This is a test bug, and is the first one ever made."); - bug.setOpenDate( new Date() ); - bug.setStatus( Status.Resolved ); - bug.setPriority( Priority.High ); - bug.setComponent( cm.getComponent(0) ); - bug.setOwner( pm.getPerson(3) ); - saveOrUpdateInternal(bug); + Bug bug = new Bug(); + bug.setShortDescription("First ever bug in the system."); + bug.setLongDescription("This is a test bug, and is the first one ever made."); + bug.setOpenDate(new Date()); + bug.setStatus(Status.Resolved); + bug.setPriority(Priority.High); + bug.setComponent(cm.getComponent(0)); + bug.setOwner(pm.getPerson(3)); + saveOrUpdateInternal(bug); - bug = new Bug(); - bug.setShortDescription("Another bug! Oh no!."); - bug.setLongDescription("How terrible - I found another bug."); - bug.setOpenDate( new Date() ); - bug.setStatus( Status.Assigned ); - bug.setPriority( Priority.Blocker ); - bug.setComponent( cm.getComponent(2) ); - bug.setOwner( pm.getPerson(4) ); - saveOrUpdateInternal(bug); + bug = new Bug(); + bug.setShortDescription("Another bug! Oh no!."); + bug.setLongDescription("How terrible - I found another bug."); + bug.setOpenDate(new Date()); + bug.setStatus(Status.Assigned); + bug.setPriority(Priority.Blocker); + bug.setComponent(cm.getComponent(2)); + bug.setOwner(pm.getPerson(4)); + saveOrUpdateInternal(bug); - bug = new Bug(); - bug.setShortDescription("Three bugs? This is just getting out of hand."); - bug.setLongDescription("What kind of system has three bugs? Egads."); - bug.setOpenDate( new Date() ); - bug.setStatus( Status.New ); - bug.setPriority( Priority.High ); - bug.setComponent( cm.getComponent(0) ); - bug.setOwner( pm.getPerson(1) ); - saveOrUpdateInternal(bug); + bug = new Bug(); + bug.setShortDescription("Three bugs? This is just getting out of hand."); + bug.setLongDescription("What kind of system has three bugs? Egads."); + bug.setOpenDate(new Date()); + bug.setStatus(Status.New); + bug.setPriority(Priority.High); + bug.setComponent(cm.getComponent(0)); + bug.setOwner(pm.getPerson(1)); + saveOrUpdateInternal(bug); - bug = new Bug(); - bug.setShortDescription("Oh good lord - I found a fourth bug."); - bug.setLongDescription("That's it, you're all fired. I need some better developers."); - bug.setOpenDate( new Date() ); - bug.setStatus( Status.New ); - bug.setPriority( Priority.Critical ); - bug.setComponent( cm.getComponent(3) ); - bug.setOwner( pm.getPerson(0) ); - saveOrUpdateInternal(bug); + bug = new Bug(); + bug.setShortDescription("Oh good lord - I found a fourth bug."); + bug.setLongDescription("That's it, you're all fired. I need some better developers."); + bug.setOpenDate(new Date()); + bug.setStatus(Status.New); + bug.setPriority(Priority.Critical); + bug.setComponent(cm.getComponent(3)); + bug.setOwner(pm.getPerson(0)); + saveOrUpdateInternal(bug); - bug = new Bug(); - bug.setShortDescription("Development team gone missing."); - bug.setLongDescription("No, wait! I didn't mean it! Please come back and fix the bugs!!"); - bug.setOpenDate( new Date() ); - bug.setStatus( Status.New ); - bug.setPriority( Priority.Blocker ); - bug.setComponent( cm.getComponent(2) ); - bug.setOwner( pm.getPerson(5) ); - saveOrUpdateInternal(bug); - } - - /** Gets the bug with the corresponding ID, or null if it does not exist. */ - public Bug getBug(int id) { - return bugs.get(id); - } + bug = new Bug(); + bug.setShortDescription("Development team gone missing."); + bug.setLongDescription("No, wait! I didn't mean it! Please come back and fix the bugs!!"); + bug.setOpenDate(new Date()); + bug.setStatus(Status.New); + bug.setPriority(Priority.Blocker); + bug.setComponent(cm.getComponent(2)); + bug.setOwner(pm.getPerson(5)); + saveOrUpdateInternal(bug); + } - /** Returns a sorted list of all bugs in the system. */ - public List getAllBugs() { - return Collections.unmodifiableList( new ArrayList(bugs.values()) ); - } + /** Gets the bug with the corresponding ID, or null if it does not exist. */ + public Bug getBug(int id) { + return bugs.get(id); + } - /** Updates an existing bug, or saves a new bug if the bug is a new one. */ - public void saveOrUpdate(Bug bug) { - saveOrUpdateInternal(bug); - } + /** Returns a sorted list of all bugs in the system. */ + public List getAllBugs() { + return Collections.unmodifiableList(new ArrayList(bugs.values())); + } - /** Static helper so that it can be used both by the instance method, and in static init. */ - private static void saveOrUpdateInternal(Bug bug) { - if (bug.getId() == null) { - bug.setId(idSequence++); - } + /** Updates an existing bug, or saves a new bug if the bug is a new one. */ + public void saveOrUpdate(Bug bug) { + saveOrUpdateInternal(bug); + } - bugs.put(bug.getId(), bug); + /** Static helper so that it can be used both by the instance method, and in static init. */ + private static void saveOrUpdateInternal(Bug bug) { + if (bug.getId() == null) { + bug.setId(idSequence++); } + + bugs.put(bug.getId(), bug); + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Component.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Component.java index caf4e1d3e..8ef56a087 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Component.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Component.java @@ -1,38 +1,45 @@ package net.sourceforge.stripes.examples.bugzooky.biz; /** - * Class that represents a compopnent of a software system against which bugs can be - * filed. + * Class that represents a compopnent of a software system against which bugs can be filed. * * @author Tim Fennell */ public class Component { - private Integer id; - private String name; - - /** Default constructor. */ - public Component() { } - - /** Constructs a new component with the supplied name. */ - public Component(String name) { - this.name = name; - } - - /** Gets the ID of the Component. */ - public Integer getId() { return id; } - - /** Sets the ID of the Component. */ - public void setId(Integer id) { this.id = id; } - - /** Gets the name of the Component - may be null if one is not set. */ - public String getName() { return name; } - - /** Sets the name of the Component. */ - public void setName(String name) { this.name = name; } - - /** Perform equality checks based on identity. */ - @Override - public boolean equals(Object obj) { - return (obj instanceof Component) && this.id == ((Component) obj).id; - } + private Integer id; + private String name; + + /** Default constructor. */ + public Component() {} + + /** Constructs a new component with the supplied name. */ + public Component(String name) { + this.name = name; + } + + /** Gets the ID of the Component. */ + public Integer getId() { + return id; + } + + /** Sets the ID of the Component. */ + public void setId(Integer id) { + this.id = id; + } + + /** Gets the name of the Component - may be null if one is not set. */ + public String getName() { + return name; + } + + /** Sets the name of the Component. */ + public void setName(String name) { + this.name = name; + } + + /** Perform equality checks based on identity. */ + @Override + public boolean equals(Object obj) { + return (obj instanceof Component) && this.id == ((Component) obj).id; + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/ComponentManager.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/ComponentManager.java index 69234e866..7e458a15c 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/ComponentManager.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/ComponentManager.java @@ -12,54 +12,54 @@ * @author Tim Fennell */ public class ComponentManager { - /** Sequence number used to generated IDs. */ - private static int idSequence = 0; + /** Sequence number used to generated IDs. */ + private static int idSequence = 0; - /** Storage for all known components. */ - private static Map components = new TreeMap(); + /** Storage for all known components. */ + private static Map components = new TreeMap(); - static { - Component component = new Component("Component 0"); - saveOrUpdateInternal(component); + static { + Component component = new Component("Component 0"); + saveOrUpdateInternal(component); - component = new Component("Component 1"); - saveOrUpdateInternal(component); + component = new Component("Component 1"); + saveOrUpdateInternal(component); - component = new Component("Component 2"); - saveOrUpdateInternal(component); + component = new Component("Component 2"); + saveOrUpdateInternal(component); - component = new Component("Component 3"); - saveOrUpdateInternal(component); + component = new Component("Component 3"); + saveOrUpdateInternal(component); - component = new Component("Component 4"); - saveOrUpdateInternal(component); - } + component = new Component("Component 4"); + saveOrUpdateInternal(component); + } - /** Gets the component with the specified ID, or null if no such component exists. */ - public Component getComponent(int id) { - return components.get(id); - } + /** Gets the component with the specified ID, or null if no such component exists. */ + public Component getComponent(int id) { + return components.get(id); + } - /** Returns a sorted list of all components in the system. */ - public List getAllComponents() { - return Collections.unmodifiableList( new ArrayList(components.values()) ); - } + /** Returns a sorted list of all components in the system. */ + public List getAllComponents() { + return Collections.unmodifiableList(new ArrayList(components.values())); + } - /** Updates an existing component if the ID matches, or saves a new one otherwise. */ - public void saveOrUpdate(Component component) { - saveOrUpdateInternal(component); - } + /** Updates an existing component if the ID matches, or saves a new one otherwise. */ + public void saveOrUpdate(Component component) { + saveOrUpdateInternal(component); + } - /** Deletes an existing Components. May leave dangling references. */ - public void deleteComponent(int componentId) { - components.remove(componentId); - } - - private static void saveOrUpdateInternal(Component component) { - if (component.getId() == null) { - component.setId(idSequence++); - } + /** Deletes an existing Components. May leave dangling references. */ + public void deleteComponent(int componentId) { + components.remove(componentId); + } - components.put(component.getId(), component); + private static void saveOrUpdateInternal(Component component) { + if (component.getId() == null) { + component.setId(idSequence++); } + + components.put(component.getId(), component); + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Person.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Person.java index fe6a536ad..374633920 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Person.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Person.java @@ -6,68 +6,88 @@ * @author Tim Fennell */ public class Person { - private Integer id; - private String username; - private String firstName; - private String lastName; - private String email; - private String password; - - /** Default constructor. */ - public Person() { } - - /** Constructs a well formed person. */ - public Person(String username, String password, String first, String last, String email) { - this.username = username; - this.password = password; - this.firstName = first; - this.lastName = last; - this.email = email; - } - - /** Gets the ID of the person. */ - public Integer getId() { return id; } - - /** Sets the ID of the person. */ - public void setId(Integer id) { this.id = id; } - - /** Gets the username of the person. */ - public String getUsername() { return username; } - - /** Sets the username of the user. */ - public void setUsername(String username) { this.username = username; } - - /** Gets the first name of the person. */ - public String getFirstName() { return firstName; } - - /** Sets the first name of the user. */ - public void setFirstName(String firstName) { this.firstName = firstName; } - - /** Gets the last name of the person. */ - public String getLastName() { return lastName; } - - /** Sets the last name of the user. */ - public void setLastName(String lastName) { this.lastName = lastName; } - - /** Gets the person's email address. */ - public String getEmail() { return email; } - - /** Sets the person's email address. */ - public void setEmail(String email) { this.email = email; } - - /** Gets the person's unencrypted password. */ - public String getPassword() { - return password; - } - - /** Sets the person's unencrypted password. */ - public void setPassword(String password) { - this.password = password; - } - - /** Equality is determined to be when the ID numbers match. */ - @Override - public boolean equals(Object obj) { - return (obj instanceof Person) && this.id == ((Person) obj).id; - } + private Integer id; + private String username; + private String firstName; + private String lastName; + private String email; + private String password; + + /** Default constructor. */ + public Person() {} + + /** Constructs a well formed person. */ + public Person(String username, String password, String first, String last, String email) { + this.username = username; + this.password = password; + this.firstName = first; + this.lastName = last; + this.email = email; + } + + /** Gets the ID of the person. */ + public Integer getId() { + return id; + } + + /** Sets the ID of the person. */ + public void setId(Integer id) { + this.id = id; + } + + /** Gets the username of the person. */ + public String getUsername() { + return username; + } + + /** Sets the username of the user. */ + public void setUsername(String username) { + this.username = username; + } + + /** Gets the first name of the person. */ + public String getFirstName() { + return firstName; + } + + /** Sets the first name of the user. */ + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + /** Gets the last name of the person. */ + public String getLastName() { + return lastName; + } + + /** Sets the last name of the user. */ + public void setLastName(String lastName) { + this.lastName = lastName; + } + + /** Gets the person's email address. */ + public String getEmail() { + return email; + } + + /** Sets the person's email address. */ + public void setEmail(String email) { + this.email = email; + } + + /** Gets the person's unencrypted password. */ + public String getPassword() { + return password; + } + + /** Sets the person's unencrypted password. */ + public void setPassword(String password) { + this.password = password; + } + + /** Equality is determined to be when the ID numbers match. */ + @Override + public boolean equals(Object obj) { + return (obj instanceof Person) && this.id == ((Person) obj).id; + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/PersonManager.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/PersonManager.java index 25dad21b6..85655916f 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/PersonManager.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/PersonManager.java @@ -6,75 +6,73 @@ import java.util.Map; import java.util.TreeMap; -/** - * Manager class that is used to access a "database" of people that is tracked in memory. - */ +/** Manager class that is used to access a "database" of people that is tracked in memory. */ public class PersonManager { - /** Sequence counter for ID generation. */ - private static int idSequence = 0; + /** Sequence counter for ID generation. */ + private static int idSequence = 0; - /** Stores the list of people in the system. */ - private static Map people = new TreeMap(); + /** Stores the list of people in the system. */ + private static Map people = new TreeMap(); - static { - Person person = new Person("scooby", "scooby", "Scooby", "Doo", "scooby@mystery.machine.tv"); - saveOrUpdateInternal(person); + static { + Person person = new Person("scooby", "scooby", "Scooby", "Doo", "scooby@mystery.machine.tv"); + saveOrUpdateInternal(person); - person = new Person("shaggy", "shaggy", "Shaggy", "Rogers", "shaggy@mystery.machine.tv"); - saveOrUpdateInternal(person); + person = new Person("shaggy", "shaggy", "Shaggy", "Rogers", "shaggy@mystery.machine.tv"); + saveOrUpdateInternal(person); - person = new Person("scrappy", "scrappy", "Scrappy", "Doo", "scrappy@mystery.machine.tv"); - saveOrUpdateInternal(person); + person = new Person("scrappy", "scrappy", "Scrappy", "Doo", "scrappy@mystery.machine.tv"); + saveOrUpdateInternal(person); - person = new Person("daphne", "daphne", "Daphne", "Blake", "daphne@mystery.machine.tv"); - saveOrUpdateInternal(person); + person = new Person("daphne", "daphne", "Daphne", "Blake", "daphne@mystery.machine.tv"); + saveOrUpdateInternal(person); - person = new Person("velma", "velma", "Velma", "Dinkly", "velma@mystery.machine.tv"); - saveOrUpdateInternal(person); + person = new Person("velma", "velma", "Velma", "Dinkly", "velma@mystery.machine.tv"); + saveOrUpdateInternal(person); - person = new Person("fred", "fred", "Fred", "Jones", "fred@mystery.machine.tv"); - saveOrUpdateInternal(person); - } - - /** Returns the person with the specified ID, or null if no such person exists. */ - public Person getPerson(int id) { - return people.get(id); - } + person = new Person("fred", "fred", "Fred", "Jones", "fred@mystery.machine.tv"); + saveOrUpdateInternal(person); + } - /** Returns a person with the specified username, if one exists. */ - public Person getPerson(String username) { - for (Person person : PersonManager.people.values()) { - if (person.getUsername().equalsIgnoreCase(username)) { - return person; - } - } - - return null; - } + /** Returns the person with the specified ID, or null if no such person exists. */ + public Person getPerson(int id) { + return people.get(id); + } - /** Gets a list of all the people in the system. */ - public List getAllPeople() { - return Collections.unmodifiableList( new ArrayList(people.values()) ); + /** Returns a person with the specified username, if one exists. */ + public Person getPerson(String username) { + for (Person person : PersonManager.people.values()) { + if (person.getUsername().equalsIgnoreCase(username)) { + return person; + } } - /** Updates the person if the ID matches an existing person, otherwise saves a new person. */ - public void saveOrUpdate(Person person) { - saveOrUpdateInternal(person); + return null; + } + + /** Gets a list of all the people in the system. */ + public List getAllPeople() { + return Collections.unmodifiableList(new ArrayList(people.values())); + } + + /** Updates the person if the ID matches an existing person, otherwise saves a new person. */ + public void saveOrUpdate(Person person) { + saveOrUpdateInternal(person); + } + + /** + * Deletes a person from the system...doesn't do anything fancy to clean up where the person is + * used. + */ + public void deletePerson(int id) { + people.remove(id); + } + + private static void saveOrUpdateInternal(Person person) { + if (person.getId() == null) { + person.setId(idSequence++); } - /** - * Deletes a person from the system...doesn't do anything fancy to clean up where the - * person is used. - */ - public void deletePerson(int id) { - people.remove(id); - } - - private static void saveOrUpdateInternal(Person person) { - if (person.getId() == null) { - person.setId(idSequence++); - } - - people.put(person.getId(), person); - } + people.put(person.getId(), person); + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Priority.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Priority.java index e91ad47ad..fbda12b10 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Priority.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Priority.java @@ -6,10 +6,10 @@ * @author Tim Fennell */ public enum Priority { - Blocker, - Critical, - High, - Medium, - Low, - Trivial; + Blocker, + Critical, + High, + Medium, + Low, + Trivial; } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Status.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Status.java index ea57e6105..e982c2005 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Status.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/biz/Status.java @@ -6,8 +6,8 @@ * @author Tim Fennell */ public enum Status { - New, - Assigned, - Resolved, - Closed; + New, + Assigned, + Resolved, + Closed; } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/BugFormatter.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/BugFormatter.java index 33893ea17..035289a08 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/BugFormatter.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/BugFormatter.java @@ -15,40 +15,32 @@ package net.sourceforge.stripes.examples.bugzooky.ext; import java.util.Locale; - import net.sourceforge.stripes.examples.bugzooky.biz.Bug; import net.sourceforge.stripes.format.Formatter; /** * A simple {@link Formatter} that formats a {@link Bug} object to text by returning its integer ID * in string form. For a more advanced formatter implementation, see {@link PersonFormatter}. - * + * * @author Ben Gunter */ public class BugFormatter implements Formatter { - /** Format the {@link Bug} object. */ - public String format(Bug bug) { - if (bug == null) - return ""; - else if (bug.getId() == null) - return ""; - else - return String.valueOf(bug.getId()); - } + /** Format the {@link Bug} object. */ + public String format(Bug bug) { + if (bug == null) return ""; + else if (bug.getId() == null) return ""; + else return String.valueOf(bug.getId()); + } - /** This method is specified by the {@link Formatter} interface, but it is not used here. */ - public void init() { - } + /** This method is specified by the {@link Formatter} interface, but it is not used here. */ + public void init() {} - /** This method is specified by the {@link Formatter} interface, but it is not used here. */ - public void setFormatPattern(String formatPattern) { - } + /** This method is specified by the {@link Formatter} interface, but it is not used here. */ + public void setFormatPattern(String formatPattern) {} - /** This method is specified by the {@link Formatter} interface, but it is not used here. */ - public void setFormatType(String formatType) { - } + /** This method is specified by the {@link Formatter} interface, but it is not used here. */ + public void setFormatType(String formatType) {} - /** This method is specified by the {@link Formatter} interface, but it is not used here. */ - public void setLocale(Locale locale) { - } -} \ No newline at end of file + /** This method is specified by the {@link Formatter} interface, but it is not used here. */ + public void setLocale(Locale locale) {} +} diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/BugTypeConverter.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/BugTypeConverter.java index 8da1abf54..fc7be9064 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/BugTypeConverter.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/BugTypeConverter.java @@ -16,7 +16,6 @@ import java.util.Collection; import java.util.Locale; - import net.sourceforge.stripes.examples.bugzooky.biz.Bug; import net.sourceforge.stripes.examples.bugzooky.biz.BugManager; import net.sourceforge.stripes.validation.SimpleError; @@ -24,40 +23,38 @@ import net.sourceforge.stripes.validation.ValidationError; /** - * A {@link TypeConverter} that parses its input string to an integer and queries the - * {@link BugManager} for a {@link Bug} object with that ID. If no {@link Bug} with the ID is found, - * then it simply returns null. If the input string cannot be parsed as an integer, then this - * {@link TypeConverter} adds a validation error and returns null. - * + * A {@link TypeConverter} that parses its input string to an integer and queries the {@link + * BugManager} for a {@link Bug} object with that ID. If no {@link Bug} with the ID is found, then + * it simply returns null. If the input string cannot be parsed as an integer, then this {@link + * TypeConverter} adds a validation error and returns null. + * * @author Ben Gunter */ public class BugTypeConverter implements TypeConverter { - /** - * Attempt to parse the input string to an integer and look up the {@link Bug} with that ID - * using a {@link BugManager}. - * - * @param input The input string to be parsed as the Bug ID. - * @param targetType The type of object we're supposed to be returning. - * @param errors The validation errors for this request. If the input string cannot be parsed, - * then we will add a new {@link ValidationError} to this collection and return null. - */ - public Bug convert(String input, Class targetType, - Collection errors) { - Bug bug = null; - - try { - int id = Integer.valueOf(input); - BugManager bugManager = new BugManager(); - bug = bugManager.getBug(id); - } - catch (NumberFormatException e) { - errors.add(new SimpleError("The number {0} is not a valid Bug ID", input)); - } + /** + * Attempt to parse the input string to an integer and look up the {@link Bug} with that ID using + * a {@link BugManager}. + * + * @param input The input string to be parsed as the Bug ID. + * @param targetType The type of object we're supposed to be returning. + * @param errors The validation errors for this request. If the input string cannot be parsed, + * then we will add a new {@link ValidationError} to this collection and return null. + */ + public Bug convert( + String input, Class targetType, Collection errors) { + Bug bug = null; - return bug; + try { + int id = Integer.valueOf(input); + BugManager bugManager = new BugManager(); + bug = bugManager.getBug(id); + } catch (NumberFormatException e) { + errors.add(new SimpleError("The number {0} is not a valid Bug ID", input)); } - /** This is specified in the {@link TypeConverter} interface, but it is not used here. */ - public void setLocale(Locale locale) { - } + return bug; + } + + /** This is specified in the {@link TypeConverter} interface, but it is not used here. */ + public void setLocale(Locale locale) {} } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/BugzookyActionBeanContext.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/BugzookyActionBeanContext.java index b84d1b1b1..c72fc3921 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/BugzookyActionBeanContext.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/BugzookyActionBeanContext.java @@ -4,25 +4,25 @@ import net.sourceforge.stripes.examples.bugzooky.biz.Person; /** - * ActionBeanContext subclass for the Bugzooky application that manages where values - * like the logged in user are stored. + * ActionBeanContext subclass for the Bugzooky application that manages where values like the logged + * in user are stored. * * @author Tim Fennell */ public class BugzookyActionBeanContext extends ActionBeanContext { - /** Gets the currently logged in user, or null if no-one is logged in. */ - public Person getUser() { - return (Person) getRequest().getSession().getAttribute("user"); - } + /** Gets the currently logged in user, or null if no-one is logged in. */ + public Person getUser() { + return (Person) getRequest().getSession().getAttribute("user"); + } - /** Sets the currently logged in user. */ - public void setUser(Person currentUser) { - getRequest().getSession().setAttribute("user", currentUser); - } + /** Sets the currently logged in user. */ + public void setUser(Person currentUser) { + getRequest().getSession().setAttribute("user", currentUser); + } - /** Logs the user out by invalidating the session. */ - public void logout() { - getRequest().getSession().invalidate(); - } + /** Logs the user out by invalidating the session. */ + public void logout() { + getRequest().getSession().invalidate(); + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/ComponentFormatter.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/ComponentFormatter.java index 296e3958c..46debdaf0 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/ComponentFormatter.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/ComponentFormatter.java @@ -15,41 +15,33 @@ package net.sourceforge.stripes.examples.bugzooky.ext; import java.util.Locale; - import net.sourceforge.stripes.examples.bugzooky.biz.Component; import net.sourceforge.stripes.format.Formatter; /** * A simple {@link Formatter} that formats a {@link Component} object to text by returning its - * integer ID in string form. For a more advanced formatter implementation, see - * {@link PersonFormatter}. - * + * integer ID in string form. For a more advanced formatter implementation, see {@link + * PersonFormatter}. + * * @author Ben Gunter */ public class ComponentFormatter implements Formatter { - /** Format the {@link Component} object. */ - public String format(Component component) { - if (component == null) - return ""; - else if (component.getId() == null) - return ""; - else - return String.valueOf(component.getId()); - } + /** Format the {@link Component} object. */ + public String format(Component component) { + if (component == null) return ""; + else if (component.getId() == null) return ""; + else return String.valueOf(component.getId()); + } - /** This method is specified by the {@link Formatter} interface, but it is not used here. */ - public void init() { - } + /** This method is specified by the {@link Formatter} interface, but it is not used here. */ + public void init() {} - /** This method is specified by the {@link Formatter} interface, but it is not used here. */ - public void setFormatPattern(String formatPattern) { - } + /** This method is specified by the {@link Formatter} interface, but it is not used here. */ + public void setFormatPattern(String formatPattern) {} - /** This method is specified by the {@link Formatter} interface, but it is not used here. */ - public void setFormatType(String formatType) { - } + /** This method is specified by the {@link Formatter} interface, but it is not used here. */ + public void setFormatType(String formatType) {} - /** This method is specified by the {@link Formatter} interface, but it is not used here. */ - public void setLocale(Locale locale) { - } -} \ No newline at end of file + /** This method is specified by the {@link Formatter} interface, but it is not used here. */ + public void setLocale(Locale locale) {} +} diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/ComponentTypeConverter.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/ComponentTypeConverter.java index de4ad3977..74a2aad34 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/ComponentTypeConverter.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/ComponentTypeConverter.java @@ -16,7 +16,6 @@ import java.util.Collection; import java.util.Locale; - import net.sourceforge.stripes.examples.bugzooky.biz.Component; import net.sourceforge.stripes.examples.bugzooky.biz.ComponentManager; import net.sourceforge.stripes.validation.SimpleError; @@ -24,40 +23,38 @@ import net.sourceforge.stripes.validation.ValidationError; /** - * A {@link TypeConverter} that parses its input string to an integer and queries the - * {@link ComponentManager} for a {@link Component} object with that ID. If no {@link Component} - * with the ID is found, then it simply returns null. If the input string cannot be parsed as an - * integer, then this {@link TypeConverter} adds a validation error and returns null. - * + * A {@link TypeConverter} that parses its input string to an integer and queries the {@link + * ComponentManager} for a {@link Component} object with that ID. If no {@link Component} with the + * ID is found, then it simply returns null. If the input string cannot be parsed as an integer, + * then this {@link TypeConverter} adds a validation error and returns null. + * * @author Ben Gunter */ public class ComponentTypeConverter implements TypeConverter { - /** - * Attempt to parse the input string to an integer and look up the {@link Component} with that - * ID using a {@link ComponentManager}. - * - * @param input The input string to be parsed as the Component ID. - * @param targetType The type of object we're supposed to be returning. - * @param errors The validation errors for this request. If the input string cannot be parsed, - * then we will add a new {@link ValidationError} to this collection and return null. - */ - public Component convert(String input, Class targetType, - Collection errors) { - Component component = null; - - try { - int id = Integer.valueOf(input); - ComponentManager componentManager = new ComponentManager(); - component = componentManager.getComponent(id); - } - catch (NumberFormatException e) { - errors.add(new SimpleError("The number {0} is not a valid Component ID", input)); - } + /** + * Attempt to parse the input string to an integer and look up the {@link Component} with that ID + * using a {@link ComponentManager}. + * + * @param input The input string to be parsed as the Component ID. + * @param targetType The type of object we're supposed to be returning. + * @param errors The validation errors for this request. If the input string cannot be parsed, + * then we will add a new {@link ValidationError} to this collection and return null. + */ + public Component convert( + String input, Class targetType, Collection errors) { + Component component = null; - return component; + try { + int id = Integer.valueOf(input); + ComponentManager componentManager = new ComponentManager(); + component = componentManager.getComponent(id); + } catch (NumberFormatException e) { + errors.add(new SimpleError("The number {0} is not a valid Component ID", input)); } - /** This is specified in the {@link TypeConverter} interface, but it is not used here. */ - public void setLocale(Locale locale) { - } + return component; + } + + /** This is specified in the {@link TypeConverter} interface, but it is not used here. */ + public void setLocale(Locale locale) {} } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/PersonFormatter.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/PersonFormatter.java index 780b5e25f..6c4275124 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/PersonFormatter.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/PersonFormatter.java @@ -17,7 +17,6 @@ import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; - import net.sourceforge.stripes.examples.bugzooky.biz.Person; import net.sourceforge.stripes.format.Formatter; @@ -28,80 +27,77 @@ * returns the {@link Person}'s username. The "full" format type returns the person's name in a form * specified by the format pattern, where %F means first name, %L means last name, %U means username * and %E means email address. - * + * * @author Ben Gunter */ public class PersonFormatter implements Formatter { - /** The default format pattern to use if no format pattern is specified. */ - private static final String DEFAULT_FORMAT_PATTTERN = "%L, %F (%U)"; - - private String formatType, formatPattern; + /** The default format pattern to use if no format pattern is specified. */ + private static final String DEFAULT_FORMAT_PATTTERN = "%L, %F (%U)"; - /** Format the {@link Person} object according to the format type and pattern. */ - public String format(Person person) { - if (person == null) { - return ""; - } - else if ("short".equals(formatType)) { - return checkNull(person.getUsername()); - } - else if ("full".equals(formatType)) { - Pattern pattern = Pattern.compile("%[EFLU]"); - String fp = formatPattern == null ? DEFAULT_FORMAT_PATTTERN : formatPattern; + private String formatType, formatPattern; - StringBuffer buf = new StringBuffer(); - Matcher matcher = pattern.matcher(fp); - while (matcher.find()) { - char spec = matcher.group().charAt(1); - switch (spec) { - case 'E': - matcher.appendReplacement(buf, checkNull(person.getEmail())); - break; - case 'F': - matcher.appendReplacement(buf, checkNull(person.getFirstName())); - break; - case 'L': - matcher.appendReplacement(buf, checkNull(person.getLastName())); - break; - case 'U': - matcher.appendReplacement(buf, checkNull(person.getUsername())); - break; - default: - buf.append(matcher.group()); - } - } - matcher.appendTail(buf); + /** Format the {@link Person} object according to the format type and pattern. */ + public String format(Person person) { + if (person == null) { + return ""; + } else if ("short".equals(formatType)) { + return checkNull(person.getUsername()); + } else if ("full".equals(formatType)) { + Pattern pattern = Pattern.compile("%[EFLU]"); + String fp = formatPattern == null ? DEFAULT_FORMAT_PATTTERN : formatPattern; - return buf.toString(); - } - else { - return String.valueOf(person.getId()); + StringBuffer buf = new StringBuffer(); + Matcher matcher = pattern.matcher(fp); + while (matcher.find()) { + char spec = matcher.group().charAt(1); + switch (spec) { + case 'E': + matcher.appendReplacement(buf, checkNull(person.getEmail())); + break; + case 'F': + matcher.appendReplacement(buf, checkNull(person.getFirstName())); + break; + case 'L': + matcher.appendReplacement(buf, checkNull(person.getLastName())); + break; + case 'U': + matcher.appendReplacement(buf, checkNull(person.getUsername())); + break; + default: + buf.append(matcher.group()); } - } + } + matcher.appendTail(buf); - protected String checkNull(String s) { - return s == null ? "" : s; + return buf.toString(); + } else { + return String.valueOf(person.getId()); } + } - /** Set the format type, which specifies the general format type: default (null), short or full. */ - public void setFormatType(String formatType) { - this.formatType = formatType; - } + protected String checkNull(String s) { + return s == null ? "" : s; + } - /** - * Set the format pattern to be used by the "full" format type. In this pattern, %F will be - * replaced with the first name, %L by the last name, %U by the username and %E by the email - * address. - */ - public void setFormatPattern(String formatPattern) { - this.formatPattern = formatPattern; - } + /** + * Set the format type, which specifies the general format type: default (null), short or full. + */ + public void setFormatType(String formatType) { + this.formatType = formatType; + } - /** This method is specified by the {@link Formatter} interface, but it is not used here. */ - public void init() { - } + /** + * Set the format pattern to be used by the "full" format type. In this pattern, %F will be + * replaced with the first name, %L by the last name, %U by the username and %E by the email + * address. + */ + public void setFormatPattern(String formatPattern) { + this.formatPattern = formatPattern; + } - /** This method is specified by the {@link Formatter} interface, but it is not used here. */ - public void setLocale(Locale locale) { - } -} \ No newline at end of file + /** This method is specified by the {@link Formatter} interface, but it is not used here. */ + public void init() {} + + /** This method is specified by the {@link Formatter} interface, but it is not used here. */ + public void setLocale(Locale locale) {} +} diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/PersonTypeConverter.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/PersonTypeConverter.java index 7c4144820..dec9eaef0 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/PersonTypeConverter.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/PersonTypeConverter.java @@ -16,7 +16,6 @@ import java.util.Collection; import java.util.Locale; - import net.sourceforge.stripes.examples.bugzooky.biz.Person; import net.sourceforge.stripes.examples.bugzooky.biz.PersonManager; import net.sourceforge.stripes.validation.SimpleError; @@ -24,40 +23,38 @@ import net.sourceforge.stripes.validation.ValidationError; /** - * A {@link TypeConverter} that parses its input string to an integer and queries the - * {@link PersonManager} for a {@link Person} object with that ID. If no {@link Person} with the ID - * is found, then it simply returns null. If the input string cannot be parsed as an integer, then - * this {@link TypeConverter} adds a validation error and returns null. - * + * A {@link TypeConverter} that parses its input string to an integer and queries the {@link + * PersonManager} for a {@link Person} object with that ID. If no {@link Person} with the ID is + * found, then it simply returns null. If the input string cannot be parsed as an integer, then this + * {@link TypeConverter} adds a validation error and returns null. + * * @author Ben Gunter */ public class PersonTypeConverter implements TypeConverter { - /** - * Attempt to parse the input string to an integer and look up the {@link Person} with that ID - * using a {@link PersonManager}. - * - * @param input The input string to be parsed as the Person ID. - * @param targetType The type of object we're supposed to be returning. - * @param errors The validation errors for this request. If the input string cannot be parsed, - * then we will add a new {@link ValidationError} to this collection and return null. - */ - public Person convert(String input, Class targetType, - Collection errors) { - Person person = null; - - try { - int id = Integer.valueOf(input); - PersonManager personManager = new PersonManager(); - person = personManager.getPerson(id); - } - catch (NumberFormatException e) { - errors.add(new SimpleError("The number {0} is not a valid Person ID", input)); - } + /** + * Attempt to parse the input string to an integer and look up the {@link Person} with that ID + * using a {@link PersonManager}. + * + * @param input The input string to be parsed as the Person ID. + * @param targetType The type of object we're supposed to be returning. + * @param errors The validation errors for this request. If the input string cannot be parsed, + * then we will add a new {@link ValidationError} to this collection and return null. + */ + public Person convert( + String input, Class targetType, Collection errors) { + Person person = null; - return person; + try { + int id = Integer.valueOf(input); + PersonManager personManager = new PersonManager(); + person = personManager.getPerson(id); + } catch (NumberFormatException e) { + errors.add(new SimpleError("The number {0} is not a valid Person ID", input)); } - /** This is specified in the {@link TypeConverter} interface, but it is not used here. */ - public void setLocale(Locale locale) { - } + return person; + } + + /** This is specified in the {@link TypeConverter} interface, but it is not used here. */ + public void setLocale(Locale locale) {} } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/Public.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/Public.java index b32ff91e8..223196c21 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/Public.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/Public.java @@ -19,7 +19,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; - import net.sourceforge.stripes.action.ActionBean; /** @@ -28,11 +27,10 @@ * class to indicate that users are allowed to access it even if they are not authenticated. Most of * this application's {@link ActionBean}s require authentication so it's easier to flag those that * do not require authentication. - * + * * @author Ben Gunter */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) -public @interface Public { -} +public @interface Public {} diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/SecurityInterceptor.java b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/SecurityInterceptor.java index 2045b9264..f81efff66 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/SecurityInterceptor.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/bugzooky/ext/SecurityInterceptor.java @@ -14,8 +14,7 @@ */ package net.sourceforge.stripes.examples.bugzooky.ext; -import javax.servlet.http.HttpServletRequest; - +import jakarta.servlet.http.HttpServletRequest; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.RedirectResolution; import net.sourceforge.stripes.action.Resolution; @@ -31,33 +30,32 @@ * After the {@link LifecycleStage#ActionBeanResolution} stage, this interceptor checks the resolved * {@link ActionBean} class for a {@link Public} annotation. If none is present, then the client is * redirected to the login page. - * + * * @author Ben Gunter */ @Intercepts(LifecycleStage.ActionBeanResolution) public class SecurityInterceptor implements Interceptor { - private Log log = Log.getInstance(SecurityInterceptor.class); + private Log log = Log.getInstance(SecurityInterceptor.class); - public Resolution intercept(ExecutionContext context) throws Exception { - HttpServletRequest request = context.getActionBeanContext().getRequest(); - String url = HttpUtil.getRequestedPath(request); - if (request.getQueryString() != null) - url = url + '?' + request.getQueryString(); - log.debug("Intercepting request: ", url); + public Resolution intercept(ExecutionContext context) throws Exception { + HttpServletRequest request = context.getActionBeanContext().getRequest(); + String url = HttpUtil.getRequestedPath(request); + if (request.getQueryString() != null) url = url + '?' + request.getQueryString(); + log.debug("Intercepting request: ", url); - Resolution resolution = context.proceed(); + Resolution resolution = context.proceed(); - // A null resolution here indicates a normal flow to the next stage - boolean authed = ((BugzookyActionBeanContext) context.getActionBeanContext()).getUser() != null; - if (!authed && resolution == null) { - ActionBean bean = context.getActionBean(); - if (bean != null && !bean.getClass().isAnnotationPresent(Public.class)) { - log.warn("Thwarted attempted to access ", bean.getClass().getSimpleName()); - return new RedirectResolution(LoginActionBean.class).addParameter("targetUrl", url); - } - } - - log.debug("Allowing public access to ", context.getActionBean().getClass().getSimpleName()); - return resolution; + // A null resolution here indicates a normal flow to the next stage + boolean authed = ((BugzookyActionBeanContext) context.getActionBeanContext()).getUser() != null; + if (!authed && resolution == null) { + ActionBean bean = context.getActionBean(); + if (bean != null && !bean.getClass().isAnnotationPresent(Public.class)) { + log.warn("Thwarted attempted to access ", bean.getClass().getSimpleName()); + return new RedirectResolution(LoginActionBean.class).addParameter("targetUrl", url); + } } + + log.debug("Allowing public access to ", context.getActionBean().getClass().getSimpleName()); + return resolution; + } } diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/quickstart/CalculatorActionBean.java b/examples/src/main/java/net/sourceforge/stripes/examples/quickstart/CalculatorActionBean.java index 61d3f10ba..2db62708b 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/quickstart/CalculatorActionBean.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/quickstart/CalculatorActionBean.java @@ -13,48 +13,74 @@ /** * A very simple calculator action. + * * @author Tim Fennell */ @Public public class CalculatorActionBean implements ActionBean { - private ActionBeanContext context; - @Validate(required=true) private double numberOne; - @Validate(required=true) private double numberTwo; - private double result; + private ActionBeanContext context; - public ActionBeanContext getContext() { return context; } - public void setContext(ActionBeanContext context) { this.context = context; } + @Validate(required = true) + private double numberOne; - public double getNumberOne() { return numberOne; } - public void setNumberOne(double numberOne) { this.numberOne = numberOne; } + @Validate(required = true) + private double numberTwo; - public double getNumberTwo() { return numberTwo; } - public void setNumberTwo(double numberTwo) { this.numberTwo = numberTwo; } + private double result; - public double getResult() { return result; } - public void setResult(double result) { this.result = result; } + public ActionBeanContext getContext() { + return context; + } - /** An event handler method that adds number one to number two. */ - @DefaultHandler - public Resolution addition() { - result = numberOne + numberTwo; - return new ForwardResolution("/quickstart/index.jsp"); - } + public void setContext(ActionBeanContext context) { + this.context = context; + } - /** An event handler method that divides number one by number two. */ - public Resolution division() { - result = numberOne / numberTwo; - return new ForwardResolution("/quickstart/index.jsp"); - } + public double getNumberOne() { + return numberOne; + } + + public void setNumberOne(double numberOne) { + this.numberOne = numberOne; + } + + public double getNumberTwo() { + return numberTwo; + } + + public void setNumberTwo(double numberTwo) { + this.numberTwo = numberTwo; + } + + public double getResult() { + return result; + } + + public void setResult(double result) { + this.result = result; + } + + /** An event handler method that adds number one to number two. */ + @DefaultHandler + public Resolution addition() { + result = numberOne + numberTwo; + return new ForwardResolution("/quickstart/index.jsp"); + } + + /** An event handler method that divides number one by number two. */ + public Resolution division() { + result = numberOne / numberTwo; + return new ForwardResolution("/quickstart/index.jsp"); + } - /** - * An example of a custom validation that checks that division operations - * are not dividing by zero. - */ - @ValidationMethod(on="division") - public void avoidDivideByZero(ValidationErrors errors) { - if (this.numberTwo == 0) { - errors.add("numberTwo", new SimpleError("Dividing by zero is not allowed.")); - } + /** + * An example of a custom validation that checks that division operations are not dividing by + * zero. + */ + @ValidationMethod(on = "division") + public void avoidDivideByZero(ValidationErrors errors) { + if (this.numberTwo == 0) { + errors.add("numberTwo", new SimpleError("Dividing by zero is not allowed.")); } + } } diff --git a/examples/src/main/resources/commons-logging.properties b/examples/src/main/resources/commons-logging.properties index 4f6ba27f8..b94f669fc 100644 --- a/examples/src/main/resources/commons-logging.properties +++ b/examples/src/main/resources/commons-logging.properties @@ -1 +1,4 @@ -org.apache.commons.logging.Log=net.sourceforge.stripes.util.Log4JLogger \ No newline at end of file +org.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger + +handlers=java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=ALL diff --git a/examples/src/main/resources/log4j.properties b/examples/src/main/resources/log4j.properties deleted file mode 100644 index a41fbc52b..000000000 --- a/examples/src/main/resources/log4j.properties +++ /dev/null @@ -1,10 +0,0 @@ -### direct log messages to stdout ### -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.Target=System.out -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n - -### set log levels - for more verbose logging change 'info' to 'debug' ### -log4j.rootLogger=INFO, stdout -log4j.logger.net.sourceforge.stripes=DEBUG - diff --git a/examples/src/main/webapp/WEB-INF/web.xml b/examples/src/main/webapp/WEB-INF/web.xml index c419ea93a..8d862e197 100644 --- a/examples/src/main/webapp/WEB-INF/web.xml +++ b/examples/src/main/webapp/WEB-INF/web.xml @@ -1,10 +1,9 @@ - + xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd" + version="6.0"> Stripes Examples diff --git a/examples/src/main/webapp/bugzooky/Register.jsp b/examples/src/main/webapp/bugzooky/Register.jsp index ace2a4cb9..f8cef1692 100644 --- a/examples/src/main/webapp/bugzooky/Register.jsp +++ b/examples/src/main/webapp/bugzooky/Register.jsp @@ -5,8 +5,7 @@ - - +

Please provide the following information:

diff --git a/examples/src/main/webapp/bugzooky/Register2.jsp b/examples/src/main/webapp/bugzooky/Register2.jsp index 2acb984bc..dea2cf006 100644 --- a/examples/src/main/webapp/bugzooky/Register2.jsp +++ b/examples/src/main/webapp/bugzooky/Register2.jsp @@ -4,7 +4,7 @@ - +

Welcome ${actionBean.user.firstName}, please pick a password:

diff --git a/examples/src/main/webapp/bugzooky/layout/header.jsp b/examples/src/main/webapp/bugzooky/layout/header.jsp index 6a1493636..f25026d34 100644 --- a/examples/src/main/webapp/bugzooky/layout/header.jsp +++ b/examples/src/main/webapp/bugzooky/layout/header.jsp @@ -8,15 +8,15 @@ Welcome: | - Logout + Logout
\ No newline at end of file diff --git a/pom.xml b/pom.xml index 91885ac78..b5047c775 100644 --- a/pom.xml +++ b/pom.xml @@ -1,15 +1,10 @@ - - + + 4.0.0 - - org.sonatype.oss - oss-parent - 9 - net.sourceforge.stripes stripes-parent - 1.6.0-SNAPSHOT + 1.6.0-JakartaEE10 pom @@ -19,18 +14,18 @@ Stripes Parent - http://stripesframework.org/ + https://stripesframework.org/ Stripes web framework parent project. Stripes Framework - http://www.stripesframework.org/ + https://www.stripesframework.org/ The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt + https://www.apache.org/licenses/LICENSE-2.0.txt repo @@ -43,126 +38,76 @@ JIRA - http://stripesframework.org/jira/browse/STS + https://stripesframework.org/jira/browse/STS Jenkins - http://www.stripesframework.org/jenkins + https://www.stripesframework.org/jenkins - javax.servlet - servlet-api - 2.5 + jakarta.servlet + jakarta.servlet-api + 6.0.0 provided - javax.servlet.jsp - jsp-api - 2.2 + jakarta.servlet.jsp + jakarta.servlet.jsp-api + 3.1.1 provided - javax.el - el-api - 2.2 + jakarta.el + jakarta.el-api + 5.0.1 provided - javax.servlet - jstl - 1.2 + jakarta.servlet.jsp.jstl + jakarta.servlet.jsp.jstl-api + 3.0.0 provided - taglibs + jakarta-taglibs standard 1.1.2 provided - - javax.mail - mail - 1.4.7 - provided - true - - - javax.activation - activation - - - - - org.testng - testng - 6.8.8 - test - commons-logging commons-logging - 1.1.3 - - - javax.servlet - servlet-api - - - log4j - log4j - - - logkit - logkit - - - avalon-framework - avalon-framework - - + 1.2 - log4j - log4j - 1.2.17 - - - - jms - javax.jms - - - jmxtools - com.sun.jdmk - - - jmxri - com.sun.jmx - - + org.springframework + spring-core + 5.3.29 + provided + true org.springframework - spring - 1.2.6 + spring-context + 5.3.29 provided true - servlets.com - cos - 05Nov2002 + org.springframework + spring-web + 5.3.29 provided true - commons-fileupload - commons-fileupload - 1.3.1 + org.apache.commons + commons-fileupload2-jakarta + 2.0.0-M1 provided true @@ -175,26 +120,26 @@ org.apache.maven.plugins maven-compiler-plugin - 3.1 + 3.11.0 - 1.5 - 1.5 + 11 + 11 org.apache.maven.plugins maven-surefire-plugin - 2.17 + 3.1.2 org.apache.maven.plugins maven-jar-plugin - 2.1 + 3.3.0 org.apache.maven.plugins maven-assembly-plugin - 2.4 + 3.6.0 net.sourceforge.maven-taglib @@ -204,7 +149,7 @@ org.apache.maven.plugins maven-source-plugin - 2.1.2 + 3.3.0 org.apache.maven.plugins @@ -214,12 +159,7 @@ org.apache.maven.plugins maven-deploy-plugin - 2.8.1 - - - org.apache.tomcat.maven - tomcat7-maven-plugin - 2.2 + 3.1.1 org.codehaus.gmaven @@ -229,12 +169,12 @@ org.codehaus.cargo cargo-maven2-plugin - 1.4.7 + 1.9.0 org.apache.maven.plugins maven-javadoc-plugin - 2.9.1 + 3.5.0 generate-javadoc @@ -244,16 +184,15 @@ ${basedir}/src/main/resources/overview.html - © Copyright 2005-2014, Stripes Development Team. + © Copyright 2005-2023, Stripes Development Team. true - true true true - http://docs.oracle.com/javase/1.5.0/docs/api/ - http://docs.oracle.com/javaee/5/api/ - http://docs.spring.io/spring/docs/1.2.x/api/ - http://commons.apache.org/proper/commons-logging/apidocs/ + https://docs.oracle.com/en/java/javase/11/docs/api/ + https://jakarta.ee/specifications/servlet/5.0/apidocs/ + https://docs.spring.io/spring-framework/docs/5.3.29/javadoc-api/ + https://jakarta.ee/specifications/expression-language/4.0/apidocs/ diff --git a/stripes/pom.xml b/stripes/pom.xml index 2dce6141b..34804c3e8 100644 --- a/stripes/pom.xml +++ b/stripes/pom.xml @@ -5,7 +5,7 @@ net.sourceforge.stripes stripes-parent - 1.6.0-SNAPSHOT + 1.6.0-JakartaEE10 .. stripes @@ -23,45 +23,45 @@ - javax.servlet - servlet-api + jakarta.servlet + jakarta.servlet-api + 6.0.0 + provided - javax.servlet.jsp - jsp-api + jakarta.servlet.jsp + jakarta.servlet.jsp-api + 3.1.1 + provided - javax.el - el-api + jakarta.el + jakarta.el-api + 5.0.1 + provided - javax.mail - mail + junit + junit + 4.13.2 + test - org.testng - testng + org.apache.commons + commons-fileupload2-jakarta + 2.0.0-M1 - commons-logging - commons-logging - - - log4j - log4j - - - servlets.com - cos - - - commons-fileupload - commons-fileupload + org.springframework + spring-core + 5.3.29 org.springframework - spring + spring-context + 5.3.29 + @@ -76,7 +76,7 @@ maven-antrun-plugin - 1.7 + 3.1.0 generate-resources @@ -90,6 +90,7 @@ + @@ -102,6 +103,7 @@ org.apache.maven.plugins maven-surefire-plugin + -Dfile.encoding=UTF-8 **/*.java @@ -123,6 +125,7 @@ org.apache.maven.plugins maven-javadoc-plugin + 3.5.0 generate-javadoc @@ -131,6 +134,7 @@ jar + none Core API @@ -185,6 +189,23 @@ org.codehaus.mojo cobertura-maven-plugin + + com.coveo + fmt-maven-plugin + 2.13 + + + + format + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.0 + diff --git a/stripes/src/main/java/net/sourceforge/stripes/Main.java b/stripes/src/main/java/net/sourceforge/stripes/Main.java index 2219acfe8..4e2cc03ee 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/Main.java +++ b/stripes/src/main/java/net/sourceforge/stripes/Main.java @@ -16,31 +16,42 @@ /** * Simple executable class that is used as the Main-Class in the Stripes jar. Outputs version - * information and other information about the environment on which the jar is being - * executed. + * information and other information about the environment on which the jar is being executed. * * @author Tim Fennell * @since Stripes 1.1.1 */ public class Main { - /** Main method that does what the class level javadoc states. */ - public static void main(String[] argv) { - Package pkg = Main.class.getPackage(); - System.out.println("Stripes version \"" + pkg.getSpecificationVersion() + "\"" + - " (build " + pkg.getImplementationVersion() + ")"); + /** Main method that does what the class level javadoc states. */ + public static void main(String[] argv) { + Package pkg = Main.class.getPackage(); + System.out.println( + "Stripes version \"" + + pkg.getSpecificationVersion() + + "\"" + + " (build " + + pkg.getImplementationVersion() + + ")"); - System.out.println( - "Running on java version \"" + System.getProperty("java.version") + "\"" + - " (build " + System.getProperty("java.runtime.version") + ")" + - " from " + System.getProperty("java.vendor") - ); + System.out.println( + "Running on java version \"" + + System.getProperty("java.version") + + "\"" + + " (build " + + System.getProperty("java.runtime.version") + + ")" + + " from " + + System.getProperty("java.vendor")); - System.out.println( - "Operating environment \"" + System.getProperty("os.name") + "\"" + - " version " + System.getProperty("os.version") + - " on " + System.getProperty("os.arch") - ); + System.out.println( + "Operating environment \"" + + System.getProperty("os.name") + + "\"" + + " version " + + System.getProperty("os.version") + + " on " + + System.getProperty("os.arch")); - System.out.println("For more information on Stripes please visit http://stripesframework.org"); - } + System.out.println("For more information on Stripes please visit http://stripesframework.org"); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/ActionBean.java b/stripes/src/main/java/net/sourceforge/stripes/action/ActionBean.java index 21ef4d6aa..676a3a3b4 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/ActionBean.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/ActionBean.java @@ -17,18 +17,18 @@ import net.sourceforge.stripes.validation.ValidationErrorHandler; /** - *

Interface for all classes that respond to user interface events. Implementations receive - * information about the event (usually a form submission) in two ways. The first is through a - * ActionBeanContext object which will always be set on the ActionBean prior to any user - * implemented "handler" methods being invoked. The second is through the setting of - * JavaBean style properties on the object (and nested JavaBeans). Values submitted in an HTML - * Form will be bound to a property on the object if such a property is defined. The - * ActionBeanContext is used primarily to provide access to Servlet APIs such as the request and - * response, and other information generated during the pre-processing of the request.

+ * Interface for all classes that respond to user interface events. Implementations receive + * information about the event (usually a form submission) in two ways. The first is through a + * ActionBeanContext object which will always be set on the ActionBean prior to any user implemented + * "handler" methods being invoked. The second is through the setting of JavaBean style + * properties on the object (and nested JavaBeans). Values submitted in an HTML Form will be bound + * to a property on the object if such a property is defined. The ActionBeanContext is used + * primarily to provide access to Servlet APIs such as the request and response, and other + * information generated during the pre-processing of the request. * *

How Stripes determines which method to invoke is based on Annotations as opposed to a strict - * interface. Firstly, each ActionBean should be annotated with a UrlBinding which specifies the - * path within the web application that the ActionBean will be bound to. E.g:

+ * interface. Firstly, each ActionBean should be annotated with a UrlBinding which specifies the + * path within the web application that the ActionBean will be bound to. E.g: * *
  * @UrlBinding("/action/SampleAction")
@@ -38,12 +38,12 @@
  *
  * 

At run time Stripes will discover all implementations of ActionBean and, when a form is * submitted, will locate the ActionBean to use by matching the path in the request to the - * UrlBinding annotation on the ActionBean.

+ * UrlBinding annotation on the ActionBean. * - *

The way in which a specific event is mapped to a "handler" method can vary. The + *

The way in which a specific event is mapped to a "handler" method can vary. The * default method used by Stripes is to identify the name of the button or image button * used to submit the request, and to find the handler method that can handle that event. The way - * this is declared in the ActionBean is like this:

+ * this is declared in the ActionBean is like this: * *
  * @HandlesEvent("SampleEvent")
@@ -54,17 +54,17 @@
  * 
* *

Event names specified in the HandlesEvent annotation should be unique within a given - * ActionBean (and it's superclasses), but can be re-used across ActionBeans. For example, a given + * ActionBean (and it's superclasses), but can be re-used across ActionBeans. For example, a given * ActionBean should not have two methods with the annotation @HandlesEvent("Update"), * but it would be perfectly reasonable to have two different ActionBeans handle events, from - * different forms, called Update.

+ * different forms, called Update. * *

It is also possible to designate a method as the default method for handling events in the - * case that Stripes cannot figure out a specific handler method to invoke. This occurs most often + * case that Stripes cannot figure out a specific handler method to invoke. This occurs most often * when a form is submitted by the user hitting the enter/return key, and so no form button is - * activated. Essentially the default handler is specifying the default operation for your form. - * In forms that have only one handler method, that method should always be declared as the - * default. For example:

+ * activated. Essentially the default handler is specifying the default operation for your form. In + * forms that have only one handler method, that method should always be declared as the default. + * For example: * *
  * @HandlesEvent("Search")
@@ -76,13 +76,13 @@
  * 
* *

Handler methods have two options for what to do when they have finished their processing. The - * preferred option is to return an instance of an implementation of Resolution. This keeps things - * clean and makes it a little easier to change how things work down the road. The other option - * is to return nothing, and simply use the Servlet API (available through the ActionBeanContext) to - * render a response to the user directly.

+ * preferred option is to return an instance of an implementation of Resolution. This keeps things + * clean and makes it a little easier to change how things work down the road. The other option is + * to return nothing, and simply use the Servlet API (available through the ActionBeanContext) to + * render a response to the user directly. * *

An ActionBean may optionally implement the {@link ValidationErrorHandler} interface which - * allows the ActionBean to modify what happens when validation errors occur.

+ * allows the ActionBean to modify what happens when validation errors occur. * * @see Resolution * @see ActionBeanContext @@ -90,20 +90,20 @@ * @author Tim Fennell */ public interface ActionBean { - /** - * Called by the Stripes dispatcher to provide context to the ActionBean before invoking the - * handler method. Implementations should store a reference to the context for use during - * event handling. - * - * @param context ActionBeanContext associated with the current request - */ - public void setContext(ActionBeanContext context); + /** + * Called by the Stripes dispatcher to provide context to the ActionBean before invoking the + * handler method. Implementations should store a reference to the context for use during event + * handling. + * + * @param context ActionBeanContext associated with the current request + */ + public void setContext(ActionBeanContext context); - /** - * Implementations must implement this method to return a reference to the context object - * provided to the ActionBean during the call to setContext(ActionBeanContext). - * - * @return ActionBeanContext associated with the current request - */ - public ActionBeanContext getContext(); + /** + * Implementations must implement this method to return a reference to the context object provided + * to the ActionBean during the call to setContext(ActionBeanContext). + * + * @return ActionBeanContext associated with the current request + */ + public ActionBeanContext getContext(); } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/ActionBeanContext.java b/stripes/src/main/java/net/sourceforge/stripes/action/ActionBeanContext.java index f16efef60..9faaa8a78 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/ActionBeanContext.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/ActionBeanContext.java @@ -14,14 +14,12 @@ */ package net.sourceforge.stripes.action; +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.List; import java.util.Locale; - -import javax.servlet.ServletContext; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import net.sourceforge.stripes.controller.FlashScope; import net.sourceforge.stripes.controller.StripesConstants; import net.sourceforge.stripes.exception.SourcePageNotFoundException; @@ -29,248 +27,254 @@ import net.sourceforge.stripes.validation.ValidationErrors; /** - *

Encapsulates information about the current request. Also provides access to the underlying - * Servlet API should you need to use it for any reason.

+ * Encapsulates information about the current request. Also provides access to the underlying + * Servlet API should you need to use it for any reason. * - *

Developers should generally consider subclassing ActionBeanContext to provide a facade - * to contextual state for their application. Type safe getters and setter can be added to - * the subclass and used by the application, thus hiding where the information is actually - * stored. This approach is documented in more detail in the Stripes documentation on - * State Management.

+ *

Developers should generally consider subclassing ActionBeanContext to provide a facade to + * contextual state for their application. Type safe getters and setter can be added to the subclass + * and used by the application, thus hiding where the information is actually stored. This approach + * is documented in more detail in the Stripes documentation on State Management. * * @author Tim Fennell */ public class ActionBeanContext { - private HttpServletRequest request; - private HttpServletResponse response; - private ServletContext servletContext; - private String eventName; - private ValidationErrors validationErrors; + private HttpServletRequest request; + private HttpServletResponse response; + private ServletContext servletContext; + private String eventName; + private ValidationErrors validationErrors; - /** - * Retrieves the HttpServletRequest object that is associated with the current request. - * @return HttpServletRequest the current request - */ - public HttpServletRequest getRequest() { - return request; - } - - /** - * Used by the DispatcherServlet to set the HttpServletRequest for the current request - * @param request the current request - */ - public void setRequest(HttpServletRequest request) { - this.request = request; - } + /** + * Retrieves the HttpServletRequest object that is associated with the current request. + * + * @return HttpServletRequest the current request + */ + public HttpServletRequest getRequest() { + return request; + } - /** - * Retrieves the HttpServletResponse that is associated with the current request. - * @return HttpServletResponse the current response - */ - public HttpServletResponse getResponse() { - return response; - } + /** + * Used by the DispatcherServlet to set the HttpServletRequest for the current request + * + * @param request the current request + */ + public void setRequest(HttpServletRequest request) { + this.request = request; + } - /** - * Used by the DispatcherServlet to set the HttpServletResponse that is associated with - * the current request. - * @param response the current response - */ - public void setResponse(HttpServletResponse response) { - this.response = response; - } + /** + * Retrieves the HttpServletResponse that is associated with the current request. + * + * @return HttpServletResponse the current response + */ + public HttpServletResponse getResponse() { + return response; + } - /** - * Retrieves the ServletContext object that is associated with the context in which the - * current request is being processed. - * @return ServletContext the current ServletContext - */ - public ServletContext getServletContext() { - return servletContext; - } + /** + * Used by the DispatcherServlet to set the HttpServletResponse that is associated with the + * current request. + * + * @param response the current response + */ + public void setResponse(HttpServletResponse response) { + this.response = response; + } - /** - * Sets the ServletContext object that is associated with the context in which the - * current request is being processed. - * @param servletContext the current ServletContext - */ - public void setServletContext(ServletContext servletContext) { - this.servletContext = servletContext; - } + /** + * Retrieves the ServletContext object that is associated with the context in which the current + * request is being processed. + * + * @return ServletContext the current ServletContext + */ + public ServletContext getServletContext() { + return servletContext; + } - /** - * Supplies the name of the event being handled. While a specific method is usually invoked on - * an ActionBean, through the use of default handlers ambiguity can arise. This allows - * ActionBeans to definitively know the name of the event that was fired. - * - * @return String the name of the event being handled - */ - public String getEventName() { - return eventName; - } + /** + * Sets the ServletContext object that is associated with the context in which the current request + * is being processed. + * + * @param servletContext the current ServletContext + */ + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } - /** - * Used by the DispatcherServlet to set the name of the even being handled. - * @param eventName the name of the event being handled - */ - public void setEventName(String eventName) { - this.eventName = eventName; - } + /** + * Supplies the name of the event being handled. While a specific method is usually invoked on an + * ActionBean, through the use of default handlers ambiguity can arise. This allows ActionBeans to + * definitively know the name of the event that was fired. + * + * @return String the name of the event being handled + */ + public String getEventName() { + return eventName; + } - /** - * Returns the set of validation errors associated with the current form. Lazily - * initialized the set of errors, and will never return null. - * - * @return a Collection of validation errors - */ - public ValidationErrors getValidationErrors() { - if (this.validationErrors == null) { - this.validationErrors = new ValidationErrors(); - } + /** + * Used by the DispatcherServlet to set the name of the even being handled. + * + * @param eventName the name of the event being handled + */ + public void setEventName(String eventName) { + this.eventName = eventName; + } - return validationErrors; + /** + * Returns the set of validation errors associated with the current form. Lazily initialized the + * set of errors, and will never return null. + * + * @return a Collection of validation errors + */ + public ValidationErrors getValidationErrors() { + if (this.validationErrors == null) { + this.validationErrors = new ValidationErrors(); } - /** - * Replaces the current set of validation errors. - * @param validationErrors a collect of validation errors - */ - public void setValidationErrors(ValidationErrors validationErrors) { - this.validationErrors = validationErrors; - } + return validationErrors; + } - /** - *

Returns the default set of non-error messages associated with the current request. - * Guaranteed to always return a List, though the list may be empty. It is envisaged that - * messages will normally be added to the request as follows:

- * - *
-     *getContext().getMessages().add( ... );
-     *
- * - *

To remove messages from the current request fetch the list of messages and invoke - * remove() or clear(). Messages will be made available to JSPs during the current - * request and in the subsequent request if a redirect is issued.

- * - * @return a List of Message objects associated with the current request, never null. - * @see ActionBeanContext#getMessages(String) - */ - public List getMessages() { - return getMessages(StripesConstants.REQ_ATTR_MESSAGES); - } + /** + * Replaces the current set of validation errors. + * + * @param validationErrors a collect of validation errors + */ + public void setValidationErrors(ValidationErrors validationErrors) { + this.validationErrors = validationErrors; + } - /** - *

Returns the set of non-error messages associated with the current request under the - * specified key. Can be used to manage multiple lists of messages, for different purposes. - * Guaranteed to always return a List, though the list may be empty. It is envisaged that - * messages will normally be added to the request as follows:

- * - *
-     *getContext().getMessages(key).add( ... );
-     *
- * - *

To remove messages from the current request fetch the list of messages and invoke - * remove() or clear().

- * - *

Messages are stored in a {@link net.sourceforge.stripes.controller.FlashScope} for - * the current request. This means that they are available in request scope using the - * supplied key during both this request, and the subsequent request if it is the result - * of a redirect.

- * - * @return a List of Message objects associated with the current request, never null. - */ - @SuppressWarnings("unchecked") - public List getMessages(String key) { - FlashScope scope = FlashScope.getCurrent(getRequest(), true); - List messages = (List) scope.get(key); + /** + * Returns the default set of non-error messages associated with the current request. Guaranteed + * to always return a List, though the list may be empty. It is envisaged that messages will + * normally be added to the request as follows: + * + *
+   * getContext().getMessages().add( ... );
+   * 
+ * + *

To remove messages from the current request fetch the list of messages and invoke remove() + * or clear(). Messages will be made available to JSPs during the current request and in the + * subsequent request if a redirect is issued. + * + * @return a List of Message objects associated with the current request, never null. + * @see ActionBeanContext#getMessages(String) + */ + public List getMessages() { + return getMessages(StripesConstants.REQ_ATTR_MESSAGES); + } - if (messages == null) { - messages = new ArrayList(); + /** + * Returns the set of non-error messages associated with the current request under the specified + * key. Can be used to manage multiple lists of messages, for different purposes. Guaranteed to + * always return a List, though the list may be empty. It is envisaged that messages will normally + * be added to the request as follows: + * + *

+   * getContext().getMessages(key).add( ... );
+   * 
+ * + *

To remove messages from the current request fetch the list of messages and invoke remove() + * or clear(). + * + *

Messages are stored in a {@link net.sourceforge.stripes.controller.FlashScope} for the + * current request. This means that they are available in request scope using the supplied key + * during both this request, and the subsequent request if it is the result of a redirect. + * + * @return a List of Message objects associated with the current request, never null. + */ + @SuppressWarnings("unchecked") + public List getMessages(String key) { + FlashScope scope = FlashScope.getCurrent(getRequest(), true); + List messages = (List) scope.get(key); - /* - * Messages imported from previous flash scope will be present in request scope but not - * in current flash scope. Handle such cases by copying the existing messages to a new - * list in the current flash and request scopes. - */ - if (getRequest().getAttribute(key) instanceof List) { - try { - for (Message message : ((List) getRequest().getAttribute(key))) { - messages.add(message); - } - } - catch (ClassCastException e) { - messages.clear(); - } - } + if (messages == null) { + messages = new ArrayList(); - scope.put(key, messages); + /* + * Messages imported from previous flash scope will be present in request scope but not + * in current flash scope. Handle such cases by copying the existing messages to a new + * list in the current flash and request scopes. + */ + if (getRequest().getAttribute(key) instanceof List) { + try { + for (Message message : ((List) getRequest().getAttribute(key))) { + messages.add(message); + } + } catch (ClassCastException e) { + messages.clear(); } + } - return messages; + scope.put(key, messages); } - /** - * Gets the Locale that is being used to service the current request. This is *not* the value - * that was submitted in the request, but the value picked by the configured LocalePicker - * which takes into consideration the locales preferred in the request. - * - * @return Locale the locale being used for the current request - * @see net.sourceforge.stripes.localization.LocalePicker - */ - public Locale getLocale() { - return this.request.getLocale(); - } + return messages; + } - /** - *

Returns a resolution that can be used to return the user to the page from which they - * submitted they current request. Most useful in situations where a user-correctable error - * has occurred that was too difficult or expensive to check at validation time. In that case - * an ActionBean can call setValidationErrors() and then return the resolution provided by - * this method.

- * - * @return Resolution a resolution that will forward the user to the page they came from - * @throws SourcePageNotFoundException if the information required to construct a source page - * resolution cannot be found in the request. - * @see #getSourcePage() - */ - public Resolution getSourcePageResolution() throws SourcePageNotFoundException { - String sourcePage = getSourcePage(); - if (sourcePage == null) { - throw new SourcePageNotFoundException(this); - } - else { - return new ForwardResolution(sourcePage); - } - } + /** + * Gets the Locale that is being used to service the current request. This is *not* the value that + * was submitted in the request, but the value picked by the configured LocalePicker which takes + * into consideration the locales preferred in the request. + * + * @return Locale the locale being used for the current request + * @see net.sourceforge.stripes.localization.LocalePicker + */ + public Locale getLocale() { + return this.request.getLocale(); + } - /** - *

- * Returns the context-relative path to the page from which the user submitted they current - * request. - *

- * - * @return Resolution a resolution that will forward the user to the page they came from - * @throws IllegalStateException if the information required to construct a source page - * resolution cannot be found in the request. - * @see #getSourcePageResolution() - */ - public String getSourcePage() { - String sourcePage = request.getParameter(StripesConstants.URL_KEY_SOURCE_PAGE); - if (sourcePage != null) { - sourcePage = CryptoUtil.decrypt(sourcePage); - } - return sourcePage; + /** + * Returns a resolution that can be used to return the user to the page from which they submitted + * they current request. Most useful in situations where a user-correctable error has occurred + * that was too difficult or expensive to check at validation time. In that case an ActionBean can + * call setValidationErrors() and then return the resolution provided by this method. + * + * @return Resolution a resolution that will forward the user to the page they came from + * @throws SourcePageNotFoundException if the information required to construct a source page + * resolution cannot be found in the request. + * @see #getSourcePage() + */ + public Resolution getSourcePageResolution() throws SourcePageNotFoundException { + String sourcePage = getSourcePage(); + if (sourcePage == null) { + throw new SourcePageNotFoundException(this); + } else { + return new ForwardResolution(sourcePage); } + } - /** - * Returns a String with the name of the event for which the instance holds context, and - * the set of validation errors, if any. - */ - @Override - public String toString() { - return getClass().getName() + "{" + - "eventName='" + eventName + "'" + - ", validationErrors=" + validationErrors + - "}"; + /** + * Returns the context-relative path to the page from which the user submitted they current + * request. + * + * @return Resolution a resolution that will forward the user to the page they came from + * @throws IllegalStateException if the information required to construct a source page resolution + * cannot be found in the request. + * @see #getSourcePageResolution() + */ + public String getSourcePage() { + String sourcePage = request.getParameter(StripesConstants.URL_KEY_SOURCE_PAGE); + if (sourcePage != null) { + sourcePage = CryptoUtil.decrypt(sourcePage); } + return sourcePage; + } + + /** + * Returns a String with the name of the event for which the instance holds context, and the set + * of validation errors, if any. + */ + @Override + public String toString() { + return getClass().getName() + + "{" + + "eventName='" + + eventName + + "'" + + ", validationErrors=" + + validationErrors + + "}"; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/After.java b/stripes/src/main/java/net/sourceforge/stripes/action/After.java index bab59593f..fa319dacb 100755 --- a/stripes/src/main/java/net/sourceforge/stripes/action/After.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/After.java @@ -14,30 +14,30 @@ */ package net.sourceforge.stripes.action; -import net.sourceforge.stripes.controller.LifecycleStage; - import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import net.sourceforge.stripes.controller.LifecycleStage; /** - *

Specifies that the annotated method should be run after the specified - * {@link LifecycleStage}(s). More than one LifecycleStage can be specified, in which case the - * method will be run after each stage completes. If no LifecycleStage is specified then the - * default is to execute the method after {@link LifecycleStage#EventHandling}. - * {@link LifecycleStage#RequestInit} cannot be specified because there is no ActionBean - * to run a method on before the ActionBean has been resolved!

+ * Specifies that the annotated method should be run after the specified {@link + * LifecycleStage}(s). More than one LifecycleStage can be specified, in which case the method will + * be run after each stage completes. If no LifecycleStage is specified then the default is to + * execute the method after {@link LifecycleStage#EventHandling}. {@link LifecycleStage#RequestInit} + * cannot be specified because there is no ActionBean to run a method on before the + * ActionBean has been resolved! + * + *

The method may have any name, any access specifier (public, private etc.) and must take no + * arguments. Methods may return values; if the value is a {@link + * net.sourceforge.stripes.action.Resolution} it will be used immediately to terminate the request. + * Any other values returned will be ignored. * - *

The method may have any name, any access specifier (public, private etc.) and must take - * no arguments. Methods may return values; if the value is a - * {@link net.sourceforge.stripes.action.Resolution} it will be used immediately to terminate - * the request. Any other values returned will be ignored.

+ *

Examples: * - *

Examples:

- *
+ * 
  * // Runs only after the event handling method has been run
  * {@literal @After}
  * public void doStuff() {
@@ -67,14 +67,14 @@
 @Inherited
 @Documented
 public @interface After {
-	/** One or more lifecycle stages after which the method should be called. */
-	LifecycleStage[] stages() default LifecycleStage.EventHandling;
+  /** One or more lifecycle stages after which the method should be called. */
+  LifecycleStage[] stages() default LifecycleStage.EventHandling;
 
-    /**
-     * Allows the method to be restricted to one or more events. By default the method will
-     * be executed on all events. Can be used to specify one or more events to apply the method
-     * to (e.g. on={"save", "update"}),  or to specify one or more events not to apply
-     * the method to (e.g. on="!delete").
-     */
-    String[] on() default {};
+  /**
+   * Allows the method to be restricted to one or more events. By default the method will be
+   * executed on all events. Can be used to specify one or more events to apply the method to (e.g.
+   * on={"save", "update"}), or to specify one or more events not to apply the method to
+   * (e.g. on="!delete").
+   */
+  String[] on() default {};
 }
diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/Before.java b/stripes/src/main/java/net/sourceforge/stripes/action/Before.java
index 4c538604f..358a57290 100755
--- a/stripes/src/main/java/net/sourceforge/stripes/action/Before.java
+++ b/stripes/src/main/java/net/sourceforge/stripes/action/Before.java
@@ -14,31 +14,30 @@
  */
 package net.sourceforge.stripes.action;
 
-import net.sourceforge.stripes.controller.LifecycleStage;
-
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Inherited;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import net.sourceforge.stripes.controller.LifecycleStage;
 
 /**
- * 

Specifies that the annotated method should be run before the specified - * {@link LifecycleStage}(s). More than one LifecycleStage can be specified, in which case the - * method will be run before each stage. If no LifecycleStage is specified then the - * default is to execute the method before {@link LifecycleStage#EventHandling}. - * {@link LifecycleStage#RequestInit} and {@link LifecycleStage#ActionBeanResolution} - * cannot be specified because there is no ActionBean to run a method on before the - * ActionBean has been resolved!

+ * Specifies that the annotated method should be run before the specified {@link + * LifecycleStage}(s). More than one LifecycleStage can be specified, in which case the method will + * be run before each stage. If no LifecycleStage is specified then the default is to execute the + * method before {@link LifecycleStage#EventHandling}. {@link LifecycleStage#RequestInit} and {@link + * LifecycleStage#ActionBeanResolution} cannot be specified because there is no ActionBean to + * run a method on before the ActionBean has been resolved! + * + *

The method may have any name, any access specifier (public, private etc.) and must take no + * arguments. Methods may return values; if the value is a {@link + * net.sourceforge.stripes.action.Resolution} it will be used immediately to terminate the request. + * Any other values returned will be ignored. * - *

The method may have any name, any access specifier (public, private etc.) and must take - * no arguments. Methods may return values; if the value is a - * {@link net.sourceforge.stripes.action.Resolution} it will be used immediately to terminate - * the request. Any other values returned will be ignored.

+ *

Examples: * - *

Examples:

- *
+ * 
  * // Runs before the event handling method has been run
  * {@literal @Before}
  * public void doStuff() {
@@ -68,14 +67,14 @@
 @Inherited
 @Documented
 public @interface Before {
-	/** One or more lifecycle stages before which the method should be called. */
-	LifecycleStage[] stages() default LifecycleStage.EventHandling;
+  /** One or more lifecycle stages before which the method should be called. */
+  LifecycleStage[] stages() default LifecycleStage.EventHandling;
 
-    /**
-     * Allows the method to be restricted to one or more events. By default the method will
-     * be executed on all events. Can be used to specify one or more events to apply the method
-     * to (e.g. on={"save", "update"}),  or to specify one or more events not to apply
-     * the method to (e.g. on="!delete").
-     */
-    String[] on() default {};
+  /**
+   * Allows the method to be restricted to one or more events. By default the method will be
+   * executed on all events. Can be used to specify one or more events to apply the method to (e.g.
+   * on={"save", "update"}), or to specify one or more events not to apply the method to
+   * (e.g. on="!delete").
+   */
+  String[] on() default {};
 }
diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/DefaultHandler.java b/stripes/src/main/java/net/sourceforge/stripes/action/DefaultHandler.java
index d6a953bad..1bc5ab754 100644
--- a/stripes/src/main/java/net/sourceforge/stripes/action/DefaultHandler.java
+++ b/stripes/src/main/java/net/sourceforge/stripes/action/DefaultHandler.java
@@ -14,21 +14,19 @@
  */
 package net.sourceforge.stripes.action;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
 import java.lang.annotation.Documented;
-import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 
 /**
- * Marker annotation to specify that a method within an ActionBean is the default handler for
- * events if a specific event cannot be identified.
+ * Marker annotation to specify that a method within an ActionBean is the default handler for events
+ * if a specific event cannot be identified.
  *
  * @author Tim Fennell
  */
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.METHOD})
 @Documented
-public @interface DefaultHandler {
-
-}
\ No newline at end of file
+public @interface DefaultHandler {}
diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/DontBind.java b/stripes/src/main/java/net/sourceforge/stripes/action/DontBind.java
index 6fd14bf8e..6ec59ad6c 100644
--- a/stripes/src/main/java/net/sourceforge/stripes/action/DontBind.java
+++ b/stripes/src/main/java/net/sourceforge/stripes/action/DontBind.java
@@ -19,19 +19,17 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
-
 import net.sourceforge.stripes.controller.LifecycleStage;
 
 /**
- * Marker annotation to specify that the event handled by the annotated method should skip
- * {@link LifecycleStage#BindingAndValidation} altogether. This is useful for events which ignore
- * user input, such as cancel events. Note that the presence of this annotation on an event handler
+ * Marker annotation to specify that the event handled by the annotated method should skip {@link
+ * LifecycleStage#BindingAndValidation} altogether. This is useful for events which ignore user
+ * input, such as cancel events. Note that the presence of this annotation on an event handler
  * implies {@link DontValidate} as well.
- * 
+ *
  * @author Ben Gunter
  */
 @Retention(RetentionPolicy.RUNTIME)
-@Target( { ElementType.METHOD })
+@Target({ElementType.METHOD})
 @Documented
-public @interface DontBind {
-}
+public @interface DontBind {}
diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/DontValidate.java b/stripes/src/main/java/net/sourceforge/stripes/action/DontValidate.java
index 765f2092c..ec515543e 100644
--- a/stripes/src/main/java/net/sourceforge/stripes/action/DontValidate.java
+++ b/stripes/src/main/java/net/sourceforge/stripes/action/DontValidate.java
@@ -14,11 +14,11 @@
  */
 package net.sourceforge.stripes.action;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
 import java.lang.annotation.Documented;
-import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 
 /**
  * Specify that the event handled by the annotated method should not have validation run on it
@@ -26,20 +26,20 @@
  * request, there may still be errors during type conversion and binding. Such errors are also
  * ignored by default. That behavior can be modified using the {@link #ignoreBindingErrors()}
  * element of this annotation.
- * 
+ *
  * @author Tim Fennell
  */
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.METHOD})
 @Documented
 public @interface DontValidate {
-    /**
-     * If true (the default) then any validation errors that might occur during type conversion and
-     * binding will be ignored. If false then Stripes will forward back to the source page as it
-     * normally would when it encounters validation errors. In either case, any errors that occur
-     * during binding will be present in the {@link ActionBeanContext}.
-     * 
-     * @see ActionBeanContext#getValidationErrors()
-     */
-    boolean ignoreBindingErrors() default true;
+  /**
+   * If true (the default) then any validation errors that might occur during type conversion and
+   * binding will be ignored. If false then Stripes will forward back to the source page as it
+   * normally would when it encounters validation errors. In either case, any errors that occur
+   * during binding will be present in the {@link ActionBeanContext}.
+   *
+   * @see ActionBeanContext#getValidationErrors()
+   */
+  boolean ignoreBindingErrors() default true;
 }
diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/ErrorResolution.java b/stripes/src/main/java/net/sourceforge/stripes/action/ErrorResolution.java
index 6cb6d370f..cd8c11620 100644
--- a/stripes/src/main/java/net/sourceforge/stripes/action/ErrorResolution.java
+++ b/stripes/src/main/java/net/sourceforge/stripes/action/ErrorResolution.java
@@ -14,65 +14,63 @@
  */
 package net.sourceforge.stripes.action;
 
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 
 /**
  * Resolution for sending HTTP error messages back to the client. errorCode is the HTTP status code
  * to be sent. errorMessage is a descriptive message.
- * 
+ *
  * @author Aaron Porter
  * @since Stripes 1.5
  */
 public class ErrorResolution implements Resolution {
-    private int errorCode;
-    private String errorMessage;
+  private int errorCode;
+  private String errorMessage;
 
-    /**
-     * Sends an error response to the client using the specified status code and clears the buffer.
-     * 
-     * @param errorCode the HTTP status code
-     */
-    public ErrorResolution(int errorCode) {
-        this.errorCode = errorCode;
-    }
+  /**
+   * Sends an error response to the client using the specified status code and clears the buffer.
+   *
+   * @param errorCode the HTTP status code
+   */
+  public ErrorResolution(int errorCode) {
+    this.errorCode = errorCode;
+  }
 
-    /**
-     * Sends an error response to the client using the specified status code and message and clears
-     * the buffer.
-     * 
-     * @param errorCode the HTTP status code
-     * @param errorMessage a descriptive message
-     */
-    public ErrorResolution(int errorCode, String errorMessage) {
-        this.errorCode = errorCode;
-        this.errorMessage = errorMessage;
-    }
+  /**
+   * Sends an error response to the client using the specified status code and message and clears
+   * the buffer.
+   *
+   * @param errorCode the HTTP status code
+   * @param errorMessage a descriptive message
+   */
+  public ErrorResolution(int errorCode, String errorMessage) {
+    this.errorCode = errorCode;
+    this.errorMessage = errorMessage;
+  }
 
-    public void execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
-        if (errorMessage != null)
-            response.sendError(errorCode, errorMessage);
-        else
-            response.sendError(errorCode);
-    }
+  public void execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
+    if (errorMessage != null) response.sendError(errorCode, errorMessage);
+    else response.sendError(errorCode);
+  }
 
-    /** Accessor for the HTTP status code. */
-    public int getErrorCode() {
-        return errorCode;
-    }
+  /** Accessor for the HTTP status code. */
+  public int getErrorCode() {
+    return errorCode;
+  }
 
-    /** Setter for the HTTP status code. */
-    public void setErrorCode(int errorCode) {
-        this.errorCode = errorCode;
-    }
+  /** Setter for the HTTP status code. */
+  public void setErrorCode(int errorCode) {
+    this.errorCode = errorCode;
+  }
 
-    /** Accessor for the descriptive error message. */
-    public String getErrorMessage() {
-        return errorMessage;
-    }
+  /** Accessor for the descriptive error message. */
+  public String getErrorMessage() {
+    return errorMessage;
+  }
 
-    /** Setter for the descriptive error message. */
-    public void setErrorMessage(String errorMessage) {
-        this.errorMessage = errorMessage;
-    }
-}
\ No newline at end of file
+  /** Setter for the descriptive error message. */
+  public void setErrorMessage(String errorMessage) {
+    this.errorMessage = errorMessage;
+  }
+}
diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/FileBean.java b/stripes/src/main/java/net/sourceforge/stripes/action/FileBean.java
index 11287364f..b5a148864 100644
--- a/stripes/src/main/java/net/sourceforge/stripes/action/FileBean.java
+++ b/stripes/src/main/java/net/sourceforge/stripes/action/FileBean.java
@@ -17,18 +17,18 @@
 import java.io.*;
 
 /**
- * 

Represents a file that was submitted as part of an HTTP POST request. Provides methods for - * examining information about the file, and the retrieving the contents of the file. When a file - * is uploaded by a user it is stored as a temporary file on the file system, which is wrapped by an + * Represents a file that was submitted as part of an HTTP POST request. Provides methods for + * examining information about the file, and the retrieving the contents of the file. When a file is + * uploaded by a user it is stored as a temporary file on the file system, which is wrapped by an * instance of this class. This is necessary because browsers may send file upload segments before - * sending any other form parameters needed to identify what to do with the uploaded files!

+ * sending any other form parameters needed to identify what to do with the uploaded files! * *

The application developer is responsible for removing this temporary file once they have - * processed it. This can be accomplished in one of two ways. Firstly a call to save(File) will - * effect a save by moving the temporary file to the desired location. In this case there + * processed it. This can be accomplished in one of two ways. Firstly a call to save(File) will + * effect a save by moving the temporary file to the desired location. In this case there * is no need to call delete(), although doing so will not delete the saved file. The second way is - * to simply call delete(). This is more applicable when consuming the file as an InputStream. An - * example code fragment for reading a text based file might look like this:

+ * to simply call delete(). This is more applicable when consuming the file as an InputStream. An + * example code fragment for reading a text based file might look like this: * *
  * FileBean bean = getUserIcon();
@@ -45,219 +45,214 @@
  * @author Tim Fennell
  */
 public class FileBean {
-    private String contentType;
-    private String fileName;
-    private File file;
-    private String charset;
-    private boolean saved;
+  private String contentType;
+  private String fileName;
+  private File file;
+  private String charset;
+  private boolean saved;
 
+  /**
+   * Constructs a FileBean pointing to an on-disk representation of the file uploaded by the user.
+   *
+   * @param file the File object on the server which holds the uploaded contents of the file
+   * @param contentType the content type of the file declared by the browser during upload
+   * @param originalName the name of the file as declared by the user's browser
+   */
+  public FileBean(File file, String contentType, String originalName) {
+    this.file = file;
+    this.contentType = contentType;
+    this.fileName = originalName;
+  }
 
-    /**
-     * Constructs a FileBean pointing to an on-disk representation of the file uploaded by the user.
-     *
-     * @param file the File object on the server which holds the uploaded contents of the file
-     * @param contentType the content type of the file declared by the browser during upload
-     * @param originalName the name of the file as declared by the user's browser
-     */
-    public FileBean(File file, String contentType, String originalName) {
-        this.file = file;
-        this.contentType = contentType;
-        this.fileName = originalName;
-    }
-    
-    /**
-     * Constructs a FileBean pointing to an on-disk representation of the file uploaded by the user.
-     * 
-     * @param file the File object on the server which holds the uploaded contents of the file
-     * @param contentType the content type of the file declared by the browser during upload
-     * @param originalName the name of the file as declared by the user's browser
-     * @param charset the charset specified by the servlet request
-     */
-    public FileBean(File file, String contentType, String originalName, String charset) {
-        this.file = file;
-        this.contentType = contentType;
-        this.fileName = originalName;
-        this.charset = charset;
-    }
+  /**
+   * Constructs a FileBean pointing to an on-disk representation of the file uploaded by the user.
+   *
+   * @param file the File object on the server which holds the uploaded contents of the file
+   * @param contentType the content type of the file declared by the browser during upload
+   * @param originalName the name of the file as declared by the user's browser
+   * @param charset the charset specified by the servlet request
+   */
+  public FileBean(File file, String contentType, String originalName, String charset) {
+    this.file = file;
+    this.contentType = contentType;
+    this.fileName = originalName;
+    this.charset = charset;
+  }
 
-    /**
-     * Returns the name of the file that the user selected and uploaded (this is not necessarily
-     * the name that the underlying file is now stored on the server using).
-     */
-    public String getFileName() {
-        return fileName;
-    }
+  /**
+   * Returns the name of the file that the user selected and uploaded (this is not necessarily the
+   * name that the underlying file is now stored on the server using).
+   */
+  public String getFileName() {
+    return fileName;
+  }
 
-    /**
-     * Returns the content type of the file that the user selected and uploaded.
-     */
-    public String getContentType() {
-        return contentType;
-    }
+  /** Returns the content type of the file that the user selected and uploaded. */
+  public String getContentType() {
+    return contentType;
+  }
 
-    /**
-     * Gets the size of the file that was uploaded.
-     */
-    public long getSize() {
-        return this.file.length();
-    }
+  /** Gets the size of the file that was uploaded. */
+  public long getSize() {
+    return this.file.length();
+  }
 
-    /**
-     * Gets an input stream to read from the file uploaded
-     */
-    public InputStream getInputStream() throws IOException {
-        return new FileInputStream(this.file);
-    }
-    
-    /**
-     * Gets a reader to read characters from the uploaded file. If the servlet request specifies a
-     * charset, then that charset is used. Otherwise, the reader uses the default charset.
-     * 
-     * @return a new reader
-     * @throws UnsupportedEncodingException
-     * @throws IOException
-     */
-    public Reader getReader() throws UnsupportedEncodingException, IOException {
-        if (charset == null) {
-            return new InputStreamReader(getInputStream());
-        }
-        else {
-            return getReader(charset);
-        }
-    }
+  /** Gets an input stream to read from the file uploaded */
+  public InputStream getInputStream() throws IOException {
+    return new FileInputStream(this.file);
+  }
 
-    /**
-     * Gets a reader to read characters from the uploaded file using the given charset.
-     * 
-     * @param charset the charset the reader should use
-     * @return a new reader
-     * @throws UnsupportedEncodingException
-     * @throws IOException
-     */
-    public Reader getReader(String charset) throws UnsupportedEncodingException, IOException {
-        return new InputStreamReader(getInputStream(), charset);
+  /**
+   * Gets a reader to read characters from the uploaded file. If the servlet request specifies a
+   * charset, then that charset is used. Otherwise, the reader uses the default charset.
+   *
+   * @return a new reader
+   * @throws UnsupportedEncodingException
+   * @throws IOException
+   */
+  public Reader getReader() throws UnsupportedEncodingException, IOException {
+    if (charset == null) {
+      return new InputStreamReader(getInputStream());
+    } else {
+      return getReader(charset);
     }
+  }
+
+  /**
+   * Gets a reader to read characters from the uploaded file using the given charset.
+   *
+   * @param charset the charset the reader should use
+   * @return a new reader
+   * @throws UnsupportedEncodingException
+   * @throws IOException
+   */
+  public Reader getReader(String charset) throws UnsupportedEncodingException, IOException {
+    return new InputStreamReader(getInputStream(), charset);
+  }
 
-    /**
-     * Saves the uploaded file to the location on disk represented by File.  First attempts a
-     * simple rename of the underlying file that was created during upload as this is the
-     * most efficient route. If the rename fails an attempt is made to copy the file bit
-     * by bit to the new File and then the temporary file is removed.
-     *
-     * @param toFile a File object representing a location
-     * @throws IOException if the save will fail for a reason that we can detect up front, for
-     *         example, missing files, permissions etc. or we try to save get a failure.
-     */
-    public void save(File toFile) throws IOException {
-        // Since File.renameTo doesn't tell you anything about why it failed, we test
-        // for some common reasons for failure ahead of time and give a bit more info
-        if (!this.file.exists()) {
-            throw new IOException
-                ("Some time between uploading and saving we lost the file "
-                    + this.file.getAbsolutePath() + " - where did it go?.");
-        }
+  /**
+   * Saves the uploaded file to the location on disk represented by File. First attempts a simple
+   * rename of the underlying file that was created during upload as this is the most efficient
+   * route. If the rename fails an attempt is made to copy the file bit by bit to the new File and
+   * then the temporary file is removed.
+   *
+   * @param toFile a File object representing a location
+   * @throws IOException if the save will fail for a reason that we can detect up front, for
+   *     example, missing files, permissions etc. or we try to save get a failure.
+   */
+  public void save(File toFile) throws IOException {
+    // Since File.renameTo doesn't tell you anything about why it failed, we test
+    // for some common reasons for failure ahead of time and give a bit more info
+    if (!this.file.exists()) {
+      throw new IOException(
+          "Some time between uploading and saving we lost the file "
+              + this.file.getAbsolutePath()
+              + " - where did it go?.");
+    }
 
-        if (!this.file.canWrite()) {
-            throw new IOException
-                ("Some time between uploading and saving we lost the ability to write to the file "
-                    + this.file.getAbsolutePath() + " - writability is required to move the file.");
-        }
+    if (!this.file.canWrite()) {
+      throw new IOException(
+          "Some time between uploading and saving we lost the ability to write to the file "
+              + this.file.getAbsolutePath()
+              + " - writability is required to move the file.");
+    }
 
-        File parent = toFile.getAbsoluteFile().getParentFile();
-        if (toFile.exists() && !toFile.canWrite()) {
-            throw new IOException("Cannot overwrite existing file at "+ toFile.getAbsolutePath());
-        }
-        else if (!parent.exists() && !parent.mkdirs()) {
-            throw new IOException("Parent directory of specified file does not exist and cannot " +
-                " be created. File location supplied: " + toFile.getAbsolutePath());
-        }
-        else if (!toFile.exists() && !parent.canWrite()) {
-            throw new IOException("Cannot create new file at location: " + toFile.getAbsolutePath());
-        }
+    File parent = toFile.getAbsoluteFile().getParentFile();
+    if (toFile.exists() && !toFile.canWrite()) {
+      throw new IOException("Cannot overwrite existing file at " + toFile.getAbsolutePath());
+    } else if (!parent.exists() && !parent.mkdirs()) {
+      throw new IOException(
+          "Parent directory of specified file does not exist and cannot "
+              + " be created. File location supplied: "
+              + toFile.getAbsolutePath());
+    } else if (!toFile.exists() && !parent.canWrite()) {
+      throw new IOException("Cannot create new file at location: " + toFile.getAbsolutePath());
+    }
 
-        this.saved = this.file.renameTo(toFile);
+    this.saved = this.file.renameTo(toFile);
 
-        // If the rename didn't work, try copying the darn thing bit by bit
-        if (this.saved == false) {
-            saveViaCopy(toFile);
-        }
+    // If the rename didn't work, try copying the darn thing bit by bit
+    if (this.saved == false) {
+      saveViaCopy(toFile);
     }
+  }
 
-    /**
-     * Attempts to save the uploaded file to the specified file by performing a stream
-     * based copy. This is only used when a rename cannot be executed, e.g. because the
-     * target file is on a different file system than the temporary file.
-     *
-     * @param toFile the file to save to
-     */
-    protected void saveViaCopy(File toFile) throws IOException {
-        OutputStream out = null;
-        InputStream in = null;
-        try {
-            out = new FileOutputStream(toFile);
-            in = new FileInputStream(this.file);
+  /**
+   * Attempts to save the uploaded file to the specified file by performing a stream based copy.
+   * This is only used when a rename cannot be executed, e.g. because the target file is on a
+   * different file system than the temporary file.
+   *
+   * @param toFile the file to save to
+   */
+  protected void saveViaCopy(File toFile) throws IOException {
+    OutputStream out = null;
+    InputStream in = null;
+    try {
+      out = new FileOutputStream(toFile);
+      in = new FileInputStream(this.file);
 
-            byte[] buffer = new byte[1024];
-            for (int count; (count = in.read(buffer)) > 0;) {
-                out.write(buffer, 0, count);
-            }
+      byte[] buffer = new byte[1024];
+      for (int count; (count = in.read(buffer)) > 0; ) {
+        out.write(buffer, 0, count);
+      }
 
-            out.close();
-            out = null;
-            in.close();
-            in = null;
+      out.close();
+      out = null;
+      in.close();
+      in = null;
 
-            this.file.delete();
-            this.saved = true;
-        }
-        finally {
-            try {
-                if (out != null)
-                    out.close();
-            }
-            catch (Exception e) {}
-            try {
-                if (in != null)
-                    in.close();
-            }
-            catch (Exception e) {}
-        }
+      this.file.delete();
+      this.saved = true;
+    } finally {
+      try {
+        if (out != null) out.close();
+      } catch (Exception e) {
+      }
+      try {
+        if (in != null) in.close();
+      } catch (Exception e) {
+      }
     }
+  }
 
-    /**
-     * Deletes the temporary file associated with this file upload if one still exists.  If save()
-     * has already been called then there is no temporary file any more, and this is a no-op.
-     *
-     * @throws IOException if the delete will fail for a reason we can detect up front, or if
-     *         we try to delete and get a failure
-     */
-    public void delete() throws IOException {
-        if (!this.saved) {
-            // Since File.delete doesn't tell you anything about why it failed, we test
-            // for some common reasons for failure ahead of time and give a bit more info
-            if (!this.file.exists()) {
-                throw new IOException
-                    ("Some time between uploading and saving we lost the file "
-                        + this.file.getAbsolutePath() + " - where did it go?.");
-            }
+  /**
+   * Deletes the temporary file associated with this file upload if one still exists. If save() has
+   * already been called then there is no temporary file any more, and this is a no-op.
+   *
+   * @throws IOException if the delete will fail for a reason we can detect up front, or if we try
+   *     to delete and get a failure
+   */
+  public void delete() throws IOException {
+    if (!this.saved) {
+      // Since File.delete doesn't tell you anything about why it failed, we test
+      // for some common reasons for failure ahead of time and give a bit more info
+      if (!this.file.exists()) {
+        throw new IOException(
+            "Some time between uploading and saving we lost the file "
+                + this.file.getAbsolutePath()
+                + " - where did it go?.");
+      }
 
-            if (!this.file.canWrite()) {
-                throw new IOException
-                    ("Some time between uploading and saving we lost the ability to write to the file "
-                        + this.file.getAbsolutePath() + " - writability is required to delete the file.");
-            }
-            this.file.delete();
-        }
+      if (!this.file.canWrite()) {
+        throw new IOException(
+            "Some time between uploading and saving we lost the ability to write to the file "
+                + this.file.getAbsolutePath()
+                + " - writability is required to delete the file.");
+      }
+      this.file.delete();
     }
+  }
 
-    /**
-     * Returns the name of the file and the content type in a String format.
-     */
-    @Override
-    public String toString() {
-        return "FileBean{" +
-            "contentType='" + contentType + "'" +
-            ", fileName='" + fileName + "'" +
-            "}";
-    }
+  /** Returns the name of the file and the content type in a String format. */
+  @Override
+  public String toString() {
+    return "FileBean{"
+        + "contentType='"
+        + contentType
+        + "'"
+        + ", fileName='"
+        + fileName
+        + "'"
+        + "}";
+  }
 }
diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/ForwardResolution.java b/stripes/src/main/java/net/sourceforge/stripes/action/ForwardResolution.java
index 4286fcbcd..dafd237de 100644
--- a/stripes/src/main/java/net/sourceforge/stripes/action/ForwardResolution.java
+++ b/stripes/src/main/java/net/sourceforge/stripes/action/ForwardResolution.java
@@ -14,124 +14,123 @@
  */
 package net.sourceforge.stripes.action;
 
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
 import net.sourceforge.stripes.controller.StripesConstants;
 import net.sourceforge.stripes.util.Log;
 
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
-
 /**
- * 

Resolution that uses the Servlet API to forward the user to another path within the - * same web application using a server side forward.

+ * Resolution that uses the Servlet API to forward the user to another path within the same + * web application using a server side forward. * - *

There is one case when this resolution will issue an include instead of a forward. The - * Servlet specification is ambiguous about what should happen when a forward is issued inside - * of an include. The behaviour varies widely by container, from outputting only the content - * of the forward, to only the content prior to the include! To make this behaviour more - * consistent the ForwardResolution will automatically determine if it is executing inside of - * an include, and if that is the case it will include the appropriate URL instead of - * forwarding to it. This behaviour can be turned off be calling - * {@literal autoInclude(false)}.

+ *

There is one case when this resolution will issue an include instead of a forward. The Servlet + * specification is ambiguous about what should happen when a forward is issued inside of an + * include. The behaviour varies widely by container, from outputting only the content of the + * forward, to only the content prior to the include! To make this behaviour more consistent the + * ForwardResolution will automatically determine if it is executing inside of an include, and if + * that is the case it will include the appropriate URL instead of forwarding to it. + * This behaviour can be turned off be calling {@literal autoInclude(false)}. * *

You can optionally set an HTTP status code with {@link #setStatus(int)}, in which case a call - * to {@code response.setStatus(status)} will be made when executing the resolution.

+ * to {@code response.setStatus(status)} will be made when executing the resolution. * * @see RedirectResolution * @author Tim Fennell */ public class ForwardResolution extends OnwardResolution { - private boolean autoInclude = true; - private static final Log log = Log.getInstance(ForwardResolution.class); - private String event; - private Integer status; + private boolean autoInclude = true; + private static final Log log = Log.getInstance(ForwardResolution.class); + private String event; + private Integer status; - /** - * Simple constructor that takes in the path to forward the user to. - * @param path the path within the web application that the user should be forwarded to - */ - public ForwardResolution(String path) { - super(path); - } - - /** - * Constructs a ForwardResolution that will forward to the URL appropriate for - * the ActionBean supplied. This constructor should be preferred when forwarding - * to an ActionBean as it will ensure the correct URL is always used. - * - * @param beanType the Class object representing the ActionBean to redirect to - */ - public ForwardResolution(Class beanType) { - super(beanType); - } + /** + * Simple constructor that takes in the path to forward the user to. + * + * @param path the path within the web application that the user should be forwarded to + */ + public ForwardResolution(String path) { + super(path); + } - /** - * Constructs a ForwardResolution that will forward to the URL appropriate for - * the ActionBean supplied. This constructor should be preferred when forwarding - * to an ActionBean as it will ensure the correct URL is always used. - * - * @param beanType the Class object representing the ActionBean to redirect to - * @param event the event that should be triggered on the redirect - */ - public ForwardResolution(Class beanType, String event) { - super(beanType, event); - this.event = event; - } + /** + * Constructs a ForwardResolution that will forward to the URL appropriate for the ActionBean + * supplied. This constructor should be preferred when forwarding to an ActionBean as it will + * ensure the correct URL is always used. + * + * @param beanType the Class object representing the ActionBean to redirect to + */ + public ForwardResolution(Class beanType) { + super(beanType); + } - /** - * If true then the ForwardResolution will automatically detect when it is executing - * as part of a server-side Include and include the supplied URL instead of - * forwarding to it. Defaults to true. - * - * @param auto whether or not to automatically detect and use includes - */ - public void autoInclude(boolean auto) { - this.autoInclude = auto; - } + /** + * Constructs a ForwardResolution that will forward to the URL appropriate for the ActionBean + * supplied. This constructor should be preferred when forwarding to an ActionBean as it will + * ensure the correct URL is always used. + * + * @param beanType the Class object representing the ActionBean to redirect to + * @param event the event that should be triggered on the redirect + */ + public ForwardResolution(Class beanType, String event) { + super(beanType, event); + this.event = event; + } - /** Get the HTTP status, or null if none was explicitly set. */ - public Integer getStatus() { - return status; - } + /** + * If true then the ForwardResolution will automatically detect when it is executing as part of a + * server-side Include and include the supplied URL instead of forwarding to it. Defaults + * to true. + * + * @param auto whether or not to automatically detect and use includes + */ + public void autoInclude(boolean auto) { + this.autoInclude = auto; + } - /** - * Explicitly sets an HTTP status code, in which case a call to {@code response.setStatus(status)} - * will be made when executing the resolution. - */ - public ForwardResolution setStatus(int status) { - this.status = status; - return this; - } + /** Get the HTTP status, or null if none was explicitly set. */ + public Integer getStatus() { + return status; + } - /** - * Attempts to forward the user to the specified path. - * @throws ServletException thrown when the Servlet container encounters an error - * @throws IOException thrown when the Servlet container encounters an error - */ - public void execute(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { + /** + * Explicitly sets an HTTP status code, in which case a call to {@code response.setStatus(status)} + * will be made when executing the resolution. + */ + public ForwardResolution setStatus(int status) { + this.status = status; + return this; + } - if (status != null) { - response.setStatus(status); - } - String path = getUrl(request.getLocale()); + /** + * Attempts to forward the user to the specified path. + * + * @throws ServletException thrown when the Servlet container encounters an error + * @throws IOException thrown when the Servlet container encounters an error + */ + public void execute(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { - // Set event name as a request attribute - String oldEvent = (String) request.getAttribute(StripesConstants.REQ_ATTR_EVENT_NAME); - request.setAttribute(StripesConstants.REQ_ATTR_EVENT_NAME, event); + if (status != null) { + response.setStatus(status); + } + String path = getUrl(request.getLocale()); - // Figure out if we're inside an include, and use an include instead of a forward - if (autoInclude && request.getAttribute(StripesConstants.REQ_ATTR_INCLUDE_PATH) != null) { - log.trace("Including URL: ", path); - request.getRequestDispatcher(path).include(request, response); - } - else { - log.trace("Forwarding to URL: ", path); - request.getRequestDispatcher(path).forward(request, response); - } + // Set event name as a request attribute + String oldEvent = (String) request.getAttribute(StripesConstants.REQ_ATTR_EVENT_NAME); + request.setAttribute(StripesConstants.REQ_ATTR_EVENT_NAME, event); - // Revert event name to its original value - request.setAttribute(StripesConstants.REQ_ATTR_EVENT_NAME, oldEvent); + // Figure out if we're inside an include, and use an include instead of a forward + if (autoInclude && request.getAttribute(StripesConstants.REQ_ATTR_INCLUDE_PATH) != null) { + log.trace("Including URL: ", path); + request.getRequestDispatcher(path).include(request, response); + } else { + log.trace("Forwarding to URL: ", path); + request.getRequestDispatcher(path).forward(request, response); } + + // Revert event name to its original value + request.setAttribute(StripesConstants.REQ_ATTR_EVENT_NAME, oldEvent); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/HandlesEvent.java b/stripes/src/main/java/net/sourceforge/stripes/action/HandlesEvent.java index 528662a0b..683b05838 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/HandlesEvent.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/HandlesEvent.java @@ -14,15 +14,15 @@ */ package net.sourceforge.stripes.action; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; import java.lang.annotation.Documented; -import java.lang.annotation.RetentionPolicy; import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; /** - * Annotation used by ActionBean to declare that a method is capable of handling a named event - * being submitted by a client. Used by the AnnotatedClassActionResolver to map requests to the + * Annotation used by ActionBean to declare that a method is capable of handling a named event being + * submitted by a client. Used by the AnnotatedClassActionResolver to map requests to the * appropriate method to handle them at run time. * * @author Tim Fennell @@ -31,6 +31,6 @@ @Target({ElementType.METHOD}) @Documented public @interface HandlesEvent { - /** The name of the event that will be handled by the annotated method. */ - String value(); + /** The name of the event that will be handled by the annotated method. */ + String value(); } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/HttpCache.java b/stripes/src/main/java/net/sourceforge/stripes/action/HttpCache.java index 921294b1e..700629125 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/HttpCache.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/HttpCache.java @@ -22,44 +22,42 @@ import java.lang.annotation.Target; /** - *

* This annotation can be applied to an event handler method or to an {@link ActionBean} class to * suggest to the HTTP client how it should cache the response. Classes will inherit this annotation * from their superclass. Method-level annotations override class-level annotations. This means, for * example, that applying {@code @HttpCache(allow=false)} to an {@link ActionBean} class turns off * client-side caching for all events except those that are annotated with * {@code @HttpCache(allow=true)}. - *

- *

- * Some examples: + * + *

Some examples: + * *

    - *
  • {@code @HttpCache} - Same behavior as if the annotation were not present. No headers are - * set.
  • - *
  • {@code @HttpCache(allow=true)} - Same as above.
  • - *
  • {@code @HttpCache(allow=false)} - Set headers to disable caching and immediately expire the - * document.
  • - *
  • {@code @HttpCache(expires=600)} - Caching is allowed. The document expires in 10 minutes.
  • + *
  • {@code @HttpCache} - Same behavior as if the annotation were not present. No headers are + * set. + *
  • {@code @HttpCache(allow=true)} - Same as above. + *
  • {@code @HttpCache(allow=false)} - Set headers to disable caching and immediately expire the + * document. + *
  • {@code @HttpCache(expires=600)} - Caching is allowed. The document expires in 10 minutes. *
- *

- * + * * @author Ben Gunter * @since Stripes 1.5 */ @Retention(RetentionPolicy.RUNTIME) -@Target( { ElementType.METHOD, ElementType.TYPE }) +@Target({ElementType.METHOD, ElementType.TYPE}) @Inherited @Documented public @interface HttpCache { - /** Default value for {@link #expires()}. */ - public static final int DEFAULT_EXPIRES = Integer.MIN_VALUE; + /** Default value for {@link #expires()}. */ + public static final int DEFAULT_EXPIRES = Integer.MIN_VALUE; - /** Indicates whether the response should be cached by the client. */ - boolean allow() default true; + /** Indicates whether the response should be cached by the client. */ + boolean allow() default true; - /** - * The number of seconds into the future that the response should expire. If {@link #allow()} is - * false, then this value is ignored and zero is used. If {@link #allow()} is true and this - * value is less than zero, then no Expires header is sent. - */ - int expires() default DEFAULT_EXPIRES; + /** + * The number of seconds into the future that the response should expire. If {@link #allow()} is + * false, then this value is ignored and zero is used. If {@link #allow()} is true and this value + * is less than zero, then no Expires header is sent. + */ + int expires() default DEFAULT_EXPIRES; } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/LocalizableMessage.java b/stripes/src/main/java/net/sourceforge/stripes/action/LocalizableMessage.java index 38739c150..cc86e3876 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/LocalizableMessage.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/LocalizableMessage.java @@ -14,86 +14,87 @@ */ package net.sourceforge.stripes.action; -import net.sourceforge.stripes.controller.StripesFilter; - import java.util.Locale; import java.util.ResourceBundle; +import net.sourceforge.stripes.controller.StripesFilter; /** - * A non-error message class that can localize (or at least externalize) the message String - * in a resource bundle. The bundle used is the Stripes error message bundle, which can be - * configured but by default is called 'StripesResources.properties'. In all other ways - * this class behaves like it's parent {@link SimpleMessage}. + * A non-error message class that can localize (or at least externalize) the message String in a + * resource bundle. The bundle used is the Stripes error message bundle, which can be configured but + * by default is called 'StripesResources.properties'. In all other ways this class behaves like + * it's parent {@link SimpleMessage}. * - * @author Tim Fennell + * @author Tim Fennell */ public class LocalizableMessage extends SimpleMessage { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private String messageKey; + private String messageKey; - /** - * Creates a new LocalizableMessage with the message key provided, and optionally zero or more - * replacement parameters to use in the message. - * - * @param messageKey a key to lookup a message in the resource bundle - * @param parameter one or more replacement parameters to insert into the message - */ - public LocalizableMessage(String messageKey, Object... parameter) { - super((String) null, parameter); - this.messageKey = messageKey; - } - - /** - * Method responsible for using the information supplied to the message object to find a - * message template. In this class this is done simply by looking up the resource - * corresponding to the messageKey supplied in the constructor. - */ - @Override - protected String getMessageTemplate(Locale locale) { - ResourceBundle bundle = StripesFilter.getConfiguration(). - getLocalizationBundleFactory().getErrorMessageBundle(locale); + /** + * Creates a new LocalizableMessage with the message key provided, and optionally zero or more + * replacement parameters to use in the message. + * + * @param messageKey a key to lookup a message in the resource bundle + * @param parameter one or more replacement parameters to insert into the message + */ + public LocalizableMessage(String messageKey, Object... parameter) { + super((String) null, parameter); + this.messageKey = messageKey; + } - return bundle.getString(messageKey); - } + /** + * Method responsible for using the information supplied to the message object to find a message + * template. In this class this is done simply by looking up the resource corresponding to the + * messageKey supplied in the constructor. + */ + @Override + protected String getMessageTemplate(Locale locale) { + ResourceBundle bundle = + StripesFilter.getConfiguration() + .getLocalizationBundleFactory() + .getErrorMessageBundle(locale); - /** - * Generated equals method which will return true if the other object is of the same - * type as this instance, and would produce the same user message. - * - * @param o an instance of LocalizableMessage or subclass thereof - * @return true if the two messages would produce the same user message, false otherwise - */ - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - if (!super.equals(o)) { - return false; - } + return bundle.getString(messageKey); + } - final LocalizableMessage that = (LocalizableMessage) o; + /** + * Generated equals method which will return true if the other object is of the same type as this + * instance, and would produce the same user message. + * + * @param o an instance of LocalizableMessage or subclass thereof + * @return true if the two messages would produce the same user message, false otherwise + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } - if (messageKey != null ? !messageKey.equals(that.messageKey) : that.messageKey != null) { - return false; - } + final LocalizableMessage that = (LocalizableMessage) o; - return true; + if (messageKey != null ? !messageKey.equals(that.messageKey) : that.messageKey != null) { + return false; } - /** Generated hashCode method. */ - @Override - public int hashCode() { - int result = super.hashCode(); - result = 29 * result + (messageKey != null ? messageKey.hashCode() : 0); - return result; - } + return true; + } - public String getMessageKey() { - return messageKey; - } + /** Generated hashCode method. */ + @Override + public int hashCode() { + int result = super.hashCode(); + result = 29 * result + (messageKey != null ? messageKey.hashCode() : 0); + return result; + } + + public String getMessageKey() { + return messageKey; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/Message.java b/stripes/src/main/java/net/sourceforge/stripes/action/Message.java index 49cab3c62..546ed389c 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/Message.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/Message.java @@ -14,23 +14,23 @@ */ package net.sourceforge.stripes.action; -import java.util.Locale; import java.io.Serializable; +import java.util.Locale; /** - * Represents a message that can be displayed to the user. Encapsulates commonalities - * between error messages produced as part of validation and other types of user messages - * such as warnings or feedback messages. + * Represents a message that can be displayed to the user. Encapsulates commonalities between error + * messages produced as part of validation and other types of user messages such as warnings or + * feedback messages. * * @author Tim Fennell */ public interface Message extends Serializable { - /** - * Provides a message that can be displayed to the user. The message must be a String, - * and should be in the language and locale appropriate for the user. - * - * @param locale the Locale picked for the current interaction with the user - * @return String the String message that will be displayed to the user - */ - String getMessage(Locale locale); + /** + * Provides a message that can be displayed to the user. The message must be a String, and should + * be in the language and locale appropriate for the user. + * + * @param locale the Locale picked for the current interaction with the user + * @return String the String message that will be displayed to the user + */ + String getMessage(Locale locale); } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/OnwardResolution.java b/stripes/src/main/java/net/sourceforge/stripes/action/OnwardResolution.java index a33484014..0a644eaad 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/OnwardResolution.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/OnwardResolution.java @@ -17,187 +17,181 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; - import net.sourceforge.stripes.controller.StripesFilter; import net.sourceforge.stripes.format.Formatter; import net.sourceforge.stripes.util.UrlBuilder; /** - *

Abstract class that provides a consistent API for all Resolutions that send the user onward to - * another view - either by forwarding, redirecting or some other mechanism. Provides methods - * for getting and setting the path that the user should be sent to next.

+ * Abstract class that provides a consistent API for all Resolutions that send the user onward to + * another view - either by forwarding, redirecting or some other mechanism. Provides methods for + * getting and setting the path that the user should be sent to next. * *

The rather odd looking generic declaration on this class is called a self-bounding generic * type. The declaration allows methods in this class like {@link #addParameter(String, Object...)} - * to return the appropriate type when accessed through subclasses. I.e. - * {@code RedirectResolution.addParameter(String, Object...)} will return a reference of type - * RedirectResolution instead of OnwardResolution.

+ * to return the appropriate type when accessed through subclasses. I.e. {@code + * RedirectResolution.addParameter(String, Object...)} will return a reference of type + * RedirectResolution instead of OnwardResolution. * * @author Tim Fennell */ public abstract class OnwardResolution> implements Resolution { - /** Initial value for fields to indicate they were not changed when null has special meaning */ - private static final String VALUE_NOT_SET = "VALUE_NOT_SET"; - - private String path; - private String event = VALUE_NOT_SET; - private Map parameters = new HashMap(); - private String anchor; - - /** - * Default constructor that takes the supplied path and stores it for use. - * @param path the path to which the resolution should navigate - */ - public OnwardResolution(String path) { - if (path==null) { - throw new IllegalArgumentException("path cannot be null"); - } - this.path = path; - } - - /** - * Constructor that will extract the url binding for the ActionBean class supplied and - * use that as the path for the resolution. - * - * @param beanType a Class that represents an ActionBean - */ - public OnwardResolution(Class beanType) { - this(StripesFilter.getConfiguration().getActionResolver().getUrlBinding(beanType)); + /** Initial value for fields to indicate they were not changed when null has special meaning */ + private static final String VALUE_NOT_SET = "VALUE_NOT_SET"; + + private String path; + private String event = VALUE_NOT_SET; + private Map parameters = new HashMap(); + private String anchor; + + /** + * Default constructor that takes the supplied path and stores it for use. + * + * @param path the path to which the resolution should navigate + */ + public OnwardResolution(String path) { + if (path == null) { + throw new IllegalArgumentException("path cannot be null"); } - - /** - * Constructor that will extract the url binding for the ActionBean class supplied and - * use that as the path for the resolution and adds a parameter to ensure that the - * specified event is invoked. - * - * @param beanType a Class that represents an ActionBean - * @param event the String name of the event to trigger on navigation - */ - public OnwardResolution(Class beanType, String event) { - this(beanType); - this.event = event; - } - - /** Get the event name that was specified in one of the constructor calls. */ - public String getEvent() { - return event == VALUE_NOT_SET ? null : event; + this.path = path; + } + + /** + * Constructor that will extract the url binding for the ActionBean class supplied and use that as + * the path for the resolution. + * + * @param beanType a Class that represents an ActionBean + */ + public OnwardResolution(Class beanType) { + this(StripesFilter.getConfiguration().getActionResolver().getUrlBinding(beanType)); + } + + /** + * Constructor that will extract the url binding for the ActionBean class supplied and use that as + * the path for the resolution and adds a parameter to ensure that the specified event is invoked. + * + * @param beanType a Class that represents an ActionBean + * @param event the String name of the event to trigger on navigation + */ + public OnwardResolution(Class beanType, String event) { + this(beanType); + this.event = event; + } + + /** Get the event name that was specified in one of the constructor calls. */ + public String getEvent() { + return event == VALUE_NOT_SET ? null : event; + } + + /** Return true if an event name was specified when this instance was constructed. */ + public boolean isEventSpecified() { + return event != VALUE_NOT_SET; + } + + /** Accessor for the path that the user should be sent to. */ + public String getPath() { + return path; + } + + /** Setter for the path that the user should be sent to. */ + public void setPath(String path) { + this.path = path; + } + + /** Get the name of the anchor to be appended to the URL. */ + protected String getAnchor() { + return anchor; + } + + /** Set the name of the anchor to be appended to the URL. */ + @SuppressWarnings("unchecked") + protected T setAnchor(String anchor) { + this.anchor = anchor; + return (T) this; + } + + /** + * Method that will work for this class and subclasses; returns a String containing the class + * name, and the path to which it will send the user. + */ + @Override + public String toString() { + return getClass().getSimpleName() + "{" + "path='" + getPath() + "'" + "}"; + } + + /** + * Adds a request parameter with zero or more values to the URL. Values may be supplied using + * varargs, or alternatively by suppling a single value parameter which is an instance of + * Collection. + * + *

Note that this method is additive. Therefore writing things like {@code + * builder.addParameter("p", "one").addParameter("p", "two");} will add both {@code p=one} and + * {@code p=two} to the URL. + * + * @param name the name of the URL parameter + * @param values zero or more scalar values, or a single Collection + * @return this Resolution so that methods can be chained + */ + @SuppressWarnings("unchecked") + public T addParameter(String name, Object... values) { + if (this.parameters.containsKey(name)) { + Object[] src = (Object[]) this.parameters.get(name); + Object[] dst = new Object[src.length + values.length]; + System.arraycopy(src, 0, dst, 0, src.length); + System.arraycopy(values, 0, dst, src.length, values.length); + this.parameters.put(name, dst); + } else { + this.parameters.put(name, values); } - /** Return true if an event name was specified when this instance was constructed. */ - public boolean isEventSpecified() { - return event != VALUE_NOT_SET; + return (T) this; + } + + /** + * Bulk adds one or more request parameters to the URL. Each entry in the Map represents a single + * named parameter, with the values being either a scalar value, an array or a Collection. + * + *

Note that this method is additive. If a parameter with name X has already been added and the + * map contains X as a key, the value(s) in the map will be added to the URL as well as the + * previously held values for X. + * + * @param parameters a Map of parameters as described above + * @return this Resolution so that methods can be chained + */ + @SuppressWarnings("unchecked") + public T addParameters(Map parameters) { + for (Map.Entry entry : parameters.entrySet()) { + addParameter(entry.getKey(), entry.getValue()); } - /** Accessor for the path that the user should be sent to. */ - public String getPath() { - return path; + return (T) this; + } + + /** + * Provides access to the Map of parameters that has been accumulated so far for this resolution. + * The reference returned is to the internal parameters map! As such any changed made to the Map + * will be reflected in the Resolution, and any subsequent calls to addParameter(s) will be + * reflected in the Map. + * + * @return the Map of parameters for the resolution + */ + public Map getParameters() { + return parameters; + } + + /** + * Constructs the URL for the resolution by taking the path and appending any parameters supplied. + * + * @param locale the locale to be used by {@link Formatter}s when formatting parameters + */ + public String getUrl(Locale locale) { + UrlBuilder builder = new UrlBuilder(locale, getPath(), false); + if (event != VALUE_NOT_SET) { + builder.setEvent(event == null || event.length() < 1 ? null : event); } - - /** Setter for the path that the user should be sent to. */ - public void setPath(String path) { - this.path = path; - } - - /** Get the name of the anchor to be appended to the URL. */ - protected String getAnchor() { - return anchor; - } - - /** Set the name of the anchor to be appended to the URL. */ - @SuppressWarnings("unchecked") - protected T setAnchor(String anchor) { - this.anchor = anchor; - return (T) this; - } - - /** - * Method that will work for this class and subclasses; returns a String containing the - * class name, and the path to which it will send the user. - */ - @Override - public String toString() { - return getClass().getSimpleName() + "{" + - "path='" + getPath() + "'" + - "}"; - } - - /** - *

Adds a request parameter with zero or more values to the URL. Values may - * be supplied using varargs, or alternatively by suppling a single value parameter which is - * an instance of Collection.

- * - *

Note that this method is additive. Therefore writing things like - * {@code builder.addParameter("p", "one").addParameter("p", "two");} - * will add both {@code p=one} and {@code p=two} to the URL.

- * - * @param name the name of the URL parameter - * @param values zero or more scalar values, or a single Collection - * @return this Resolution so that methods can be chained - */ - @SuppressWarnings("unchecked") - public T addParameter(String name, Object... values) { - if (this.parameters.containsKey(name)) { - Object[] src = (Object[]) this.parameters.get(name); - Object[] dst = new Object[src.length + values.length]; - System.arraycopy(src, 0, dst, 0, src.length); - System.arraycopy(values, 0, dst, src.length, values.length); - this.parameters.put(name, dst); - } - else { - this.parameters.put(name, values); - } - - return (T) this; - } - - /** - *

Bulk adds one or more request parameters to the URL. Each entry in the Map - * represents a single named parameter, with the values being either a scalar value, - * an array or a Collection.

- * - *

Note that this method is additive. If a parameter with name X has already been - * added and the map contains X as a key, the value(s) in the map will be added to the - * URL as well as the previously held values for X.

- * - * @param parameters a Map of parameters as described above - * @return this Resolution so that methods can be chained - */ - @SuppressWarnings("unchecked") - public T addParameters(Map parameters) { - for (Map.Entry entry : parameters.entrySet()) { - addParameter(entry.getKey(), entry.getValue()); - } - - return (T) this; - } - - /** - *

Provides access to the Map of parameters that has been accumulated so far - * for this resolution. The reference returned is to the internal parameters - * map! As such any changed made to the Map will be reflected in the Resolution, - * and any subsequent calls to addParameter(s) will be reflected in the Map.

- * - * @return the Map of parameters for the resolution - */ - public Map getParameters() { - return parameters; - } - - /** - * Constructs the URL for the resolution by taking the path and appending any parameters - * supplied. - * - * @param locale the locale to be used by {@link Formatter}s when formatting parameters - */ - public String getUrl(Locale locale) { - UrlBuilder builder = new UrlBuilder(locale, getPath(), false); - if (event != VALUE_NOT_SET) { - builder.setEvent(event == null || event.length() < 1 ? null : event); - } - if (anchor != null) { - builder.setAnchor(anchor); - } - builder.addParameters(this.parameters); - return builder.toString(); + if (anchor != null) { + builder.setAnchor(anchor); } + builder.addParameters(this.parameters); + return builder.toString(); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/RedirectResolution.java b/stripes/src/main/java/net/sourceforge/stripes/action/RedirectResolution.java index 249cde0be..ec563424b 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/RedirectResolution.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/RedirectResolution.java @@ -14,196 +14,192 @@ */ package net.sourceforge.stripes.action; -import net.sourceforge.stripes.controller.FlashScope; -import net.sourceforge.stripes.controller.StripesConstants; -import net.sourceforge.stripes.util.Log; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; - +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; import java.io.IOException; import java.util.Collection; import java.util.HashSet; +import net.sourceforge.stripes.controller.FlashScope; +import net.sourceforge.stripes.controller.StripesConstants; +import net.sourceforge.stripes.util.Log; /** - *

Resolution that uses the Servlet API to redirect the user to another path by issuing - * a client side redirect. Unlike the ForwardResolution the RedirectResolution can send the user to + * Resolution that uses the Servlet API to redirect the user to another path by issuing a + * client side redirect. Unlike the ForwardResolution the RedirectResolution can send the user to * any URL anywhere on the web - though it is more commonly used to send the user to a location - * within the same application.

+ * within the same application. * - *

By default the RedirectResolution will prepend the context path of the web application to - * any URL before redirecting the request. To prevent the context path from being prepended - * use the constructor: {@code RedirectResolution(String,boolean)}.

+ *

* - *

It is also possible to append parameters to the URL to which the user will be redirected. - * This can be done by manually adding parameters with the addParameter() and addParameters() - * methods, and by invoking includeRequestParameters() which will cause all of the current - * request parameters to be included into the URL.

+ *

By default the RedirectResolution will prepend the context path of the web application to any + * URL before redirecting the request. To prevent the context path from being prepended use the + * constructor: {@code RedirectResolution(String,boolean)}. * - *

- * The redirect type can be switched from a 302 temporary redirect (default) to a 301 permanent + *

It is also possible to append parameters to the URL to which the user will be redirected. This + * can be done by manually adding parameters with the addParameter() and addParameters() methods, + * and by invoking includeRequestParameters() which will cause all of the current request parameters + * to be included into the URL. + * + *

The redirect type can be switched from a 302 temporary redirect (default) to a 301 permanent * redirect using the setPermanent method. - *

- * + * * @see ForwardResolution * @author Tim Fennell */ public class RedirectResolution extends OnwardResolution { - private static final Log log = Log.getInstance(RedirectResolution.class); - private boolean prependContext = true; - private boolean includeRequestParameters; - private Collection beans; // used to flash action beans - private boolean permanent = false; - - /** - * Simple constructor that takes the URL to which to forward the user. Defaults to - * prepending the context path to the url supplied before redirecting. - * - * @param url the URL to which the user's browser should be re-directed. - */ - public RedirectResolution(String url) { - this(url, true); + private static final Log log = Log.getInstance(RedirectResolution.class); + private boolean prependContext = true; + private boolean includeRequestParameters; + private Collection beans; // used to flash action beans + private boolean permanent = false; + + /** + * Simple constructor that takes the URL to which to forward the user. Defaults to prepending the + * context path to the url supplied before redirecting. + * + * @param url the URL to which the user's browser should be re-directed. + */ + public RedirectResolution(String url) { + this(url, true); + } + + /** + * Constructor that allows explicit control over whether or not the context path is prepended to + * the URL before redirecting. + * + * @param url the URL to which the user's browser should be re-directed. + * @param prependContext true if the context should be prepended, false otherwise + */ + public RedirectResolution(String url, boolean prependContext) { + super(url); + this.prependContext = prependContext; + } + + /** + * Constructs a RedirectResolution that will redirect to the URL appropriate for the ActionBean + * supplied. This constructor should be preferred when redirecting to an ActionBean as it will + * ensure the correct URL is always used. + * + * @param beanType the Class object representing the ActionBean to redirect to + */ + public RedirectResolution(Class beanType) { + super(beanType); + } + + /** + * Constructs a RedirectResolution that will redirect to the URL appropriate for the ActionBean + * supplied. This constructor should be preferred when redirecting to an ActionBean as it will + * ensure the correct URL is always used. + * + * @param beanType the Class object representing the ActionBean to redirect to + * @param event the event that should be triggered on the redirect + */ + public RedirectResolution(Class beanType, String event) { + super(beanType, event); + } + + /** This method is overridden to make it public. */ + @Override + public String getAnchor() { + return super.getAnchor(); + } + + /** This method is overridden to make it public. */ + @Override + public RedirectResolution setAnchor(String anchor) { + return super.setAnchor(anchor); + } + + /** + * If set to true, will cause absolutely all request parameters present in the current request to + * be appended to the redirect URL that will be sent to the browser. Since some browsers and + * servers cannot handle extremely long URLs, care should be taken when using this method with + * large form posts. + * + * @param inc whether or not current request parameters should be included in the redirect + * @return RedirectResolution, this resolution so that methods can be chained + */ + public RedirectResolution includeRequestParameters(boolean inc) { + this.includeRequestParameters = inc; + return this; + } + + /** + * Causes the ActionBean supplied to be added to the Flash scope and made available during the + * next request cycle. + * + * @param bean the ActionBean to be added to flash scope + * @since Stripes 1.2 + */ + public RedirectResolution flash(ActionBean bean) { + if (this.beans == null) { + this.beans = new HashSet(); } - /** - * Constructor that allows explicit control over whether or not the context path is - * prepended to the URL before redirecting. - * - * @param url the URL to which the user's browser should be re-directed. - * @param prependContext true if the context should be prepended, false otherwise - */ - public RedirectResolution(String url, boolean prependContext) { - super(url); - this.prependContext = prependContext; - } - - /** - * Constructs a RedirectResolution that will redirect to the URL appropriate for - * the ActionBean supplied. This constructor should be preferred when redirecting - * to an ActionBean as it will ensure the correct URL is always used. - * - * @param beanType the Class object representing the ActionBean to redirect to - */ - public RedirectResolution(Class beanType) { - super(beanType); + this.beans.add(bean); + return this; + } + + /** + * Attempts to redirect the user to the specified URL. + * + * @throws ServletException thrown when the Servlet container encounters an error + * @throws IOException thrown when the Servlet container encounters an error + */ + @SuppressWarnings("unchecked") + public void execute(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + if (permanent) { + response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); + response = + new HttpServletResponseWrapper(response) { + + @Override + public void setStatus(int sc) {} + + @Override + public void sendRedirect(String location) throws IOException { + setHeader("Location", location); + } + }; } - - /** - * Constructs a RedirectResolution that will redirect to the URL appropriate for - * the ActionBean supplied. This constructor should be preferred when redirecting - * to an ActionBean as it will ensure the correct URL is always used. - * - * @param beanType the Class object representing the ActionBean to redirect to - * @param event the event that should be triggered on the redirect - */ - public RedirectResolution(Class beanType, String event) { - super(beanType, event); + if (this.includeRequestParameters) { + addParameters(request.getParameterMap()); } - /** This method is overridden to make it public. */ - @Override - public String getAnchor() { - return super.getAnchor(); + // Add any beans to the flash scope + if (this.beans != null) { + FlashScope flash = FlashScope.getCurrent(request, true); + for (ActionBean bean : this.beans) { + flash.put(bean); + } } - /** This method is overridden to make it public. */ - @Override - public RedirectResolution setAnchor(String anchor) { - return super.setAnchor(anchor); + // If a flash scope exists, add the parameter to the request + FlashScope flash = FlashScope.getCurrent(request, false); + if (flash != null) { + addParameter(StripesConstants.URL_KEY_FLASH_SCOPE_ID, flash.key()); } - /** - * If set to true, will cause absolutely all request parameters present in the current request - * to be appended to the redirect URL that will be sent to the browser. Since some browsers - * and servers cannot handle extremely long URLs, care should be taken when using this - * method with large form posts. - * - * @param inc whether or not current request parameters should be included in the redirect - * @return RedirectResolution, this resolution so that methods can be chained - */ - public RedirectResolution includeRequestParameters(boolean inc) { - this.includeRequestParameters = inc; - return this; + // Prepend the context path if requested + String url = getUrl(request.getLocale()); + if (prependContext) { + String contextPath = request.getContextPath(); + if (contextPath.length() > 1) url = contextPath + url; } - /** - * Causes the ActionBean supplied to be added to the Flash scope and made available - * during the next request cycle. - * - * @param bean the ActionBean to be added to flash scope - * @since Stripes 1.2 - */ - public RedirectResolution flash(ActionBean bean) { - if (this.beans == null) { - this.beans = new HashSet(); - } - - this.beans.add(bean); - return this; - } + url = response.encodeRedirectURL(url); + log.trace("Redirecting ", this.beans == null ? "" : "(w/flashed bean) ", "to URL: ", url); - /** - * Attempts to redirect the user to the specified URL. - * - * @throws ServletException thrown when the Servlet container encounters an error - * @throws IOException thrown when the Servlet container encounters an error - */ - @SuppressWarnings("unchecked") - public void execute(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - if (permanent) { - response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); - response = new HttpServletResponseWrapper(response) { - - @Override - public void setStatus(int sc) { - } - - @Override - public void sendRedirect(String location) throws IOException { - setHeader("Location", location); - } - - }; - } - if (this.includeRequestParameters) { - addParameters(request.getParameterMap()); - } - - // Add any beans to the flash scope - if (this.beans != null) { - FlashScope flash = FlashScope.getCurrent(request, true); - for (ActionBean bean : this.beans) { - flash.put(bean); - } - } - - // If a flash scope exists, add the parameter to the request - FlashScope flash = FlashScope.getCurrent(request, false); - if (flash != null) { - addParameter(StripesConstants.URL_KEY_FLASH_SCOPE_ID, flash.key()); - } - - // Prepend the context path if requested - String url = getUrl(request.getLocale()); - if (prependContext) { - String contextPath = request.getContextPath(); - if (contextPath.length() > 1) - url = contextPath + url; - } - - url = response.encodeRedirectURL(url); - log.trace("Redirecting ", this.beans == null ? "" : "(w/flashed bean) ", "to URL: ", url); - - response.sendRedirect(url); - } + response.sendRedirect(url); + } - /** Sets the redirect type to permanent (301) instead of temporary (302). */ - public RedirectResolution setPermanent(boolean permanent) { - this.permanent = permanent; - return this; - } + /** Sets the redirect type to permanent (301) instead of temporary (302). */ + public RedirectResolution setPermanent(boolean permanent) { + this.permanent = permanent; + return this; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/Resolution.java b/stripes/src/main/java/net/sourceforge/stripes/action/Resolution.java index 1df2b1455..a036e05ba 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/Resolution.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/Resolution.java @@ -14,27 +14,26 @@ */ package net.sourceforge.stripes.action; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; /** * Type that is designed to be returned by "handler" methods in ActionBeans. The - * Resolution is responsible for executing the next step after the ActionBean has handled the - * user's request. In most cases this will likely be to forward the user on to the next page. + * Resolution is responsible for executing the next step after the ActionBean has handled the user's + * request. In most cases this will likely be to forward the user on to the next page. * * @see ForwardResolution * @author Tim Fennell */ public interface Resolution { - /** - * Called by the Stripes dispatcher to invoke the Resolution. Should use the request and - * response provided to direct the user to an appropriate view. - * - * @param request the current HttpServletRequest - * @param response the current HttpServletResponse - * @throws Exception exceptions of any type may be thrown if the Resolution cannot be - * executed as intended - */ - void execute(HttpServletRequest request, HttpServletResponse response) - throws Exception; + /** + * Called by the Stripes dispatcher to invoke the Resolution. Should use the request and response + * provided to direct the user to an appropriate view. + * + * @param request the current HttpServletRequest + * @param response the current HttpServletResponse + * @throws Exception exceptions of any type may be thrown if the Resolution cannot be executed as + * intended + */ + void execute(HttpServletRequest request, HttpServletResponse response) throws Exception; } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/SessionScope.java b/stripes/src/main/java/net/sourceforge/stripes/action/SessionScope.java index b9e4603ff..6677ce151 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/SessionScope.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/SessionScope.java @@ -14,47 +14,47 @@ */ package net.sourceforge.stripes.action; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.lang.annotation.ElementType; -import java.lang.annotation.Documented; /** - *

Annotation that is used to specify that an ActionBean should be instantiated and stored across - * requests in the Session scope. By default ActionBeans are instantiated per-request, populated, - * used and then discarded at the end of the request cycle. Using this annotation causes an - * ActionBean to live for multiple request cycles. It will be instantiated and put into session - * on the first request that references the ActionBean. A reference to the bean will also be - * placed into RequestScope for each request that references the bean, thereby allowing the rest - * of Stripes to treat it like any other ActionBean.

+ * Annotation that is used to specify that an ActionBean should be instantiated and stored across + * requests in the Session scope. By default ActionBeans are instantiated per-request, populated, + * used and then discarded at the end of the request cycle. Using this annotation causes an + * ActionBean to live for multiple request cycles. It will be instantiated and put into session on + * the first request that references the ActionBean. A reference to the bean will also be placed + * into RequestScope for each request that references the bean, thereby allowing the rest of Stripes + * to treat it like any other ActionBean. * *

Since session scope ActionBeans are not generally encouraged by the author, very few - * allowances will be made in Stripes to accommodate session scope beans. This means that - * additional mechanisms to handle session scope beans do not exist. However, there are - * general mechanisms built in to Stripes that will allow you to overcome most if not all issues - * that arise from Session scoping ActionBeans.

+ * allowances will be made in Stripes to accommodate session scope beans. This means that additional + * mechanisms to handle session scope beans do not exist. However, there are general mechanisms + * built in to Stripes that will allow you to overcome most if not all issues that arise from + * Session scoping ActionBeans. * *

One major issue is how to clear out values from an ActionBean before the next request cycle. * It is suggested that this be done in the ActionBean.setContext() method, which is guaranteed to - * be invoked before any binding occurs. Note that this problem is two-fold. Firstly the browser - * does not submit values for checkboxes that are de-selected. Secondly Stripes does not invoke + * be invoked before any binding occurs. Note that this problem is two-fold. Firstly the browser + * does not submit values for checkboxes that are de-selected. Secondly Stripes does not invoke * setters for parameters submitted in the request with values equal to the empty-string. You may * choose to simply null out such fields in setContext() or use the available reference to the - * HttpServletRequest to find out if empty values were submitted for fields, and null out just - * those fields.

+ * HttpServletRequest to find out if empty values were submitted for fields, and null out just those + * fields. * - *

A second major issue is in using the validation service. The validation service validates - * what was submitted in the request. Therefore if a property is marked are required, - * is present in the session scope bean, but is not submitted by the user, it will generate a - * required field error. This may or may not be desired behaviour. If it is not, it is suggested - * that the ActionBean implement the ValidationErrorHandler interface to find out about the - * validation errors generated, and take action accordingly.

+ *

A second major issue is in using the validation service. The validation service validates + * what was submitted in the request. Therefore if a property is marked are required, is + * present in the session scope bean, but is not submitted by the user, it will generate a required + * field error. This may or may not be desired behaviour. If it is not, it is suggested that the + * ActionBean implement the ValidationErrorHandler interface to find out about the validation errors + * generated, and take action accordingly. * *

Lastly, an alternative to session scoping for wizard pattern/page-spanning forms that - * ActionBean authors may wish to consider is the use of the - * {@link net.sourceforge.stripes.tag.WizardFieldsTag} which will carry all the fields submitted - * in the request into the next request by writing hidden form fields.

+ * ActionBean authors may wish to consider is the use of the {@link + * net.sourceforge.stripes.tag.WizardFieldsTag} which will carry all the fields submitted in the + * request into the next request by writing hidden form fields. * * @see net.sourceforge.stripes.validation.ValidationErrorHandler * @author Tim Fennell @@ -62,5 +62,4 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented -public @interface SessionScope { -} +public @interface SessionScope {} diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/SimpleMessage.java b/stripes/src/main/java/net/sourceforge/stripes/action/SimpleMessage.java index 2de889f25..feff93bbb 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/SimpleMessage.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/SimpleMessage.java @@ -19,146 +19,144 @@ import java.util.Locale; /** - *

A simple non-error message that uses the String supplied to it as the message (i.e. it does - * not look up the message in a resource bundle).

+ * A simple non-error message that uses the String supplied to it as the message (i.e. it does not + * look up the message in a resource bundle). * *

Messages may contain one or more "replacement parameters". To use replacement - * parameters a message must contain the replacement token {#} where # is the numeric index of - * the replacement parameter.

+ * parameters a message must contain the replacement token {#} where # is the numeric index of the + * replacement parameter. * - *

For example, to construct a message with two replacement parameters you might supply - * a message like:

+ *

For example, to construct a message with two replacement parameters you might supply a message + * like: * *

Welcome back {0}, your last login was on {1,date,short}.
* - *

At runtime this might get replaced out to result in a message for the user that looks - * like "Welcome back Johnnie, your last login was on 01/01/2006".

+ *

At runtime this might get replaced out to result in a message for the user that looks like + * "Welcome back Johnnie, your last login was on 01/01/2006". * - *

{@link java.text.MessageFormat} is used to merge the parameters in to the message and as - * a result the parameters can themselves receive formatting through the various java.text.* - * formatters.

+ *

{@link java.text.MessageFormat} is used to merge the parameters in to the message and as a + * result the parameters can themselves receive formatting through the various java.text.* + * formatters. * * @author Tim Fennell * @see java.text.MessageFormat */ public class SimpleMessage implements Message { - private static final long serialVersionUID = 1L; - - private String message; - - /** - * The set of replacement parameters that will be used to create the message from the message - * template. Note that position 0 is reserved for the field name and position 1 is reserved - * for the field value. - */ - private Object[] replacementParameters; - - /** - * Constructs a message with the supplied message string and zero or more parameters - * to be merged into the message. When constructing a SimpleMessage a non-null message - * string must be supplied (though subclasses may return null if they do not rely upon it). - * - * @param message the String message to display to the user, optionally with placeholders - * for replacement parameters - * @param parameters - */ - public SimpleMessage(String message, Object... parameters) { - this.replacementParameters = parameters; - this.message = message; + private static final long serialVersionUID = 1L; + + private String message; + + /** + * The set of replacement parameters that will be used to create the message from the message + * template. Note that position 0 is reserved for the field name and position 1 is reserved for + * the field value. + */ + private Object[] replacementParameters; + + /** + * Constructs a message with the supplied message string and zero or more parameters to be merged + * into the message. When constructing a SimpleMessage a non-null message string must be supplied + * (though subclasses may return null if they do not rely upon it). + * + * @param message the String message to display to the user, optionally with placeholders for + * replacement parameters + * @param parameters + */ + public SimpleMessage(String message, Object... parameters) { + this.replacementParameters = parameters; + this.message = message; + } + + /** + * Helper constructor to allow subclasses to provide and manipulate replacement parameters without + * having to supply a message String. + * + * @param parameters zero or more parameters for replacement into the message + */ + protected SimpleMessage(Object... parameters) { + this.replacementParameters = parameters; + } + + /** + * Uses the String message passed in as the message template and combines it with any replacement + * parameters provided to construct a message for display to the user. Although SimpleMessage does + * not localize it's message string, any formatters invoked as a result of using replacement + * parameters will be in the correct locale. + * + * @param locale the locale of the current request + * @return String the message stored under the messageKey supplied + */ + public String getMessage(Locale locale) { + // Now get the message itself + String messageTemplate = getMessageTemplate(locale); + + // For compatibility with JSTL, only apply formatting if there are replacement parameters + if (this.replacementParameters != null && this.replacementParameters.length > 0) { + MessageFormat format = new MessageFormat(messageTemplate, locale); + return format.format(this.replacementParameters, new StringBuffer(), null).toString(); + } else { + return messageTemplate; } - - /** - * Helper constructor to allow subclasses to provide and manipulate replacement - * parameters without having to supply a message String. - * - * @param parameters zero or more parameters for replacement into the message - */ - protected SimpleMessage(Object... parameters) { - this.replacementParameters = parameters; + } + + /** + * Simply returns the message passed in at Construction time. Designed to be overridden by + * subclasses to lookup messages from resource bundles. + * + * @param locale the Locale of the message template desired + * @return the message (potentially with TextFormat replacement tokens). + */ + protected String getMessageTemplate(Locale locale) { + return this.message; + } + + /** + * Returns the exact message that was supplied in the constructor. This should not be called to + * render user output, but only when direct access to the String is needed for some reason. + * + * @return the exact message String passed in to the constructor + */ + public String getMessage() { + return this.message; + } + + /** Allows subclasses to access the replacement parameters for this message. */ + public Object[] getReplacementParameters() { + return this.replacementParameters; + } + + /** + * Checks equality by ensuring that the current instance and the 'other' instance are instances of + * the same class (though not necessarily SimpleMessage!) and that the message String and + * replacement parameters provided are the same. + * + * @param o another object that is a SimpleMessage or subclass thereof + * @return true if the two objects will generate the same user message, false otherwise + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; } - - /** - * Uses the String message passed in as the message template and combines it with any - * replacement parameters provided to construct a message for display to the user. Although - * SimpleMessage does not localize it's message string, any formatters invoked as a result - * of using replacement parameters will be in the correct locale. - * - * @param locale the locale of the current request - * @return String the message stored under the messageKey supplied - */ - public String getMessage(Locale locale) { - // Now get the message itself - String messageTemplate = getMessageTemplate(locale); - - // For compatibility with JSTL, only apply formatting if there are replacement parameters - if (this.replacementParameters != null && this.replacementParameters.length > 0) { - MessageFormat format = new MessageFormat(messageTemplate, locale); - return format.format(this.replacementParameters, new StringBuffer(), null).toString(); - } - else { - return messageTemplate; - } + if (o == null || getClass() != o.getClass()) { + return false; } - /** - * Simply returns the message passed in at Construction time. Designed to be overridden by - * subclasses to lookup messages from resource bundles. - * - * @param locale the Locale of the message template desired - * @return the message (potentially with TextFormat replacement tokens). - */ - protected String getMessageTemplate(Locale locale) { - return this.message; - } + final SimpleMessage that = (SimpleMessage) o; - /** - * Returns the exact message that was supplied in the constructor. This should not - * be called to render user output, but only when direct access to the String is - * needed for some reason. - * - * @return the exact message String passed in to the constructor - */ - public String getMessage() { - return this.message; + if (message != null ? !message.equals(that.message) : that.message != null) { + return false; } - - /** Allows subclasses to access the replacement parameters for this message. */ - public Object[] getReplacementParameters() { - return this.replacementParameters; + if (!Arrays.equals(replacementParameters, that.replacementParameters)) { + return false; } - /** - * Checks equality by ensuring that the current instance and the 'other' instance are - * instances of the same class (though not necessarily SimpleMessage!) and that the - * message String and replacement parameters provided are the same. - * - * @param o another object that is a SimpleMessage or subclass thereof - * @return true if the two objects will generate the same user message, false otherwise - */ - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - final SimpleMessage that = (SimpleMessage) o; - - if (message != null ? !message.equals(that.message) : that.message != null) { - return false; - } - if (!Arrays.equals(replacementParameters, that.replacementParameters)) { - return false; - } - - return true; - } + return true; + } - /** Generated hash code method. */ - @Override - public int hashCode() { - return (message != null ? message.hashCode() : 0); - } + /** Generated hash code method. */ + @Override + public int hashCode() { + return (message != null ? message.hashCode() : 0); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/StreamingResolution.java b/stripes/src/main/java/net/sourceforge/stripes/action/StreamingResolution.java index 7f97f6de0..c722b0dd2 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/StreamingResolution.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/StreamingResolution.java @@ -14,6 +14,9 @@ */ package net.sourceforge.stripes.action; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; @@ -25,476 +28,452 @@ import java.util.Date; import java.util.Iterator; import java.util.List; - -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import net.sourceforge.stripes.exception.StripesRuntimeException; import net.sourceforge.stripes.util.Log; import net.sourceforge.stripes.util.Range; /** - *

Resolution for streaming data back to the client (in place of forwarding the user to - * another page). Designed to be used for streaming non-page data such as generated images/charts - * and XML islands.

+ * Resolution for streaming data back to the client (in place of forwarding the user to another + * page). Designed to be used for streaming non-page data such as generated images/charts and XML + * islands. * - *

Optionally supports the use of a file name which, if set, will cause a - * Content-Disposition header to be written to the output, resulting in a "Save As" type - * dialog box appearing in the user's browser. If you do not wish to supply a file name, but - * wish to achieve this behaviour, simple supply a file name of "".

+ *

Optionally supports the use of a file name which, if set, will cause a Content-Disposition + * header to be written to the output, resulting in a "Save As" type dialog box appearing in the + * user's browser. If you do not wish to supply a file name, but wish to achieve this behaviour, + * simple supply a file name of "". * - *

StreamingResolution is designed to be subclassed where necessary to provide streaming - * output where the data being streamed is not contained in an InputStream or Reader. This - * would normally be done using an anonymous inner class as follows:

+ *

StreamingResolution is designed to be subclassed where necessary to provide streaming output + * where the data being streamed is not contained in an InputStream or Reader. This would normally + * be done using an anonymous inner class as follows: * - *

- *return new StreamingResolution("text/xml") {
+ * 
+ * return new StreamingResolution("text/xml") {
  *    public void stream(HttpServletResponse response) throws Exception {
  *        // custom output generation code
  *        response.getWriter().write(...);
  *        // or
  *        response.getOutputStream().write(...);
  *    }
- *}.setFilename("your-filename.xml");
- *
+ * }.setFilename("your-filename.xml"); + *
* * @author Tim Fennell */ public class StreamingResolution implements Resolution { - /** Date format string for RFC 822 dates. */ - private static final String RFC_822_DATE_FORMAT = "EEE, d MMM yyyy HH:mm:ss Z"; - /** Boundary for use in multipart responses. */ - private static final String MULTIPART_BOUNDARY = "BOUNDARY_F7C98B76AEF711DF86D1B4FCDFD72085"; - private static final Log log = Log.getInstance(StreamingResolution.class); - private InputStream inputStream; - private Reader reader; - private String filename; - private String contentType; - private String characterEncoding; - private long lastModified = -1; - private long length = -1; - private boolean attachment; - private boolean rangeSupport = false; - private List> byteRanges; - - /** - * Constructor only to be used when subclassing the StreamingResolution (usually using - * an anonymous inner class. If this constructor is used, and stream() is not overridden - * then an exception will be thrown! - * - * @param contentType the content type of the data in the stream (e.g. image/png) - */ - public StreamingResolution(String contentType) { - this.contentType = contentType; + /** Date format string for RFC 822 dates. */ + private static final String RFC_822_DATE_FORMAT = "EEE, d MMM yyyy HH:mm:ss Z"; + /** Boundary for use in multipart responses. */ + private static final String MULTIPART_BOUNDARY = "BOUNDARY_F7C98B76AEF711DF86D1B4FCDFD72085"; + + private static final Log log = Log.getInstance(StreamingResolution.class); + private InputStream inputStream; + private Reader reader; + private String filename; + private String contentType; + private String characterEncoding; + private long lastModified = -1; + private long length = -1; + private boolean attachment; + private boolean rangeSupport = false; + private List> byteRanges; + + /** + * Constructor only to be used when subclassing the StreamingResolution (usually using an + * anonymous inner class. If this constructor is used, and stream() is not overridden then an + * exception will be thrown! + * + * @param contentType the content type of the data in the stream (e.g. image/png) + */ + public StreamingResolution(String contentType) { + this.contentType = contentType; + } + + /** + * Constructor that builds a StreamingResolution that will stream binary data back to the client + * and identify the data as being of the specified content type. + * + * @param contentType the content type of the data in the stream (e.g. image/png) + * @param inputStream an InputStream from which to read the data to return to the client + */ + public StreamingResolution(String contentType, InputStream inputStream) { + this.contentType = contentType; + this.inputStream = inputStream; + } + + /** + * Constructor that builds a StreamingResolution that will stream character data back to the + * client and identify the data as being of the specified content type. + * + * @param contentType the content type of the data in the stream (e.g. text/xml) + * @param reader a Reader from which to read the character data to return to the client + */ + public StreamingResolution(String contentType, Reader reader) { + this.contentType = contentType; + this.reader = reader; + } + + /** + * Constructor that builds a StreamingResolution that will stream character data from a String + * back to the client and identify the data as being of the specified content type. + * + * @param contentType the content type of the data in the stream (e.g. text/xml) + * @param output a String to stream back to the client + */ + public StreamingResolution(String contentType, String output) { + this(contentType, new StringReader(output)); + } + + /** + * Sets the filename that will be the default name suggested when the user is prompted to save the + * file/stream being sent back. If the stream is not for saving by the user (i.e. it should be + * displayed or used by the browser) this value should not be set. + * + * @param filename the default filename the user will see + * @return StreamingResolution so that this method call can be chained to the constructor and + * returned + */ + public StreamingResolution setFilename(String filename) { + this.filename = filename; + setAttachment(filename != null); + return this; + } + + /** + * Sets the character encoding that will be set on the request when executing this resolution. If + * none is set, then the current character encoding (either the one selected by the LocalePicker + * or the container default one) will be used. + * + * @param characterEncoding the character encoding to use instead of the default + */ + public void setCharacterEncoding(String characterEncoding) { + this.characterEncoding = characterEncoding; + } + + /** + * Sets the modification-date timestamp. If this property is set, the browser may be able to apply + * it to the downloaded file. If this property is unset, the modification-date parameter will be + * omitted. + * + * @param lastModified The date-time (as a long) that the file was last modified. Optional. + * @return StreamingResolution so that this method call can be chained to the constructor and + * returned. + */ + public StreamingResolution setLastModified(long lastModified) { + this.lastModified = lastModified; + return this; + } + + /** + * Sets the file length. If this property is set, the file size will be reported in the HTTP + * header. This may help with file download progress indicators. If this property is unset, the + * size parameter will be omitted. + * + * @param length The length of the file in bytes. + * @return StreamingResolution so that this method call can be chained to the constructor and + * returned. + */ + public StreamingResolution setLength(long length) { + this.length = length; + return this; + } + + /** + * Indicates whether to use content-disposition attachment headers or not. (Defaults to true). + * + * @param attachment Whether the content should be treated as an attachment, or a direct download. + * @return StreamingResolution so that this method call can be chained to the constructor and + * returned. + */ + public StreamingResolution setAttachment(boolean attachment) { + this.attachment = attachment; + return this; + } + + /** + * Indicates whether byte range serving is supported by stream method. (Defaults to false). + * Besides setting this flag, the ActionBean also needs to set the length of the response and + * provide an {@link InputStream}-based input. Reasons for disabling byte range serving: + * + *
    + *
  • The stream method is overridden and does not support byte range serving + *
  • The input to this {@link StreamingResolution} was created on-demand, and retrieving in + * byte ranges would redo this process for every byte range. + *
+ * + * Reasons for enabling byte range serving: + * + *
    + *
  • Streaming static multimedia files + *
  • Supporting resuming download managers + *
+ * + * @param rangeSupport Whether byte range serving is supported by stream method. + * @return StreamingResolution so that this method call can be chained to the constructor and + * returned. + */ + public StreamingResolution setRangeSupport(boolean rangeSupport) { + this.rangeSupport = rangeSupport; + return this; + } + + /** + * Streams data from the InputStream or Reader to the response's OutputStream or PrinterWriter, + * using a moderately sized buffer to ensure that the operation is reasonable efficient. Once the + * InputStream or Reader signaled the end of the stream, close() is called on it. + * + * @param request the HttpServletRequest being processed + * @param response the paired HttpServletResponse + * @throws IOException if there is a problem accessing one of the streams or reader/writer objects + * used. + */ + public final void execute(HttpServletRequest request, HttpServletResponse response) + throws Exception { + /*- + * Process byte ranges only when the following three conditions are met: + * - Length has been defined (without length it is impossible to efficiently stream) + * - rangeSupport has not been set to false + * - Output is binary and not character based + -*/ + if (rangeSupport && (length >= 0) && (inputStream != null)) + byteRanges = parseRangeHeader(request.getHeader("Range")); + + applyHeaders(response); + stream(response); + } + + /** + * Sets the response headers, based on what is known about the file or stream being handled. + * + * @param response the current HttpServletResponse + */ + protected void applyHeaders(HttpServletResponse response) { + if (byteRanges != null) { + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); } - /** - * Constructor that builds a StreamingResolution that will stream binary data back to the - * client and identify the data as being of the specified content type. - * - * @param contentType the content type of the data in the stream (e.g. image/png) - * @param inputStream an InputStream from which to read the data to return to the client - */ - public StreamingResolution(String contentType, InputStream inputStream) { - this.contentType = contentType; - this.inputStream = inputStream; + if ((byteRanges == null) || (byteRanges.size() == 1)) { + response.setContentType(this.contentType); + } else { + response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY); } - /** - * Constructor that builds a StreamingResolution that will stream character data back to the - * client and identify the data as being of the specified content type. - * - * @param contentType the content type of the data in the stream (e.g. text/xml) - * @param reader a Reader from which to read the character data to return to the client - */ - public StreamingResolution(String contentType, Reader reader) { - this.contentType = contentType; - this.reader = reader; + if (this.characterEncoding != null) { + response.setCharacterEncoding(characterEncoding); } - /** - * Constructor that builds a StreamingResolution that will stream character data from a String - * back to the client and identify the data as being of the specified content type. - * - * @param contentType the content type of the data in the stream (e.g. text/xml) - * @param output a String to stream back to the client - */ - public StreamingResolution(String contentType, String output) { - this(contentType, new StringReader(output)); + // Set Content-Length header + if (length >= 0) { + if (byteRanges == null) { + // Odd that ServletResponse.setContentLength is limited to int. + // requires downcast from long to int e.g. + // response.setContentLength((int)length); + // Workaround to allow large files: + response.addHeader("Content-Length", Long.toString(length)); + } else if (byteRanges.size() == 1) { + Range byteRange; + + byteRange = byteRanges.get(0); + response.setHeader( + "Content-Length", Long.toString(byteRange.getEnd() - byteRange.getStart() + 1)); + response.setHeader( + "Content-Range", + "bytes " + byteRange.getStart() + "-" + byteRange.getEnd() + "/" + length); + } } - /** - * Sets the filename that will be the default name suggested when the user is prompted - * to save the file/stream being sent back. If the stream is not for saving by the user - * (i.e. it should be displayed or used by the browser) this value should not be set. - * - * @param filename the default filename the user will see - * @return StreamingResolution so that this method call can be chained to the constructor - * and returned - */ - public StreamingResolution setFilename(String filename) { - this.filename = filename; - setAttachment(filename != null); - return this; + // Set Last-Modified header + if (lastModified >= 0) { + response.setDateHeader("Last-Modified", lastModified); } - /** - * Sets the character encoding that will be set on the request when executing this - * resolution. If none is set, then the current character encoding (either the one - * selected by the LocalePicker or the container default one) will be used. - * - * @param characterEncoding the character encoding to use instead of the default - */ - public void setCharacterEncoding(String characterEncoding) { - this.characterEncoding = characterEncoding; + // For Content-Disposition spec, see http://www.ietf.org/rfc/rfc2183.txt + if (attachment || filename != null) { + // Value of filename should be RFC 2047 encoded here (see RFC 2616) but few browsers + // support that, so just escape the quote for now + StringBuilder header = new StringBuilder(attachment ? "attachment" : "inline"); + if (filename != null) { + String escaped = this.filename.replace("\"", "\\\""); + header.append(";filename=\"").append(escaped).append("\""); + } + if (lastModified >= 0) { + SimpleDateFormat format = new SimpleDateFormat(RFC_822_DATE_FORMAT); + String value = format.format(new Date(lastModified)); + header.append(";modification-date=\"").append(value).append("\""); + } + if (length >= 0) { + header.append(";size=").append(length); + } + response.setHeader("Content-Disposition", header.toString()); } - - /** - * Sets the modification-date timestamp. If this property is set, the browser may be able to - * apply it to the downloaded file. If this property is unset, the modification-date parameter - * will be omitted. - * - * @param lastModified The date-time (as a long) that the file was last modified. Optional. - * @return StreamingResolution so that this method call can be chained to the constructor and - * returned. - */ - public StreamingResolution setLastModified(long lastModified) { - this.lastModified = lastModified; - return this; + } + + /** + * Parse the Range header according to RFC 2616 section 14.35.1. Example ranges from this section: + * + *
    + *
  • The first 500 bytes (byte offsets 0-499, inclusive): bytes=0-499 + *
  • The second 500 bytes (byte offsets 500-999, inclusive): bytes=500-999 + *
  • The final 500 bytes (byte offsets 9500-9999, inclusive): bytes=-500 - Or bytes=9500- + *
  • The first and last bytes only (bytes 0 and 9999): bytes=0-0,-1 + *
  • Several legal but not canonical specifications of the second 500 bytes (byte offsets + * 500-999, inclusive): bytes=500-600,601-999 bytes=500-700,601-999 + *
+ * + * @param value the value of the Range header + * @return List of sorted, non-overlapping ranges + */ + protected List> parseRangeHeader(String value) { + Iterator> i; + String byteRangesSpecifier[], bytesUnit, byteRangeSet[]; + List> res; + long lastEnd = -1; + + if (value == null) return null; + res = new ArrayList>(); + // Parse prelude + byteRangesSpecifier = value.split("="); + if (byteRangesSpecifier.length != 2) return null; + bytesUnit = byteRangesSpecifier[0]; + byteRangeSet = byteRangesSpecifier[1].split(","); + if (!bytesUnit.equals("bytes")) return null; + // Parse individual byte ranges + for (String byteRangeSpec : byteRangeSet) { + String[] bytePos; + Long firstBytePos = null, lastBytePos = null; + + bytePos = byteRangeSpec.split("-", -1); + try { + if (bytePos[0].trim().length() > 0) firstBytePos = Long.valueOf(bytePos[0].trim()); + if (bytePos[1].trim().length() > 0) lastBytePos = Long.valueOf(bytePos[1].trim()); + } catch (NumberFormatException e) { + log.warn("Unable to parse Range header", e); + } + if ((firstBytePos == null) && (lastBytePos == null)) { + return null; + } else if (firstBytePos == null) { + firstBytePos = length - lastBytePos; + lastBytePos = length - 1; + } else if (lastBytePos == null) { + lastBytePos = length - 1; + } + if (firstBytePos > lastBytePos) return null; + if (firstBytePos < 0) return null; + if (lastBytePos >= length) return null; + res.add(new Range(firstBytePos, lastBytePos)); } - - /** - * Sets the file length. If this property is set, the file size will be reported in the HTTP - * header. This may help with file download progress indicators. If this property is unset, the - * size parameter will be omitted. - * - * @param length The length of the file in bytes. - * @return StreamingResolution so that this method call can be chained to the constructor and - * returned. - */ - public StreamingResolution setLength(long length) { - this.length = length; - return this; - } - - /** - * Indicates whether to use content-disposition attachment headers or not. (Defaults to true). - * - * @param attachment Whether the content should be treated as an attachment, or a direct - * download. - * @return StreamingResolution so that this method call can be chained to the constructor and - * returned. - */ - public StreamingResolution setAttachment(boolean attachment) { - this.attachment = attachment; - return this; + // Sort byte ranges + Collections.sort(res); + // Remove overlapping ranges + i = res.listIterator(); + while (i.hasNext()) { + Range range; + + range = i.next(); + if (lastEnd >= range.getStart()) { + range.setStart(lastEnd + 1); + if ((range.getStart() >= length) || (range.getStart() > range.getEnd())) i.remove(); + else lastEnd = range.getEnd(); + } else { + lastEnd = range.getEnd(); + } } - - /** - * Indicates whether byte range serving is supported by stream method. (Defaults to false). - * Besides setting this flag, the ActionBean also needs to set the length of the response and - * provide an {@link InputStream}-based input. Reasons for disabling byte range serving: - *
    - *
  • The stream method is overridden and does not support byte range serving
  • - *
  • The input to this {@link StreamingResolution} was created on-demand, and retrieving in - * byte ranges would redo this process for every byte range.
  • - *
- * Reasons for enabling byte range serving: - *
    - *
  • Streaming static multimedia files
  • - *
  • Supporting resuming download managers
  • - *
- * - * @param rangeSupport Whether byte range serving is supported by stream method. - * @return StreamingResolution so that this method call can be chained to the constructor and - * returned. - */ - public StreamingResolution setRangeSupport(boolean rangeSupport) { - this.rangeSupport = rangeSupport; - return this; - } - - /** - * Streams data from the InputStream or Reader to the response's OutputStream or PrinterWriter, - * using a moderately sized buffer to ensure that the operation is reasonable efficient. - * Once the InputStream or Reader signaled the end of the stream, close() is called on it. - * - * @param request the HttpServletRequest being processed - * @param response the paired HttpServletResponse - * @throws IOException if there is a problem accessing one of the streams or reader/writer - * objects used. - */ - final public void execute(HttpServletRequest request, HttpServletResponse response) - throws Exception { - /*- - * Process byte ranges only when the following three conditions are met: - * - Length has been defined (without length it is impossible to efficiently stream) - * - rangeSupport has not been set to false - * - Output is binary and not character based - -*/ - if (rangeSupport && (length >= 0) && (inputStream != null)) - byteRanges = parseRangeHeader(request.getHeader("Range")); - - applyHeaders(response); - stream(response); - } - - /** - * Sets the response headers, based on what is known about the file or stream being handled. - * - * @param response the current HttpServletResponse - */ - protected void applyHeaders(HttpServletResponse response) { - if (byteRanges != null) { - response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); - } - - if ((byteRanges == null) || (byteRanges.size() == 1)) { - response.setContentType(this.contentType); - } - else { - response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY); - } - - if (this.characterEncoding != null) { - response.setCharacterEncoding(characterEncoding); - } - - // Set Content-Length header - if (length >= 0) { - if (byteRanges == null) { - // Odd that ServletResponse.setContentLength is limited to int. - // requires downcast from long to int e.g. - // response.setContentLength((int)length); - // Workaround to allow large files: - response.addHeader("Content-Length", Long.toString(length)); - } - else if (byteRanges.size() == 1) { - Range byteRange; - - byteRange = byteRanges.get(0); - response.setHeader("Content-Length", - Long.toString(byteRange.getEnd() - byteRange.getStart() + 1)); - response.setHeader("Content-Range", "bytes " + byteRange.getStart() + "-" - + byteRange.getEnd() + "/" + length); - } - } - - // Set Last-Modified header - if (lastModified >= 0) { - response.setDateHeader("Last-Modified", lastModified); + if (res.isEmpty()) return null; + else return res; + } + + /** + * Does the actual streaming of data through the response. If subclassed, this method should be + * overridden to stream back data other than data supplied by an InputStream or a Reader supplied + * to a constructor. If not implementing byte range serving, be sure not to set rangeSupport to + * true. + * + *

If an InputStream or Reader was supplied to a constructor, this implementation uses a + * moderately sized buffer to stream data from it to the response to make the operation reasonably + * efficient, and closes the InputStream or the Reader. If an IOException occurs when closing it, + * that exception will be logged as a warning, and not thrown to avoid masking a possibly + * previously thrown exception. + * + * @param response the HttpServletResponse from which either the output stream or writer can be + * obtained + * @throws Exception if any problems arise when streaming data + */ + protected void stream(HttpServletResponse response) throws Exception { + int length = 0; + if (this.reader != null) { + char[] buffer = new char[512]; + try { + PrintWriter out = response.getWriter(); + + while ((length = this.reader.read(buffer)) != -1) { + out.write(buffer, 0, length); } - - // For Content-Disposition spec, see http://www.ietf.org/rfc/rfc2183.txt - if (attachment || filename != null) { - // Value of filename should be RFC 2047 encoded here (see RFC 2616) but few browsers - // support that, so just escape the quote for now - StringBuilder header = new StringBuilder(attachment ? "attachment" : "inline"); - if (filename != null) { - String escaped = this.filename.replace("\"", "\\\""); - header.append(";filename=\"").append(escaped).append("\""); - } - if (lastModified >= 0) { - SimpleDateFormat format = new SimpleDateFormat(RFC_822_DATE_FORMAT); - String value = format.format(new Date(lastModified)); - header.append(";modification-date=\"").append(value).append("\""); - } - if (length >= 0) { - header.append(";size=").append(length); - } - response.setHeader("Content-Disposition", header.toString()); + } finally { + try { + this.reader.close(); + } catch (Exception e) { + log.warn("Error closing reader", e); } - } - - /** - * Parse the Range header according to RFC 2616 section 14.35.1. Example ranges from this - * section: - *

    - *
  • The first 500 bytes (byte offsets 0-499, inclusive): bytes=0-499
  • - *
  • The second 500 bytes (byte offsets 500-999, inclusive): bytes=500-999
  • - *
  • The final 500 bytes (byte offsets 9500-9999, inclusive): bytes=-500 - Or bytes=9500-
  • - *
  • The first and last bytes only (bytes 0 and 9999): bytes=0-0,-1
  • - *
  • Several legal but not canonical specifications of the second 500 bytes (byte offsets - * 500-999, inclusive): bytes=500-600,601-999 bytes=500-700,601-999
  • - *
- * - * @param value the value of the Range header - * @return List of sorted, non-overlapping ranges - */ - protected List> parseRangeHeader(String value) { - Iterator> i; - String byteRangesSpecifier[], bytesUnit, byteRangeSet[]; - List> res; - long lastEnd = -1; - - if (value == null) - return null; - res = new ArrayList>(); - // Parse prelude - byteRangesSpecifier = value.split("="); - if (byteRangesSpecifier.length != 2) - return null; - bytesUnit = byteRangesSpecifier[0]; - byteRangeSet = byteRangesSpecifier[1].split(","); - if (!bytesUnit.equals("bytes")) - return null; - // Parse individual byte ranges - for (String byteRangeSpec : byteRangeSet) { - String[] bytePos; - Long firstBytePos = null, lastBytePos = null; - - bytePos = byteRangeSpec.split("-", -1); - try { - if (bytePos[0].trim().length() > 0) - firstBytePos = Long.valueOf(bytePos[0].trim()); - if (bytePos[1].trim().length() > 0) - lastBytePos = Long.valueOf(bytePos[1].trim()); - } - catch (NumberFormatException e) { - log.warn("Unable to parse Range header", e); - } - if ((firstBytePos == null) && (lastBytePos == null)) { - return null; + } + } else if (this.inputStream != null) { + byte[] buffer = new byte[512]; + long count = 0; + + try { + ServletOutputStream out = response.getOutputStream(); + + if (byteRanges == null) { + while ((length = this.inputStream.read(buffer)) != -1) { + out.write(buffer, 0, length); + } + } else { + for (Range byteRange : byteRanges) { + // See RFC 2616 section 14.16 + if (byteRanges.size() > 1) { + out.print("--" + MULTIPART_BOUNDARY + "\r\n"); + out.print("Content-Type: " + contentType + "\r\n"); + out.print( + "Content-Range: bytes " + + byteRange.getStart() + + "-" + + byteRange.getEnd() + + "/" + + this.length + + "\r\n"); + out.print("\r\n"); } - else if (firstBytePos == null) { - firstBytePos = length - lastBytePos; - lastBytePos = length - 1; - } - else if (lastBytePos == null) { - lastBytePos = length - 1; - } - if (firstBytePos > lastBytePos) - return null; - if (firstBytePos < 0) - return null; - if (lastBytePos >= length) - return null; - res.add(new Range(firstBytePos, lastBytePos)); - } - // Sort byte ranges - Collections.sort(res); - // Remove overlapping ranges - i = res.listIterator(); - while (i.hasNext()) { - Range range; + if (count < byteRange.getStart()) { + long skip; - range = i.next(); - if (lastEnd >= range.getStart()) { - range.setStart(lastEnd + 1); - if ((range.getStart() >= length) || (range.getStart() > range.getEnd())) - i.remove(); - else - lastEnd = range.getEnd(); + skip = byteRange.getStart() - count; + this.inputStream.skip(skip); + count += skip; } - else { - lastEnd = range.getEnd(); + while ((length = + this.inputStream.read( + buffer, 0, (int) Math.min(buffer.length, byteRange.getEnd() + 1 - count))) + != -1) { + out.write(buffer, 0, length); + count += length; + if (byteRange.getEnd() + 1 == count) break; } - } - if (res.isEmpty()) - return null; - else - return res; - } - - /** - *

- * Does the actual streaming of data through the response. If subclassed, this method should be - * overridden to stream back data other than data supplied by an InputStream or a Reader - * supplied to a constructor. If not implementing byte range serving, be sure not to set - * rangeSupport to true. - *

- * - *

- * If an InputStream or Reader was supplied to a constructor, this implementation uses a - * moderately sized buffer to stream data from it to the response to make the operation - * reasonably efficient, and closes the InputStream or the Reader. If an IOException occurs when - * closing it, that exception will be logged as a warning, and not thrown to avoid - * masking a possibly previously thrown exception. - *

- * - * @param response the HttpServletResponse from which either the output stream or writer can be - * obtained - * @throws Exception if any problems arise when streaming data - */ - protected void stream(HttpServletResponse response) throws Exception { - int length = 0; - if (this.reader != null) { - char[] buffer = new char[512]; - try { - PrintWriter out = response.getWriter(); - - while ( (length = this.reader.read(buffer)) != -1 ) { - out.write(buffer, 0, length); - } - } - finally { - try { - this.reader.close(); - } - catch (Exception e) { - log.warn("Error closing reader", e); - } - } - } - else if (this.inputStream != null) { - byte[] buffer = new byte[512]; - long count = 0; - - try { - ServletOutputStream out = response.getOutputStream(); - - if (byteRanges == null) { - while ((length = this.inputStream.read(buffer)) != -1) { - out.write(buffer, 0, length); - } - } - else { - for (Range byteRange : byteRanges) { - // See RFC 2616 section 14.16 - if (byteRanges.size() > 1) { - out.print("--" + MULTIPART_BOUNDARY + "\r\n"); - out.print("Content-Type: " + contentType + "\r\n"); - out.print("Content-Range: bytes " + byteRange.getStart() + "-" - + byteRange.getEnd() + "/" + this.length + "\r\n"); - out.print("\r\n"); - } - if (count < byteRange.getStart()) { - long skip; - - skip = byteRange.getStart() - count; - this.inputStream.skip(skip); - count += skip; - } - while ((length = this.inputStream.read(buffer, 0, (int) Math.min( - buffer.length, byteRange.getEnd() + 1 - count))) != -1) { - out.write(buffer, 0, length); - count += length; - if (byteRange.getEnd() + 1 == count) - break; - } - if (byteRanges.size() > 1) { - out.print("\r\n"); - } - } - if (byteRanges.size() > 1) - out.print("--" + MULTIPART_BOUNDARY + "--\r\n"); - } - } - finally { - try { - this.inputStream.close(); - } - catch (Exception e) { - log.warn("Error closing input stream", e); - } + if (byteRanges.size() > 1) { + out.print("\r\n"); } + } + if (byteRanges.size() > 1) out.print("--" + MULTIPART_BOUNDARY + "--\r\n"); } - else { - throw new StripesRuntimeException("A StreamingResolution was constructed without " + - "supplying a Reader or InputStream, but stream() was not overridden. Please " + - "either supply a source of streaming data, or override the stream() method."); + } finally { + try { + this.inputStream.close(); + } catch (Exception e) { + log.warn("Error closing input stream", e); } + } + } else { + throw new StripesRuntimeException( + "A StreamingResolution was constructed without " + + "supplying a Reader or InputStream, but stream() was not overridden. Please " + + "either supply a source of streaming data, or override the stream() method."); } - + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/StrictBinding.java b/stripes/src/main/java/net/sourceforge/stripes/action/StrictBinding.java index 2449948db..901a62d8e 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/StrictBinding.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/StrictBinding.java @@ -19,12 +19,10 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; - import net.sourceforge.stripes.validation.Validate; import net.sourceforge.stripes.validation.ValidateNestedProperties; /** - *

* When applied to an {@link ActionBean}, this annotation turns on binding access controls. The * default policy is to deny binding to all properties. To enable binding on any given property, the * preferred method is to apply a {@link Validate} annotation to the property. (For nested @@ -32,54 +30,49 @@ * property in question, a naked {@link Validate} annotation may still be used to enable binding. * Alternatively, binding can be enabled or disabled through the use of the {@link #allow()} and * {@link #deny()} elements of this annotation. - *

- *

- * Properties may be named explicitly or by using globs. A single star (*) matches any property of - * an element. Two stars (**) indicate any property of an element, including properties of that + * + *

Properties may be named explicitly or by using globs. A single star (*) matches any property + * of an element. Two stars (**) indicate any property of an element, including properties of that * property and so on. For security reasons, partial matches are not allowed so globs like * user.pass* will never match anything. Some examples: + * *

    - *
  • {@code *} - any property of the {@link ActionBean} itself
  • - *
  • {@code **} - any property of the {@link ActionBean} itself or its properties or their - * properties, and so on
  • - *
  • {@code user.username, user.email} - the username and email property of the user property of - * the {@link ActionBean}
  • - *
  • {@code user, user.*} - the user property and any property of the user + *
  • {@code *} - any property of the {@link ActionBean} itself + *
  • {@code **} - any property of the {@link ActionBean} itself or its properties or their + * properties, and so on + *
  • {@code user.username, user.email} - the username and email property of the user property of + * the {@link ActionBean} + *
  • {@code user, user.*} - the user property and any property of the user *
- *

- *

- * The {@link #allow()} and {@link #deny()} elements are of type String[], but each string in the - * array may be a comma-separated list of properties. Thus the - * {@code @StrictBinding(allow="user, user.*")} is equivalent to - * {@code @StrictBinding(allow={ "user", "user.*" }}. - *

- * + * + *

The {@link #allow()} and {@link #deny()} elements are of type String[], but each string in the + * array may be a comma-separated list of properties. Thus the {@code @StrictBinding(allow="user, + * user.*")} is equivalent to {@code @StrictBinding(allow={ "user", "user.*" }}. + * * @author Ben Gunter */ @Retention(RetentionPolicy.RUNTIME) -@Target( { ElementType.TYPE }) +@Target({ElementType.TYPE}) @Documented public @interface StrictBinding { - /** - * The options for the {@link StrictBinding#defaultPolicy()} element. - */ - public enum Policy { - /** In the event of a conflict, binding is allowed */ - ALLOW, + /** The options for the {@link StrictBinding#defaultPolicy()} element. */ + public enum Policy { + /** In the event of a conflict, binding is allowed */ + ALLOW, - /** In the event of a conflict, binding is denied */ - DENY - } + /** In the event of a conflict, binding is denied */ + DENY + } - /** - * The policy to observe when a property name matches both the deny and allow lists, or when a - * property name does not match either list. - */ - Policy defaultPolicy() default Policy.DENY; + /** + * The policy to observe when a property name matches both the deny and allow lists, or when a + * property name does not match either list. + */ + Policy defaultPolicy() default Policy.DENY; - /** The list of properties that may be bound. */ - String[] allow() default ""; + /** The list of properties that may be bound. */ + String[] allow() default ""; - /** The list of properties that may not be bound. */ - String[] deny() default ""; + /** The list of properties that may not be bound. */ + String[] deny() default ""; } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/UrlBinding.java b/stripes/src/main/java/net/sourceforge/stripes/action/UrlBinding.java index d4c7f3e38..c557cdc73 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/UrlBinding.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/UrlBinding.java @@ -14,51 +14,46 @@ */ package net.sourceforge.stripes.action; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; import java.lang.annotation.Documented; -import java.lang.annotation.RetentionPolicy; import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; /** - *

* Annotation used to bind ActionBean classes to a specific path within the web application. The * AnnotatedClassActionResolver will examine the URL submitted and extract the section that is * relative to the web-app root. That will be compared with the URL specified in the UrlBinding * annotation, to find the ActionBean that should process the chosen request. - *

- *

- * Stripes supports "Clean URLs" through the {@link UrlBinding} annotation. Parameters may be + * + *

Stripes supports "Clean URLs" through the {@link UrlBinding} annotation. Parameters may be * embedded in the URL by placing the parameter name inside braces ({}). For example, * {@code @UrlBinding("/foo/{bar}/{baz}")} maps the action to "/foo" and indicates that the "bar" * and "baz" parameters may be embedded in the URL. In this case, the URL /foo/abc/123 would invoke * the action with bar set to "abc" and baz set to "123". The literal strings between parameters can * be any string. - *

- *

- * The special parameter name $event may be used to embed the event name in a clean URL. For - * example, given {@code @UrlBinding("/foo/{$event}")} the "bar" event could be invoked with the - * URL /foo/bar. - *

- *

- * Clean URL parameters can be assigned default values using the {@code =} operator. For example, + * + *

The special parameter name $event may be used to embed the event name in a clean URL. For + * example, given {@code @UrlBinding("/foo/{$event}")} the "bar" event could be invoked with the URL + * /foo/bar. + * + *

Clean URL parameters can be assigned default values using the {@code =} operator. For example, * {@code @UrlBinding("/foo/{bar=abc}/{baz=123}")}. If a parameter with a default value is missing * from a request URL, it will still be made available as a request parameter with the default * value. Default values are automatically embedded when building URLs with the Stripes JSP tags. * The default value for $event is determined from the {@link DefaultHandler} and may not be set in * the {@code @UrlBinding}. - *

- *

- * Clean URLs support both prefix mapping ({@code /action/foo/{bar}}) and extension mapping ({@code /foo/{bar}.action}). - * Any number of parameters and/or literals may be omitted from the end of a request URL. - *

- * + * + *

Clean URLs support both prefix mapping ({@code /action/foo/{bar}}) and extension mapping + * ({@code /foo/{bar}.action}). Any number of parameters and/or literals may be omitted from the end + * of a request URL. + * * @author Tim Fennell */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented public @interface UrlBinding { - /** The web-app relative URL that the ActionBean will respond to. */ - String value(); + /** The web-app relative URL that the ActionBean will respond to. */ + String value(); } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/ValidationErrorReportResolution.java b/stripes/src/main/java/net/sourceforge/stripes/action/ValidationErrorReportResolution.java index 37c0144f6..06cc7071e 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/ValidationErrorReportResolution.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/ValidationErrorReportResolution.java @@ -14,15 +14,13 @@ */ package net.sourceforge.stripes.action; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.PrintWriter; import java.util.List; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import net.sourceforge.stripes.controller.StripesFilter; import net.sourceforge.stripes.exception.SourcePageNotFoundException; import net.sourceforge.stripes.tag.ErrorsTag; @@ -33,108 +31,110 @@ /** * A resolution that streams a simple HTML response to the client detailing the validation errors * that apply for an {@link ActionBeanContext}. - * + * * @author Ben Gunter * @since Stripes 1.5.5 */ public class ValidationErrorReportResolution implements Resolution { - private static final Log log = Log.getInstance(ValidationErrorReportResolution.class); - private ActionBeanContext context; - - /** Construct a new instance to report validation errors in the specified context. */ - public ValidationErrorReportResolution(ActionBeanContext context) { - this.context = context; + private static final Log log = Log.getInstance(ValidationErrorReportResolution.class); + private ActionBeanContext context; + + /** Construct a new instance to report validation errors in the specified context. */ + public ValidationErrorReportResolution(ActionBeanContext context) { + this.context = context; + } + + /** Get the action bean context on which the validation errors occurred. */ + public ActionBeanContext getContext() { + return context; + } + + public void execute(HttpServletRequest request, HttpServletResponse response) throws Exception { + // log an exception for the stack trace + SourcePageNotFoundException exception = new SourcePageNotFoundException(getContext()); + log.error(exception); + + // start the HTML error report + response.setContentType("text/html"); + PrintWriter writer = response.getWriter(); + writer.println("

"); + writer.println("

Stripes validation error report

"); + writer.println(HtmlUtil.encode(exception.getMessage())); + writer.println("

Validation errors

"); + sendErrors(request, response); + writer.println("

"); + } + + /** + * Called by {@link #execute(HttpServletRequest, HttpServletResponse)} to write the actual + * validation errors to the client. The {@code header}, {@code footer}, {@code beforeError} and + * {@code afterError} resources are used by this method. + * + * @param request The servlet request. + * @param response The servlet response. + */ + protected void sendErrors(HttpServletRequest request, HttpServletResponse response) + throws Exception { + // Output all errors in a standard format + Locale locale = request.getLocale(); + ResourceBundle bundle = null; + + try { + bundle = + StripesFilter.getConfiguration() + .getLocalizationBundleFactory() + .getErrorMessageBundle(locale); + } catch (MissingResourceException mre) { + log.warn( + getClass().getName(), + " could not find the error messages resource bundle. ", + "As a result default headers/footers etc. will be used. Check that ", + "you have a StripesResources.properties in your classpath (unless ", + "of course you have configured a different bundle)."); } - /** Get the action bean context on which the validation errors occurred. */ - public ActionBeanContext getContext() { - return context; + // Fetch the header and footer + String header = getResource(bundle, "header", ErrorsTag.DEFAULT_HEADER); + String footer = getResource(bundle, "footer", ErrorsTag.DEFAULT_FOOTER); + String openElement = getResource(bundle, "beforeError", "
  • "); + String closeElement = getResource(bundle, "afterError", "
  • "); + + // Write out the error messages + PrintWriter writer = response.getWriter(); + writer.write(header); + + for (List list : getContext().getValidationErrors().values()) { + for (ValidationError fieldError : list) { + writer.write(openElement); + writer.write(HtmlUtil.encode(fieldError.getMessage(locale))); + writer.write(closeElement); + } } - public void execute(HttpServletRequest request, HttpServletResponse response) throws Exception { - // log an exception for the stack trace - SourcePageNotFoundException exception = new SourcePageNotFoundException(getContext()); - log.error(exception); - - // start the HTML error report - response.setContentType("text/html"); - PrintWriter writer = response.getWriter(); - writer.println("
    "); - writer.println("

    Stripes validation error report

    "); - writer.println(HtmlUtil.encode(exception.getMessage())); - writer.println("

    Validation errors

    "); - sendErrors(request, response); - writer.println("

    "); + writer.write(footer); + } + + /** + * Utility method that is used to lookup the resources used for the error header, footer, and the + * strings that go before and after each error. + * + * @param bundle the bundle to look up the resource from + * @param name the name of the resource to lookup (prefixes will be added) + * @param fallback a value to return if no resource can be found + * @return the value to use for the named resource + */ + protected String getResource(ResourceBundle bundle, String name, String fallback) { + if (bundle == null) { + return fallback; } - /** - * Called by {@link #execute(HttpServletRequest, HttpServletResponse)} to write the actual - * validation errors to the client. The {@code header}, {@code footer}, {@code beforeError} and - * {@code afterError} resources are used by this method. - * - * @param request The servlet request. - * @param response The servlet response. - */ - protected void sendErrors(HttpServletRequest request, HttpServletResponse response) - throws Exception { - // Output all errors in a standard format - Locale locale = request.getLocale(); - ResourceBundle bundle = null; - - try { - bundle = StripesFilter.getConfiguration().getLocalizationBundleFactory() - .getErrorMessageBundle(locale); - } - catch (MissingResourceException mre) { - log.warn(getClass().getName(), " could not find the error messages resource bundle. ", - "As a result default headers/footers etc. will be used. Check that ", - "you have a StripesResources.properties in your classpath (unless ", - "of course you have configured a different bundle)."); - } - - // Fetch the header and footer - String header = getResource(bundle, "header", ErrorsTag.DEFAULT_HEADER); - String footer = getResource(bundle, "footer", ErrorsTag.DEFAULT_FOOTER); - String openElement = getResource(bundle, "beforeError", "
  • "); - String closeElement = getResource(bundle, "afterError", "
  • "); - - // Write out the error messages - PrintWriter writer = response.getWriter(); - writer.write(header); - - for (List list : getContext().getValidationErrors().values()) { - for (ValidationError fieldError : list) { - writer.write(openElement); - writer.write(HtmlUtil.encode(fieldError.getMessage(locale))); - writer.write(closeElement); - } - } - - writer.write(footer); + String resource; + try { + resource = bundle.getString("stripes.errors." + name); + } catch (MissingResourceException mre) { + resource = fallback; } - /** - * Utility method that is used to lookup the resources used for the error header, footer, and - * the strings that go before and after each error. - * - * @param bundle the bundle to look up the resource from - * @param name the name of the resource to lookup (prefixes will be added) - * @param fallback a value to return if no resource can be found - * @return the value to use for the named resource - */ - protected String getResource(ResourceBundle bundle, String name, String fallback) { - if (bundle == null) { - return fallback; - } - - String resource; - try { - resource = bundle.getString("stripes.errors." + name); - } - catch (MissingResourceException mre) { - resource = fallback; - } - - return resource; - } + return resource; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/Wizard.java b/stripes/src/main/java/net/sourceforge/stripes/action/Wizard.java index b4e17ead9..d08eea4a4 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/Wizard.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/Wizard.java @@ -14,20 +14,20 @@ */ package net.sourceforge.stripes.action; -import java.lang.annotation.Target; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Documented; +import java.lang.annotation.Target; /** - *

    Annotation that marks an ActionBean as representing a wizard user interface (i.e. one logical - * form or operation spread across several pages/request cycles). ActionBeans that are marked - * as Wizards are treated differently in the following ways:

    + * Annotation that marks an ActionBean as representing a wizard user interface (i.e. one logical + * form or operation spread across several pages/request cycles). ActionBeans that are marked as + * Wizards are treated differently in the following ways: * *
      - *
    • Data from previous request cycles is maintained automatically through hidden fields
    • - *
    • Required field validation is performed only on those fields present on the page
    • + *
    • Data from previous request cycles is maintained automatically through hidden fields + *
    • Required field validation is performed only on those fields present on the page *
    * * @author Tim Fennell @@ -37,13 +37,12 @@ @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Wizard { - /** - * An optional list of events which mark the start of the wizard flow. An event is a - * start event if it is executed before the first page in the wizard flow is - * rendered - not if it is the result of a form that targets the wizard action. - * The list is used by Stripes to disable security validation of the 'fields present' - * field in the request, as it is not necessary for start events in a wizard flow, and - * can cause problems. - */ - String[] startEvents() default {}; + /** + * An optional list of events which mark the start of the wizard flow. An event is a start event + * if it is executed before the first page in the wizard flow is rendered - not if + * it is the result of a form that targets the wizard action. The list is used by Stripes to + * disable security validation of the 'fields present' field in the request, as it is not + * necessary for start events in a wizard flow, and can cause problems. + */ + String[] startEvents() default {}; } diff --git a/stripes/src/main/java/net/sourceforge/stripes/ajax/JavaScriptBuilder.java b/stripes/src/main/java/net/sourceforge/stripes/ajax/JavaScriptBuilder.java index 0b2ef112f..e3b6a43ef 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/ajax/JavaScriptBuilder.java +++ b/stripes/src/main/java/net/sourceforge/stripes/ajax/JavaScriptBuilder.java @@ -14,13 +14,11 @@ */ package net.sourceforge.stripes.ajax; -import net.sourceforge.stripes.exception.StripesRuntimeException; -import net.sourceforge.stripes.util.Log; -import net.sourceforge.stripes.util.ReflectUtil; - import java.beans.PropertyDescriptor; import java.io.StringWriter; import java.io.Writer; +import java.lang.reflect.Array; +import java.lang.reflect.Method; import java.util.Collection; import java.util.Date; import java.util.HashMap; @@ -28,499 +26,495 @@ import java.util.Map; import java.util.Random; import java.util.Set; -import java.lang.reflect.Method; -import java.lang.reflect.Array; +import net.sourceforge.stripes.exception.StripesRuntimeException; +import net.sourceforge.stripes.util.Log; +import net.sourceforge.stripes.util.ReflectUtil; /** - *

    Builds a set of JavaScript statements that will re-construct the value of a Java object, - * including all Number, String, Enum, Boolean, Collection, Map and Array properties. Safely handles - * object graph circularities - each object will be translated only once, and all references will - * be valid.

    + * Builds a set of JavaScript statements that will re-construct the value of a Java object, + * including all Number, String, Enum, Boolean, Collection, Map and Array properties. Safely handles + * object graph circularities - each object will be translated only once, and all references will be + * valid. * - *

    The JavaScript created by the builder can be evaluated in JavaScript using:

    + *

    The JavaScript created by the builder can be evaluated in JavaScript using: * - *

    - *var myObject = eval(generatedFragment);
    - *
    + *
    + * var myObject = eval(generatedFragment);
    + * 
    * * @author Tim Fennell * @since Stripes 1.1 */ public class JavaScriptBuilder { - /** Log instance used to log messages. */ - private static final Log log = Log.getInstance(JavaScriptBuilder.class); - - /** Holds the set of classes representing the primitive types in Java. */ - static Set> simpleTypes = new HashSet>(); - - /** Holds the set of types that will be skipped over by default. */ - static Set> ignoredTypes = new HashSet>(); - - static { - simpleTypes.add(Byte.TYPE); - simpleTypes.add(Short.TYPE); - simpleTypes.add(Integer.TYPE); - simpleTypes.add(Long.TYPE); - simpleTypes.add(Float.TYPE); - simpleTypes.add(Double.TYPE); - simpleTypes.add(Boolean.TYPE); - simpleTypes.add(Character.TYPE); - - ignoredTypes.add(Class.class); + /** Log instance used to log messages. */ + private static final Log log = Log.getInstance(JavaScriptBuilder.class); + + /** Holds the set of classes representing the primitive types in Java. */ + static Set> simpleTypes = new HashSet>(); + + /** Holds the set of types that will be skipped over by default. */ + static Set> ignoredTypes = new HashSet>(); + + static { + simpleTypes.add(Byte.TYPE); + simpleTypes.add(Short.TYPE); + simpleTypes.add(Integer.TYPE); + simpleTypes.add(Long.TYPE); + simpleTypes.add(Float.TYPE); + simpleTypes.add(Double.TYPE); + simpleTypes.add(Boolean.TYPE); + simpleTypes.add(Character.TYPE); + + ignoredTypes.add(Class.class); + } + + /** Holds the set of objects that have been visited during conversion. */ + private Set visitedIdentities = new HashSet(); + + /** Holds a map of name to JSON value for JS Objects and Arrays. */ + private Map objectValues = new HashMap(); + + /** Holds a map of object.property = object. */ + private Map assignments = new HashMap(); + + /** Holds the root object which is to be converted to JavaScript. */ + private Object rootObject; + + /** Holds the (potentially empty) set of user classes that should be skipped over. */ + private Set> excludeClasses; + + /** Holds the (potentially empty) set of properties that should be skipped over. */ + private Set excludeProperties; + + /** Holds an optional user-supplied name for the root property. */ + private String rootVariableName = "_sj_root_" + new Random().nextInt(Integer.MAX_VALUE); + + /** + * Constructs a new JavaScriptBuilder to build JS for the root object supplied. + * + * @param root The root object from which to being translation into JavaScript + * @param objectsToExclude Zero or more Strings and/or Classes to be excluded from translation. + */ + public JavaScriptBuilder(Object root, Object... objectsToExclude) { + this.rootObject = root; + this.excludeClasses = new HashSet>(); + this.excludeProperties = new HashSet(); + + for (Object object : objectsToExclude) { + if (object instanceof Class) addClassExclusion((Class) object); + else if (object instanceof String) addPropertyExclusion((String) object); + else + log.warn( + "Don't know to determine exclusion for objects of type ", + object.getClass().getName(), + ". You may only pass in instances of Class and/or String."); } - /** Holds the set of objects that have been visited during conversion. */ - private Set visitedIdentities = new HashSet(); - - /** Holds a map of name to JSON value for JS Objects and Arrays. */ - private Map objectValues = new HashMap(); - - /** Holds a map of object.property = object. */ - private Map assignments = new HashMap(); - - /** Holds the root object which is to be converted to JavaScript. */ - private Object rootObject; - - /** Holds the (potentially empty) set of user classes that should be skipped over. */ - private Set> excludeClasses; - - /** Holds the (potentially empty) set of properties that should be skipped over. */ - private Set excludeProperties; - - /** Holds an optional user-supplied name for the root property. */ - private String rootVariableName = "_sj_root_" + new Random().nextInt(Integer.MAX_VALUE); - - /** - * Constructs a new JavaScriptBuilder to build JS for the root object supplied. - * - * @param root The root object from which to being translation into JavaScript - * @param objectsToExclude Zero or more Strings and/or Classes to be excluded - * from translation. - */ - public JavaScriptBuilder(Object root, Object... objectsToExclude) { - this.rootObject = root; - this.excludeClasses = new HashSet>(); - this.excludeProperties = new HashSet(); - - for (Object object : objectsToExclude) { - if (object instanceof Class) - addClassExclusion((Class) object); - else if (object instanceof String) - addPropertyExclusion((String) object); - else - log.warn("Don't know to determine exclusion for objects of type ", object.getClass().getName(), ". You may only pass in instances of Class and/or String."); - } - - this.excludeClasses.addAll(ignoredTypes); + this.excludeClasses.addAll(ignoredTypes); + } + + /** + * Adds one or more properties to the list of property to exclude when translating to JavaScript. + * + * @param property one or more property names to be excluded + * @return the JavaScripBuilder instance to simplify method chaining + */ + public JavaScriptBuilder addPropertyExclusion(String... property) { + for (String prop : property) { + this.excludeProperties.add(prop); } - - /** - * Adds one or more properties to the list of property to exclude when translating - * to JavaScript. - * - * @param property one or more property names to be excluded - * @return the JavaScripBuilder instance to simplify method chaining - */ - public JavaScriptBuilder addPropertyExclusion(String... property) { - for (String prop : property) { - this.excludeProperties.add(prop); - } - return this; + return this; + } + + /** + * Adds one or more properties to the list of properties to exclude when translating to + * JavaScript. + * + * @param clazz one or more classes to exclude + * @return the JavaScripBuilder instance to simplify method chaining + */ + public JavaScriptBuilder addClassExclusion(Class... clazz) { + for (Class c : clazz) { + this.excludeClasses.add(c); } - - /** - * Adds one or more properties to the list of properties to exclude when translating - * to JavaScript. - * - * @param clazz one or more classes to exclude - * @return the JavaScripBuilder instance to simplify method chaining - */ - public JavaScriptBuilder addClassExclusion(Class... clazz) { - for (Class c : clazz) { - this.excludeClasses.add(c); - } - return this; - } - - /** - * Sets an optional user-supplied root variable name. If set this name will be used - * by the building when declarind the root variable to which the JS is assigned. If - * not provided then a randomly generated name will be used. - * - * @param rootVariableName the name to use when declaring the root variable - */ - public void setRootVariableName(final String rootVariableName) { - this.rootVariableName = rootVariableName; + return this; + } + + /** + * Sets an optional user-supplied root variable name. If set this name will be used by the + * building when declarind the root variable to which the JS is assigned. If not provided then a + * randomly generated name will be used. + * + * @param rootVariableName the name to use when declaring the root variable + */ + public void setRootVariableName(final String rootVariableName) { + this.rootVariableName = rootVariableName; + } + + /** + * Returns the name used to declare the root variable to which the built JavaScript object is + * assigned. + */ + public String getRootVariableName() { + return rootVariableName; + } + + /** + * Causes the JavaScriptBuilder to navigate the properties of the supplied object and convert them + * to JavaScript. + * + * @return String a fragment of JavaScript that will define and return the JavaScript equivalent + * of the Java object supplied to the builder. + */ + public String build() { + Writer writer = new StringWriter(); + build(writer); + return writer.toString(); + } + + /** + * Causes the JavaScriptBuilder to navigate the properties of the supplied object and convert them + * to JavaScript, writing them to the supplied writer as it goes. + */ + public void build(Writer writer) { + try { + // If for some reason a caller provided us with a simple scalar object, then + // convert it and short-circuit return + if (isScalarType(this.rootObject)) { + writer.write(getScalarAsString(this.rootObject)); + writer.write(";\n"); + return; + } + + buildNode(this.rootVariableName, this.rootObject, ""); + + writer.write("var "); + writer.write(rootVariableName); + writer.write(";\n"); + + for (Map.Entry entry : objectValues.entrySet()) { + writer.append("var "); + writer.append(entry.getKey()); + writer.append(" = "); + writer.append(entry.getValue()); + writer.append(";\n"); + } + + for (Map.Entry entry : assignments.entrySet()) { + writer.append(entry.getKey()); + writer.append(" = "); + writer.append(entry.getValue()); + writer.append(";\n"); + } + + writer.append(rootVariableName).append(";\n"); + } catch (Exception e) { + throw new StripesRuntimeException( + "Could not build JavaScript for object. An " + + "exception was thrown while trying to convert a property from Java to " + + "JavaScript. The object being converted is: " + + this.rootObject, + e); } - - /** - * Returns the name used to declare the root variable to which the built - * JavaScript object is assigned. - */ - public String getRootVariableName() { - return rootVariableName; + } + + /** + * Returns true if the supplied type should be excluded from conversion, otherwise returns false. + * A class should be excluded if it is assignable to one of the types listed for exclusion, or, it + * is an array of such a type. + */ + public boolean isExcludedType(Class type) { + for (Class excludedType : this.excludeClasses) { + if (excludedType.isAssignableFrom(type)) { + return true; + } else if (type.isArray() && excludedType.isAssignableFrom(type.getComponentType())) { + return true; + } } - /** - * Causes the JavaScriptBuilder to navigate the properties of the supplied object and - * convert them to JavaScript. - * - * @return String a fragment of JavaScript that will define and return the JavaScript - * equivalent of the Java object supplied to the builder. - */ - public String build() { - Writer writer = new StringWriter(); - build(writer); - return writer.toString(); + return false; + } + + /** + * Returns true if the object is of a type that can be converted to a simple JavaScript scalar, + * and false otherwise. + */ + public boolean isScalarType(Object in) { + if (in == null) return true; // Though not strictly scalar, null can be treated as such + + Class type = in.getClass(); + return simpleTypes.contains(type) + || Number.class.isAssignableFrom(type) + || String.class.isAssignableFrom(type) + || Boolean.class.isAssignableFrom(type) + || Character.class.isAssignableFrom(type) + || Date.class.isAssignableFrom(type); + } + + /** + * Fetches the value of a scalar type as a String. The input to this method may not be null, and + * must be a of a type that will return true when supplied to isScalarType(). + */ + public String getScalarAsString(Object in) { + if (in == null) return "null"; + + Class type = in.getClass(); + + if (String.class.isAssignableFrom(type)) { + return quote((String) in); + } else if (Character.class.isAssignableFrom(type)) { + return quote(((Character) in).toString()); + } else if (Date.class.isAssignableFrom(type)) { + return "new Date(" + ((Date) in).getTime() + ")"; + } else { + return in.toString(); } - - /** - * Causes the JavaScriptBuilder to navigate the properties of the supplied object and - * convert them to JavaScript, writing them to the supplied writer as it goes. - */ - public void build(Writer writer) { - try { - // If for some reason a caller provided us with a simple scalar object, then - // convert it and short-circuit return - if (isScalarType(this.rootObject)) { - writer.write(getScalarAsString(this.rootObject)); - writer.write(";\n"); - return; - } - - buildNode(this.rootVariableName, this.rootObject, ""); - - writer.write("var "); - writer.write(rootVariableName); - writer.write(";\n"); - - for (Map.Entry entry : objectValues.entrySet()) { - writer.append("var "); - writer.append(entry.getKey()); - writer.append(" = "); - writer.append(entry.getValue()); - writer.append(";\n"); - } - - for (Map.Entry entry : assignments.entrySet()) { - writer.append(entry.getKey()); - writer.append(" = "); - writer.append(entry.getValue()); - writer.append(";\n"); - } - - writer.append(rootVariableName).append(";\n"); - } - catch (Exception e) { - throw new StripesRuntimeException("Could not build JavaScript for object. An " + - "exception was thrown while trying to convert a property from Java to " + - "JavaScript. The object being converted is: " + this.rootObject, e); - } - + } + + /** + * Quotes the supplied String and escapes all characters that could be problematic when eval()'ing + * the String in JavaScript. + * + * @param string a String to be escaped and quoted + * @return the escaped and quoted String + * @since Stripes 1.2 (thanks to Sergey Pariev) + */ + public static String quote(String string) { + if (string == null || string.length() == 0) { + return "\"\""; } - /** - * Returns true if the supplied type should be excluded from conversion, otherwise - * returns false. A class should be excluded if it is assignable to one of the types - * listed for exclusion, or, it is an array of such a type. - */ - public boolean isExcludedType(Class type) { - for (Class excludedType : this.excludeClasses) { - if (excludedType.isAssignableFrom(type)) { - return true; + char c = 0; + int len = string.length(); + StringBuilder sb = new StringBuilder(len + 10); + + sb.append('"'); + for (int i = 0; i < len; ++i) { + c = string.charAt(i); + switch (c) { + case '\\': + case '"': + sb.append('\\').append(c); + break; + case '\b': + sb.append("\\b"); + break; + case '\t': + sb.append("\\t"); + break; + case '\n': + sb.append("\\n"); + break; + case '\f': + sb.append("\\f"); + break; + case '\r': + sb.append("\\r"); + break; + default: + if (c < ' ') { + // The following takes lower order chars and creates unicode style + // char literals for them (e.g. \u00F3) + sb.append("\\u"); + String hex = Integer.toHexString(c); + int pad = 4 - hex.length(); + for (int j = 0; j < pad; ++j) { + sb.append("0"); } - else if (type.isArray() && excludedType.isAssignableFrom(type.getComponentType())) { - return true; - } - } - - return false; - } - - /** - * Returns true if the object is of a type that can be converted to a simple JavaScript - * scalar, and false otherwise. - */ - public boolean isScalarType(Object in) { - if (in == null) return true; // Though not strictly scalar, null can be treated as such - - Class type = in.getClass(); - return simpleTypes.contains(type) - || Number.class.isAssignableFrom(type) - || String.class.isAssignableFrom(type) - || Boolean.class.isAssignableFrom(type) - || Character.class.isAssignableFrom(type) - || Date.class.isAssignableFrom(type); - } - - /** - * Fetches the value of a scalar type as a String. The input to this method may not be null, - * and must be a of a type that will return true when supplied to isScalarType(). - */ - public String getScalarAsString(Object in) { - if (in == null) return "null"; - - Class type = in.getClass(); - - if (String.class.isAssignableFrom(type)) { - return quote((String) in); - } - else if (Character.class.isAssignableFrom(type)) { - return quote(((Character) in).toString()); - } - else if(Date.class.isAssignableFrom(type)) { - return "new Date(" + ((Date) in).getTime() + ")"; - } - else { - return in.toString(); - } + sb.append(hex); + } else { + sb.append(c); + } + } } - /** - * Quotes the supplied String and escapes all characters that could be problematic - * when eval()'ing the String in JavaScript. - * - * @param string a String to be escaped and quoted - * @return the escaped and quoted String - * @since Stripes 1.2 (thanks to Sergey Pariev) - */ - public static String quote(String string) { - if (string == null || string.length() == 0) { - return "\"\""; - } - - char c = 0; - int len = string.length(); - StringBuilder sb = new StringBuilder(len + 10); - - sb.append('"'); - for (int i = 0; i < len; ++i) { - c = string.charAt(i); - switch (c) { - case '\\': - case '"': - sb.append('\\').append(c); - break; - case '\b': - sb.append("\\b"); - break; - case '\t': - sb.append("\\t"); - break; - case '\n': - sb.append("\\n"); - break; - case '\f': - sb.append("\\f"); - break; - case '\r': - sb.append("\\r"); - break; - default: - if (c < ' ') { - // The following takes lower order chars and creates unicode style - // char literals for them (e.g. \u00F3) - sb.append("\\u"); - String hex = Integer.toHexString(c); - int pad = 4 - hex.length(); - for (int j=0; j) in, propertyPrefix); + } else if (in.getClass().isArray()) { + buildArrayNode(targetName, in, propertyPrefix); + } else if (Map.class.isAssignableFrom(in.getClass())) { + buildMapNode(targetName, (Map) in, propertyPrefix); + } else { + buildObjectNode(targetName, in, propertyPrefix); + } + + this.assignments.put(name, targetName); } - - /** - * Determines the type of the object being translated and dispatches to the - * build*Node() method. Generates the temporary name of the object being translated, - * checks to ensure that the object has not already been translated, and ensure that - * the object is correctly inserted into the set of assignments. - * - * @param name The name that should appear on the left hand side of the assignment - * statement once a value for the object has been generated. - * @param in The object being translated. - */ - void buildNode(String name, Object in, String propertyPrefix) throws Exception { - int systemId = System.identityHashCode(in); - String targetName = "_sj_" + systemId; - - if (this.visitedIdentities.contains(systemId)) { - this.assignments.put(name, targetName); - } - else if (isExcludedType(in.getClass())) { - // Do nothing, it's being excluded!! - } - else { - this.visitedIdentities.add(systemId); - - if (Collection.class.isAssignableFrom(in.getClass())) { - buildCollectionNode(targetName, (Collection) in, propertyPrefix); - } - else if (in.getClass().isArray()) { - buildArrayNode(targetName, in, propertyPrefix); - } - else if (Map.class.isAssignableFrom(in.getClass())) { - buildMapNode(targetName, (Map) in, propertyPrefix); - } - else { - buildObjectNode(targetName, in, propertyPrefix); + } + + /** + * Processes a Java Object that conforms to JavaBean conventions. Scalar properties of the object + * are converted to a JSON format object declaration which is inserted into the "objectValues" + * instance level map. Nested non-scalar objects are processed separately and then setup for + * re-attachment using the instance level "assignments" map. + * + *

    In most cases just the JavaBean properties will be translated. In the case of Java 5 enums, + * two additional properties will be translated, one each for the enum's 'ordinal' and 'name' + * properties. + * + * @param targetName The generated name assigned to the Object being translated + * @param in The Object who's JavaBean properties are to be translated + */ + void buildObjectNode(String targetName, Object in, String propertyPrefix) throws Exception { + StringBuilder out = new StringBuilder(); + out.append("{"); + PropertyDescriptor[] props = ReflectUtil.getPropertyDescriptors(in.getClass()); + + for (PropertyDescriptor property : props) { + try { + Method readMethod = property.getReadMethod(); + String fullPropertyName = + (propertyPrefix != null && propertyPrefix.length() > 0 ? propertyPrefix + '.' : "") + + property.getName(); + if ((readMethod != null) && !this.excludeProperties.contains(fullPropertyName)) { + Object value = property.getReadMethod().invoke(in); + + if (isExcludedType(property.getPropertyType())) { + continue; + } + + if (isScalarType(value)) { + if (out.length() > 1) { + out.append(", "); } - - this.assignments.put(name, targetName); + out.append(property.getName()); + out.append(":"); + out.append(getScalarAsString(value)); + } else { + buildNode(targetName + "." + property.getName(), value, fullPropertyName); + } } + } catch (Exception e) { + log.warn( + e, + "Could not translate property [", + property.getName(), + "] of type [", + property.getPropertyType().getName(), + "] due to an exception."); + } } - /** - *

    Processes a Java Object that conforms to JavaBean conventions. Scalar properties of the - * object are converted to a JSON format object declaration which is inserted into the - * "objectValues" instance level map. Nested non-scalar objects are processed separately and - * then setup for re-attachment using the instance level "assignments" map.

    - * - *

    In most cases just the JavaBean properties will be translated. In the case of Java 5 - * enums, two additional properties will be translated, one each for the enum's 'ordinal' - * and 'name' properties.

    - * - * @param targetName The generated name assigned to the Object being translated - * @param in The Object who's JavaBean properties are to be translated - */ - void buildObjectNode(String targetName, Object in, String propertyPrefix) throws Exception { - StringBuilder out = new StringBuilder(); - out.append("{"); - PropertyDescriptor[] props = ReflectUtil.getPropertyDescriptors(in.getClass()); - - for (PropertyDescriptor property : props) { - try { - Method readMethod = property.getReadMethod(); - String fullPropertyName = (propertyPrefix != null && propertyPrefix.length() > 0 ? propertyPrefix + '.' : "") + - property.getName(); - if ((readMethod != null) && !this.excludeProperties.contains(fullPropertyName)) { - Object value = property.getReadMethod().invoke(in); - - if (isExcludedType(property.getPropertyType())) { - continue; - } - - if (isScalarType(value)) { - if (out.length() > 1) { - out.append(", "); - } - out.append(property.getName()); - out.append(":"); - out.append( getScalarAsString(value) ); - } - else { - buildNode(targetName + "." + property.getName(), value, fullPropertyName); - } - } - } - catch (Exception e) { - log.warn(e, "Could not translate property [", property.getName(), "] of type [", - property.getPropertyType().getName(), "] due to an exception."); - } - } - - // Do something a little extra for enums - if (Enum.class.isAssignableFrom(in.getClass())) { - Enum e = (Enum) in; + // Do something a little extra for enums + if (Enum.class.isAssignableFrom(in.getClass())) { + Enum e = (Enum) in; - if (out.length() > 1) { out.append(", "); } - out.append("ordinal:").append( getScalarAsString(e.ordinal()) ); - out.append(", name:").append( getScalarAsString(e.name()) ); - } - - out.append("}"); - this.objectValues.put(targetName, out.toString()); + if (out.length() > 1) { + out.append(", "); + } + out.append("ordinal:").append(getScalarAsString(e.ordinal())); + out.append(", name:").append(getScalarAsString(e.name())); } - /** - * Builds a JavaScript object node from a java Map. The keys of the map are used to - * define the properties of the JavaScript object. As such it is assumed that the keys - * are either primitives, Strings or toString() cleanly. The values of the map are used - * to generate the values of the object properties. Scalar values are inserted directly - * into the JSON representation, while complex types are converted separately and then - * attached using assignments. - * - * @param targetName The generated name assigned to the Map being translated - * @param in The Map being translated - */ - void buildMapNode(String targetName, Map in, String propertyPrefix) throws Exception { - StringBuilder out = new StringBuilder(); - out.append("{"); - - for (Map.Entry entry : in.entrySet()) { - String propertyName = getScalarAsString(entry.getKey()); - Object value = entry.getValue(); - - if (this.excludeProperties.contains(propertyPrefix + '[' + propertyName + ']')) { - // Do nothing, it's being excluded!! - } - else if (isScalarType(value)) { - if (out.length() > 1) { - out.append(", "); - } - out.append(propertyName); - out.append(":"); - out.append( getScalarAsString(value) ); - } - else { - buildNode(targetName + "[" + propertyName + "]", value, propertyPrefix + "[" + propertyName + "]"); - } + out.append("}"); + this.objectValues.put(targetName, out.toString()); + } + + /** + * Builds a JavaScript object node from a java Map. The keys of the map are used to define the + * properties of the JavaScript object. As such it is assumed that the keys are either primitives, + * Strings or toString() cleanly. The values of the map are used to generate the values of the + * object properties. Scalar values are inserted directly into the JSON representation, while + * complex types are converted separately and then attached using assignments. + * + * @param targetName The generated name assigned to the Map being translated + * @param in The Map being translated + */ + void buildMapNode(String targetName, Map in, String propertyPrefix) throws Exception { + StringBuilder out = new StringBuilder(); + out.append("{"); + + for (Map.Entry entry : in.entrySet()) { + String propertyName = getScalarAsString(entry.getKey()); + Object value = entry.getValue(); + + if (this.excludeProperties.contains(propertyPrefix + '[' + propertyName + ']')) { + // Do nothing, it's being excluded!! + } else if (isScalarType(value)) { + if (out.length() > 1) { + out.append(", "); } - - out.append("}"); - this.objectValues.put(targetName, out.toString()); + out.append(propertyName); + out.append(":"); + out.append(getScalarAsString(value)); + } else { + buildNode( + targetName + "[" + propertyName + "]", + value, + propertyPrefix + "[" + propertyName + "]"); + } } - /** - * Builds a JavaScript array node from a Java array. Scalar values are inserted directly - * into the array definition. Complex values are processed separately - they are inserted - * into the JSON array as null to maintain ordering, and re-attached later using assignments. - * - * @param targetName The generated name of the array node being translated. - * @param in The Array being translated. - */ - void buildArrayNode(String targetName, Object in, String propertyPrefix) throws Exception { - StringBuilder out = new StringBuilder(); - out.append("["); - - int length = Array.getLength(in); - for (int i=0; i in, String propertyPrefix) throws Exception { - buildArrayNode(targetName, in.toArray(), propertyPrefix); - } + out.append("]"); + this.objectValues.put(targetName, out.toString()); + } + + /** + * Builds an object node that is of type collection. Simply converts the collection to an array, + * and delegates to buildArrayNode(). + */ + void buildCollectionNode(String targetName, Collection in, String propertyPrefix) + throws Exception { + buildArrayNode(targetName, in.toArray(), propertyPrefix); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/ajax/JavaScriptResolution.java b/stripes/src/main/java/net/sourceforge/stripes/ajax/JavaScriptResolution.java index aeded5223..ade9f409b 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/ajax/JavaScriptResolution.java +++ b/stripes/src/main/java/net/sourceforge/stripes/ajax/JavaScriptResolution.java @@ -14,65 +14,60 @@ */ package net.sourceforge.stripes.ajax; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import net.sourceforge.stripes.action.Resolution; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - /** - *

    Resolution that will convert a Java object web to a web of JavaScript objects and arrays, and - * stream the JavaScript back to the client. The output of this resolution can be evaluated in + * Resolution that will convert a Java object web to a web of JavaScript objects and arrays, and + * stream the JavaScript back to the client. The output of this resolution can be evaluated in * JavaScript using the eval() function, and will return a reference to the top level JavaScript - * object. For more information see {@link JavaScriptBuilder}

    + * object. For more information see {@link JavaScriptBuilder} * * @author Tim Fennell * @since Stripes 1.1 */ public class JavaScriptResolution implements Resolution { - private JavaScriptBuilder builder; + private JavaScriptBuilder builder; - /** - * Constructs a new JavaScriptResolution that will convert the supplied object to JavaScript. - * - * @param rootObject an Object of any type supported by {@link JavaScriptBuilder}. In most cases - * this will either be a JavaBean, Map, Collection or Array, but may also be any one of - * the basic Java types including String, Date, Number etc. - * @param objectsToExclude Classes and/or property names to exclude from the output. - */ - public JavaScriptResolution(Object rootObject, Object... objectsToExclude) { - this.builder = new JavaScriptBuilder(rootObject, objectsToExclude); - } + /** + * Constructs a new JavaScriptResolution that will convert the supplied object to JavaScript. + * + * @param rootObject an Object of any type supported by {@link JavaScriptBuilder}. In most cases + * this will either be a JavaBean, Map, Collection or Array, but may also be any one of the + * basic Java types including String, Date, Number etc. + * @param objectsToExclude Classes and/or property names to exclude from the output. + */ + public JavaScriptResolution(Object rootObject, Object... objectsToExclude) { + this.builder = new JavaScriptBuilder(rootObject, objectsToExclude); + } - /** - * Adds one or more properties to the list of types to exclude when translating - * to JavaScript. - * - * @param property one or more property names to exclude - * @return the JavaScripResolution instance to simplify method chaining - */ - public JavaScriptResolution addPropertyExclusion(final String... property) { - this.builder.addPropertyExclusion(property); - return this; - } + /** + * Adds one or more properties to the list of types to exclude when translating to JavaScript. + * + * @param property one or more property names to exclude + * @return the JavaScripResolution instance to simplify method chaining + */ + public JavaScriptResolution addPropertyExclusion(final String... property) { + this.builder.addPropertyExclusion(property); + return this; + } - /** - * Adds one or more classes to the list of types to exclude when translating - * to JavaScript. - * - * @param clazz one or more classes to exclude - * @return the JavaScripResolution instance to simplify method chaining - */ - public JavaScriptResolution addClassExclusion(final Class... clazz) { - this.builder.addClassExclusion(clazz); - return this; - } + /** + * Adds one or more classes to the list of types to exclude when translating to JavaScript. + * + * @param clazz one or more classes to exclude + * @return the JavaScripResolution instance to simplify method chaining + */ + public JavaScriptResolution addClassExclusion(final Class... clazz) { + this.builder.addClassExclusion(clazz); + return this; + } - /** - * Converts the object passed in to JavaScript and streams it back to the client. - */ - public void execute(HttpServletRequest request, HttpServletResponse response) throws Exception { - response.setContentType("text/javascript"); - this.builder.build(response.getWriter()); - response.flushBuffer(); - } + /** Converts the object passed in to JavaScript and streams it back to the client. */ + public void execute(HttpServletRequest request, HttpServletResponse response) throws Exception { + response.setContentType("text/javascript"); + this.builder.build(response.getWriter()); + response.flushBuffer(); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/config/BootstrapPropertyResolver.java b/stripes/src/main/java/net/sourceforge/stripes/config/BootstrapPropertyResolver.java index dbd1b0639..2cd09fe22 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/config/BootstrapPropertyResolver.java +++ b/stripes/src/main/java/net/sourceforge/stripes/config/BootstrapPropertyResolver.java @@ -14,6 +14,7 @@ */ package net.sourceforge.stripes.config; +import jakarta.servlet.FilterConfig; import java.lang.reflect.Modifier; import java.security.AccessControlException; import java.util.ArrayList; @@ -21,8 +22,6 @@ import java.util.Iterator; import java.util.List; import java.util.Set; -import javax.servlet.FilterConfig; - import net.sourceforge.stripes.exception.StripesRuntimeException; import net.sourceforge.stripes.util.Log; import net.sourceforge.stripes.util.ReflectUtil; @@ -31,249 +30,257 @@ import net.sourceforge.stripes.vfs.VFS; /** - *

    Resolves configuration properties that are used to bootstrap the system. Essentially this boils - * down to a handful of properties that are needed to figure out which configuration class should - * be instantiated, and any values needed by that configuration class to locate configuration - * information.

    + * Resolves configuration properties that are used to bootstrap the system. Essentially this boils + * down to a handful of properties that are needed to figure out which configuration class should be + * instantiated, and any values needed by that configuration class to locate configuration + * information. * *

    Properties are looked for in the following order: - *

      - *
    • Initialization Parameters for the Dispatcher servlet
    • - *
    • Initialization Parameters for the Servlet Context
    • - *
    • Java System Properties
    • - *
    - *

    + * + *
      + *
    • Initialization Parameters for the Dispatcher servlet + *
    • Initialization Parameters for the Servlet Context + *
    • Java System Properties + *
    * * @author Tim Fennell */ public class BootstrapPropertyResolver { - private static final Log log = Log.getInstance(BootstrapPropertyResolver.class); - - private FilterConfig filterConfig; + private static final Log log = Log.getInstance(BootstrapPropertyResolver.class); - /** The Configuration Key for looking up the comma separated list of VFS classes. */ - public static final String VFS_CLASSES = "VFS.Classes"; + private FilterConfig filterConfig; - /** The Configuration Key for looking up the comma separated list of extension packages. */ - public static final String PACKAGES = "Extension.Packages"; + /** The Configuration Key for looking up the comma separated list of VFS classes. */ + public static final String VFS_CLASSES = "VFS.Classes"; - /** Constructs a new BootstrapPropertyResolver with the given ServletConfig. */ - public BootstrapPropertyResolver(FilterConfig filterConfig) { - setFilterConfig(filterConfig); - initVFS(); - } + /** The Configuration Key for looking up the comma separated list of extension packages. */ + public static final String PACKAGES = "Extension.Packages"; + + /** Constructs a new BootstrapPropertyResolver with the given ServletConfig. */ + public BootstrapPropertyResolver(FilterConfig filterConfig) { + setFilterConfig(filterConfig); + initVFS(); + } - /** Stores a reference to the filter's FilterConfig object. */ - public void setFilterConfig(FilterConfig filterConfig) { - this.filterConfig = filterConfig; + /** Stores a reference to the filter's FilterConfig object. */ + public void setFilterConfig(FilterConfig filterConfig) { + this.filterConfig = filterConfig; + } + + /** Returns a reference to the StripesFilter's FilterConfig object. */ + public FilterConfig getFilterConfig() { + return this.filterConfig; + } + + /** Add {@link VFS} implementations that are specified in the filter configuration. */ + @SuppressWarnings("unchecked") + protected void initVFS() { + List> vfsImpls = getClassPropertyList(VFS_CLASSES); + for (Class clazz : vfsImpls) { + if (!VFS.class.isAssignableFrom(clazz)) + log.warn("Class ", clazz.getName(), " does not extend ", VFS.class.getName()); + else VFS.addImplClass((Class) clazz); } + } + + /** + * Fetches a configuration property in the manner described in the class level javadoc for this + * class. + * + * @param key the String name of the configuration value to be looked up + * @return String the value of the configuration item or null + */ + public String getProperty(String key) { + String value = null; - /** Returns a reference to the StripesFilter's FilterConfig object. */ - public FilterConfig getFilterConfig() { - return this.filterConfig; + try { + value = this.filterConfig.getInitParameter(key); + } catch (AccessControlException e) { + log.debug( + "Security manager prevented " + + getClass().getName() + + " from reading filter init-param" + + key); } - /** Add {@link VFS} implementations that are specified in the filter configuration. */ - @SuppressWarnings("unchecked") - protected void initVFS() { - List> vfsImpls = getClassPropertyList(VFS_CLASSES); - for (Class clazz : vfsImpls) { - if (!VFS.class.isAssignableFrom(clazz)) - log.warn("Class ", clazz.getName(), " does not extend ", VFS.class.getName()); - else - VFS.addImplClass((Class) clazz); - } + if (value == null) { + try { + value = this.filterConfig.getServletContext().getInitParameter(key); + } catch (AccessControlException e) { + log.debug( + "Security manager prevented " + + getClass().getName() + + " from reading servlet context init-param" + + key); + } } - /** - * Fetches a configuration property in the manner described in the class level javadoc for - * this class. - * - * @param key the String name of the configuration value to be looked up - * @return String the value of the configuration item or null - */ - public String getProperty(String key) { - String value = null; + if (value == null) { + try { + value = System.getProperty(key); + } catch (AccessControlException e) { + log.debug( + "Security manager prevented " + + getClass().getName() + + " from reading system property " + + key); + } + } - try { - value = this.filterConfig.getInitParameter(key); - } - catch (AccessControlException e) { - log.debug("Security manager prevented " + getClass().getName() - + " from reading filter init-param" + key); - } + return value; + } - if (value == null) { - try { - value = this.filterConfig.getServletContext().getInitParameter(key); - } - catch (AccessControlException e) { - log.debug("Security manager prevented " + getClass().getName() - + " from reading servlet context init-param" + key); - } - } + /** + * Attempts to find a class the user has specified in web.xml or by auto-discovery in packages + * listed in web.xml under Extension.Packages. Classes specified in web.xml take precedence. + * + * @param paramName the parameter to look for in web.xml + * @param targetType the type that we're looking for + * @return the Class that was found + */ + @SuppressWarnings("unchecked") + public Class getClassProperty(String paramName, Class targetType) { + Class clazz = null; - if (value == null) { - try { - value = System.getProperty(key); - } - catch (AccessControlException e) { - log.debug("Security manager prevented " + getClass().getName() - + " from reading system property " + key); - } - } + String className = getProperty(paramName); - return value; + if (className != null) { + // web.xml takes precedence + try { + clazz = ReflectUtil.findClass(className); + log.info( + "Class implementing/extending ", + targetType.getSimpleName(), + " found in web.xml: ", + className); + } catch (ClassNotFoundException e) { + log.error( + "Couldn't find class specified in web.xml under param ", paramName, ": ", className); + } + } else { + // we didn't find it in web.xml so now we check any extension packages + ResolverUtil resolver = new ResolverUtil(); + String[] packages = StringUtil.standardSplit(getProperty(PACKAGES)); + resolver.findImplementations(targetType, packages); + Set> classes = resolver.getClasses(); + removeDontAutoloadClasses(classes); + removeAbstractClasses(classes); + if (classes.size() == 1) { + clazz = classes.iterator().next(); + className = clazz.getName(); + log.info( + "Class implementing/extending ", + targetType.getSimpleName(), + " found via auto-discovery: ", + className); + } else if (classes.size() > 1) { + throw new StripesRuntimeException( + StringUtil.combineParts( + "Found too many classes implementing/extending ", + targetType.getSimpleName(), + ": ", + classes)); + } } - - /** - * Attempts to find a class the user has specified in web.xml or by auto-discovery in packages - * listed in web.xml under Extension.Packages. Classes specified in web.xml take precedence. - * - * @param paramName the parameter to look for in web.xml - * @param targetType the type that we're looking for - * @return the Class that was found - */ - @SuppressWarnings("unchecked") - public Class getClassProperty(String paramName, Class targetType) - { - Class clazz = null; - String className = getProperty(paramName); + return clazz; + } - if (className != null) { - // web.xml takes precedence - try { - clazz = ReflectUtil.findClass(className); - log.info("Class implementing/extending ", targetType.getSimpleName(), - " found in web.xml: ", className); - } - catch (ClassNotFoundException e) { - log.error("Couldn't find class specified in web.xml under param ", paramName, ": ", - className); - } - } - else { - // we didn't find it in web.xml so now we check any extension packages - ResolverUtil resolver = new ResolverUtil(); - String[] packages = StringUtil.standardSplit(getProperty(PACKAGES)); - resolver.findImplementations(targetType, packages); - Set> classes = resolver.getClasses(); - removeDontAutoloadClasses(classes); - removeAbstractClasses(classes); - if (classes.size() == 1) { - clazz = classes.iterator().next(); - className = clazz.getName(); - log.info("Class implementing/extending ", targetType.getSimpleName(), - " found via auto-discovery: ", className); - } - else if (classes.size() > 1) { - throw new StripesRuntimeException(StringUtil.combineParts( - "Found too many classes implementing/extending ", targetType - .getSimpleName(), ": ", classes)); - } - } - - return clazz; - } - - /** - * Attempts to find all classes the user has specified in web.xml. - * - * @param paramName the parameter to look for in web.xml - * @return a List of classes found - */ - public List> getClassPropertyList(String paramName) - { - List> classes = new ArrayList>(); + /** + * Attempts to find all classes the user has specified in web.xml. + * + * @param paramName the parameter to look for in web.xml + * @return a List of classes found + */ + public List> getClassPropertyList(String paramName) { + List> classes = new ArrayList>(); - String classList = getProperty(paramName); + String classList = getProperty(paramName); - if (classList != null) { - String[] classNames = StringUtil.standardSplit(classList); - for (String className : classNames) { - className = className.trim(); - try { - classes.add(ReflectUtil.findClass(className)); - } - catch (ClassNotFoundException e) { - throw new StripesRuntimeException("Could not find class [" + className - + "] specified by the configuration parameter [" + paramName - + "]. This value must contain fully qualified class names separated " - + " by commas."); - } - } + if (classList != null) { + String[] classNames = StringUtil.standardSplit(classList); + for (String className : classNames) { + className = className.trim(); + try { + classes.add(ReflectUtil.findClass(className)); + } catch (ClassNotFoundException e) { + throw new StripesRuntimeException( + "Could not find class [" + + className + + "] specified by the configuration parameter [" + + paramName + + "]. This value must contain fully qualified class names separated " + + " by commas."); } - - return classes; - } - - /** - * Attempts to find classes by auto-discovery in packages listed in web.xml under - * Extension.Packages. - * - * @param targetType the type that we're looking for - * @return a List of classes found - */ - public List> getClassPropertyList(Class targetType) - { - ResolverUtil resolver = new ResolverUtil(); - String[] packages = StringUtil.standardSplit(getProperty(PACKAGES)); - resolver.findImplementations(targetType, packages); - Set> classes = resolver.getClasses(); - removeDontAutoloadClasses(classes); - removeAbstractClasses(classes); - return new ArrayList>(classes); + } } - /** - * Attempts to find all matching classes the user has specified in web.xml or by auto-discovery - * in packages listed in web.xml under Extension.Packages. - * - * @param paramName the parameter to look for in web.xml - * @param targetType the type that we're looking for - * @return the Class that was found - */ - @SuppressWarnings("unchecked") - public List> getClassPropertyList(String paramName, Class targetType) - { - List> classes = new ArrayList>(); + return classes; + } - for (Class clazz : getClassPropertyList(paramName)) { - // can't use addAll :( - classes.add((Class) clazz); - } + /** + * Attempts to find classes by auto-discovery in packages listed in web.xml under + * Extension.Packages. + * + * @param targetType the type that we're looking for + * @return a List of classes found + */ + public List> getClassPropertyList(Class targetType) { + ResolverUtil resolver = new ResolverUtil(); + String[] packages = StringUtil.standardSplit(getProperty(PACKAGES)); + resolver.findImplementations(targetType, packages); + Set> classes = resolver.getClasses(); + removeDontAutoloadClasses(classes); + removeAbstractClasses(classes); + return new ArrayList>(classes); + } - classes.addAll(getClassPropertyList(targetType)); + /** + * Attempts to find all matching classes the user has specified in web.xml or by auto-discovery in + * packages listed in web.xml under Extension.Packages. + * + * @param paramName the parameter to look for in web.xml + * @param targetType the type that we're looking for + * @return the Class that was found + */ + @SuppressWarnings("unchecked") + public List> getClassPropertyList(String paramName, Class targetType) { + List> classes = new ArrayList>(); - return classes; + for (Class clazz : getClassPropertyList(paramName)) { + // can't use addAll :( + classes.add((Class) clazz); } - /** Removes any classes from the collection that are marked with {@link DontAutoLoad}. */ - protected void removeDontAutoloadClasses(Collection> classes) { - Iterator> iterator = classes.iterator(); - while (iterator.hasNext()) { - Class clazz = iterator.next(); - if (clazz.isAnnotationPresent(DontAutoLoad.class)) { - log.debug("Ignoring ", clazz, " because @DontAutoLoad is present."); - iterator.remove(); - } - } + classes.addAll(getClassPropertyList(targetType)); + + return classes; + } + + /** Removes any classes from the collection that are marked with {@link DontAutoLoad}. */ + protected void removeDontAutoloadClasses(Collection> classes) { + Iterator> iterator = classes.iterator(); + while (iterator.hasNext()) { + Class clazz = iterator.next(); + if (clazz.isAnnotationPresent(DontAutoLoad.class)) { + log.debug("Ignoring ", clazz, " because @DontAutoLoad is present."); + iterator.remove(); + } } - - /** Removes any classes from the collection that are abstract or interfaces. */ - protected void removeAbstractClasses(Collection> classes) { - Iterator> iterator = classes.iterator(); - while (iterator.hasNext()) { - Class clazz = iterator.next(); - if (clazz.isInterface()) { - log.trace("Ignoring ", clazz, " because it is an interface."); - iterator.remove(); - } - else if ((clazz.getModifiers() & Modifier.ABSTRACT) == Modifier.ABSTRACT) { - log.trace("Ignoring ", clazz, " because it is abstract."); - iterator.remove(); - } - } + } + + /** Removes any classes from the collection that are abstract or interfaces. */ + protected void removeAbstractClasses(Collection> classes) { + Iterator> iterator = classes.iterator(); + while (iterator.hasNext()) { + Class clazz = iterator.next(); + if (clazz.isInterface()) { + log.trace("Ignoring ", clazz, " because it is an interface."); + iterator.remove(); + } else if ((clazz.getModifiers() & Modifier.ABSTRACT) == Modifier.ABSTRACT) { + log.trace("Ignoring ", clazz, " because it is abstract."); + iterator.remove(); + } } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/config/ConfigurableComponent.java b/stripes/src/main/java/net/sourceforge/stripes/config/ConfigurableComponent.java index c5edafd27..dffcb387e 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/config/ConfigurableComponent.java +++ b/stripes/src/main/java/net/sourceforge/stripes/config/ConfigurableComponent.java @@ -15,22 +15,22 @@ package net.sourceforge.stripes.config; /** - * Interface which is extended by all the major configurable chunks of Stripes. Allows a + * Interface which is extended by all the major configurable chunks of Stripes. Allows a * Configuration to instantiate and pass configuration to each of the main components in a - * standardized manner. It is expected that all ConfigurableComponents will have a public - * no-arg constructor. + * standardized manner. It is expected that all ConfigurableComponents will have a public no-arg + * constructor. * * @author Tim Fennell */ public interface ConfigurableComponent { - /** - * Invoked directly after instantiation to allow the configured component to perform - * one time initialization. Components are expected to fail loudly if they are not - * going to be in a valid state after initialization. - * - * @param configuration the Configuration object being used by Stripes - * @throws Exception should be thrown if the component cannot be configured well enough to use. - */ - void init(Configuration configuration) throws Exception; + /** + * Invoked directly after instantiation to allow the configured component to perform one time + * initialization. Components are expected to fail loudly if they are not going to be in a valid + * state after initialization. + * + * @param configuration the Configuration object being used by Stripes + * @throws Exception should be thrown if the component cannot be configured well enough to use. + */ + void init(Configuration configuration) throws Exception; } diff --git a/stripes/src/main/java/net/sourceforge/stripes/config/Configuration.java b/stripes/src/main/java/net/sourceforge/stripes/config/Configuration.java index 9bfb24fb0..163c396e8 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/config/Configuration.java +++ b/stripes/src/main/java/net/sourceforge/stripes/config/Configuration.java @@ -14,200 +14,197 @@ */ package net.sourceforge.stripes.config; +import jakarta.servlet.ServletContext; +import java.util.Collection; +import net.sourceforge.stripes.controller.ActionBeanContextFactory; import net.sourceforge.stripes.controller.ActionBeanPropertyBinder; import net.sourceforge.stripes.controller.ActionResolver; -import net.sourceforge.stripes.controller.ActionBeanContextFactory; -import net.sourceforge.stripes.controller.ObjectFactory; -import net.sourceforge.stripes.localization.LocalizationBundleFactory; -import net.sourceforge.stripes.localization.LocalePicker; -import net.sourceforge.stripes.validation.TypeConverterFactory; -import net.sourceforge.stripes.validation.ValidationMetadataProvider; -import net.sourceforge.stripes.tag.TagErrorRendererFactory; -import net.sourceforge.stripes.tag.PopulationStrategy; -import net.sourceforge.stripes.format.FormatterFactory; import net.sourceforge.stripes.controller.Interceptor; import net.sourceforge.stripes.controller.LifecycleStage; +import net.sourceforge.stripes.controller.ObjectFactory; import net.sourceforge.stripes.controller.multipart.MultipartWrapperFactory; import net.sourceforge.stripes.exception.ExceptionHandler; - -import javax.servlet.ServletContext; -import java.util.Collection; +import net.sourceforge.stripes.format.FormatterFactory; +import net.sourceforge.stripes.localization.LocalePicker; +import net.sourceforge.stripes.localization.LocalizationBundleFactory; +import net.sourceforge.stripes.tag.PopulationStrategy; +import net.sourceforge.stripes.tag.TagErrorRendererFactory; +import net.sourceforge.stripes.validation.TypeConverterFactory; +import net.sourceforge.stripes.validation.ValidationMetadataProvider; /** - *

    Type safe interface for accessing configuration information used to configure Stripes. All - * Configuration implementations are handed a reference to the BootstrapPropertyResolver to - * enable them to find initial values and fully initialize themselves. Through the + * Type safe interface for accessing configuration information used to configure Stripes. All + * Configuration implementations are handed a reference to the BootstrapPropertyResolver to enable + * them to find initial values and fully initialize themselves. Through the * BootstrapPropertyResolver implementations also get access to the ServletConfig of the - * DispatcherServlet which can be used for locating configuration values if desired.

    + * DispatcherServlet which can be used for locating configuration values if desired. * - *

    Implementations of Configuration should fail fast. At initialization time they should - * detect as many failures as possible and raise an exception. Since exceptions in Configuration - * are considered fatal there are no exception specifications and implementations are expected to - * throw runtime exceptions with plenty of details about the failure and its suspected cause(s).

    + *

    Implementations of Configuration should fail fast. At initialization time they should detect + * as many failures as possible and raise an exception. Since exceptions in Configuration are + * considered fatal there are no exception specifications and implementations are expected to throw + * runtime exceptions with plenty of details about the failure and its suspected cause(s). * * @author Tim Fennell */ public interface Configuration { - /** - * Supplies the Configuration with a BootstrapPropertyResolver. This method is guaranteed to - * be invoked prior to the init method. - * - * @param resolver a BootStrapPropertyResolver which can be used to find any values required - * by the Configuration in order to initialize - */ - void setBootstrapPropertyResolver(BootstrapPropertyResolver resolver); - - /** - * Called by the DispatcherServlet to initialize the Configuration. Any operations which may - * fail and cause the Configuration to be inaccessible should be performed here (e.g. - * opening a configuration file and reading the contents). - */ - void init(); - - /** - * Implementations should implement this method to simply return a reference to the - * BootstrapPropertyResolver passed to the Configuration at initialization time. - * - * @return BootstrapPropertyResolver the instance passed to the init() method - */ - BootstrapPropertyResolver getBootstrapPropertyResolver(); - - /** - * Retrieves the ServletContext for the context within which the Stripes application - * is executing. - * - * @return the ServletContext in which the application is running - */ - ServletContext getServletContext(); - - /** Enable or disable debug mode. */ - void setDebugMode(boolean debugMode); - - /** Returns true if the Stripes application is running in debug mode. */ - boolean isDebugMode(); - - /** - * Returns an instance of {@link ObjectFactory} that is used throughout Stripes to instantiate - * classes. - * - * @return an instance of {@link ObjectFactory}. - */ - ObjectFactory getObjectFactory(); - - /** - * Returns an instance of ActionResolver that will be used by Stripes to lookup and resolve - * ActionBeans. The instance should be cached by the Configuration since multiple entities - * in the system may access the ActionResolver throughout the lifetime of the application. - * - * @return the Class representing the configured ActionResolver - */ - ActionResolver getActionResolver(); - - /** - * Returns an instance of ActionBeanPropertyBinder that is responsible for binding all - * properties to all ActionBeans at runtime. The instance should be cached by the Configuration - * since multiple entities in the system may access the ActionBeanPropertyBinder throughout the - * lifetime of the application. - * - * @return ActionBeanPropertyBinder the property binder to be used by Stripes - */ - ActionBeanPropertyBinder getActionBeanPropertyBinder(); - - /** - * Returns an instance of TypeConverterFactory that is responsible for providing lookups and - * instances of TypeConverters for the validation system. The instance should be cached by the - * Configuration since multiple entities in the system may access the TypeConverterFactory - * throughout the lifetime of the application. - * - * @return TypeConverterFactory an instance of a TypeConverterFactory implementation - */ - TypeConverterFactory getTypeConverterFactory(); - - /** - * Returns an instance of LocalizationBundleFactory that is responsible for looking up - * resource bundles for the varying localization needs of a web application. The instance should - * be cached by the Configuration since multiple entities in the system may access the - * LocalizationBundleFactory throughout the lifetime of the application. - * - * @return LocalizationBundleFactory an instance of a LocalizationBundleFactory implementation - */ - LocalizationBundleFactory getLocalizationBundleFactory(); - - /** - * Returns an instance of LocalePicker that is responsible for choosing the Locale for - * each request that enters the system. - * - * @return LocalePicker an instance of a LocalePicker implementation - */ - LocalePicker getLocalePicker(); - - /** - * Returns an instance of FormatterFactory that is responsible for creating Formatter objects - * for converting rich types into Strings for display on pages. - * - * @return LocalePicker an instance of a LocalePicker implementation - */ - FormatterFactory getFormatterFactory(); - - /** - * Returns an instance of a tag error renderer factory for building custom error renderers - * for form input tags that have field errors. - * - * @return TagErrorRendererFactory an instance of TagErrorRendererFactory - */ - TagErrorRendererFactory getTagErrorRendererFactory(); - - /** - * Returns an instance of a PopulationStrategy that determines from where a tag's value - * should be repopulated. - * - * @return PopulationStrategy an instance of PopulationStrategy - */ - PopulationStrategy getPopulationStrategy(); - - /** - * Returns an instance of an action bean context factory which will used throughout Stripes - * to manufacture ActionBeanContext objects. This allows projects to extend ActionBeanContext - * and provide additional type safe methods for accessing contextual information cleanly. - * - * @return ActionBeanContextFactory an instance of ActionBeanContextFactory - */ - ActionBeanContextFactory getActionBeanContextFactory(); - - /** - * Fetches the interceptors that should be executed around the lifecycle stage applied. - * Must return a non-null collection, but the collection may be empty. The Interceptors - * are invoked around the code which executes the given lifecycle function (e.g. - * ActionBeanResolution), and as a result can execute code both before and after it. - * - * @return Collection an ordered collection of interceptors to be executed - * around the given lifecycle stage. - */ - Collection getInterceptors(LifecycleStage stage); - - /** - * Returns an instance of ExceptionHandler that can be used by Stripes to handle any - * exceptions that arise as the result of processing a request. - * - * @return ExceptionHandler an instance of ExceptionHandler - */ - ExceptionHandler getExceptionHandler(); - - /** - * Returns an instance of MultipartWrapperFactory that can be used by Stripes to construct - * MultipartWrapper instances for dealing with multipart requests (those containing file - * uploads). - * - * @return MultipartWrapperFactory an instance of the wrapper factory - */ - MultipartWrapperFactory getMultipartWrapperFactory(); - - /** - * Returns an instance of {@link ValidationMetadataProvider} that can be used by Stripes to - * determine what validations need to be applied during - * {@link LifecycleStage#BindingAndValidation}. - * - * @return an instance of {@link ValidationMetadataProvider} - */ - ValidationMetadataProvider getValidationMetadataProvider(); + /** + * Supplies the Configuration with a BootstrapPropertyResolver. This method is guaranteed to be + * invoked prior to the init method. + * + * @param resolver a BootStrapPropertyResolver which can be used to find any values required by + * the Configuration in order to initialize + */ + void setBootstrapPropertyResolver(BootstrapPropertyResolver resolver); + + /** + * Called by the DispatcherServlet to initialize the Configuration. Any operations which may fail + * and cause the Configuration to be inaccessible should be performed here (e.g. opening a + * configuration file and reading the contents). + */ + void init(); + + /** + * Implementations should implement this method to simply return a reference to the + * BootstrapPropertyResolver passed to the Configuration at initialization time. + * + * @return BootstrapPropertyResolver the instance passed to the init() method + */ + BootstrapPropertyResolver getBootstrapPropertyResolver(); + + /** + * Retrieves the ServletContext for the context within which the Stripes application is executing. + * + * @return the ServletContext in which the application is running + */ + ServletContext getServletContext(); + + /** Enable or disable debug mode. */ + void setDebugMode(boolean debugMode); + + /** Returns true if the Stripes application is running in debug mode. */ + boolean isDebugMode(); + + /** + * Returns an instance of {@link ObjectFactory} that is used throughout Stripes to instantiate + * classes. + * + * @return an instance of {@link ObjectFactory}. + */ + ObjectFactory getObjectFactory(); + + /** + * Returns an instance of ActionResolver that will be used by Stripes to lookup and resolve + * ActionBeans. The instance should be cached by the Configuration since multiple entities in the + * system may access the ActionResolver throughout the lifetime of the application. + * + * @return the Class representing the configured ActionResolver + */ + ActionResolver getActionResolver(); + + /** + * Returns an instance of ActionBeanPropertyBinder that is responsible for binding all properties + * to all ActionBeans at runtime. The instance should be cached by the Configuration since + * multiple entities in the system may access the ActionBeanPropertyBinder throughout the lifetime + * of the application. + * + * @return ActionBeanPropertyBinder the property binder to be used by Stripes + */ + ActionBeanPropertyBinder getActionBeanPropertyBinder(); + + /** + * Returns an instance of TypeConverterFactory that is responsible for providing lookups and + * instances of TypeConverters for the validation system. The instance should be cached by the + * Configuration since multiple entities in the system may access the TypeConverterFactory + * throughout the lifetime of the application. + * + * @return TypeConverterFactory an instance of a TypeConverterFactory implementation + */ + TypeConverterFactory getTypeConverterFactory(); + + /** + * Returns an instance of LocalizationBundleFactory that is responsible for looking up resource + * bundles for the varying localization needs of a web application. The instance should be cached + * by the Configuration since multiple entities in the system may access the + * LocalizationBundleFactory throughout the lifetime of the application. + * + * @return LocalizationBundleFactory an instance of a LocalizationBundleFactory implementation + */ + LocalizationBundleFactory getLocalizationBundleFactory(); + + /** + * Returns an instance of LocalePicker that is responsible for choosing the Locale for each + * request that enters the system. + * + * @return LocalePicker an instance of a LocalePicker implementation + */ + LocalePicker getLocalePicker(); + + /** + * Returns an instance of FormatterFactory that is responsible for creating Formatter objects for + * converting rich types into Strings for display on pages. + * + * @return LocalePicker an instance of a LocalePicker implementation + */ + FormatterFactory getFormatterFactory(); + + /** + * Returns an instance of a tag error renderer factory for building custom error renderers for + * form input tags that have field errors. + * + * @return TagErrorRendererFactory an instance of TagErrorRendererFactory + */ + TagErrorRendererFactory getTagErrorRendererFactory(); + + /** + * Returns an instance of a PopulationStrategy that determines from where a tag's value should be + * repopulated. + * + * @return PopulationStrategy an instance of PopulationStrategy + */ + PopulationStrategy getPopulationStrategy(); + + /** + * Returns an instance of an action bean context factory which will used throughout Stripes to + * manufacture ActionBeanContext objects. This allows projects to extend ActionBeanContext and + * provide additional type safe methods for accessing contextual information cleanly. + * + * @return ActionBeanContextFactory an instance of ActionBeanContextFactory + */ + ActionBeanContextFactory getActionBeanContextFactory(); + + /** + * Fetches the interceptors that should be executed around the lifecycle stage applied. Must + * return a non-null collection, but the collection may be empty. The Interceptors are invoked + * around the code which executes the given lifecycle function (e.g. ActionBeanResolution), and as + * a result can execute code both before and after it. + * + * @return Collection an ordered collection of interceptors to be executed around the + * given lifecycle stage. + */ + Collection getInterceptors(LifecycleStage stage); + + /** + * Returns an instance of ExceptionHandler that can be used by Stripes to handle any exceptions + * that arise as the result of processing a request. + * + * @return ExceptionHandler an instance of ExceptionHandler + */ + ExceptionHandler getExceptionHandler(); + + /** + * Returns an instance of MultipartWrapperFactory that can be used by Stripes to construct + * MultipartWrapper instances for dealing with multipart requests (those containing file uploads). + * + * @return MultipartWrapperFactory an instance of the wrapper factory + */ + MultipartWrapperFactory getMultipartWrapperFactory(); + + /** + * Returns an instance of {@link ValidationMetadataProvider} that can be used by Stripes to + * determine what validations need to be applied during {@link + * LifecycleStage#BindingAndValidation}. + * + * @return an instance of {@link ValidationMetadataProvider} + */ + ValidationMetadataProvider getValidationMetadataProvider(); } diff --git a/stripes/src/main/java/net/sourceforge/stripes/config/DefaultConfiguration.java b/stripes/src/main/java/net/sourceforge/stripes/config/DefaultConfiguration.java index 0a9cf4996..ee6cd3c90 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/config/DefaultConfiguration.java +++ b/stripes/src/main/java/net/sourceforge/stripes/config/DefaultConfiguration.java @@ -14,6 +14,7 @@ */ package net.sourceforge.stripes.config; +import jakarta.servlet.ServletContext; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -23,9 +24,6 @@ import java.util.List; import java.util.Map; import java.util.Set; - -import javax.servlet.ServletContext; - import net.sourceforge.stripes.controller.ActionBeanContextFactory; import net.sourceforge.stripes.controller.ActionBeanPropertyBinder; import net.sourceforge.stripes.controller.ActionResolver; @@ -62,454 +60,492 @@ import net.sourceforge.stripes.validation.ValidationMetadataProvider; /** - *

    Centralized location for defaults for all Configuration properties. This implementation does - * not lookup configuration information anywhere! It returns hard-coded defaults that will result - * in a working system without any user intervention.

    + * Centralized location for defaults for all Configuration properties. This implementation does not + * lookup configuration information anywhere! It returns hard-coded defaults that will result in a + * working system without any user intervention. * *

    Despite it's name the DefaultConfiguration is not in fact the default Configuration - * implementation in Stripes! Instead it is the retainer of default configuration values. The - * Configuration implementation that is used when no alternative is configured is the - * {@link RuntimeConfiguration}, which is a direct subclass of DefaultConfiguration, and when no - * further configuration properties are supplied behaves identically to the DefaultConfiguration.

    + * implementation in Stripes! Instead it is the retainer of default configuration values. The + * Configuration implementation that is used when no alternative is configured is the {@link + * RuntimeConfiguration}, which is a direct subclass of DefaultConfiguration, and when no further + * configuration properties are supplied behaves identically to the DefaultConfiguration. * - *

    The DefaultConfiguration is designed to be easily extended as needed. The init() method + *

    The DefaultConfiguration is designed to be easily extended as needed. The init() method * ensures that components are initialized in the correct order (taking dependencies into account), * and should generally not be overridden. It invokes a number of initXXX() methods, one per * configurable component. Subclasses should override any of the initXXX() methods desirable to * return a fully initialized instance of the relevant component type, or null if the default is - * desired.

    + * desired. * * @author Tim Fennell */ public class DefaultConfiguration implements Configuration { - /** Log implementation for use within this class. */ - private static final Log log = Log.getInstance(DefaultConfiguration.class); - - private boolean debugMode; - private BootstrapPropertyResolver resolver; - private ObjectFactory objectFactory; - private ActionResolver actionResolver; - private ActionBeanPropertyBinder actionBeanPropertyBinder; - private ActionBeanContextFactory actionBeanContextFactory; - private TypeConverterFactory typeConverterFactory; - private LocalizationBundleFactory localizationBundleFactory; - private LocalePicker localePicker; - private FormatterFactory formatterFactory; - private TagErrorRendererFactory tagErrorRendererFactory; - private PopulationStrategy populationStrategy; - private Map> interceptors; - private ExceptionHandler exceptionHandler; - private MultipartWrapperFactory multipartWrapperFactory; - private ValidationMetadataProvider validationMetadataProvider; - - /** Gratefully accepts the BootstrapPropertyResolver handed to the Configuration. */ - public void setBootstrapPropertyResolver(BootstrapPropertyResolver resolver) { - this.resolver = resolver; - } - - /** - * Creates and stores instances of the objects of the type that the Configuration is - * responsible for providing, except where subclasses have already provided instances. - */ - @SuppressWarnings("unchecked") - public void init() { - try { - Boolean debugMode = initDebugMode(); - if (debugMode != null) { - this.debugMode = debugMode; - } - else { - this.debugMode = false; - } - - this.objectFactory = initObjectFactory(); - if (this.objectFactory == null) { - this.objectFactory = new DefaultObjectFactory(); - this.objectFactory.init(this); - } - if (this.objectFactory instanceof DefaultObjectFactory) { - List> classes = getBootstrapPropertyResolver() - .getClassPropertyList(ObjectPostProcessor.class); - List instances = new ArrayList(); - for (Class clazz : classes) { - log.debug("Instantiating object post-processor ", clazz); - instances.add(this.objectFactory.newInstance(clazz)); - } - for (ObjectPostProcessor pp : instances) { - ((DefaultObjectFactory) this.objectFactory).addPostProcessor(pp); - } - } - - this.actionResolver = initActionResolver(); - if (this.actionResolver == null) { - this.actionResolver = new NameBasedActionResolver(); - this.actionResolver.init(this); - } - - this.actionBeanPropertyBinder = initActionBeanPropertyBinder(); - if (this.actionBeanPropertyBinder == null) { - this.actionBeanPropertyBinder = new DefaultActionBeanPropertyBinder(); - this.actionBeanPropertyBinder.init(this); - } - - this.actionBeanContextFactory = initActionBeanContextFactory(); - if (this.actionBeanContextFactory == null) { - this.actionBeanContextFactory = new DefaultActionBeanContextFactory(); - this.actionBeanContextFactory.init(this); - } - - this.typeConverterFactory = initTypeConverterFactory(); - if (this.typeConverterFactory == null) { - this.typeConverterFactory = new DefaultTypeConverterFactory(); - this.typeConverterFactory.init(this); - } - - this.localizationBundleFactory = initLocalizationBundleFactory(); - if (this.localizationBundleFactory == null) { - this.localizationBundleFactory = new DefaultLocalizationBundleFactory(); - this.localizationBundleFactory.init(this); - } - - this.localePicker = initLocalePicker(); - if (this.localePicker == null) { - this.localePicker = new DefaultLocalePicker(); - this.localePicker.init(this); - } - - this.formatterFactory = initFormatterFactory(); - if (this.formatterFactory == null) { - this.formatterFactory = new DefaultFormatterFactory(); - this.formatterFactory.init(this); - } - - this.tagErrorRendererFactory = initTagErrorRendererFactory(); - if (this.tagErrorRendererFactory == null) { - this.tagErrorRendererFactory = new DefaultTagErrorRendererFactory(); - this.tagErrorRendererFactory.init(this); - } - - this.populationStrategy = initPopulationStrategy(); - if (this.populationStrategy == null) { - this.populationStrategy = new BeanFirstPopulationStrategy(); - this.populationStrategy.init(this); - } - - this.exceptionHandler = initExceptionHandler(); - if (this.exceptionHandler == null) { - this.exceptionHandler = new DefaultExceptionHandler(); - this.exceptionHandler.init(this); - } - - this.multipartWrapperFactory = initMultipartWrapperFactory(); - if (this.multipartWrapperFactory == null) { - this.multipartWrapperFactory = new DefaultMultipartWrapperFactory(); - this.multipartWrapperFactory.init(this); - } - - this.validationMetadataProvider = initValidationMetadataProvider(); - if (this.validationMetadataProvider == null) { - this.validationMetadataProvider = new DefaultValidationMetadataProvider(); - this.validationMetadataProvider.init(this); - } - - this.interceptors = new HashMap>(); - Map> map = initCoreInterceptors(); - if (map != null) { - mergeInterceptorMaps(this.interceptors, map); - } - map = initInterceptors(); - if (map != null) { - mergeInterceptorMaps(this.interceptors, map); - } - - // do a quick check to see if any interceptor classes are configured more than once - for (Map.Entry> entry : this.interceptors.entrySet()) { - Set> classes = new HashSet>(); - Collection interceptors = entry.getValue(); - if (interceptors == null) - continue; - - for (Interceptor interceptor : interceptors) { - Class clazz = interceptor.getClass(); - if (classes.contains(clazz)) { - log.warn("Interceptor ", clazz, - " is configured to run more than once for ", entry.getKey()); - } - else { - classes.add(clazz); - } - } - } + /** Log implementation for use within this class. */ + private static final Log log = Log.getInstance(DefaultConfiguration.class); + + private boolean debugMode; + private BootstrapPropertyResolver resolver; + private ObjectFactory objectFactory; + private ActionResolver actionResolver; + private ActionBeanPropertyBinder actionBeanPropertyBinder; + private ActionBeanContextFactory actionBeanContextFactory; + private TypeConverterFactory typeConverterFactory; + private LocalizationBundleFactory localizationBundleFactory; + private LocalePicker localePicker; + private FormatterFactory formatterFactory; + private TagErrorRendererFactory tagErrorRendererFactory; + private PopulationStrategy populationStrategy; + private Map> interceptors; + private ExceptionHandler exceptionHandler; + private MultipartWrapperFactory multipartWrapperFactory; + private ValidationMetadataProvider validationMetadataProvider; + + /** Gratefully accepts the BootstrapPropertyResolver handed to the Configuration. */ + public void setBootstrapPropertyResolver(BootstrapPropertyResolver resolver) { + this.resolver = resolver; + } + + /** + * Creates and stores instances of the objects of the type that the Configuration is responsible + * for providing, except where subclasses have already provided instances. + */ + @SuppressWarnings("unchecked") + public void init() { + try { + Boolean debugMode = initDebugMode(); + if (debugMode != null) { + this.debugMode = debugMode; + } else { + this.debugMode = false; + } + + this.objectFactory = initObjectFactory(); + if (this.objectFactory == null) { + this.objectFactory = new DefaultObjectFactory(); + this.objectFactory.init(this); + } + if (this.objectFactory instanceof DefaultObjectFactory) { + List> classes = + getBootstrapPropertyResolver().getClassPropertyList(ObjectPostProcessor.class); + List instances = new ArrayList(); + for (Class clazz : classes) { + log.debug("Instantiating object post-processor ", clazz); + instances.add(this.objectFactory.newInstance(clazz)); } - catch (Exception e) { - throw new StripesRuntimeException - ("Problem instantiating default configuration objects.", e); + for (ObjectPostProcessor pp : instances) { + ((DefaultObjectFactory) this.objectFactory).addPostProcessor(pp); } + } + + this.actionResolver = initActionResolver(); + if (this.actionResolver == null) { + this.actionResolver = new NameBasedActionResolver(); + this.actionResolver.init(this); + } + + this.actionBeanPropertyBinder = initActionBeanPropertyBinder(); + if (this.actionBeanPropertyBinder == null) { + this.actionBeanPropertyBinder = new DefaultActionBeanPropertyBinder(); + this.actionBeanPropertyBinder.init(this); + } + + this.actionBeanContextFactory = initActionBeanContextFactory(); + if (this.actionBeanContextFactory == null) { + this.actionBeanContextFactory = new DefaultActionBeanContextFactory(); + this.actionBeanContextFactory.init(this); + } + + this.typeConverterFactory = initTypeConverterFactory(); + if (this.typeConverterFactory == null) { + this.typeConverterFactory = new DefaultTypeConverterFactory(); + this.typeConverterFactory.init(this); + } + + this.localizationBundleFactory = initLocalizationBundleFactory(); + if (this.localizationBundleFactory == null) { + this.localizationBundleFactory = new DefaultLocalizationBundleFactory(); + this.localizationBundleFactory.init(this); + } + + this.localePicker = initLocalePicker(); + if (this.localePicker == null) { + this.localePicker = new DefaultLocalePicker(); + this.localePicker.init(this); + } + + this.formatterFactory = initFormatterFactory(); + if (this.formatterFactory == null) { + this.formatterFactory = new DefaultFormatterFactory(); + this.formatterFactory.init(this); + } + + this.tagErrorRendererFactory = initTagErrorRendererFactory(); + if (this.tagErrorRendererFactory == null) { + this.tagErrorRendererFactory = new DefaultTagErrorRendererFactory(); + this.tagErrorRendererFactory.init(this); + } + + this.populationStrategy = initPopulationStrategy(); + if (this.populationStrategy == null) { + this.populationStrategy = new BeanFirstPopulationStrategy(); + this.populationStrategy.init(this); + } + + this.exceptionHandler = initExceptionHandler(); + if (this.exceptionHandler == null) { + this.exceptionHandler = new DefaultExceptionHandler(); + this.exceptionHandler.init(this); + } + + this.multipartWrapperFactory = initMultipartWrapperFactory(); + if (this.multipartWrapperFactory == null) { + this.multipartWrapperFactory = new DefaultMultipartWrapperFactory(); + this.multipartWrapperFactory.init(this); + } + + this.validationMetadataProvider = initValidationMetadataProvider(); + if (this.validationMetadataProvider == null) { + this.validationMetadataProvider = new DefaultValidationMetadataProvider(); + this.validationMetadataProvider.init(this); + } + + this.interceptors = new HashMap>(); + Map> map = initCoreInterceptors(); + if (map != null) { + mergeInterceptorMaps(this.interceptors, map); + } + map = initInterceptors(); + if (map != null) { + mergeInterceptorMaps(this.interceptors, map); + } + + // do a quick check to see if any interceptor classes are configured more than once + for (Map.Entry> entry : + this.interceptors.entrySet()) { + Set> classes = new HashSet>(); + Collection interceptors = entry.getValue(); + if (interceptors == null) continue; + + for (Interceptor interceptor : interceptors) { + Class clazz = interceptor.getClass(); + if (classes.contains(clazz)) { + log.warn( + "Interceptor ", clazz, " is configured to run more than once for ", entry.getKey()); + } else { + classes.add(clazz); + } + } + } + } catch (Exception e) { + throw new StripesRuntimeException("Problem instantiating default configuration objects.", e); } - - /** Returns a reference to the resolver supplied at initialization time. */ - public BootstrapPropertyResolver getBootstrapPropertyResolver() { - return this.resolver; - } - - /** - * Retrieves the ServletContext for the context within which the Stripes application is - * executing. - * - * @return the ServletContext in which the application is running - */ - public ServletContext getServletContext() { - return getBootstrapPropertyResolver().getFilterConfig().getServletContext(); - } - - /** Enable or disable debug mode. */ - public void setDebugMode(boolean debugMode) { - this.debugMode = debugMode; - } - - /** Returns true if the Stripes application is running in debug mode. */ - public boolean isDebugMode() { - return debugMode; - } - - /** Allows subclasses to initialize a non-default debug mode value. */ - protected Boolean initDebugMode() { - return null; - } - - /** - * Returns an instance of {@link ObjectFactory} that is used throughout Stripes to instantiate - * classes. - * - * @return an instance of {@link ObjectFactory}. - */ - public ObjectFactory getObjectFactory() { - return this.objectFactory; - } - - /** Allows subclasses to initialize a non-default {@link ObjectFactory}. */ - protected ObjectFactory initObjectFactory() { return null; } - - /** - * Returns an instance of {@link NameBasedActionResolver} unless a subclass has - * overridden the default. - * @return ActionResolver an instance of the configured resolver - */ - public ActionResolver getActionResolver() { - return this.actionResolver; - } - - /** Allows subclasses to initialize a non-default ActionResovler. */ - protected ActionResolver initActionResolver() { return null; } - - /** - * Returns an instance of {@link DefaultActionBeanPropertyBinder} unless a subclass has - * overridden the default. - * @return ActionBeanPropertyBinder an instance of the configured binder - */ - public ActionBeanPropertyBinder getActionBeanPropertyBinder() { - return this.actionBeanPropertyBinder; - } - - /** Allows subclasses to initialize a non-default ActionBeanPropertyBinder. */ - protected ActionBeanPropertyBinder initActionBeanPropertyBinder() { return null; } - - /** - * Returns the configured ActionBeanContextFactory. Unless a subclass has configured a custom - * one, the instance will be a DefaultActionBeanContextFactory. - * - * @return ActionBeanContextFactory an instance of a factory for creating ActionBeanContexts - */ - public ActionBeanContextFactory getActionBeanContextFactory() { - return this.actionBeanContextFactory; - } - - /** Allows subclasses to initialize a non-default ActionBeanContextFactory. */ - protected ActionBeanContextFactory initActionBeanContextFactory() { return null; } - - /** - * Returns an instance of {@link DefaultTypeConverterFactory} unless a subclass has - * overridden the default.. - * @return TypeConverterFactory an instance of the configured factory. - */ - public TypeConverterFactory getTypeConverterFactory() { - return this.typeConverterFactory; - } - - /** Allows subclasses to initialize a non-default TypeConverterFactory. */ - protected TypeConverterFactory initTypeConverterFactory() { return null; } - - /** - * Returns an instance of a LocalizationBundleFactory. By default this will be an instance of - * DefaultLocalizationBundleFactory unless another type has been configured. - */ - public LocalizationBundleFactory getLocalizationBundleFactory() { - return this.localizationBundleFactory; - } - - /** Allows subclasses to initialize a non-default LocalizationBundleFactory. */ - protected LocalizationBundleFactory initLocalizationBundleFactory() { return null; } - - /** - * Returns an instance of a LocalePicker. Unless a subclass has picked another implementation - * will return an instance of DefaultLocalePicker. - */ - public LocalePicker getLocalePicker() { return this.localePicker; } - - /** Allows subclasses to initialize a non-default LocalePicker. */ - protected LocalePicker initLocalePicker() { return null; } - - /** - * Returns an instance of a FormatterFactory. Unless a subclass has picked another implementation - * will return an instance of DefaultFormatterFactory. - */ - public FormatterFactory getFormatterFactory() { return this.formatterFactory; } - - /** Allows subclasses to initialize a non-default FormatterFactory. */ - protected FormatterFactory initFormatterFactory() { return null; } - - /** - * Returns an instance of a TagErrorRendererFactory. Unless a subclass has picked another - * implementation, will return an instance of DefaultTagErrorRendererFactory. - */ - public TagErrorRendererFactory getTagErrorRendererFactory() { - return tagErrorRendererFactory; + } + + /** Returns a reference to the resolver supplied at initialization time. */ + public BootstrapPropertyResolver getBootstrapPropertyResolver() { + return this.resolver; + } + + /** + * Retrieves the ServletContext for the context within which the Stripes application is executing. + * + * @return the ServletContext in which the application is running + */ + public ServletContext getServletContext() { + return getBootstrapPropertyResolver().getFilterConfig().getServletContext(); + } + + /** Enable or disable debug mode. */ + public void setDebugMode(boolean debugMode) { + this.debugMode = debugMode; + } + + /** Returns true if the Stripes application is running in debug mode. */ + public boolean isDebugMode() { + return debugMode; + } + + /** Allows subclasses to initialize a non-default debug mode value. */ + protected Boolean initDebugMode() { + return null; + } + + /** + * Returns an instance of {@link ObjectFactory} that is used throughout Stripes to instantiate + * classes. + * + * @return an instance of {@link ObjectFactory}. + */ + public ObjectFactory getObjectFactory() { + return this.objectFactory; + } + + /** Allows subclasses to initialize a non-default {@link ObjectFactory}. */ + protected ObjectFactory initObjectFactory() { + return null; + } + + /** + * Returns an instance of {@link NameBasedActionResolver} unless a subclass has overridden the + * default. + * + * @return ActionResolver an instance of the configured resolver + */ + public ActionResolver getActionResolver() { + return this.actionResolver; + } + + /** Allows subclasses to initialize a non-default ActionResovler. */ + protected ActionResolver initActionResolver() { + return null; + } + + /** + * Returns an instance of {@link DefaultActionBeanPropertyBinder} unless a subclass has overridden + * the default. + * + * @return ActionBeanPropertyBinder an instance of the configured binder + */ + public ActionBeanPropertyBinder getActionBeanPropertyBinder() { + return this.actionBeanPropertyBinder; + } + + /** Allows subclasses to initialize a non-default ActionBeanPropertyBinder. */ + protected ActionBeanPropertyBinder initActionBeanPropertyBinder() { + return null; + } + + /** + * Returns the configured ActionBeanContextFactory. Unless a subclass has configured a custom one, + * the instance will be a DefaultActionBeanContextFactory. + * + * @return ActionBeanContextFactory an instance of a factory for creating ActionBeanContexts + */ + public ActionBeanContextFactory getActionBeanContextFactory() { + return this.actionBeanContextFactory; + } + + /** Allows subclasses to initialize a non-default ActionBeanContextFactory. */ + protected ActionBeanContextFactory initActionBeanContextFactory() { + return null; + } + + /** + * Returns an instance of {@link DefaultTypeConverterFactory} unless a subclass has overridden the + * default.. + * + * @return TypeConverterFactory an instance of the configured factory. + */ + public TypeConverterFactory getTypeConverterFactory() { + return this.typeConverterFactory; + } + + /** Allows subclasses to initialize a non-default TypeConverterFactory. */ + protected TypeConverterFactory initTypeConverterFactory() { + return null; + } + + /** + * Returns an instance of a LocalizationBundleFactory. By default this will be an instance of + * DefaultLocalizationBundleFactory unless another type has been configured. + */ + public LocalizationBundleFactory getLocalizationBundleFactory() { + return this.localizationBundleFactory; + } + + /** Allows subclasses to initialize a non-default LocalizationBundleFactory. */ + protected LocalizationBundleFactory initLocalizationBundleFactory() { + return null; + } + + /** + * Returns an instance of a LocalePicker. Unless a subclass has picked another implementation will + * return an instance of DefaultLocalePicker. + */ + public LocalePicker getLocalePicker() { + return this.localePicker; + } + + /** Allows subclasses to initialize a non-default LocalePicker. */ + protected LocalePicker initLocalePicker() { + return null; + } + + /** + * Returns an instance of a FormatterFactory. Unless a subclass has picked another implementation + * will return an instance of DefaultFormatterFactory. + */ + public FormatterFactory getFormatterFactory() { + return this.formatterFactory; + } + + /** Allows subclasses to initialize a non-default FormatterFactory. */ + protected FormatterFactory initFormatterFactory() { + return null; + } + + /** + * Returns an instance of a TagErrorRendererFactory. Unless a subclass has picked another + * implementation, will return an instance of DefaultTagErrorRendererFactory. + */ + public TagErrorRendererFactory getTagErrorRendererFactory() { + return tagErrorRendererFactory; + } + + /** Allows subclasses to initialize a non-default TagErrorRendererFactory instance to be used. */ + protected TagErrorRendererFactory initTagErrorRendererFactory() { + return null; + } + + /** + * Returns an instance of a PopulationsStrategy. Unless a subclass has picked another + * implementation, will return an instance of {@link + * net.sourceforge.stripes.tag.BeanFirstPopulationStrategy}. + * + * @since Stripes 1.6 + */ + public PopulationStrategy getPopulationStrategy() { + return this.populationStrategy; + } + + /** Allows subclasses to initialize a non-default PopulationStrategy instance to be used. */ + protected PopulationStrategy initPopulationStrategy() { + return null; + } + + /** + * Returns an instance of an ExceptionHandler. Unless a subclass has picked another + * implementation, will return an instance of {@link + * net.sourceforge.stripes.exception.DefaultExceptionHandler}. + */ + public ExceptionHandler getExceptionHandler() { + return this.exceptionHandler; + } + + /** Allows subclasses to initialize a non-default ExceptionHandler instance to be used. */ + protected ExceptionHandler initExceptionHandler() { + return null; + } + + /** + * Returns an instance of MultipartWrapperFactory that can be used by Stripes to construct + * MultipartWrapper instances for dealing with multipart requests (those containing file uploads). + * + * @return MultipartWrapperFactory an instance of the wrapper factory + */ + public MultipartWrapperFactory getMultipartWrapperFactory() { + return this.multipartWrapperFactory; + } + + /** Allows subclasses to initialize a non-default MultipartWrapperFactory. */ + protected MultipartWrapperFactory initMultipartWrapperFactory() { + return null; + } + + /** + * Returns an instance of {@link ValidationMetadataProvider} that can be used by Stripes to + * determine what validations need to be applied during {@link + * LifecycleStage#BindingAndValidation}. + * + * @return an instance of {@link ValidationMetadataProvider} + */ + public ValidationMetadataProvider getValidationMetadataProvider() { + return this.validationMetadataProvider; + } + + /** Allows subclasses to initialize a non-default {@link ValidationMetadataProvider}. */ + protected ValidationMetadataProvider initValidationMetadataProvider() { + return null; + } + + /** + * Returns a list of interceptors that should be executed around the lifecycle stage indicated. By + * default returns a single element list containing the {@link BeforeAfterMethodInterceptor}. + */ + public Collection getInterceptors(LifecycleStage stage) { + Collection interceptors = this.interceptors.get(stage); + if (interceptors == null) { + interceptors = Collections.emptyList(); } - - /** Allows subclasses to initialize a non-default TagErrorRendererFactory instance to be used. */ - protected TagErrorRendererFactory initTagErrorRendererFactory() { return null; } - - /** - * Returns an instance of a PopulationsStrategy. Unless a subclass has picked another - * implementation, will return an instance of - * {@link net.sourceforge.stripes.tag.BeanFirstPopulationStrategy}. - * @since Stripes 1.6 - */ - public PopulationStrategy getPopulationStrategy() { return this.populationStrategy; } - - /** Allows subclasses to initialize a non-default PopulationStrategy instance to be used. */ - protected PopulationStrategy initPopulationStrategy() { return null; } - - /** - * Returns an instance of an ExceptionHandler. Unless a subclass has picked another - * implementation, will return an instance of - * {@link net.sourceforge.stripes.exception.DefaultExceptionHandler}. - */ - public ExceptionHandler getExceptionHandler() { return this.exceptionHandler; } - - /** Allows subclasses to initialize a non-default ExceptionHandler instance to be used. */ - protected ExceptionHandler initExceptionHandler() { return null; } - - /** - * Returns an instance of MultipartWrapperFactory that can be used by Stripes to construct - * MultipartWrapper instances for dealing with multipart requests (those containing file - * uploads). - * - * @return MultipartWrapperFactory an instance of the wrapper factory - */ - public MultipartWrapperFactory getMultipartWrapperFactory() { - return this.multipartWrapperFactory; + return interceptors; + } + + /** + * Merges the two {@link Map}s of {@link LifecycleStage} to {@link Collection} of {@link + * Interceptor}. A simple {@link Map#putAll(Map)} does not work because it overwrites the + * collections in the map instead of adding to them. + */ + protected void mergeInterceptorMaps( + Map> dst, + Map> src) { + for (Map.Entry> entry : src.entrySet()) { + Collection collection = dst.get(entry.getKey()); + if (collection == null) { + collection = new LinkedList(); + dst.put(entry.getKey(), collection); + } + collection.addAll(entry.getValue()); } - - - /** Allows subclasses to initialize a non-default MultipartWrapperFactory. */ - protected MultipartWrapperFactory initMultipartWrapperFactory() { return null; } - - /** - * Returns an instance of {@link ValidationMetadataProvider} that can be used by Stripes to - * determine what validations need to be applied during - * {@link LifecycleStage#BindingAndValidation}. - * - * @return an instance of {@link ValidationMetadataProvider} - */ - public ValidationMetadataProvider getValidationMetadataProvider() { - return this.validationMetadataProvider; + } + + /** + * Adds the interceptor to the map, associating it with the {@link LifecycleStage}s indicated by + * the {@link Intercepts} annotation. If the interceptor implements {@link ConfigurableComponent}, + * then its init() method will be called. + */ + protected void addInterceptor( + Map> map, Interceptor interceptor) { + Class type = interceptor.getClass(); + Intercepts intercepts = type.getAnnotation(Intercepts.class); + if (intercepts == null) { + log.error( + "An interceptor of type ", + type.getName(), + " was configured ", + "but was not marked with an @Intercepts annotation. As a ", + "result it is not possible to determine at which ", + "lifecycle stages the interceptor should be applied. This ", + "interceptor will be ignored."); + return; + } else { + log.debug( + "Configuring interceptor '", + type.getSimpleName(), + "', for lifecycle stages: ", + intercepts.value()); } - /** Allows subclasses to initialize a non-default {@link ValidationMetadataProvider}. */ - protected ValidationMetadataProvider initValidationMetadataProvider() { return null; } - - /** - * Returns a list of interceptors that should be executed around the lifecycle stage - * indicated. By default returns a single element list containing the - * {@link BeforeAfterMethodInterceptor}. - */ - public Collection getInterceptors(LifecycleStage stage) { - Collection interceptors = this.interceptors.get(stage); - if (interceptors == null) { - interceptors = Collections.emptyList(); - } - return interceptors; + // call init() if the interceptor implements ConfigurableComponent + if (interceptor instanceof ConfigurableComponent) { + try { + ((ConfigurableComponent) interceptor).init(this); + } catch (Exception e) { + log.error("Error initializing interceptor of type " + type.getName(), e); + } } - - /** - * Merges the two {@link Map}s of {@link LifecycleStage} to {@link Collection} of - * {@link Interceptor}. A simple {@link Map#putAll(Map)} does not work because it overwrites - * the collections in the map instead of adding to them. - */ - protected void mergeInterceptorMaps(Map> dst, - Map> src) { - for (Map.Entry> entry : src.entrySet()) { - Collection collection = dst.get(entry.getKey()); - if (collection == null) { - collection = new LinkedList(); - dst.put(entry.getKey(), collection); - } - collection.addAll(entry.getValue()); - } - } - - /** - * Adds the interceptor to the map, associating it with the {@link LifecycleStage}s indicated - * by the {@link Intercepts} annotation. If the interceptor implements - * {@link ConfigurableComponent}, then its init() method will be called. - */ - protected void addInterceptor(Map> map, - Interceptor interceptor) { - Class type = interceptor.getClass(); - Intercepts intercepts = type.getAnnotation(Intercepts.class); - if (intercepts == null) { - log.error("An interceptor of type ", type.getName(), " was configured ", - "but was not marked with an @Intercepts annotation. As a ", - "result it is not possible to determine at which ", - "lifecycle stages the interceptor should be applied. This ", - "interceptor will be ignored."); - return; - } - else { - log.debug("Configuring interceptor '", type.getSimpleName(), - "', for lifecycle stages: ", intercepts.value()); - } - - // call init() if the interceptor implements ConfigurableComponent - if (interceptor instanceof ConfigurableComponent) { - try { - ((ConfigurableComponent) interceptor).init(this); - } - catch (Exception e) { - log.error("Error initializing interceptor of type " + type.getName(), e); - } - } - for (LifecycleStage stage : intercepts.value()) { - Collection stack = map.get(stage); - if (stack == null) { - stack = new LinkedList(); - map.put(stage, stack); - } + for (LifecycleStage stage : intercepts.value()) { + Collection stack = map.get(stage); + if (stack == null) { + stack = new LinkedList(); + map.put(stage, stack); + } - stack.add(interceptor); - } + stack.add(interceptor); } - - /** Instantiates the core interceptors, allowing subclasses to override the default behavior */ - protected Map> initCoreInterceptors() { - Map> interceptors = new HashMap>(); - addInterceptor(interceptors, new BeforeAfterMethodInterceptor()); - addInterceptor(interceptors, new HttpCacheInterceptor()); - return interceptors; - } - - /** Allows subclasses to initialize a non-default Map of Interceptor instances. */ - protected Map> initInterceptors() { return null; } + } + + /** Instantiates the core interceptors, allowing subclasses to override the default behavior */ + protected Map> initCoreInterceptors() { + Map> interceptors = + new HashMap>(); + addInterceptor(interceptors, new BeforeAfterMethodInterceptor()); + addInterceptor(interceptors, new HttpCacheInterceptor()); + return interceptors; + } + + /** Allows subclasses to initialize a non-default Map of Interceptor instances. */ + protected Map> initInterceptors() { + return null; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/config/DontAutoLoad.java b/stripes/src/main/java/net/sourceforge/stripes/config/DontAutoLoad.java index bd31c3046..a6418cc7b 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/config/DontAutoLoad.java +++ b/stripes/src/main/java/net/sourceforge/stripes/config/DontAutoLoad.java @@ -19,24 +19,22 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; - import net.sourceforge.stripes.exception.AutoExceptionHandler; import net.sourceforge.stripes.format.Formatter; import net.sourceforge.stripes.validation.TypeConverter; import net.sourceforge.stripes.validation.Validate; /** - * When applied to a Stripes extension class (e.g., one that implements {@link Formatter}, - * {@link TypeConverter}, {@link AutoExceptionHandler}, etc.), this annotation indicates that the - * class should not be loaded via autodiscovery. This is useful, for example, when you - * have a {@link TypeConverter} that is applied in special cases via {@link Validate#converter()} - * but should not be used for all the type conversions to which it applies. - * + * When applied to a Stripes extension class (e.g., one that implements {@link Formatter}, {@link + * TypeConverter}, {@link AutoExceptionHandler}, etc.), this annotation indicates that the class + * should not be loaded via autodiscovery. This is useful, for example, when you have a + * {@link TypeConverter} that is applied in special cases via {@link Validate#converter()} but + * should not be used for all the type conversions to which it applies. + * * @author Ben Gunter * @since Stripes 1.5 */ @Retention(RetentionPolicy.RUNTIME) -@Target( { ElementType.TYPE }) +@Target({ElementType.TYPE}) @Documented -public @interface DontAutoLoad { -} +public @interface DontAutoLoad {} diff --git a/stripes/src/main/java/net/sourceforge/stripes/config/RuntimeConfiguration.java b/stripes/src/main/java/net/sourceforge/stripes/config/RuntimeConfiguration.java index 24708c040..ac04adb91 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/config/RuntimeConfiguration.java +++ b/stripes/src/main/java/net/sourceforge/stripes/config/RuntimeConfiguration.java @@ -19,7 +19,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import net.sourceforge.stripes.controller.ActionBeanContextFactory; import net.sourceforge.stripes.controller.ActionBeanPropertyBinder; import net.sourceforge.stripes.controller.ActionResolver; @@ -42,300 +41,338 @@ import net.sourceforge.stripes.validation.ValidationMetadataProvider; /** - *

    Configuration class that uses the BootstrapPropertyResolver to look for configuration values, - * and when it cannot find a value, falls back on the DefaultConfiguration to supply default - * values. In general, the RuntimeConfiguration will operate in the following pattern:

    + * Configuration class that uses the BootstrapPropertyResolver to look for configuration values, and + * when it cannot find a value, falls back on the DefaultConfiguration to supply default values. In + * general, the RuntimeConfiguration will operate in the following pattern: * *
      - *
    • Look for the value of a configuration property in the BootstrapProperties
    • - *
    • If the value exists, the configuration will attempt to use it (usually to instantiate - * a class). If an exception occurs, the RuntimeConfiguration will throw an exception and - * not provide a value. In most cases this will be fatal!
    • - *
    • If the value does not exist, the default from DefaultConfiguration will be used.
    • + *
    • Look for the value of a configuration property in the BootstrapProperties + *
    • If the value exists, the configuration will attempt to use it (usually to instantiate a + * class). If an exception occurs, the RuntimeConfiguration will throw an exception and not + * provide a value. In most cases this will be fatal! + *
    • If the value does not exist, the default from DefaultConfiguration will be used. *
    * * @author Tim Fennell */ public class RuntimeConfiguration extends DefaultConfiguration { - /** Log implementation for use within this class. */ - private static final Log log = Log.getInstance(RuntimeConfiguration.class); + /** Log implementation for use within this class. */ + private static final Log log = Log.getInstance(RuntimeConfiguration.class); - /** The Configuration Key for enabling debug mode. */ - public static final String DEBUG_MODE = "Stripes.DebugMode"; + /** The Configuration Key for enabling debug mode. */ + public static final String DEBUG_MODE = "Stripes.DebugMode"; - /** The Configuration Key for looking up the name of the ObjectFactory class */ - public static final String OBJECT_FACTORY = "ObjectFactory.Class"; + /** The Configuration Key for looking up the name of the ObjectFactory class */ + public static final String OBJECT_FACTORY = "ObjectFactory.Class"; - /** The Configuration Key for looking up the name of the ActionResolver class. */ - public static final String ACTION_RESOLVER = "ActionResolver.Class"; + /** The Configuration Key for looking up the name of the ActionResolver class. */ + public static final String ACTION_RESOLVER = "ActionResolver.Class"; - /** The Configuration Key for looking up the name of the ActionResolver class. */ - public static final String ACTION_BEAN_PROPERTY_BINDER = "ActionBeanPropertyBinder.Class"; + /** The Configuration Key for looking up the name of the ActionResolver class. */ + public static final String ACTION_BEAN_PROPERTY_BINDER = "ActionBeanPropertyBinder.Class"; - /** The Configuration Key for looking up the name of an ActionBeanContextFactory class. */ - public static final String ACTION_BEAN_CONTEXT_FACTORY = "ActionBeanContextFactory.Class"; + /** The Configuration Key for looking up the name of an ActionBeanContextFactory class. */ + public static final String ACTION_BEAN_CONTEXT_FACTORY = "ActionBeanContextFactory.Class"; - /** The Configuration Key for looking up the name of the TypeConverterFactory class. */ - public static final String TYPE_CONVERTER_FACTORY = "TypeConverterFactory.Class"; + /** The Configuration Key for looking up the name of the TypeConverterFactory class. */ + public static final String TYPE_CONVERTER_FACTORY = "TypeConverterFactory.Class"; - /** The Configuration Key for looking up the name of the LocalizationBundleFactory class. */ - public static final String LOCALIZATION_BUNDLE_FACTORY = "LocalizationBundleFactory.Class"; + /** The Configuration Key for looking up the name of the LocalizationBundleFactory class. */ + public static final String LOCALIZATION_BUNDLE_FACTORY = "LocalizationBundleFactory.Class"; - /** The Configuration Key for looking up the name of the LocalizationBundleFactory class. */ - public static final String LOCALE_PICKER = "LocalePicker.Class"; + /** The Configuration Key for looking up the name of the LocalizationBundleFactory class. */ + public static final String LOCALE_PICKER = "LocalePicker.Class"; - /** The Configuration Key for looking up the name of the FormatterFactory class. */ - public static final String FORMATTER_FACTORY = "FormatterFactory.Class"; + /** The Configuration Key for looking up the name of the FormatterFactory class. */ + public static final String FORMATTER_FACTORY = "FormatterFactory.Class"; - /** The Configuration Key for looking up the name of the TagErrorRendererFactory class */ - public static final String TAG_ERROR_RENDERER_FACTORY = "TagErrorRendererFactory.Class"; + /** The Configuration Key for looking up the name of the TagErrorRendererFactory class */ + public static final String TAG_ERROR_RENDERER_FACTORY = "TagErrorRendererFactory.Class"; - /** The Configuration Key for looking up the name of the PopulationStrategy class */ - public static final String POPULATION_STRATEGY = "PopulationStrategy.Class"; + /** The Configuration Key for looking up the name of the PopulationStrategy class */ + public static final String POPULATION_STRATEGY = "PopulationStrategy.Class"; - /** The Configuration Key for looking up the name of the ExceptionHandler class */ - public static final String EXCEPTION_HANDLER = "ExceptionHandler.Class"; + /** The Configuration Key for looking up the name of the ExceptionHandler class */ + public static final String EXCEPTION_HANDLER = "ExceptionHandler.Class"; - /** The Configuration Key for looking up the name of the MultipartWrapperFactory class */ - public static final String MULTIPART_WRAPPER_FACTORY = "MultipartWrapperFactory.Class"; + /** The Configuration Key for looking up the name of the MultipartWrapperFactory class */ + public static final String MULTIPART_WRAPPER_FACTORY = "MultipartWrapperFactory.Class"; - /** The Configuration Key for looking up the name of the ValidationMetadataProvider class */ - public static final String VALIDATION_METADATA_PROVIDER = "ValidationMetadataProvider.Class"; + /** The Configuration Key for looking up the name of the ValidationMetadataProvider class */ + public static final String VALIDATION_METADATA_PROVIDER = "ValidationMetadataProvider.Class"; - /** The Configuration Key for looking up the comma separated list of core interceptor classes. */ - public static final String CORE_INTERCEPTOR_LIST = "CoreInterceptor.Classes"; + /** The Configuration Key for looking up the comma separated list of core interceptor classes. */ + public static final String CORE_INTERCEPTOR_LIST = "CoreInterceptor.Classes"; - /** The Configuration Key for looking up the comma separated list of interceptor classes. */ - public static final String INTERCEPTOR_LIST = "Interceptor.Classes"; - - /** Looks for a true/false value in config. */ - @Override protected Boolean initDebugMode() { - try { - return Boolean.valueOf(getBootstrapPropertyResolver().getProperty(DEBUG_MODE) - .toLowerCase()); - } - catch (Exception e) { - return null; - } - } + /** The Configuration Key for looking up the comma separated list of interceptor classes. */ + public static final String INTERCEPTOR_LIST = "Interceptor.Classes"; - /** Looks for a class name in config and uses that to create the component. */ - @Override protected ObjectFactory initObjectFactory() { - return initializeComponent(ObjectFactory.class, OBJECT_FACTORY); + /** Looks for a true/false value in config. */ + @Override + protected Boolean initDebugMode() { + try { + return Boolean.valueOf(getBootstrapPropertyResolver().getProperty(DEBUG_MODE).toLowerCase()); + } catch (Exception e) { + return null; } - - /** Looks for a class name in config and uses that to create the component. */ - @Override protected ActionResolver initActionResolver() { - return initializeComponent(ActionResolver.class, ACTION_RESOLVER); - } - - /** Looks for a class name in config and uses that to create the component. */ - @Override protected ActionBeanPropertyBinder initActionBeanPropertyBinder() { - return initializeComponent(ActionBeanPropertyBinder.class, ACTION_BEAN_PROPERTY_BINDER); - } - - /** Looks for a class name in config and uses that to create the component. */ - @Override protected ActionBeanContextFactory initActionBeanContextFactory() { - return initializeComponent(ActionBeanContextFactory.class, ACTION_BEAN_CONTEXT_FACTORY); - } - - /** Looks for a class name in config and uses that to create the component. */ - @Override protected TypeConverterFactory initTypeConverterFactory() { - return initializeComponent(TypeConverterFactory.class, TYPE_CONVERTER_FACTORY); - } - - /** Looks for a class name in config and uses that to create the component. */ - @Override protected LocalizationBundleFactory initLocalizationBundleFactory() { - return initializeComponent(LocalizationBundleFactory.class, LOCALIZATION_BUNDLE_FACTORY); - } - - /** Looks for a class name in config and uses that to create the component. */ - @Override protected LocalePicker initLocalePicker() { - return initializeComponent(LocalePicker.class, LOCALE_PICKER); - } - - /** Looks for a class name in config and uses that to create the component. */ - @Override protected FormatterFactory initFormatterFactory() { - return initializeComponent(FormatterFactory.class, FORMATTER_FACTORY); + } + + /** Looks for a class name in config and uses that to create the component. */ + @Override + protected ObjectFactory initObjectFactory() { + return initializeComponent(ObjectFactory.class, OBJECT_FACTORY); + } + + /** Looks for a class name in config and uses that to create the component. */ + @Override + protected ActionResolver initActionResolver() { + return initializeComponent(ActionResolver.class, ACTION_RESOLVER); + } + + /** Looks for a class name in config and uses that to create the component. */ + @Override + protected ActionBeanPropertyBinder initActionBeanPropertyBinder() { + return initializeComponent(ActionBeanPropertyBinder.class, ACTION_BEAN_PROPERTY_BINDER); + } + + /** Looks for a class name in config and uses that to create the component. */ + @Override + protected ActionBeanContextFactory initActionBeanContextFactory() { + return initializeComponent(ActionBeanContextFactory.class, ACTION_BEAN_CONTEXT_FACTORY); + } + + /** Looks for a class name in config and uses that to create the component. */ + @Override + protected TypeConverterFactory initTypeConverterFactory() { + return initializeComponent(TypeConverterFactory.class, TYPE_CONVERTER_FACTORY); + } + + /** Looks for a class name in config and uses that to create the component. */ + @Override + protected LocalizationBundleFactory initLocalizationBundleFactory() { + return initializeComponent(LocalizationBundleFactory.class, LOCALIZATION_BUNDLE_FACTORY); + } + + /** Looks for a class name in config and uses that to create the component. */ + @Override + protected LocalePicker initLocalePicker() { + return initializeComponent(LocalePicker.class, LOCALE_PICKER); + } + + /** Looks for a class name in config and uses that to create the component. */ + @Override + protected FormatterFactory initFormatterFactory() { + return initializeComponent(FormatterFactory.class, FORMATTER_FACTORY); + } + + /** Looks for a class name in config and uses that to create the component. */ + @Override + protected TagErrorRendererFactory initTagErrorRendererFactory() { + return initializeComponent(TagErrorRendererFactory.class, TAG_ERROR_RENDERER_FACTORY); + } + + /** Looks for a class name in config and uses that to create the component. */ + @Override + protected PopulationStrategy initPopulationStrategy() { + return initializeComponent(PopulationStrategy.class, POPULATION_STRATEGY); + } + + /** Looks for a class name in config and uses that to create the component. */ + @Override + protected ExceptionHandler initExceptionHandler() { + return initializeComponent(ExceptionHandler.class, EXCEPTION_HANDLER); + } + + /** Looks for a class name in config and uses that to create the component. */ + @Override + protected MultipartWrapperFactory initMultipartWrapperFactory() { + return initializeComponent(MultipartWrapperFactory.class, MULTIPART_WRAPPER_FACTORY); + } + + /** Looks for a class name in config and uses that to create the component. */ + @Override + protected ValidationMetadataProvider initValidationMetadataProvider() { + return initializeComponent(ValidationMetadataProvider.class, VALIDATION_METADATA_PROVIDER); + } + + /** + * Looks for a list of class names separated by commas under the configuration key {@link + * #CORE_INTERCEPTOR_LIST}. White space surrounding the class names is trimmed, the classes + * instantiated and then stored under the lifecycle stage(s) they should intercept. + * + * @return a Map of {@link LifecycleStage} to Collection of {@link Interceptor} + */ + @Override + protected Map> initCoreInterceptors() { + List> coreInterceptorClasses = + getBootstrapPropertyResolver().getClassPropertyList(CORE_INTERCEPTOR_LIST); + if (coreInterceptorClasses.size() == 0) return super.initCoreInterceptors(); + else return initInterceptors(coreInterceptorClasses); + } + + /** + * Looks for a list of class names separated by commas under the configuration key {@link + * #INTERCEPTOR_LIST}. White space surrounding the class names is trimmed, the classes + * instantiated and then stored under the lifecycle stage(s) they should intercept. + * + * @return a Map of {@link LifecycleStage} to Collection of {@link Interceptor} + */ + @Override + protected Map> initInterceptors() { + return initInterceptors( + getBootstrapPropertyResolver().getClassPropertyList(INTERCEPTOR_LIST, Interceptor.class)); + } + + /** + * Splits a comma-separated list of class names and maps each {@link LifecycleStage} to the + * interceptors in the list that intercept it. Also automatically finds Interceptors in packages + * listed in {@link BootstrapPropertyResolver#PACKAGES} if searchExtensionPackages is true. + * + * @return a Map of {@link LifecycleStage} to Collection of {@link Interceptor} + */ + @SuppressWarnings("unchecked") + protected Map> initInterceptors(List classes) { + + Map> map = + new HashMap>(); + + for (Object type : classes) { + try { + Interceptor interceptor = + getObjectFactory().newInstance((Class) type); + addInterceptor(map, interceptor); + } catch (Exception e) { + throw new StripesRuntimeException( + "Could not instantiate configured Interceptor [" + type.getClass().getName() + "].", e); + } } - /** Looks for a class name in config and uses that to create the component. */ - @Override protected TagErrorRendererFactory initTagErrorRendererFactory() { - return initializeComponent(TagErrorRendererFactory.class, TAG_ERROR_RENDERER_FACTORY); - } - - /** Looks for a class name in config and uses that to create the component. */ - @Override protected PopulationStrategy initPopulationStrategy() { - return initializeComponent(PopulationStrategy.class, POPULATION_STRATEGY); - } - - /** Looks for a class name in config and uses that to create the component. */ - @Override protected ExceptionHandler initExceptionHandler() { - return initializeComponent(ExceptionHandler.class, EXCEPTION_HANDLER); - } - - /** Looks for a class name in config and uses that to create the component. */ - @Override protected MultipartWrapperFactory initMultipartWrapperFactory() { - return initializeComponent(MultipartWrapperFactory.class, MULTIPART_WRAPPER_FACTORY); - } - - /** Looks for a class name in config and uses that to create the component. */ - @Override protected ValidationMetadataProvider initValidationMetadataProvider() { - return initializeComponent(ValidationMetadataProvider.class, VALIDATION_METADATA_PROVIDER); - } - - /** - * Looks for a list of class names separated by commas under the configuration key - * {@link #CORE_INTERCEPTOR_LIST}. White space surrounding the class names is trimmed, - * the classes instantiated and then stored under the lifecycle stage(s) they should - * intercept. - * - * @return a Map of {@link LifecycleStage} to Collection of {@link Interceptor} - */ - @Override - protected Map> initCoreInterceptors() { - List> coreInterceptorClasses = getBootstrapPropertyResolver().getClassPropertyList(CORE_INTERCEPTOR_LIST); - if (coreInterceptorClasses.size() == 0) - return super.initCoreInterceptors(); - else - return initInterceptors(coreInterceptorClasses); - } - - /** - * Looks for a list of class names separated by commas under the configuration key - * {@link #INTERCEPTOR_LIST}. White space surrounding the class names is trimmed, - * the classes instantiated and then stored under the lifecycle stage(s) they should - * intercept. - * - * @return a Map of {@link LifecycleStage} to Collection of {@link Interceptor} - */ - @Override - protected Map> initInterceptors() { - return initInterceptors(getBootstrapPropertyResolver().getClassPropertyList(INTERCEPTOR_LIST, Interceptor.class)); - } - - /** - * Splits a comma-separated list of class names and maps each {@link LifecycleStage} to the - * interceptors in the list that intercept it. Also automatically finds Interceptors in - * packages listed in {@link BootstrapPropertyResolver#PACKAGES} if searchExtensionPackages is true. - * - * @return a Map of {@link LifecycleStage} to Collection of {@link Interceptor} - */ - @SuppressWarnings("unchecked") - protected Map> initInterceptors(List classes) { - - Map> map = new HashMap>(); - - for (Object type : classes) { - try { - Interceptor interceptor = getObjectFactory().newInstance( - (Class) type); - addInterceptor(map, interceptor); - } - catch (Exception e) { - throw new StripesRuntimeException("Could not instantiate configured Interceptor [" - + type.getClass().getName() + "].", e); - } + return map; + } + + /** + * Internal utility method that is used to implement the main pattern of this class: lookup the + * name of a class based on a property name, instantiate the named class and initialize it. + * + * @param componentType a Class object representing a subclass of ConfigurableComponent + * @param propertyName the name of the property to look up for the class name + * @return an instance of the component, or null if one was not configured. + */ + @SuppressWarnings("unchecked") + protected T initializeComponent( + Class componentType, String propertyName) { + Class clazz = getBootstrapPropertyResolver().getClassProperty(propertyName, componentType); + if (clazz != null) { + try { + T component; + + ObjectFactory objectFactory = getObjectFactory(); + if (objectFactory != null) { + component = objectFactory.newInstance((Class) clazz); + } else { + component = (T) clazz.newInstance(); } - return map; + component.init(this); + return component; + } catch (Exception e) { + throw new StripesRuntimeException( + "Could not instantiate configured " + + componentType.getSimpleName() + + " of type [" + + clazz.getSimpleName() + + "]. Please check " + + "the configuration parameters specified in your web.xml.", + e); + } + } else { + return null; } - - /** - * Internal utility method that is used to implement the main pattern of this class: lookup the - * name of a class based on a property name, instantiate the named class and initialize it. - * - * @param componentType a Class object representing a subclass of ConfigurableComponent - * @param propertyName the name of the property to look up for the class name - * @return an instance of the component, or null if one was not configured. - */ - @SuppressWarnings("unchecked") - protected T initializeComponent(Class componentType, - String propertyName) { - Class clazz = getBootstrapPropertyResolver().getClassProperty(propertyName, componentType); - if (clazz != null) { - try { - T component; - - ObjectFactory objectFactory = getObjectFactory(); - if (objectFactory != null) { - component = objectFactory.newInstance((Class) clazz); - } - else { - component = (T) clazz.newInstance(); - } - - component.init(this); - return component; - } - catch (Exception e) { - throw new StripesRuntimeException("Could not instantiate configured " - + componentType.getSimpleName() + " of type [" + clazz.getSimpleName() - + "]. Please check " - + "the configuration parameters specified in your web.xml.", e); - - } + } + + /** + * Calls super.init() then adds Formatters and TypeConverters found in packages listed in {@link + * BootstrapPropertyResolver#PACKAGES} to their respective factories. + */ + @SuppressWarnings("unchecked") + @Override + public void init() { + super.init(); + + List> formatters = + getBootstrapPropertyResolver().getClassPropertyList(Formatter.class); + for (Class formatter : formatters) { + Type[] typeArguments = ReflectUtil.getActualTypeArguments(formatter, Formatter.class); + log.trace("Found Formatter [", formatter, "] - type parameters: ", typeArguments); + if ((typeArguments != null) + && (typeArguments.length == 1) + && !typeArguments[0].equals(Object.class)) { + if (typeArguments[0] instanceof Class) { + log.debug( + "Adding auto-discovered Formatter [", + formatter, + "] for [", + typeArguments[0], + "] (from type parameter)"); + getFormatterFactory() + .add((Class) typeArguments[0], (Class>) formatter); + } else { + log.warn("Type parameter for non-abstract Formatter [", formatter, "] is not a class."); } - else { - return null; + } + + TargetTypes targetTypes = formatter.getAnnotation(TargetTypes.class); + if (targetTypes != null) { + for (Class targetType : targetTypes.value()) { + log.debug( + "Adding auto-discovered Formatter [", + formatter, + "] for [", + targetType, + "] (from TargetTypes annotation)"); + getFormatterFactory().add(targetType, (Class>) formatter); } + } } - /** - * Calls super.init() then adds Formatters and TypeConverters found in - * packages listed in {@link BootstrapPropertyResolver#PACKAGES} to their respective factories. - */ - @SuppressWarnings("unchecked") - @Override - public void init() { - super.init(); - - List> formatters = getBootstrapPropertyResolver().getClassPropertyList(Formatter.class); - for (Class formatter : formatters) { - Type[] typeArguments = ReflectUtil.getActualTypeArguments(formatter, Formatter.class); - log.trace("Found Formatter [", formatter, "] - type parameters: ", typeArguments); - if ((typeArguments != null) && (typeArguments.length == 1) - && !typeArguments[0].equals(Object.class)) { - if (typeArguments[0] instanceof Class) { - log.debug("Adding auto-discovered Formatter [", formatter, "] for [", typeArguments[0], "] (from type parameter)"); - getFormatterFactory().add((Class) typeArguments[0], (Class>) formatter); - } - else { - log.warn("Type parameter for non-abstract Formatter [", formatter, "] is not a class."); - } - } - - TargetTypes targetTypes = formatter.getAnnotation(TargetTypes.class); - if (targetTypes != null) { - for (Class targetType : targetTypes.value()) { - log.debug("Adding auto-discovered Formatter [", formatter, "] for [", targetType, "] (from TargetTypes annotation)"); - getFormatterFactory().add(targetType, (Class>) formatter); - } - } + List> typeConverters = + getBootstrapPropertyResolver().getClassPropertyList(TypeConverter.class); + for (Class typeConverter : typeConverters) { + Type[] typeArguments = ReflectUtil.getActualTypeArguments(typeConverter, TypeConverter.class); + log.trace("Found TypeConverter [", typeConverter, "] - type parameters: ", typeArguments); + if ((typeArguments != null) + && (typeArguments.length == 1) + && !typeArguments[0].equals(Object.class)) { + if (typeArguments[0] instanceof Class) { + log.debug( + "Adding auto-discovered TypeConverter [", + typeConverter, + "] for [", + typeArguments[0], + "] (from type parameter)"); + getTypeConverterFactory() + .add((Class) typeArguments[0], (Class>) typeConverter); + } else { + log.warn( + "Type parameter for non-abstract TypeConverter [", + typeConverter, + "] is not a class."); } - - List> typeConverters = getBootstrapPropertyResolver().getClassPropertyList(TypeConverter.class); - for (Class typeConverter : typeConverters) { - Type[] typeArguments = ReflectUtil.getActualTypeArguments(typeConverter, TypeConverter.class); - log.trace("Found TypeConverter [", typeConverter, "] - type parameters: ", typeArguments); - if ((typeArguments != null) && (typeArguments.length == 1) - && !typeArguments[0].equals(Object.class)) { - if (typeArguments[0] instanceof Class) { - log.debug("Adding auto-discovered TypeConverter [", typeConverter, "] for [", typeArguments[0], "] (from type parameter)"); - getTypeConverterFactory().add((Class) typeArguments[0], (Class>) typeConverter); - } - else { - log.warn("Type parameter for non-abstract TypeConverter [", typeConverter, "] is not a class."); - } - } - - TargetTypes targetTypes = typeConverter.getAnnotation(TargetTypes.class); - if (targetTypes != null) { - for (Class targetType : targetTypes.value()) { - log.debug("Adding auto-discovered TypeConverter [", typeConverter, "] for [", targetType, "] (from TargetTypes annotation)"); - getTypeConverterFactory().add(targetType, (Class>) typeConverter); - } - } + } + + TargetTypes targetTypes = typeConverter.getAnnotation(TargetTypes.class); + if (targetTypes != null) { + for (Class targetType : targetTypes.value()) { + log.debug( + "Adding auto-discovered TypeConverter [", + typeConverter, + "] for [", + targetType, + "] (from TargetTypes annotation)"); + getTypeConverterFactory() + .add(targetType, (Class>) typeConverter); } + } } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/config/TargetTypes.java b/stripes/src/main/java/net/sourceforge/stripes/config/TargetTypes.java index 71c95eb30..818739cef 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/config/TargetTypes.java +++ b/stripes/src/main/java/net/sourceforge/stripes/config/TargetTypes.java @@ -21,15 +21,14 @@ import java.lang.annotation.Target; /** - * Annotation used to indicate classes, interfaces, and annotations that - * a Formatter or TypeConverter can handle. - * - * @author Aaron Porter + * Annotation used to indicate classes, interfaces, and annotations that a Formatter or + * TypeConverter can handle. * + * @author Aaron Porter */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented public @interface TargetTypes { - Class[] value(); -} \ No newline at end of file + Class[] value(); +} diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/ActionBeanContextFactory.java b/stripes/src/main/java/net/sourceforge/stripes/controller/ActionBeanContextFactory.java index 906483580..c69c0015a 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/ActionBeanContextFactory.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/ActionBeanContextFactory.java @@ -14,30 +14,28 @@ */ package net.sourceforge.stripes.controller; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.config.ConfigurableComponent; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletRequest; - /** - * Interface for classes that can instantiate and supply new instances of the - * ActionBeanContext class, or subclasses thereof. + * Interface for classes that can instantiate and supply new instances of the ActionBeanContext + * class, or subclasses thereof. * * @author Tim Fennell */ public interface ActionBeanContextFactory extends ConfigurableComponent { - - /** - * Creates and returns a new instance of ActionBeanContext or a subclass. - * - * @param request the current HttpServletRequest - * @param response the current HttpServletResponse - * @return a new instance of ActionBeanContext - * @throws ServletException if the ActionBeanContext class configured cannot be instantiated - */ - ActionBeanContext getContextInstance(HttpServletRequest request, - HttpServletResponse response) throws ServletException; -} \ No newline at end of file + /** + * Creates and returns a new instance of ActionBeanContext or a subclass. + * + * @param request the current HttpServletRequest + * @param response the current HttpServletResponse + * @return a new instance of ActionBeanContext + * @throws ServletException if the ActionBeanContext class configured cannot be instantiated + */ + ActionBeanContext getContextInstance(HttpServletRequest request, HttpServletResponse response) + throws ServletException; +} diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/ActionBeanPropertyBinder.java b/stripes/src/main/java/net/sourceforge/stripes/controller/ActionBeanPropertyBinder.java index e36817156..ea9970e19 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/ActionBeanPropertyBinder.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/ActionBeanPropertyBinder.java @@ -16,45 +16,45 @@ import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; -import net.sourceforge.stripes.validation.ValidationErrors; import net.sourceforge.stripes.config.ConfigurableComponent; +import net.sourceforge.stripes.validation.ValidationErrors; /** - *

    Interface for class(es) responsible for taking the String/String[] properties contained in the + * Interface for class(es) responsible for taking the String/String[] properties contained in the * HttpServletRequest and: + * *

      - *
    • Converting them to the rich type of the property on the target JavaBean
    • - *
    • Setting the properties on the JavaBean using the appropriate mechanism
    • + *
    • Converting them to the rich type of the property on the target JavaBean + *
    • Setting the properties on the JavaBean using the appropriate mechanism *
    - *

    * - *

    Implementations may also perform validations of the fields during binding. If validation + *

    Implementations may also perform validations of the fields during binding. If validation * errors occur then the collection of ValidationErrors contained within the ActionBeanContext - * should be populated before returning.

    + * should be populated before returning. * * @author Tim Fennell */ public interface ActionBeanPropertyBinder extends ConfigurableComponent { - /** - * Populates all the properties in the request which have a matching property in the target - * bean. If additional properties exist in the request which are not present in the bean a - * message should be logged, but binding should continue without throwing any errors. - * - * @param bean the ActionBean to bind properties to - * @param context the ActionBeanContext containing the current request - * @param validate true indicates that validation should be run, false indicates that only - * type conversion should occur - */ - ValidationErrors bind(ActionBean bean, ActionBeanContext context, boolean validate); + /** + * Populates all the properties in the request which have a matching property in the target bean. + * If additional properties exist in the request which are not present in the bean a message + * should be logged, but binding should continue without throwing any errors. + * + * @param bean the ActionBean to bind properties to + * @param context the ActionBeanContext containing the current request + * @param validate true indicates that validation should be run, false indicates that only type + * conversion should occur + */ + ValidationErrors bind(ActionBean bean, ActionBeanContext context, boolean validate); - /** - * Bind an individual property with the name specified to the bean supplied. - * - * @param bean the ActionBean to bind the property to - * @param propertyName the name (including nested, indexed and mapped property names) of the - * property being bound - * @param propertyValue the value to be bound to the property on the bean - * @throws Exception thrown if the property cannot be bound for any reason - */ - void bind(ActionBean bean, String propertyName, Object propertyValue) throws Exception; + /** + * Bind an individual property with the name specified to the bean supplied. + * + * @param bean the ActionBean to bind the property to + * @param propertyName the name (including nested, indexed and mapped property names) of the + * property being bound + * @param propertyValue the value to be bound to the property on the bean + * @throws Exception thrown if the property cannot be bound for any reason + */ + void bind(ActionBean bean, String propertyName, Object propertyValue) throws Exception; } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/ActionResolver.java b/stripes/src/main/java/net/sourceforge/stripes/controller/ActionResolver.java index a487ef304..32b975b8f 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/ActionResolver.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/ActionResolver.java @@ -14,158 +14,157 @@ */ package net.sourceforge.stripes.controller; +import java.lang.reflect.Method; +import java.util.Collection; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; -import net.sourceforge.stripes.exception.StripesServletException; import net.sourceforge.stripes.config.ConfigurableComponent; - -import java.lang.reflect.Method; -import java.util.Collection; +import net.sourceforge.stripes.exception.StripesServletException; /** - *

    Resolvers are responsible for locating ActionBean instances that can handle the submitted - * request. Once an appropriate ActionBean has been identified the ActionResolver is also - * responsible for identifying the individual method on the ActionBean class that should handle - * this specific request.

    + * Resolvers are responsible for locating ActionBean instances that can handle the submitted + * request. Once an appropriate ActionBean has been identified the ActionResolver is also + * responsible for identifying the individual method on the ActionBean class that should handle this + * specific request. * *

    Throughout this class two terms are used that refer to similar but not interchangeable - * concepts. {@code UrlBinding} refers to the exact URL to which a bean is bound, e.g. - * {@code /account/Profile.action}. {@code Path} refers to the path segment of the requested - * URL and is generally composed of the URL binding and possibly some additional information, - * e.g. {@code /account/Profile.action/edit}. In general the methods in this class are capable - * of taking in a {@code path} and extracting the {@code UrlBinding} from it.

    + * concepts. {@code UrlBinding} refers to the exact URL to which a bean is bound, e.g. {@code + * /account/Profile.action}. {@code Path} refers to the path segment of the requested URL and is + * generally composed of the URL binding and possibly some additional information, e.g. {@code + * /account/Profile.action/edit}. In general the methods in this class are capable of taking in a + * {@code path} and extracting the {@code UrlBinding} from it. * * @author Tim Fennell */ public interface ActionResolver extends ConfigurableComponent { - /** - * Key that is to be used by ActionResolvers to store, as a request attribute, the - * action that was resolved in the current request. The 'action' stored is always a String. - */ - String RESOLVED_ACTION = "__stripes_resolved_action"; + /** + * Key that is to be used by ActionResolvers to store, as a request attribute, the action that was + * resolved in the current request. The 'action' stored is always a String. + */ + String RESOLVED_ACTION = "__stripes_resolved_action"; - /** - * Returns the URL binding that is a substring of the path provided. For example, if there - * is an ActionBean bound to {@code /user/Profile.action}, invoking - * {@code getUrlBindingFromPath("/user/Profile.action/view"} should return - * {@code "/user/Profile.action"}. - * - * @param path the path being used to access an ActionBean, either in a form or link tag, - * or in a request that is hitting the DispatcherServlet. - * @return the UrlBinding of the ActionBean appropriate for the request, or null if the path - * supplied cannot be mapped to an ActionBean. - */ - String getUrlBindingFromPath(String path); + /** + * Returns the URL binding that is a substring of the path provided. For example, if there is an + * ActionBean bound to {@code /user/Profile.action}, invoking {@code + * getUrlBindingFromPath("/user/Profile.action/view"} should return {@code + * "/user/Profile.action"}. + * + * @param path the path being used to access an ActionBean, either in a form or link tag, or in a + * request that is hitting the DispatcherServlet. + * @return the UrlBinding of the ActionBean appropriate for the request, or null if the path + * supplied cannot be mapped to an ActionBean. + */ + String getUrlBindingFromPath(String path); - /** - * Resolves the Class, implementing ActionBean, that should be used to handle the request. - * If more than one class can be mapped to the request the results of this method are undefined - - * implementations may return one of the implementations located or throw an exception. - * - * @param context the ActionBeanContext for the current request - * @return an instance of ActionBean to handle the current request - * @throws StripesServletException thrown if a ActionBean cannot be resolved for any reason - */ - ActionBean getActionBean(ActionBeanContext context) throws StripesServletException; + /** + * Resolves the Class, implementing ActionBean, that should be used to handle the request. If more + * than one class can be mapped to the request the results of this method are undefined - + * implementations may return one of the implementations located or throw an exception. + * + * @param context the ActionBeanContext for the current request + * @return an instance of ActionBean to handle the current request + * @throws StripesServletException thrown if a ActionBean cannot be resolved for any reason + */ + ActionBean getActionBean(ActionBeanContext context) throws StripesServletException; - /** - * Returns the ActionBean class that responds to the path provided. If the path does - * not contain a UrlBinding to which an ActionBean is bound a StripesServletException - * will be thrown. - * - * @param context the current action bean context - * @param path the path segment of the request (or link or action) - * @return an instance of ActionBean that is bound to the UrlBinding contained within - * the path supplied - * @throws StripesServletException if a matching ActionBean cannot be found - */ - ActionBean getActionBean(ActionBeanContext context, String path) - throws StripesServletException; + /** + * Returns the ActionBean class that responds to the path provided. If the path does not contain a + * UrlBinding to which an ActionBean is bound a StripesServletException will be thrown. + * + * @param context the current action bean context + * @param path the path segment of the request (or link or action) + * @return an instance of ActionBean that is bound to the UrlBinding contained within the path + * supplied + * @throws StripesServletException if a matching ActionBean cannot be found + */ + ActionBean getActionBean(ActionBeanContext context, String path) throws StripesServletException; - /** - * Fetches the Class representing the type of ActionBean that has been bound to - * the URL contained within the path supplied. Will not cause any ActionBean to be - * instantiated. If no ActionBean has been bound to the URL then null will be returned. - * - * @param path the path segment of a request url or form action or link - * @return the class object for the type of action bean bound to the url, or - * null if no bean is bound to that url - */ - Class getActionBeanType(String path); + /** + * Fetches the Class representing the type of ActionBean that has been bound to the URL contained + * within the path supplied. Will not cause any ActionBean to be instantiated. If no ActionBean + * has been bound to the URL then null will be returned. + * + * @param path the path segment of a request url or form action or link + * @return the class object for the type of action bean bound to the url, or null if no bean is + * bound to that url + */ + Class getActionBeanType(String path); - /** - * Takes a class that implements ActionBean and returns the URL binding of that class. - * Essentially the inverse of the getActionBean() methods, this method allows you to find - * out the URL binding of any ActionBean class. The binding can then be used to generate - * URLs or for any other purpose. If the bean is not bound, this method may return null. - * - * @param clazz a class that implements ActionBean - * @return the UrlBinding or null if none can be determined - * @since Stripes 1.2 - */ - String getUrlBinding(Class clazz); + /** + * Takes a class that implements ActionBean and returns the URL binding of that class. Essentially + * the inverse of the getActionBean() methods, this method allows you to find out the URL binding + * of any ActionBean class. The binding can then be used to generate URLs or for any other + * purpose. If the bean is not bound, this method may return null. + * + * @param clazz a class that implements ActionBean + * @return the UrlBinding or null if none can be determined + * @since Stripes 1.2 + */ + String getUrlBinding(Class clazz); - /** - * Determines the name of the event fired by the front end. Allows implementations to - * easiliy vary their strategy for specifying event names (e.g. button names, hidden field - * rewrites via JavaScript etc.). - * - * @param bean the ActionBean type that has been bound to the request - * @param context the ActionBeanContext for the current request - * @return the name of the event fired by the front end, or null if none is found - */ - String getEventName(Class bean, ActionBeanContext context); + /** + * Determines the name of the event fired by the front end. Allows implementations to easiliy vary + * their strategy for specifying event names (e.g. button names, hidden field rewrites via + * JavaScript etc.). + * + * @param bean the ActionBean type that has been bound to the request + * @param context the ActionBeanContext for the current request + * @return the name of the event fired by the front end, or null if none is found + */ + String getEventName(Class bean, ActionBeanContext context); - /** - * Resolves the Method which handles the named event. If more than one method is declared as - * able to handle the event the results of this method are undefined - implementations may - * return one of the implementations or throw an exception. - * - * @param bean the ActionBean type that has been bound to the request - * @param eventName the named event being handled by the ActionBean - * @return a Method object representing the handling method - never null - * @throws StripesServletException thrown if a method cannot be resolved for any reason, - * including but not limited to, when a Method does not exist that handles the event. - */ - Method getHandler(Class bean, String eventName) throws StripesServletException; + /** + * Resolves the Method which handles the named event. If more than one method is declared as able + * to handle the event the results of this method are undefined - implementations may return one + * of the implementations or throw an exception. + * + * @param bean the ActionBean type that has been bound to the request + * @param eventName the named event being handled by the ActionBean + * @return a Method object representing the handling method - never null + * @throws StripesServletException thrown if a method cannot be resolved for any reason, including + * but not limited to, when a Method does not exist that handles the event. + */ + Method getHandler(Class bean, String eventName) + throws StripesServletException; - /** - * Locates and returns the default handler method that should be invoked when no specific - * event is named. This occurs most often when a user submits a form via the Enter button - * and no button or image button name is passed. - * - * @param bean the ActionBean type that has been bound to the request - * @return a Method object representing the handling method - never null - * @throws StripesServletException thrown if a default handler method cannot be found. - */ - Method getDefaultHandler(Class bean) throws StripesServletException; + /** + * Locates and returns the default handler method that should be invoked when no specific event is + * named. This occurs most often when a user submits a form via the Enter button and no button or + * image button name is passed. + * + * @param bean the ActionBean type that has been bound to the request + * @return a Method object representing the handling method - never null + * @throws StripesServletException thrown if a default handler method cannot be found. + */ + Method getDefaultHandler(Class bean) throws StripesServletException; - /** - * Returns the name of the event to which a given handler method responds. Primarily useful - * when the default event is fired and it is necessary to figure out if the handler also - * responds to a named event. - * - * @param handler the handler method who's event name to find - * @return String the name of the event handled by this method, or null if an event is - * not mapped to this method. - */ - String getHandledEvent(Method handler) throws StripesServletException; + /** + * Returns the name of the event to which a given handler method responds. Primarily useful when + * the default event is fired and it is necessary to figure out if the handler also responds to a + * named event. + * + * @param handler the handler method who's event name to find + * @return String the name of the event handled by this method, or null if an event is not mapped + * to this method. + */ + String getHandledEvent(Method handler) throws StripesServletException; - /** - * Get all the classes implementing {@link ActionBean} that are recognized by this - * {@link ActionResolver}. This method must return the full set of {@link ActionBean} classes - * after the call to init(). - */ - Collection> getActionBeanClasses(); + /** + * Get all the classes implementing {@link ActionBean} that are recognized by this {@link + * ActionResolver}. This method must return the full set of {@link ActionBean} classes after the + * call to init(). + */ + Collection> getActionBeanClasses(); - /** - * Gets the {@link ActionBean} type that matches actionBeanName. Implementers may use different - * strategies for naming {@link ActionBean}s. This method can return null if no - * action beans exist with the given actionBeanName or multiple action beans resolve to the same - * actionBeanName - * - * @param actionBeanName The name that identifies the {@link ActionBean} - * @return the ActionBean class that matches actionBeanName, or null if an ActionBean can't be resolved for the name - */ - Class getActionBeanByName(String actionBeanName); -} \ No newline at end of file + /** + * Gets the {@link ActionBean} type that matches actionBeanName. Implementers may use + * different strategies for naming {@link ActionBean}s. This method can return null if no action + * beans exist with the given actionBeanName or multiple action beans resolve to the + * same actionBeanName + * + * @param actionBeanName The name that identifies the {@link ActionBean} + * @return the ActionBean class that matches actionBeanName, or null if an ActionBean can't be + * resolved for the name + */ + Class getActionBeanByName(String actionBeanName); +} diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/AnnotatedClassActionResolver.java b/stripes/src/main/java/net/sourceforge/stripes/controller/AnnotatedClassActionResolver.java index 5d9017fc6..36f3efd81 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/AnnotatedClassActionResolver.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/AnnotatedClassActionResolver.java @@ -14,6 +14,19 @@ */ package net.sourceforge.stripes.controller; +import jakarta.servlet.http.HttpServletRequest; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.action.DefaultHandler; @@ -30,645 +43,643 @@ import net.sourceforge.stripes.util.ResolverUtil; import net.sourceforge.stripes.util.StringUtil; -import javax.servlet.http.HttpServletRequest; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - /** - *

    Uses Annotations on classes to identify the ActionBean that corresponds to the current - * request. ActionBeans are annotated with an {@code @UrlBinding} annotation, which denotes the - * web application relative URL that the ActionBean should respond to.

    + * Uses Annotations on classes to identify the ActionBean that corresponds to the current request. + * ActionBeans are annotated with an {@code @UrlBinding} annotation, which denotes the web + * application relative URL that the ActionBean should respond to. * *

    Individual methods on ActionBean classes are expected to be annotated with @HandlesEvent - * annotations, and potentially a @DefaultHandler annotation. Using these annotations the - * Resolver will determine which method should be executed for the current request.

    + * annotations, and potentially a @DefaultHandler annotation. Using these annotations the Resolver + * will determine which method should be executed for the current request. * * @see net.sourceforge.stripes.action.UrlBinding * @author Tim Fennell */ public class AnnotatedClassActionResolver implements ActionResolver { - /** - * Configuration key used to lookup a comma-separated list of package names. The - * packages (and their sub-packages) will be scanned for implementations of - * ActionBean. - * @since Stripes 1.5 - */ - public static final String PACKAGES = "ActionResolver.Packages"; - - /** Key used to store the default handler in the Map of handler methods. */ - private static final String DEFAULT_HANDLER_KEY = "__default_handler"; - - /** Log instance for use within in this class. */ - private static final Log log = Log.getInstance(AnnotatedClassActionResolver.class); - - /** Handle to the configuration. */ - private Configuration configuration; - - /** Parses {@link UrlBinding} values and maps request URLs to {@link ActionBean}s. */ - private UrlBindingFactory urlBindingFactory = new UrlBindingFactory(); - - /** Maps action bean classes simple name -> action bean class */ - protected final Map> actionBeansByName = - new ConcurrentHashMap>(); - - /** - * Map used to resolve the methods handling events within form beans. Maps the class - * representing a subclass of ActionBean to a Map of event names to Method objects. - */ - private Map,Map> eventMappings = - new HashMap,Map>() { - private static final long serialVersionUID = 1L; - - @Override - public Map get(Object key) { - Map value = super.get(key); - if (value == null) - return Collections.emptyMap(); - else - return value; - } - }; - - /** - * Scans the classpath of the current classloader (not including parents) to find implementations - * of the ActionBean interface. Examines annotations on the classes found to determine what - * forms and events they map to, and stores this information in a pair of maps for fast - * access during request processing. - */ - public void init(Configuration configuration) throws Exception { - this.configuration = configuration; - - // Process each ActionBean - for (Class clazz : findClasses()) { - addActionBean(clazz); + /** + * Configuration key used to lookup a comma-separated list of package names. The packages (and + * their sub-packages) will be scanned for implementations of ActionBean. + * + * @since Stripes 1.5 + */ + public static final String PACKAGES = "ActionResolver.Packages"; + + /** Key used to store the default handler in the Map of handler methods. */ + private static final String DEFAULT_HANDLER_KEY = "__default_handler"; + + /** Log instance for use within in this class. */ + private static final Log log = Log.getInstance(AnnotatedClassActionResolver.class); + + /** Handle to the configuration. */ + private Configuration configuration; + + /** Parses {@link UrlBinding} values and maps request URLs to {@link ActionBean}s. */ + private UrlBindingFactory urlBindingFactory = new UrlBindingFactory(); + + /** Maps action bean classes simple name -> action bean class */ + protected final Map> actionBeansByName = + new ConcurrentHashMap>(); + + /** + * Map used to resolve the methods handling events within form beans. Maps the class representing + * a subclass of ActionBean to a Map of event names to Method objects. + */ + private Map, Map> eventMappings = + new HashMap, Map>() { + private static final long serialVersionUID = 1L; + + @Override + public Map get(Object key) { + Map value = super.get(key); + if (value == null) return Collections.emptyMap(); + else return value; } - - addBeanNameMappings(); + }; + + /** + * Scans the classpath of the current classloader (not including parents) to find implementations + * of the ActionBean interface. Examines annotations on the classes found to determine what forms + * and events they map to, and stores this information in a pair of maps for fast access during + * request processing. + */ + public void init(Configuration configuration) throws Exception { + this.configuration = configuration; + + // Process each ActionBean + for (Class clazz : findClasses()) { + addActionBean(clazz); } - protected void addBeanNameMappings() { - Set foundBeanNames = new HashSet(); - for (Class clazz : getActionBeanClasses()) { - if (foundBeanNames.contains(clazz.getSimpleName())) { - log.warn("Found multiple action beans with the same simple name: ", clazz.getSimpleName(), ". You will " + - "need to reference these action beans by their fully qualified names"); - actionBeansByName.remove(clazz.getSimpleName()); - continue; - } - - foundBeanNames.add(clazz.getSimpleName()); - actionBeansByName.put(clazz.getSimpleName(), clazz); - } + addBeanNameMappings(); + } + + protected void addBeanNameMappings() { + Set foundBeanNames = new HashSet(); + for (Class clazz : getActionBeanClasses()) { + if (foundBeanNames.contains(clazz.getSimpleName())) { + log.warn( + "Found multiple action beans with the same simple name: ", + clazz.getSimpleName(), + ". You will " + "need to reference these action beans by their fully qualified names"); + actionBeansByName.remove(clazz.getSimpleName()); + continue; + } + + foundBeanNames.add(clazz.getSimpleName()); + actionBeansByName.put(clazz.getSimpleName(), clazz); } - - /** Get the {@link UrlBindingFactory} that is being used by this action resolver. */ - public UrlBindingFactory getUrlBindingFactory() { - return urlBindingFactory; + } + + /** Get the {@link UrlBindingFactory} that is being used by this action resolver. */ + public UrlBindingFactory getUrlBindingFactory() { + return urlBindingFactory; + } + + /** + * Adds an ActionBean class to the set that this resolver can resolve. Identifies the URL binding + * and the events managed by the class and stores them in Maps for fast lookup. + * + * @param clazz a class that implements ActionBean + */ + protected void addActionBean(Class clazz) { + // Ignore abstract classes + if (Modifier.isAbstract(clazz.getModifiers()) || clazz.isAnnotationPresent(DontAutoLoad.class)) + return; + + String binding = getUrlBinding(clazz); + if (binding == null) return; + + // make sure mapping exists in cache + UrlBinding proto = getUrlBindingFactory().getBindingPrototype(clazz); + if (proto == null) { + getUrlBindingFactory().addBinding(clazz, new UrlBinding(clazz, binding)); } - /** - * Adds an ActionBean class to the set that this resolver can resolve. Identifies - * the URL binding and the events managed by the class and stores them in Maps - * for fast lookup. - * - * @param clazz a class that implements ActionBean - */ - protected void addActionBean(Class clazz) { - // Ignore abstract classes - if (Modifier.isAbstract(clazz.getModifiers()) || clazz.isAnnotationPresent(DontAutoLoad.class)) - return; - - String binding = getUrlBinding(clazz); - if (binding == null) - return; - - // make sure mapping exists in cache - UrlBinding proto = getUrlBindingFactory().getBindingPrototype(clazz); - if (proto == null) { - getUrlBindingFactory().addBinding(clazz, new UrlBinding(clazz, binding)); - } - - // Construct the mapping of event->method for the class - Map classMappings = new HashMap(); - processMethods(clazz, classMappings); + // Construct the mapping of event->method for the class + Map classMappings = new HashMap(); + processMethods(clazz, classMappings); - // Put the event->method mapping for the class into the set of mappings - this.eventMappings.put(clazz, classMappings); + // Put the event->method mapping for the class into the set of mappings + this.eventMappings.put(clazz, classMappings); - if (proto != null) { - proto.initDefaultValueWithDefaultHandlerIfNeeded(this); - } - - if (log.getRealLog().isDebugEnabled()) { - // Print out the event mappings nicely - for (Map.Entry entry : classMappings.entrySet()) { - String event = entry.getKey(); - Method handler = entry.getValue(); - boolean isDefault = DEFAULT_HANDLER_KEY.equals(event); - - log.debug("Bound: ", clazz.getSimpleName(), ".", handler.getName(), "() ==> ", - binding, isDefault ? "" : "?" + event); - } - } + if (proto != null) { + proto.initDefaultValueWithDefaultHandlerIfNeeded(this); } - /** - * Removes an ActionBean class from the set that this resolver can resolve. The URL binding - * and the events managed by the class are removed from the cache. - * - * @param clazz a class that implements ActionBean - */ - protected void removeActionBean(Class clazz) { - String binding = getUrlBinding(clazz); - if (binding != null) { - getUrlBindingFactory().removeBinding(clazz); - } - eventMappings.remove(clazz); + if (log.getRealLog().isDebugEnabled()) { + // Print out the event mappings nicely + for (Map.Entry entry : classMappings.entrySet()) { + String event = entry.getKey(); + Method handler = entry.getValue(); + boolean isDefault = DEFAULT_HANDLER_KEY.equals(event); + + log.debug( + "Bound: ", + clazz.getSimpleName(), + ".", + handler.getName(), + "() ==> ", + binding, + isDefault ? "" : "?" + event); + } } - - /** - * Returns the URL binding that is a substring of the path provided. For example, if there - * is an ActionBean bound to {@code /user/Profile.action} the path - * {@code /user/Profile.action/view} would return {@code /user/Profile.action}. - * - * @param path the path being used to access an ActionBean, either in a form or link tag, - * or in a request that is hitting the DispatcherServlet. - * @return the UrlBinding of the ActionBean appropriate for the request, or null if the path - * supplied cannot be mapped to an ActionBean. - */ - public String getUrlBindingFromPath(String path) { - UrlBinding mapping = getUrlBindingFactory().getBindingPrototype(path); - return mapping == null ? null : mapping.toString(); + } + + /** + * Removes an ActionBean class from the set that this resolver can resolve. The URL binding and + * the events managed by the class are removed from the cache. + * + * @param clazz a class that implements ActionBean + */ + protected void removeActionBean(Class clazz) { + String binding = getUrlBinding(clazz); + if (binding != null) { + getUrlBindingFactory().removeBinding(clazz); } - - /** - * Takes a class that implements ActionBean and returns the URL binding of that class. - * The default implementation retrieves the UrlBinding annotations and returns its - * value. Subclasses could do more complex things like parse the class and package names - * and construct a "default" binding when one is not specified. - * - * @param clazz a class that implements ActionBean - * @return the UrlBinding or null if none can be determined - */ - public String getUrlBinding(Class clazz) { - UrlBinding mapping = getUrlBindingFactory().getBindingPrototype(clazz); - return mapping == null ? null : mapping.toString(); + eventMappings.remove(clazz); + } + + /** + * Returns the URL binding that is a substring of the path provided. For example, if there is an + * ActionBean bound to {@code /user/Profile.action} the path {@code /user/Profile.action/view} + * would return {@code /user/Profile.action}. + * + * @param path the path being used to access an ActionBean, either in a form or link tag, or in a + * request that is hitting the DispatcherServlet. + * @return the UrlBinding of the ActionBean appropriate for the request, or null if the path + * supplied cannot be mapped to an ActionBean. + */ + public String getUrlBindingFromPath(String path) { + UrlBinding mapping = getUrlBindingFactory().getBindingPrototype(path); + return mapping == null ? null : mapping.toString(); + } + + /** + * Takes a class that implements ActionBean and returns the URL binding of that class. The default + * implementation retrieves the UrlBinding annotations and returns its value. Subclasses could do + * more complex things like parse the class and package names and construct a "default" binding + * when one is not specified. + * + * @param clazz a class that implements ActionBean + * @return the UrlBinding or null if none can be determined + */ + public String getUrlBinding(Class clazz) { + UrlBinding mapping = getUrlBindingFactory().getBindingPrototype(clazz); + return mapping == null ? null : mapping.toString(); + } + + /** + * Helper method that examines a class, starting at it's highest super class and working it's way + * down again, to find method annotations and ensure that child class annotations take precedence. + */ + protected void processMethods(Class clazz, Map classMappings) { + // Do the super class first if there is one + Class superclass = clazz.getSuperclass(); + if (superclass != null) { + processMethods(superclass, classMappings); } - /** - * Helper method that examines a class, starting at it's highest super class and - * working it's way down again, to find method annotations and ensure that child - * class annotations take precedence. - */ - protected void processMethods(Class clazz, Map classMappings) { - // Do the super class first if there is one - Class superclass = clazz.getSuperclass(); - if (superclass != null) { - processMethods(superclass, classMappings); + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + if (Modifier.isPublic(method.getModifiers()) && !method.isBridge()) { + String eventName = getHandledEvent(method); + + // look for duplicate event names within the current class + if (classMappings.containsKey(eventName) + && clazz.equals(classMappings.get(eventName).getDeclaringClass())) { + throw new StripesRuntimeException( + "The ActionBean " + + clazz + + " declares multiple event handlers for event '" + + eventName + + "'"); } - Method[] methods = clazz.getDeclaredMethods(); - for (Method method : methods) { - if ( Modifier.isPublic(method.getModifiers()) && !method.isBridge() ) { - String eventName = getHandledEvent(method); - - // look for duplicate event names within the current class - if (classMappings.containsKey(eventName) - && clazz.equals(classMappings.get(eventName).getDeclaringClass())) { - throw new StripesRuntimeException("The ActionBean " + clazz - + " declares multiple event handlers for event '" + eventName + "'"); - } - - DefaultHandler defaultMapping = method.getAnnotation(DefaultHandler.class); - if (eventName != null) { - classMappings.put(eventName, method); - } - if (defaultMapping != null) { - // look for multiple default handlers within the current class - if (classMappings.containsKey(DEFAULT_HANDLER_KEY) - && clazz.equals(classMappings.get(DEFAULT_HANDLER_KEY).getDeclaringClass())) { - throw new StripesRuntimeException("The ActionBean " + clazz - + " declares multiple default event handlers"); - } - - // Makes sure we catch the default handler - classMappings.put(DEFAULT_HANDLER_KEY, method); - } - } + DefaultHandler defaultMapping = method.getAnnotation(DefaultHandler.class); + if (eventName != null) { + classMappings.put(eventName, method); } - } + if (defaultMapping != null) { + // look for multiple default handlers within the current class + if (classMappings.containsKey(DEFAULT_HANDLER_KEY) + && clazz.equals(classMappings.get(DEFAULT_HANDLER_KEY).getDeclaringClass())) { + throw new StripesRuntimeException( + "The ActionBean " + clazz + " declares multiple default event handlers"); + } - /** - * Responsible for determining the name of the event handled by this method, if indeed - * it handles one at all. By default looks for the HandlesEvent annotations and returns - * it's value if present. - * - * @param handler a method that might or might not be a handler method - * @return the name of the event handled, or null - */ - public String getHandledEvent(Method handler) { - HandlesEvent mapping = handler.getAnnotation(HandlesEvent.class); - if (mapping != null) { - return mapping.value(); - } - else { - return null; + // Makes sure we catch the default handler + classMappings.put(DEFAULT_HANDLER_KEY, method); } + } } - - /** - *

    Fetches the Class representing the type of ActionBean that would respond were a - * request made with the path specified. Checks to see if the full path matches any - * bean's UrlBinding. If no ActionBean matches then successively removes path segments - * (separated by slashes) from the end of the path until a match is found.

    - * - * @param path the path segment of a URL - * @return the Class object for the type of action bean that will respond if a request - * is made using the path specified or null if no ActionBean matches. - */ - public Class getActionBeanType(String path) { - UrlBinding binding = getUrlBindingFactory().getBindingPrototype(path); - return binding == null ? null : binding.getBeanType(); + } + + /** + * Responsible for determining the name of the event handled by this method, if indeed it handles + * one at all. By default looks for the HandlesEvent annotations and returns it's value if + * present. + * + * @param handler a method that might or might not be a handler method + * @return the name of the event handled, or null + */ + public String getHandledEvent(Method handler) { + HandlesEvent mapping = handler.getAnnotation(HandlesEvent.class); + if (mapping != null) { + return mapping.value(); + } else { + return null; } - - /** - * Gets the logical name of the ActionBean that should handle the request. Implemented to look - * up the name of the form based on the name assigned to the form in the form tag, and - * encoded in a hidden field. - * - * @param context the ActionBeanContext for the current request - * @return the name of the form to be used for this request - */ - public ActionBean getActionBean(ActionBeanContext context) throws StripesServletException { - HttpServletRequest request = context.getRequest(); - String path = HttpUtil.getRequestedPath(request); - ActionBean bean = getActionBean(context, path); - request.setAttribute(RESOLVED_ACTION, getUrlBindingFromPath(path)); - return bean; + } + + /** + * Fetches the Class representing the type of ActionBean that would respond were a request made + * with the path specified. Checks to see if the full path matches any bean's UrlBinding. If no + * ActionBean matches then successively removes path segments (separated by slashes) from the end + * of the path until a match is found. + * + * @param path the path segment of a URL + * @return the Class object for the type of action bean that will respond if a request is made + * using the path specified or null if no ActionBean matches. + */ + public Class getActionBeanType(String path) { + UrlBinding binding = getUrlBindingFactory().getBindingPrototype(path); + return binding == null ? null : binding.getBeanType(); + } + + /** + * Gets the logical name of the ActionBean that should handle the request. Implemented to look up + * the name of the form based on the name assigned to the form in the form tag, and encoded in a + * hidden field. + * + * @param context the ActionBeanContext for the current request + * @return the name of the form to be used for this request + */ + public ActionBean getActionBean(ActionBeanContext context) throws StripesServletException { + HttpServletRequest request = context.getRequest(); + String path = HttpUtil.getRequestedPath(request); + ActionBean bean = getActionBean(context, path); + request.setAttribute(RESOLVED_ACTION, getUrlBindingFromPath(path)); + return bean; + } + + /** + * Returns the ActionBean class that is bound to the UrlBinding supplied. If the action bean + * already exists in the appropriate scope (request or session) then the existing instance will be + * supplied. If not, then a new instance will be manufactured and have the supplied + * ActionBeanContext set on it. + * + * @param path a URL to which an ActionBean is bound, or a path starting with the URL to which an + * ActionBean has been bound. + * @param context the current ActionBeanContext + * @return a Class for the ActionBean requested + * @throws StripesServletException if the UrlBinding does not match an ActionBean binding + */ + public ActionBean getActionBean(ActionBeanContext context, String path) + throws StripesServletException { + Class beanClass = getActionBeanType(path); + ActionBean bean; + + if (beanClass == null) { + throw new ActionBeanNotFoundException(path, getUrlBindingFactory().getPathMap()); } - /** - * Returns the ActionBean class that is bound to the UrlBinding supplied. If the action - * bean already exists in the appropriate scope (request or session) then the existing - * instance will be supplied. If not, then a new instance will be manufactured and have - * the supplied ActionBeanContext set on it. - * - * @param path a URL to which an ActionBean is bound, or a path starting with the URL - * to which an ActionBean has been bound. - * @param context the current ActionBeanContext - * @return a Class for the ActionBean requested - * @throws StripesServletException if the UrlBinding does not match an ActionBean binding - */ - public ActionBean getActionBean(ActionBeanContext context, String path) throws StripesServletException { - Class beanClass = getActionBeanType(path); - ActionBean bean; - - if (beanClass == null) { - throw new ActionBeanNotFoundException(path, getUrlBindingFactory().getPathMap()); - } - - String bindingPath = getUrlBinding(beanClass); - try { - HttpServletRequest request = context.getRequest(); - - if (beanClass.isAnnotationPresent(SessionScope.class)) { - bean = (ActionBean) request.getSession().getAttribute(bindingPath); - - if (bean == null) { - bean = makeNewActionBean(beanClass, context); - request.getSession().setAttribute(bindingPath, bean); - } - } - else { - bean = (ActionBean) request.getAttribute(bindingPath); - if (bean == null) { - bean = makeNewActionBean(beanClass, context); - request.setAttribute(bindingPath, bean); - } - } - - setActionBeanContext(bean, context); - } - catch (Exception e) { - StripesServletException sse = new StripesServletException( - "Could not create instance of ActionBean type [" + beanClass.getName() + "].", e); - throw sse; - } - - assertGetContextWorks(bean); - return bean; + String bindingPath = getUrlBinding(beanClass); + try { + HttpServletRequest request = context.getRequest(); - } + if (beanClass.isAnnotationPresent(SessionScope.class)) { + bean = (ActionBean) request.getSession().getAttribute(bindingPath); - /** - * Calls {@link ActionBean#setContext(ActionBeanContext)} with the given {@code context} only if - * necessary. Subclasses should use this method instead of setting the context directly because - * it can be somewhat tricky to determine when it needs to be done. - * - * @param bean The bean whose context may need to be set. - * @param context The context to pass to the bean if necessary. - */ - protected void setActionBeanContext(ActionBean bean, ActionBeanContext context) { - ActionBeanContext abcFromBean = bean.getContext(); - if (abcFromBean == null) { - bean.setContext(context); + if (bean == null) { + bean = makeNewActionBean(beanClass, context); + request.getSession().setAttribute(bindingPath, bean); } - else { - StripesRequestWrapper wrapperFromBean = StripesRequestWrapper - .findStripesWrapper(abcFromBean.getRequest()); - StripesRequestWrapper wrapperFromRequest = StripesRequestWrapper - .findStripesWrapper(context.getRequest()); - if (wrapperFromBean != wrapperFromRequest) - bean.setContext(context); - } - } - - /** - * Since many down stream parts of Stripes rely on the ActionBean properly returning the - * context it is given, we'll just test it up front. Called after the bean is instantiated. - * - * @param bean the ActionBean to test to see if getContext() works correctly - * @throws StripesServletException if getContext() returns null - */ - protected void assertGetContextWorks(final ActionBean bean) throws StripesServletException { - if (bean.getContext() == null) { - throw new StripesServletException("Ahem. Stripes has just resolved and instantiated " + - "the ActionBean class " + bean.getClass().getName() + " and set the ActionBeanContext " + - "on it. However calling getContext() isn't returning the context back! Since " + - "this is required for several parts of Stripes to function correctly you should " + - "now stop and implement setContext()/getContext() correctly. Thank you."); + } else { + bean = (ActionBean) request.getAttribute(bindingPath); + if (bean == null) { + bean = makeNewActionBean(beanClass, context); + request.setAttribute(bindingPath, bean); } + } + + setActionBeanContext(bean, context); + } catch (Exception e) { + StripesServletException sse = + new StripesServletException( + "Could not create instance of ActionBean type [" + beanClass.getName() + "].", e); + throw sse; } - /** - * Helper method to construct and return a new ActionBean instance. Called whenever a new - * instance needs to be manufactured. Provides a convenient point for subclasses to add - * specific behaviour during action bean creation. - * - * @param type the type of ActionBean to create - * @param context the current ActionBeanContext - * @return the new ActionBean instance - * @throws Exception if anything goes wrong! - */ - protected ActionBean makeNewActionBean(Class type, ActionBeanContext context) - throws Exception { - - return getConfiguration().getObjectFactory().newInstance(type); + assertGetContextWorks(bean); + return bean; + } + + /** + * Calls {@link ActionBean#setContext(ActionBeanContext)} with the given {@code context} only if + * necessary. Subclasses should use this method instead of setting the context directly because it + * can be somewhat tricky to determine when it needs to be done. + * + * @param bean The bean whose context may need to be set. + * @param context The context to pass to the bean if necessary. + */ + protected void setActionBeanContext(ActionBean bean, ActionBeanContext context) { + ActionBeanContext abcFromBean = bean.getContext(); + if (abcFromBean == null) { + bean.setContext(context); + } else { + StripesRequestWrapper wrapperFromBean = + StripesRequestWrapper.findStripesWrapper(abcFromBean.getRequest()); + StripesRequestWrapper wrapperFromRequest = + StripesRequestWrapper.findStripesWrapper(context.getRequest()); + if (wrapperFromBean != wrapperFromRequest) bean.setContext(context); } - - - /** - *

    - * Try various means to determine which event is to be executed on the current ActionBean. If a - * 'special' request attribute ({@link StripesConstants#REQ_ATTR_EVENT_NAME}) is present in - * the request, then return its value. This attribute is used to handle internal forwards, when - * request parameters are merged and cannot reliably determine the desired event name. - *

    - * - *

    - * If that doesn't work, the value of a 'special' request parameter ({@link StripesConstants#URL_KEY_EVENT_NAME}) - * is checked to see if contains a single value matching an event name. - *

    - * - *

    - * Failing that, search for a parameter in the request whose name matches one of the named - * events handled by the ActionBean. For example, if the ActionBean can handle events foo and - * bar, this method will scan the request for foo=somevalue and bar=somevalue. If it finds a - * request parameter with a matching name it will return that name. If there are multiple - * matching names, the result of this method cannot be guaranteed and a - * {@link StripesRuntimeException} will be thrown. - *

    - * - *

    - * Finally, if the event name cannot be determined through the parameter names and there is - * extra path information beyond the URL binding of the ActionBean, it is checked to see if it - * matches an event name. - *

    - * - * @param bean the ActionBean type bound to the request - * @param context the ActionBeanContect for the current request - * @return String the name of the event submitted, or null if none can be found - */ - public String getEventName(Class bean, ActionBeanContext context) { - String event = getEventNameFromRequestAttribute(bean, context); - if (event == null) event = getEventNameFromEventNameParam(bean, context); - if (event == null) event = getEventNameFromRequestParams(bean, context); - if (event == null) event = getEventNameFromPath(bean, context); - return event; + } + + /** + * Since many down stream parts of Stripes rely on the ActionBean properly returning the context + * it is given, we'll just test it up front. Called after the bean is instantiated. + * + * @param bean the ActionBean to test to see if getContext() works correctly + * @throws StripesServletException if getContext() returns null + */ + protected void assertGetContextWorks(final ActionBean bean) throws StripesServletException { + if (bean.getContext() == null) { + throw new StripesServletException( + "Ahem. Stripes has just resolved and instantiated " + + "the ActionBean class " + + bean.getClass().getName() + + " and set the ActionBeanContext " + + "on it. However calling getContext() isn't returning the context back! Since " + + "this is required for several parts of Stripes to function correctly you should " + + "now stop and implement setContext()/getContext() correctly. Thank you."); } - - /** - * Checks a special request attribute to get the event name. This attribute - * may be set when the presence of the original request parameters on a - * forwarded request makes it difficult to determine which event to fire. - * - * @param bean the ActionBean type bound to the request - * @param context the ActionBeanContect for the current request - * @return the name of the event submitted, or null if none can be found - * @see StripesConstants#REQ_ATTR_EVENT_NAME - */ - protected String getEventNameFromRequestAttribute( - Class bean, ActionBeanContext context) { - return (String) context.getRequest().getAttribute( - StripesConstants.REQ_ATTR_EVENT_NAME); + } + + /** + * Helper method to construct and return a new ActionBean instance. Called whenever a new instance + * needs to be manufactured. Provides a convenient point for subclasses to add specific behaviour + * during action bean creation. + * + * @param type the type of ActionBean to create + * @param context the current ActionBeanContext + * @return the new ActionBean instance + * @throws Exception if anything goes wrong! + */ + protected ActionBean makeNewActionBean( + Class type, ActionBeanContext context) throws Exception { + + return getConfiguration().getObjectFactory().newInstance(type); + } + + /** + * Try various means to determine which event is to be executed on the current ActionBean. If a + * 'special' request attribute ({@link StripesConstants#REQ_ATTR_EVENT_NAME}) is present in the + * request, then return its value. This attribute is used to handle internal forwards, when + * request parameters are merged and cannot reliably determine the desired event name. + * + *

    If that doesn't work, the value of a 'special' request parameter ({@link + * StripesConstants#URL_KEY_EVENT_NAME}) is checked to see if contains a single value matching an + * event name. + * + *

    Failing that, search for a parameter in the request whose name matches one of the named + * events handled by the ActionBean. For example, if the ActionBean can handle events foo and bar, + * this method will scan the request for foo=somevalue and bar=somevalue. If it finds a request + * parameter with a matching name it will return that name. If there are multiple matching names, + * the result of this method cannot be guaranteed and a {@link StripesRuntimeException} will be + * thrown. + * + *

    Finally, if the event name cannot be determined through the parameter names and there is + * extra path information beyond the URL binding of the ActionBean, it is checked to see if it + * matches an event name. + * + * @param bean the ActionBean type bound to the request + * @param context the ActionBeanContect for the current request + * @return String the name of the event submitted, or null if none can be found + */ + public String getEventName(Class bean, ActionBeanContext context) { + String event = getEventNameFromRequestAttribute(bean, context); + if (event == null) event = getEventNameFromEventNameParam(bean, context); + if (event == null) event = getEventNameFromRequestParams(bean, context); + if (event == null) event = getEventNameFromPath(bean, context); + return event; + } + + /** + * Checks a special request attribute to get the event name. This attribute may be set when the + * presence of the original request parameters on a forwarded request makes it difficult to + * determine which event to fire. + * + * @param bean the ActionBean type bound to the request + * @param context the ActionBeanContect for the current request + * @return the name of the event submitted, or null if none can be found + * @see StripesConstants#REQ_ATTR_EVENT_NAME + */ + protected String getEventNameFromRequestAttribute( + Class bean, ActionBeanContext context) { + return (String) context.getRequest().getAttribute(StripesConstants.REQ_ATTR_EVENT_NAME); + } + + /** + * Loops through the set of known events for the ActionBean to see if the event names are present + * as parameter names in the request. Returns the first event name found in the request, or null + * if none is found. + * + * @param bean the ActionBean type bound to the request + * @param context the ActionBeanContext for the current request + * @return String the name of the event submitted, or null if none can be found + */ + @SuppressWarnings("unchecked") + protected String getEventNameFromRequestParams( + Class bean, ActionBeanContext context) { + + List eventParams = new ArrayList(); + Map parameterMap = context.getRequest().getParameterMap(); + for (String event : this.eventMappings.get(bean).keySet()) { + if (parameterMap.containsKey(event) || parameterMap.containsKey(event + ".x")) { + eventParams.add(event); + } } - /** - * Loops through the set of known events for the ActionBean to see if the event - * names are present as parameter names in the request. Returns the first event - * name found in the request, or null if none is found. - * - * @param bean the ActionBean type bound to the request - * @param context the ActionBeanContext for the current request - * @return String the name of the event submitted, or null if none can be found - */ - @SuppressWarnings("unchecked") - protected String getEventNameFromRequestParams(Class bean, - ActionBeanContext context) { - - List eventParams = new ArrayList(); - Map parameterMap = context.getRequest().getParameterMap(); - for (String event : this.eventMappings.get(bean).keySet()) { - if (parameterMap.containsKey(event) || parameterMap.containsKey(event + ".x")) { - eventParams.add(event); - } - } - - if (eventParams.size() == 0) { - return null; - } - else if (eventParams.size() == 1) { - return eventParams.get(0); - } - else { - throw new StripesRuntimeException("Multiple event parameters " + eventParams - + " are present in this request. Only one event parameter may be specified " - + "per request. Otherwise, Stripes would be unable to determine which event " - + "to execute."); - } + if (eventParams.size() == 0) { + return null; + } else if (eventParams.size() == 1) { + return eventParams.get(0); + } else { + throw new StripesRuntimeException( + "Multiple event parameters " + + eventParams + + " are present in this request. Only one event parameter may be specified " + + "per request. Otherwise, Stripes would be unable to determine which event " + + "to execute."); } - - /** - * Looks to see if there is extra path information beyond simply the url binding of the - * bean. If it does and the next /-separated part of the path matches one of the known - * event names for the bean, that event name will be returned, otherwise null. - * - * @param bean the ActionBean type bound to the request - * @param context the ActionBeanContect for the current request - * @return String the name of the event submitted, or null if none can be found - */ - protected String getEventNameFromPath(Class bean, - ActionBeanContext context) { - Map mappings = this.eventMappings.get(bean); - String path = HttpUtil.getRequestedPath(context.getRequest()); - UrlBinding prototype = getUrlBindingFactory().getBindingPrototype(path); - String binding = prototype == null ? null : prototype.getPath(); - - if (binding != null && path.length() != binding.length()) { - String extra = path.substring(binding.length() + 1); - int index = extra.indexOf("/"); - String event = extra.substring(0, (index != -1) ? index : extra.length()); - if (mappings.containsKey(event)) { - return event; - } - } - - return null; - } - - /** - * Looks to see if there is a single non-empty parameter value for the parameter name - * specified by {@link StripesConstants#URL_KEY_EVENT_NAME}. If there is, and it - * matches a known event it is returned, otherwise returns null. - * - * @param bean the ActionBean type bound to the request - * @param context the ActionBeanContect for the current request - * @return String the name of the event submitted, or null if none can be found - */ - protected String getEventNameFromEventNameParam(Class bean, - ActionBeanContext context) { - String[] values = context.getRequest().getParameterValues(StripesConstants.URL_KEY_EVENT_NAME); - String event = null; - if (values != null && values.length == 1 && this.eventMappings.get(bean).containsKey(values[0])) { - event = values[0]; - } - - // Warn of non-backward-compatible behavior - if (event != null) { - try { - String otherName = getEventNameFromRequestParams(bean, context); - if (otherName != null && !otherName.equals(event)) { - String[] otherValue = context.getRequest().getParameterValues(otherName); - log.warn("The event name was specified by two request parameters: ", - StripesConstants.URL_KEY_EVENT_NAME, "=", event, " and ", otherName, - "=", Arrays.toString(otherValue), ". ", "As of Stripes 1.5, ", - StripesConstants.URL_KEY_EVENT_NAME, - " overrides all other request parameters."); - } - } - catch (StripesRuntimeException e) { - // Ignore this. It means there were too many event params, which is OK in this case. - } - } - + } + + /** + * Looks to see if there is extra path information beyond simply the url binding of the bean. If + * it does and the next /-separated part of the path matches one of the known event names for the + * bean, that event name will be returned, otherwise null. + * + * @param bean the ActionBean type bound to the request + * @param context the ActionBeanContect for the current request + * @return String the name of the event submitted, or null if none can be found + */ + protected String getEventNameFromPath( + Class bean, ActionBeanContext context) { + Map mappings = this.eventMappings.get(bean); + String path = HttpUtil.getRequestedPath(context.getRequest()); + UrlBinding prototype = getUrlBindingFactory().getBindingPrototype(path); + String binding = prototype == null ? null : prototype.getPath(); + + if (binding != null && path.length() != binding.length()) { + String extra = path.substring(binding.length() + 1); + int index = extra.indexOf("/"); + String event = extra.substring(0, (index != -1) ? index : extra.length()); + if (mappings.containsKey(event)) { return event; + } } - /** - * Uses the Maps constructed earlier to locate the Method which can handle the event. - * - * @param bean the subclass of ActionBean that is bound to the request. - * @param eventName the name of the event being handled - * @return a Method object representing the handling method. - * @throws StripesServletException thrown when no method handles the named event. - */ - public Method getHandler(Class bean, String eventName) - throws StripesServletException { - Map mappings = this.eventMappings.get(bean); - Method handler = mappings.get(eventName); - - // If we could not find a handler then we should blow up quickly - if (handler == null) { - throw new StripesServletException( - "Could not find handler method for event name [" + eventName + "] on class [" + - bean.getName() + "]. Known handler mappings are: " + mappings); - } - - return handler; + return null; + } + + /** + * Looks to see if there is a single non-empty parameter value for the parameter name specified by + * {@link StripesConstants#URL_KEY_EVENT_NAME}. If there is, and it matches a known event it is + * returned, otherwise returns null. + * + * @param bean the ActionBean type bound to the request + * @param context the ActionBeanContect for the current request + * @return String the name of the event submitted, or null if none can be found + */ + protected String getEventNameFromEventNameParam( + Class bean, ActionBeanContext context) { + String[] values = context.getRequest().getParameterValues(StripesConstants.URL_KEY_EVENT_NAME); + String event = null; + if (values != null + && values.length == 1 + && this.eventMappings.get(bean).containsKey(values[0])) { + event = values[0]; } - /** - * Returns the Method that is the default handler for events in the ActionBean class supplied. - * If only one handler method is defined in the class, that is assumed to be the default. If - * there is more than one then the method marked with @DefaultHandler will be returned. - * - * @param bean the ActionBean type bound to the request - * @return Method object that should handle the request - * @throws StripesServletException if no default handler could be located - */ - public Method getDefaultHandler(Class bean) throws StripesServletException { - Map handlers = this.eventMappings.get(bean); - - if (handlers.size() == 1) { - return handlers.values().iterator().next(); - } - else { - Method handler = handlers.get(DEFAULT_HANDLER_KEY); - if (handler != null) return handler; + // Warn of non-backward-compatible behavior + if (event != null) { + try { + String otherName = getEventNameFromRequestParams(bean, context); + if (otherName != null && !otherName.equals(event)) { + String[] otherValue = context.getRequest().getParameterValues(otherName); + log.warn( + "The event name was specified by two request parameters: ", + StripesConstants.URL_KEY_EVENT_NAME, + "=", + event, + " and ", + otherName, + "=", + Arrays.toString(otherValue), + ". ", + "As of Stripes 1.5, ", + StripesConstants.URL_KEY_EVENT_NAME, + " overrides all other request parameters."); } - - // If we get this far, there is no sensible default! Kaboom! - throw new StripesServletException("No default handler could be found for ActionBean of " + - "type: " + bean.getName()); + } catch (StripesRuntimeException e) { + // Ignore this. It means there were too many event params, which is OK in this case. + } } - /** Provides subclasses with access to the configuration object. */ - protected Configuration getConfiguration() { return this.configuration; } - - /** - * Helper method to find implementations of ActionBean in the packages specified in - * Configuration using the {@link ResolverUtil} class. - * - * @return a set of Class objects that represent subclasses of ActionBean - */ - protected Set> findClasses() { - BootstrapPropertyResolver bootstrap = getConfiguration().getBootstrapPropertyResolver(); - - String packages = bootstrap.getProperty(PACKAGES); - if (packages == null) { - throw new StripesRuntimeException( - "You must supply a value for the configuration parameter '" + PACKAGES + "'. The " + - "value should be a list of one or more package roots (comma separated) that are " + - "to be scanned for ActionBean implementations. The packages specified and all " + - "subpackages are examined for implementations of ActionBean." - ); - } - - String[] pkgs = StringUtil.standardSplit(packages); - ResolverUtil resolver = new ResolverUtil(); - resolver.findImplementations(ActionBean.class, pkgs); - return resolver.getClasses(); + return event; + } + + /** + * Uses the Maps constructed earlier to locate the Method which can handle the event. + * + * @param bean the subclass of ActionBean that is bound to the request. + * @param eventName the name of the event being handled + * @return a Method object representing the handling method. + * @throws StripesServletException thrown when no method handles the named event. + */ + public Method getHandler(Class bean, String eventName) + throws StripesServletException { + Map mappings = this.eventMappings.get(bean); + Method handler = mappings.get(eventName); + + // If we could not find a handler then we should blow up quickly + if (handler == null) { + throw new StripesServletException( + "Could not find handler method for event name [" + + eventName + + "] on class [" + + bean.getName() + + "]. Known handler mappings are: " + + mappings); } - /** - * Get all the classes implementing {@link ActionBean} that are recognized by this - * {@link ActionResolver}. - */ - public Collection> getActionBeanClasses() { - return getUrlBindingFactory().getActionBeanClasses(); + return handler; + } + + /** + * Returns the Method that is the default handler for events in the ActionBean class supplied. If + * only one handler method is defined in the class, that is assumed to be the default. If there is + * more than one then the method marked with @DefaultHandler will be returned. + * + * @param bean the ActionBean type bound to the request + * @return Method object that should handle the request + * @throws StripesServletException if no default handler could be located + */ + public Method getDefaultHandler(Class bean) throws StripesServletException { + Map handlers = this.eventMappings.get(bean); + + if (handlers.size() == 1) { + return handlers.values().iterator().next(); + } else { + Method handler = handlers.get(DEFAULT_HANDLER_KEY); + if (handler != null) return handler; } - public Class getActionBeanByName(String actionBeanName) { - return actionBeansByName.get(actionBeanName); + // If we get this far, there is no sensible default! Kaboom! + throw new StripesServletException( + "No default handler could be found for ActionBean of " + "type: " + bean.getName()); + } + + /** Provides subclasses with access to the configuration object. */ + protected Configuration getConfiguration() { + return this.configuration; + } + + /** + * Helper method to find implementations of ActionBean in the packages specified in Configuration + * using the {@link ResolverUtil} class. + * + * @return a set of Class objects that represent subclasses of ActionBean + */ + protected Set> findClasses() { + BootstrapPropertyResolver bootstrap = getConfiguration().getBootstrapPropertyResolver(); + + String packages = bootstrap.getProperty(PACKAGES); + if (packages == null) { + throw new StripesRuntimeException( + "You must supply a value for the configuration parameter '" + + PACKAGES + + "'. The " + + "value should be a list of one or more package roots (comma separated) that are " + + "to be scanned for ActionBean implementations. The packages specified and all " + + "subpackages are examined for implementations of ActionBean."); } -} \ No newline at end of file + + String[] pkgs = StringUtil.standardSplit(packages); + ResolverUtil resolver = new ResolverUtil(); + resolver.findImplementations(ActionBean.class, pkgs); + return resolver.getClasses(); + } + + /** + * Get all the classes implementing {@link ActionBean} that are recognized by this {@link + * ActionResolver}. + */ + public Collection> getActionBeanClasses() { + return getUrlBindingFactory().getActionBeanClasses(); + } + + public Class getActionBeanByName(String actionBeanName) { + return actionBeansByName.get(actionBeanName); + } +} diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/BeforeAfterMethodInterceptor.java b/stripes/src/main/java/net/sourceforge/stripes/controller/BeforeAfterMethodInterceptor.java index 3151810e7..c2d999634 100755 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/BeforeAfterMethodInterceptor.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/BeforeAfterMethodInterceptor.java @@ -14,35 +14,34 @@ */ package net.sourceforge.stripes.controller; -import net.sourceforge.stripes.action.ActionBean; -import net.sourceforge.stripes.action.After; -import net.sourceforge.stripes.action.Before; -import net.sourceforge.stripes.action.Resolution; -import net.sourceforge.stripes.action.ActionBeanContext; -import net.sourceforge.stripes.util.Log; -import net.sourceforge.stripes.util.ReflectUtil; -import net.sourceforge.stripes.util.CollectionUtil; - import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Collection; import java.util.concurrent.ConcurrentHashMap; +import net.sourceforge.stripes.action.ActionBean; +import net.sourceforge.stripes.action.ActionBeanContext; +import net.sourceforge.stripes.action.After; +import net.sourceforge.stripes.action.Before; +import net.sourceforge.stripes.action.Resolution; +import net.sourceforge.stripes.util.CollectionUtil; +import net.sourceforge.stripes.util.Log; +import net.sourceforge.stripes.util.ReflectUtil; /** - *

    Interceptor that inspects ActionBeans for {@link Before} and {@link After} annotations and - * runs the annotated methods at the requested point in the request lifecycle. There is no limit - * on the number of methods within an ActionBean that can be marked with {@code @Before} and - * {@code @After} annotations, and individual methods may be marked with one or both annotations.

    + * Interceptor that inspects ActionBeans for {@link Before} and {@link After} annotations and runs + * the annotated methods at the requested point in the request lifecycle. There is no limit on the + * number of methods within an ActionBean that can be marked with {@code @Before} and {@code @After} + * annotations, and individual methods may be marked with one or both annotations. * *

    To configure the BeforeAfterMethodInterceptor for use you will need to add the following to - * your {@code web.xml} (assuming no other interceptors are yet configured):

    + * your {@code web.xml} (assuming no other interceptors are yet configured): * *
      * <init-param>
    @@ -51,283 +50,317 @@
      * </init-param>
      * 
    * - *

    If one or more interceptors are already configured in your {@code web.xml} simply separate - * the fully qualified names of the interceptors with commas (additional whitespace is ok).

    - * + *

    If one or more interceptors are already configured in your {@code web.xml} simply separate the + * fully qualified names of the interceptors with commas (additional whitespace is ok). + * * @see net.sourceforge.stripes.action.Before * @see net.sourceforge.stripes.action.After * @author Jeppe Cramon * @since Stripes 1.3 */ -@Intercepts({LifecycleStage.RequestInit, - LifecycleStage.ActionBeanResolution, - LifecycleStage.HandlerResolution, - LifecycleStage.BindingAndValidation, - LifecycleStage.CustomValidation, - LifecycleStage.EventHandling, - LifecycleStage.ResolutionExecution, - LifecycleStage.RequestComplete}) +@Intercepts({ + LifecycleStage.RequestInit, + LifecycleStage.ActionBeanResolution, + LifecycleStage.HandlerResolution, + LifecycleStage.BindingAndValidation, + LifecycleStage.CustomValidation, + LifecycleStage.EventHandling, + LifecycleStage.ResolutionExecution, + LifecycleStage.RequestComplete +}) public class BeforeAfterMethodInterceptor implements Interceptor { - /** Log used throughout the intercetor */ - private static final Log log = Log.getInstance(BeforeAfterMethodInterceptor.class); + /** Log used throughout the intercetor */ + private static final Log log = Log.getInstance(BeforeAfterMethodInterceptor.class); - /** Cache of the FilterMethods for the different ActionBean classes */ - private Map, FilterMethods> filterMethodsCache = - new ConcurrentHashMap, FilterMethods>(); + /** Cache of the FilterMethods for the different ActionBean classes */ + private Map, FilterMethods> filterMethodsCache = + new ConcurrentHashMap, FilterMethods>(); - /** - * Does the main work of the interceptor as described in the class level javadoc. - * Executed the before and after methods for the ActionBean as appropriate for the - * current lifecycle stage. Lazily examines the ActionBean to determine the set - * of methods to execute, if it has not yet been examined. - * - * @param context the current ExecutionContext - * @return a resolution if one of the Before or After methods returns one, or if the - * nested interceptors return one - * @throws Exception if one of the before/after methods raises an exception - */ - public Resolution intercept(ExecutionContext context) throws Exception { - LifecycleStage stage = context.getLifecycleStage(); - ActionBeanContext abc = context.getActionBeanContext(); - String event = abc == null ? null : abc.getEventName(); - Resolution resolution = null; + /** + * Does the main work of the interceptor as described in the class level javadoc. Executed the + * before and after methods for the ActionBean as appropriate for the current lifecycle stage. + * Lazily examines the ActionBean to determine the set of methods to execute, if it has not yet + * been examined. + * + * @param context the current ExecutionContext + * @return a resolution if one of the Before or After methods returns one, or if the nested + * interceptors return one + * @throws Exception if one of the before/after methods raises an exception + */ + public Resolution intercept(ExecutionContext context) throws Exception { + LifecycleStage stage = context.getLifecycleStage(); + ActionBeanContext abc = context.getActionBeanContext(); + String event = abc == null ? null : abc.getEventName(); + Resolution resolution = null; - // Run @Before methods, as long as there's a bean to run them on - if (context.getActionBean() != null) { - ActionBean bean = context.getActionBean(); - FilterMethods filterMethods = getFilterMethods(bean.getClass()); - List beforeMethods = filterMethods.getBeforeMethods(stage); + // Run @Before methods, as long as there's a bean to run them on + if (context.getActionBean() != null) { + ActionBean bean = context.getActionBean(); + FilterMethods filterMethods = getFilterMethods(bean.getClass()); + List beforeMethods = filterMethods.getBeforeMethods(stage); - for (Method method : beforeMethods) { - String[] on = method.getAnnotation(Before.class).on(); - if (event == null || CollectionUtil.applies(on, event)) { - resolution = invoke(bean, method, stage, Before.class); - if (resolution != null) { - return resolution; - } - } - } + for (Method method : beforeMethods) { + String[] on = method.getAnnotation(Before.class).on(); + if (event == null || CollectionUtil.applies(on, event)) { + resolution = invoke(bean, method, stage, Before.class); + if (resolution != null) { + return resolution; + } } + } + } - // Continue on and execute other filters and the lifecycle code - resolution = context.proceed(); + // Continue on and execute other filters and the lifecycle code + resolution = context.proceed(); - // Run After filter methods (if any) - if (context.getActionBean() != null) { - ActionBean bean = context.getActionBean(); - FilterMethods filterMethods = getFilterMethods(bean.getClass()); - List afterMethods = filterMethods.getAfterMethods(stage); + // Run After filter methods (if any) + if (context.getActionBean() != null) { + ActionBean bean = context.getActionBean(); + FilterMethods filterMethods = getFilterMethods(bean.getClass()); + List afterMethods = filterMethods.getAfterMethods(stage); - // Re-get the event name in case we're executing after handler resolution - // in which case the name will have been null before, and non-null now - event = abc == null ? null : abc.getEventName(); + // Re-get the event name in case we're executing after handler resolution + // in which case the name will have been null before, and non-null now + event = abc == null ? null : abc.getEventName(); - Resolution overrideResolution = null; - for (Method method : afterMethods) { - String[] on = method.getAnnotation(After.class).on(); - if (event == null || CollectionUtil.applies(on, event)) { - overrideResolution = invoke(bean, method, stage, After.class); - if (overrideResolution != null) { - return overrideResolution; - } - } - } + Resolution overrideResolution = null; + for (Method method : afterMethods) { + String[] on = method.getAnnotation(After.class).on(); + if (event == null || CollectionUtil.applies(on, event)) { + overrideResolution = invoke(bean, method, stage, After.class); + if (overrideResolution != null) { + return overrideResolution; + } } - - return resolution; - } + } + } - /** - * Helper method that will invoke the supplied method and manage any exceptions and - * returns from the object. Specifically it will log any exceptions except for - * InvocationTargetExceptions which it will attempt to unwrap and rethrow. If the method - * returns a Resolution it will be returned; returns of other types will be ignored. - */ - protected Resolution invoke(ActionBean bean, Method m, LifecycleStage stage, - Class when) throws Exception { - Class beanClass = bean.getClass(); - Object retval = null; + return resolution; + } - log.debug("Calling @", when.getSimpleName(), " method '", m.getName(), "' at LifecycleStage '", - stage, "' on ActionBean '", beanClass.getSimpleName(), "'"); - try { - retval = m.invoke(bean); - } - catch (IllegalArgumentException e) { - log.error(e, "An InvalidArgumentException was raised when calling @", - when.getSimpleName(), " method '", m.getName(), "' at LifecycleStage '", - stage, "' on ActionBean '", beanClass.getSimpleName(), - "'. See java.lang.reflect.Method.invoke() for possible reasons."); - } - catch (IllegalAccessException e) { - log.error(e, "An IllegalAccessException was raised when calling @", - when.getSimpleName(), " method '", m.getName(), "' at LifecycleStage '", - stage, "' on ActionBean '", beanClass.getSimpleName(), "'"); - } - catch (InvocationTargetException e) { - // Method threw an exception, so throw the real cause of it - if (e.getCause() != null && e.getCause() instanceof Exception) { - throw (Exception)e.getCause(); - } - else { - throw e; - } - } + /** + * Helper method that will invoke the supplied method and manage any exceptions and returns from + * the object. Specifically it will log any exceptions except for InvocationTargetExceptions which + * it will attempt to unwrap and rethrow. If the method returns a Resolution it will be returned; + * returns of other types will be ignored. + */ + protected Resolution invoke( + ActionBean bean, Method m, LifecycleStage stage, Class when) + throws Exception { + Class beanClass = bean.getClass(); + Object retval = null; - // If we got a return value and it is a resolution, return it - if (retval != null && retval instanceof Resolution) { - return (Resolution) retval; - } - else { - return null; - } + log.debug( + "Calling @", + when.getSimpleName(), + " method '", + m.getName(), + "' at LifecycleStage '", + stage, + "' on ActionBean '", + beanClass.getSimpleName(), + "'"); + try { + retval = m.invoke(bean); + } catch (IllegalArgumentException e) { + log.error( + e, + "An InvalidArgumentException was raised when calling @", + when.getSimpleName(), + " method '", + m.getName(), + "' at LifecycleStage '", + stage, + "' on ActionBean '", + beanClass.getSimpleName(), + "'. See java.lang.reflect.Method.invoke() for possible reasons."); + } catch (IllegalAccessException e) { + log.error( + e, + "An IllegalAccessException was raised when calling @", + when.getSimpleName(), + " method '", + m.getName(), + "' at LifecycleStage '", + stage, + "' on ActionBean '", + beanClass.getSimpleName(), + "'"); + } catch (InvocationTargetException e) { + // Method threw an exception, so throw the real cause of it + if (e.getCause() != null && e.getCause() instanceof Exception) { + throw (Exception) e.getCause(); + } else { + throw e; + } + } + // If we got a return value and it is a resolution, return it + if (retval != null && retval instanceof Resolution) { + return (Resolution) retval; + } else { + return null; } + } - /** - * Gets the Before/After methods for the ActionBean. Lazily examines the ActionBean - * and stores the information in a cache. Looks for all non-abstract, no-arg methods - * that are annotated with either {@code @Before} or {@code @After}. - * - * @param beanClass The action bean class to get methods for. - * @return The before and after methods for the ActionBean - */ - protected FilterMethods getFilterMethods(Class beanClass) { - FilterMethods filterMethods = filterMethodsCache.get(beanClass); - if (filterMethods == null) { - filterMethods = new FilterMethods(); + /** + * Gets the Before/After methods for the ActionBean. Lazily examines the ActionBean and stores the + * information in a cache. Looks for all non-abstract, no-arg methods that are annotated with + * either {@code @Before} or {@code @After}. + * + * @param beanClass The action bean class to get methods for. + * @return The before and after methods for the ActionBean + */ + protected FilterMethods getFilterMethods(Class beanClass) { + FilterMethods filterMethods = filterMethodsCache.get(beanClass); + if (filterMethods == null) { + filterMethods = new FilterMethods(); - // Look for @Before and @After annotations on the methods in the ActionBean class - Collection methods = ReflectUtil.getMethods(beanClass); - for (Method method : methods) { - if (method.isAnnotationPresent(Before.class) || method.isAnnotationPresent(After.class)) { - // Check to ensure that the method has an appropriate signature - int mods = method.getModifiers(); - if (method.getParameterTypes().length != 0 || Modifier.isAbstract(mods)) { - log.warn("Method '", beanClass.getName(), ".", method.getName(), "' is ", - "annotated with @Before or @After but has an incompatible ", - "signature. @Before/@After methods must be non-abstract ", - "zero-argument methods."); - continue; - } + // Look for @Before and @After annotations on the methods in the ActionBean class + Collection methods = ReflectUtil.getMethods(beanClass); + for (Method method : methods) { + if (method.isAnnotationPresent(Before.class) || method.isAnnotationPresent(After.class)) { + // Check to ensure that the method has an appropriate signature + int mods = method.getModifiers(); + if (method.getParameterTypes().length != 0 || Modifier.isAbstract(mods)) { + log.warn( + "Method '", + beanClass.getName(), + ".", + method.getName(), + "' is ", + "annotated with @Before or @After but has an incompatible ", + "signature. @Before/@After methods must be non-abstract ", + "zero-argument methods."); + continue; + } - // Now try and make private/protected/package methods callable - if (!method.isAccessible()) { - try { - method.setAccessible(true); - } - catch (SecurityException se) { - log.warn("Method '", beanClass.getName(), ".", method.getName(), "' is ", - "annotated with @Before or @After but is not public and ", - "calling setAccessible(true) on it threw a SecurityException. ", - "Please either declare the method as public, or change your ", - "JVM security policy to allow Stripes code to call ", - "Method.setAccessible() on your code base."); - continue; - } - } + // Now try and make private/protected/package methods callable + if (!method.isAccessible()) { + try { + method.setAccessible(true); + } catch (SecurityException se) { + log.warn( + "Method '", + beanClass.getName(), + ".", + method.getName(), + "' is ", + "annotated with @Before or @After but is not public and ", + "calling setAccessible(true) on it threw a SecurityException. ", + "Please either declare the method as public, or change your ", + "JVM security policy to allow Stripes code to call ", + "Method.setAccessible() on your code base."); + continue; + } + } - if (method.isAnnotationPresent(Before.class)) { - Before annotation = method.getAnnotation(Before.class); - filterMethods.addBeforeMethod(annotation.stages(), method); - } + if (method.isAnnotationPresent(Before.class)) { + Before annotation = method.getAnnotation(Before.class); + filterMethods.addBeforeMethod(annotation.stages(), method); + } - if (method.isAnnotationPresent(After.class)) { - After annotation = method.getAnnotation(After.class); - filterMethods.addAfterMethod(annotation.stages(), method); - } - } - } + if (method.isAnnotationPresent(After.class)) { + After annotation = method.getAnnotation(After.class); + filterMethods.addAfterMethod(annotation.stages(), method); + } + } + } - filterMethodsCache.put(beanClass, filterMethods); - } + filterMethodsCache.put(beanClass, filterMethods); + } - return filterMethods; - } - - /** - * Helper class used to collect Before and After methods for a class and provide easy - * and rapid access to them by LifecycleStage. - * - * @author Jeppe Cramon - */ - protected static class FilterMethods { - /** Map of Before methods, keyed by the LifecycleStage that they should be invoked before. */ - private Map> beforeMethods = new HashMap>(); + return filterMethods; + } - /** Map of After methods, keyed by the LifecycleStage that they should be invoked after. */ - private Map> afterMethods = new HashMap>(); - - /** - * Adds a method to be executed before the supplied LifecycleStages. - * - * @param stages All the LifecycleStages that the given filter method should be invoked before - * @param method The filter method to be invoked before the given LifecycleStage(s) - */ - public void addBeforeMethod(LifecycleStage[] stages, Method method) { - for (LifecycleStage stage : stages) { - if (stage == LifecycleStage.ActionBeanResolution) { - log.warn("LifecycleStage.ActionBeanResolution is unsupported for @Before ", - "methods. Method '", method.getDeclaringClass().getName(), ".", - method.getName(), "' will not be invoked for this stage."); - } - else { - addFilterMethod(beforeMethods, stage, method); - } - } - } - - /** - * Adds a method to be executed after the supplied LifecycleStages. - * - * @param stages All the LifecycleStages that the given filter method should be invoked after - * @param method The filter method to be invoked after the given LifecycleStage(s) - */ - public void addAfterMethod(LifecycleStage[] stages, Method method) { - for (LifecycleStage stage : stages) { - addFilterMethod(afterMethods, stage, method); - } - } + /** + * Helper class used to collect Before and After methods for a class and provide easy and rapid + * access to them by LifecycleStage. + * + * @author Jeppe Cramon + */ + protected static class FilterMethods { + /** Map of Before methods, keyed by the LifecycleStage that they should be invoked before. */ + private Map> beforeMethods = + new HashMap>(); - /** - * Helper method to add methods to a method map keyed by the LifecycleStage. - * - * @param methodMap The map of methods - * @param stage The LifecycleStage under which to put the method - * @param method The method that should be added to the method map - */ - private void addFilterMethod(Map> methodMap, - LifecycleStage stage, Method method) { - List methods = methodMap.get(stage); - if (methods == null) { - methods = new ArrayList(); - methodMap.put(stage, methods); - } - methods.add(method); - } - - /** - * Gets the Before methods for the given LifecycleStage. - * - * @param stage The LifecycleStage to find Before methods for. - * @return A List of before methods, possibly zero length but never null - */ - public List getBeforeMethods(LifecycleStage stage) { - List methods = beforeMethods.get(stage); - if (methods == null) methods = Collections.emptyList(); - return methods; + /** Map of After methods, keyed by the LifecycleStage that they should be invoked after. */ + private Map> afterMethods = + new HashMap>(); + + /** + * Adds a method to be executed before the supplied LifecycleStages. + * + * @param stages All the LifecycleStages that the given filter method should be invoked before + * @param method The filter method to be invoked before the given LifecycleStage(s) + */ + public void addBeforeMethod(LifecycleStage[] stages, Method method) { + for (LifecycleStage stage : stages) { + if (stage == LifecycleStage.ActionBeanResolution) { + log.warn( + "LifecycleStage.ActionBeanResolution is unsupported for @Before ", + "methods. Method '", + method.getDeclaringClass().getName(), + ".", + method.getName(), + "' will not be invoked for this stage."); + } else { + addFilterMethod(beforeMethods, stage, method); } + } + } + + /** + * Adds a method to be executed after the supplied LifecycleStages. + * + * @param stages All the LifecycleStages that the given filter method should be invoked after + * @param method The filter method to be invoked after the given LifecycleStage(s) + */ + public void addAfterMethod(LifecycleStage[] stages, Method method) { + for (LifecycleStage stage : stages) { + addFilterMethod(afterMethods, stage, method); + } + } + + /** + * Helper method to add methods to a method map keyed by the LifecycleStage. + * + * @param methodMap The map of methods + * @param stage The LifecycleStage under which to put the method + * @param method The method that should be added to the method map + */ + private void addFilterMethod( + Map> methodMap, LifecycleStage stage, Method method) { + List methods = methodMap.get(stage); + if (methods == null) { + methods = new ArrayList(); + methodMap.put(stage, methods); + } + methods.add(method); + } - /** - * Gets the Before methods for the given LifecycleStage. - * - * @param stage The LifecycleStage to find Before methods for. - * @return A List of before methods, possibly zero length but never null - */ - public List getAfterMethods(LifecycleStage stage) { - List methods = afterMethods.get(stage); - if (methods == null) methods = Collections.emptyList(); - return methods; - } - } + /** + * Gets the Before methods for the given LifecycleStage. + * + * @param stage The LifecycleStage to find Before methods for. + * @return A List of before methods, possibly zero length but never null + */ + public List getBeforeMethods(LifecycleStage stage) { + List methods = beforeMethods.get(stage); + if (methods == null) methods = Collections.emptyList(); + return methods; + } + + /** + * Gets the Before methods for the given LifecycleStage. + * + * @param stage The LifecycleStage to find Before methods for. + * @return A List of before methods, possibly zero length but never null + */ + public List getAfterMethods(LifecycleStage stage) { + List methods = afterMethods.get(stage); + if (methods == null) methods = Collections.emptyList(); + return methods; + } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/BindingPolicyManager.java b/stripes/src/main/java/net/sourceforge/stripes/controller/BindingPolicyManager.java index 9d3ab526d..537b5e027 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/BindingPolicyManager.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/BindingPolicyManager.java @@ -14,6 +14,9 @@ */ package net.sourceforge.stripes.controller; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpSession; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; @@ -24,11 +27,6 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; - -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpSession; - import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.action.StrictBinding; @@ -43,281 +41,283 @@ /** * Manages the policies observed by {@link DefaultActionBeanPropertyBinder} when binding properties * to an {@link ActionBean}. - * + * * @author Ben Gunter * @see StrictBinding */ @StrictBinding(defaultPolicy = Policy.ALLOW) public class BindingPolicyManager { - /** List of classes that, for security reasons, are not allowed as a {@link NodeEvaluation} value type. */ - private static final List> ILLEGAL_NODE_VALUE_TYPES = Arrays.> asList( - ActionBeanContext.class, - Class.class, - ClassLoader.class, - HttpSession.class, - ServletRequest.class, - ServletResponse.class); - - /** The regular expression that a property name must match */ - private static final String PROPERTY_REGEX = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"; - - /** The compiled form of {@link #PROPERTY_REGEX} */ - private static final Pattern PROPERTY_PATTERN = Pattern.compile(PROPERTY_REGEX); - - /** Log */ - private static final Log log = Log.getInstance(BindingPolicyManager.class); - - /** Cached instances */ - private static final Map, BindingPolicyManager> instances = new HashMap, BindingPolicyManager>(); - - /** - * Get the policy manager for the given class. Instances are cached and returned on subsequent - * calls. - * - * @param beanType the class whose policy manager is to be retrieved - * @return a policy manager - */ - public static BindingPolicyManager getInstance(Class beanType) { - if (instances.containsKey(beanType)) - return instances.get(beanType); - - BindingPolicyManager instance = new BindingPolicyManager(beanType); - instances.put(beanType, instance); - return instance; + /** + * List of classes that, for security reasons, are not allowed as a {@link NodeEvaluation} value + * type. + */ + private static final List> ILLEGAL_NODE_VALUE_TYPES = + Arrays.>asList( + ActionBeanContext.class, + Class.class, + ClassLoader.class, + HttpSession.class, + ServletRequest.class, + ServletResponse.class); + + /** The regular expression that a property name must match */ + private static final String PROPERTY_REGEX = + "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"; + + /** The compiled form of {@link #PROPERTY_REGEX} */ + private static final Pattern PROPERTY_PATTERN = Pattern.compile(PROPERTY_REGEX); + + /** Log */ + private static final Log log = Log.getInstance(BindingPolicyManager.class); + + /** Cached instances */ + private static final Map, BindingPolicyManager> instances = + new HashMap, BindingPolicyManager>(); + + /** + * Get the policy manager for the given class. Instances are cached and returned on subsequent + * calls. + * + * @param beanType the class whose policy manager is to be retrieved + * @return a policy manager + */ + public static BindingPolicyManager getInstance(Class beanType) { + if (instances.containsKey(beanType)) return instances.get(beanType); + + BindingPolicyManager instance = new BindingPolicyManager(beanType); + instances.put(beanType, instance); + return instance; + } + + /** The class to which the binding policy applies */ + private Class beanClass; + + /** The default policy to honor, in case of conflicts */ + private Policy defaultPolicy; + + /** The regular expression that allowed properties must match */ + private Pattern allowPattern; + + /** The regular expression that denied properties must match */ + private Pattern denyPattern; + + /** The regular expression that matches properties with {@literal @Validate} */ + private Pattern validatePattern; + + /** + * Create a new instance to handle binding security for the given type. + * + * @param beanClass the class to which the binding policy applies + */ + protected BindingPolicyManager(Class beanClass) { + try { + log.debug( + "Creating ", + getClass().getName(), + " for ", + beanClass, + " with default policy ", + defaultPolicy); + this.beanClass = beanClass; + + // process the annotation + StrictBinding annotation = getAnnotation(beanClass); + if (annotation != null) { + // set default policy + this.defaultPolicy = annotation.defaultPolicy(); + + // construct the allow pattern + this.allowPattern = globToPattern(annotation.allow()); + + // construct the deny pattern + this.denyPattern = globToPattern(annotation.deny()); + + // construct the validated properties pattern + this.validatePattern = globToPattern(getValidatedProperties(beanClass)); + } + } catch (Exception e) { + log.error(e, "%%% Failure instantiating ", getClass().getName()); + StripesRuntimeException sre = new StripesRuntimeException(e.getMessage(), e); + sre.setStackTrace(e.getStackTrace()); + throw sre; } - - /** The class to which the binding policy applies */ - private Class beanClass; - - /** The default policy to honor, in case of conflicts */ - private Policy defaultPolicy; - - /** The regular expression that allowed properties must match */ - private Pattern allowPattern; - - /** The regular expression that denied properties must match */ - private Pattern denyPattern; - - /** The regular expression that matches properties with {@literal @Validate} */ - private Pattern validatePattern; - - /** - * Create a new instance to handle binding security for the given type. - * - * @param beanClass the class to which the binding policy applies - */ - protected BindingPolicyManager(Class beanClass) { - try { - log.debug("Creating ", getClass().getName(), " for ", beanClass, - " with default policy ", defaultPolicy); - this.beanClass = beanClass; - - // process the annotation - StrictBinding annotation = getAnnotation(beanClass); - if (annotation != null) { - // set default policy - this.defaultPolicy = annotation.defaultPolicy(); - - // construct the allow pattern - this.allowPattern = globToPattern(annotation.allow()); - - // construct the deny pattern - this.denyPattern = globToPattern(annotation.deny()); - - // construct the validated properties pattern - this.validatePattern = globToPattern(getValidatedProperties(beanClass)); - } - } - catch (Exception e) { - log.error(e, "%%% Failure instantiating ", getClass().getName()); - StripesRuntimeException sre = new StripesRuntimeException(e.getMessage(), e); - sre.setStackTrace(e.getStackTrace()); - throw sre; - } + } + + /** + * Indicates if binding is allowed for the given expression. + * + * @param eval a property expression that has been evaluated against an {@link ActionBean} + * @return true if binding is allowed; false if not + */ + public boolean isBindingAllowed(PropertyExpressionEvaluation eval) { + // Ensure no-one is trying to bind into a protected type + if (usesIllegalNodeValueType(eval)) { + return false; } - /** - * Indicates if binding is allowed for the given expression. - * - * @param eval a property expression that has been evaluated against an {@link ActionBean} - * @return true if binding is allowed; false if not - */ - public boolean isBindingAllowed(PropertyExpressionEvaluation eval) { - // Ensure no-one is trying to bind into a protected type - if (usesIllegalNodeValueType(eval)) { - return false; - } - - // check parameter name against access lists - String paramName = new ParameterName(eval.getExpression().getSource()).getStrippedName(); - boolean deny = denyPattern != null && denyPattern.matcher(paramName).matches(); - boolean allow = (allowPattern != null && allowPattern.matcher(paramName).matches()) - || (validatePattern != null && validatePattern.matcher(paramName).matches()); - - /* - * if path appears on neither or both lists ( i.e. !(allow ^ deny) ) and default policy is - * to deny access, then fail - */ - if (defaultPolicy == Policy.DENY && !(allow ^ deny)) - return false; - - /* - * regardless of default policy, if it's in the deny list but not in the allow list, then - * fail - */ - if (!allow && deny) - return false; - - // any other conditions pass the test - return true; - } + // check parameter name against access lists + String paramName = new ParameterName(eval.getExpression().getSource()).getStrippedName(); + boolean deny = denyPattern != null && denyPattern.matcher(paramName).matches(); + boolean allow = + (allowPattern != null && allowPattern.matcher(paramName).matches()) + || (validatePattern != null && validatePattern.matcher(paramName).matches()); - /** - * Indicates if any node in the given {@link PropertyExpressionEvaluation} has a value type that is assignable from - * any of the classes listed in {@link #ILLEGAL_NODE_VALUE_TYPES}. - * - * @param eval a property expression that has been evaluated against an {@link ActionBean} - * @return true if the expression uses an illegal node value type; false otherwise + /* + * if path appears on neither or both lists ( i.e. !(allow ^ deny) ) and default policy is + * to deny access, then fail */ - protected boolean usesIllegalNodeValueType(PropertyExpressionEvaluation eval) { - for (NodeEvaluation node = eval.getRootNode(); node != null; node = node.getNext()) { - Type type = node.getValueType(); - if (type instanceof ParameterizedType) { - type = ((ParameterizedType) type).getRawType(); - } - if (type instanceof Class) { - final Class nodeClass = (Class) type; - for (Class protectedClass : ILLEGAL_NODE_VALUE_TYPES) { - if (protectedClass.isAssignableFrom(nodeClass)) { - return true; - } - } - } - } - return false; - } + if (defaultPolicy == Policy.DENY && !(allow ^ deny)) return false; - /** - * Get the {@link StrictBinding} annotation for a class, checking all its superclasses if - * necessary. If no annotation is found, then one will be returned whose default policy is to - * allow binding to all properties. - * - * @param beanType the class to get the {@link StrictBinding} annotation for - * @return An annotation. This method never returns null. + /* + * regardless of default policy, if it's in the deny list but not in the allow list, then + * fail */ - protected StrictBinding getAnnotation(Class beanType) { - StrictBinding annotation; - do { - annotation = beanType.getAnnotation(StrictBinding.class); - } while (annotation == null && (beanType = beanType.getSuperclass()) != null); - if (annotation == null) { - annotation = getClass().getAnnotation(StrictBinding.class); + if (!allow && deny) return false; + + // any other conditions pass the test + return true; + } + + /** + * Indicates if any node in the given {@link PropertyExpressionEvaluation} has a value type that + * is assignable from any of the classes listed in {@link #ILLEGAL_NODE_VALUE_TYPES}. + * + * @param eval a property expression that has been evaluated against an {@link ActionBean} + * @return true if the expression uses an illegal node value type; false otherwise + */ + protected boolean usesIllegalNodeValueType(PropertyExpressionEvaluation eval) { + for (NodeEvaluation node = eval.getRootNode(); node != null; node = node.getNext()) { + Type type = node.getValueType(); + if (type instanceof ParameterizedType) { + type = ((ParameterizedType) type).getRawType(); + } + if (type instanceof Class) { + final Class nodeClass = (Class) type; + for (Class protectedClass : ILLEGAL_NODE_VALUE_TYPES) { + if (protectedClass.isAssignableFrom(nodeClass)) { + return true; + } } - return annotation; + } } - - /** - * Get all the properties and nested properties of the given class for which there is a - * corresponding {@link ValidationMetadata}, as returned by - * {@link ValidationMetadataProvider#getValidationMetadata(Class, ParameterName)}. The idea - * here is that if the bean property must be validated, then it is expected that the property - * may be bound to the bean. - * - * @param beanClass a class - * @return The validated properties. If no properties are annotated then null. - * @see ValidationMetadataProvider#getValidationMetadata(Class) - */ - protected String[] getValidatedProperties(Class beanClass) { - Set properties = StripesFilter.getConfiguration().getValidationMetadataProvider() - .getValidationMetadata(beanClass).keySet(); - return new ArrayList(properties).toArray(new String[properties.size()]); + return false; + } + + /** + * Get the {@link StrictBinding} annotation for a class, checking all its superclasses if + * necessary. If no annotation is found, then one will be returned whose default policy is to + * allow binding to all properties. + * + * @param beanType the class to get the {@link StrictBinding} annotation for + * @return An annotation. This method never returns null. + */ + protected StrictBinding getAnnotation(Class beanType) { + StrictBinding annotation; + do { + annotation = beanType.getAnnotation(StrictBinding.class); + } while (annotation == null && (beanType = beanType.getSuperclass()) != null); + if (annotation == null) { + annotation = getClass().getAnnotation(StrictBinding.class); } - - /** - * Get the bean class. - * - * @return the bean class - */ - public Class getBeanClass() { - return beanClass; + return annotation; + } + + /** + * Get all the properties and nested properties of the given class for which there is a + * corresponding {@link ValidationMetadata}, as returned by {@link + * ValidationMetadataProvider#getValidationMetadata(Class, ParameterName)}. The idea here is that + * if the bean property must be validated, then it is expected that the property may be bound to + * the bean. + * + * @param beanClass a class + * @return The validated properties. If no properties are annotated then null. + * @see ValidationMetadataProvider#getValidationMetadata(Class) + */ + protected String[] getValidatedProperties(Class beanClass) { + Set properties = + StripesFilter.getConfiguration() + .getValidationMetadataProvider() + .getValidationMetadata(beanClass) + .keySet(); + return new ArrayList(properties).toArray(new String[properties.size()]); + } + + /** + * Get the bean class. + * + * @return the bean class + */ + public Class getBeanClass() { + return beanClass; + } + + /** + * Get the default policy. + * + * @return the policy + */ + public Policy getDefaultPolicy() { + return defaultPolicy; + } + + /** + * Converts a glob to a regex {@link Pattern}. + * + * @param globArray an array of property name globs, each of which may be a comma separated list + * of globs + * @return the pattern + */ + protected Pattern globToPattern(String... globArray) { + if (globArray == null || globArray.length == 0) return null; + + // things are much easier if we convert to a single list + List globs = new ArrayList(); + for (String glob : globArray) { + String[] subs = glob.split("(\\s*,\\s*)+"); + for (String sub : subs) { + globs.add(sub); + } } - /** - * Get the default policy. - * - * @return the policy - */ - public Policy getDefaultPolicy() { - return defaultPolicy; - } - - /** - * Converts a glob to a regex {@link Pattern}. - * - * @param globArray an array of property name globs, each of which may be a comma separated list - * of globs - * @return the pattern - */ - protected Pattern globToPattern(String... globArray) { - if (globArray == null || globArray.length == 0) + List subs = new ArrayList(); + StringBuilder buf = new StringBuilder(); + for (String glob : globs) { + buf.setLength(0); + String[] properties = glob.split("\\."); + for (int i = 0; i < properties.length; i++) { + String property = properties[i]; + if ("*".equals(property)) { + buf.append(PROPERTY_REGEX); + } else if ("**".equals(property)) { + buf.append(PROPERTY_REGEX).append("(\\.").append(PROPERTY_REGEX).append(")*"); + } else if (property.length() > 0) { + Matcher matcher = PROPERTY_PATTERN.matcher(property); + if (matcher.matches()) { + buf.append(property); + } else { + log.warn("Invalid property name: " + property); return null; - - // things are much easier if we convert to a single list - List globs = new ArrayList(); - for (String glob : globArray) { - String[] subs = glob.split("(\\s*,\\s*)+"); - for (String sub : subs) { - globs.add(sub); - } + } } - List subs = new ArrayList(); - StringBuilder buf = new StringBuilder(); - for (String glob : globs) { - buf.setLength(0); - String[] properties = glob.split("\\."); - for (int i = 0; i < properties.length; i++) { - String property = properties[i]; - if ("*".equals(property)) { - buf.append(PROPERTY_REGEX); - } - else if ("**".equals(property)) { - buf.append(PROPERTY_REGEX).append("(\\.").append(PROPERTY_REGEX).append(")*"); - } - else if (property.length() > 0) { - Matcher matcher = PROPERTY_PATTERN.matcher(property); - if (matcher.matches()) { - buf.append(property); - } - else { - log.warn("Invalid property name: " + property); - return null; - } - } - - // add a literal dot after all but the last - if (i < properties.length - 1) - buf.append("\\."); - } - - // add to the list of subs - if (buf.length() != 0) - subs.add(buf.toString()); - } + // add a literal dot after all but the last + if (i < properties.length - 1) buf.append("\\."); + } - // join subs together with pipes and compile - buf.setLength(0); - for (String sub : subs) { - buf.append(sub).append('|'); - } - if (buf.length() > 0) - buf.setLength(buf.length() - 1); - log.debug("Translated globs ", Arrays.toString(globArray), " to regex ", buf); + // add to the list of subs + if (buf.length() != 0) subs.add(buf.toString()); + } - // return null if pattern is empty - if (buf.length() == 0) - return null; - else - return Pattern.compile(buf.toString()); + // join subs together with pipes and compile + buf.setLength(0); + for (String sub : subs) { + buf.append(sub).append('|'); } + if (buf.length() > 0) buf.setLength(buf.length() - 1); + log.debug("Translated globs ", Arrays.toString(globArray), " to regex ", buf); + + // return null if pattern is empty + if (buf.length() == 0) return null; + else return Pattern.compile(buf.toString()); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultActionBeanContextFactory.java b/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultActionBeanContextFactory.java index 040d7db31..b828d533a 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultActionBeanContextFactory.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultActionBeanContextFactory.java @@ -14,75 +14,76 @@ */ package net.sourceforge.stripes.controller; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.exception.StripesServletException; import net.sourceforge.stripes.util.Log; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - /** * Implements an ActionBeanContextFactory that allows for instantiation of application specific * ActionBeanContext classes. Looks for a configuration parameters called "ActionBeanContext.Class". * If the property is present, the named class with be instantiated and returned from the - * getContextInstance() method. If no class is named, then the default class, ActionBeanContext - * will be instantiated. + * getContextInstance() method. If no class is named, then the default class, ActionBeanContext will + * be instantiated. * * @author Tim Fennell */ public class DefaultActionBeanContextFactory implements ActionBeanContextFactory { - private static final Log log = Log.getInstance(DefaultActionBeanContextFactory.class); + private static final Log log = Log.getInstance(DefaultActionBeanContextFactory.class); - /** The name of the configuration property used for the context class name. */ - public static final String CONTEXT_CLASS_NAME = "ActionBeanContext.Class"; + /** The name of the configuration property used for the context class name. */ + public static final String CONTEXT_CLASS_NAME = "ActionBeanContext.Class"; - private Configuration configuration; - private Class contextClass; + private Configuration configuration; + private Class contextClass; - /** Stores the configuration, and looks up the ActionBeanContext class specified. */ - public void init(Configuration configuration) throws Exception { - setConfiguration(configuration); + /** Stores the configuration, and looks up the ActionBeanContext class specified. */ + public void init(Configuration configuration) throws Exception { + setConfiguration(configuration); - Class clazz = configuration.getBootstrapPropertyResolver() - .getClassProperty(CONTEXT_CLASS_NAME, ActionBeanContext.class); - if (clazz == null) { - clazz = ActionBeanContext.class; - } - else { - log.info(DefaultActionBeanContextFactory.class.getSimpleName(), " will use ", - ActionBeanContext.class.getSimpleName(), " subclass ", clazz.getName()); - } - this.contextClass = clazz; + Class clazz = + configuration + .getBootstrapPropertyResolver() + .getClassProperty(CONTEXT_CLASS_NAME, ActionBeanContext.class); + if (clazz == null) { + clazz = ActionBeanContext.class; + } else { + log.info( + DefaultActionBeanContextFactory.class.getSimpleName(), + " will use ", + ActionBeanContext.class.getSimpleName(), + " subclass ", + clazz.getName()); } + this.contextClass = clazz; + } - /** - * Returns a new instance of the configured class, or ActionBeanContext if a class is - * not specified. - */ - public ActionBeanContext getContextInstance(HttpServletRequest request, - HttpServletResponse response) throws ServletException { - try { - ActionBeanContext context = getConfiguration().getObjectFactory().newInstance( - this.contextClass); - context.setRequest(request); - context.setResponse(response); - return context; - } - catch (Exception e) { - throw new StripesServletException("Could not instantiate configured " + - "ActionBeanContext class: " + this.contextClass, e); - } + /** + * Returns a new instance of the configured class, or ActionBeanContext if a class is not + * specified. + */ + public ActionBeanContext getContextInstance( + HttpServletRequest request, HttpServletResponse response) throws ServletException { + try { + ActionBeanContext context = + getConfiguration().getObjectFactory().newInstance(this.contextClass); + context.setRequest(request); + context.setResponse(response); + return context; + } catch (Exception e) { + throw new StripesServletException( + "Could not instantiate configured " + "ActionBeanContext class: " + this.contextClass, e); } + } - protected Configuration getConfiguration() - { - return configuration; - } + protected Configuration getConfiguration() { + return configuration; + } - protected void setConfiguration(Configuration configuration) - { - this.configuration = configuration; - } + protected void setConfiguration(Configuration configuration) { + this.configuration = configuration; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultActionBeanPropertyBinder.java b/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultActionBeanPropertyBinder.java index 78305402b..48cfd645d 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultActionBeanPropertyBinder.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultActionBeanPropertyBinder.java @@ -14,6 +14,21 @@ */ package net.sourceforge.stripes.controller; +import jakarta.servlet.http.HttpServletRequest; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.action.FileBean; @@ -38,851 +53,855 @@ import net.sourceforge.stripes.validation.ValidationMetadata; import net.sourceforge.stripes.validation.expression.ExpressionValidator; -import javax.servlet.http.HttpServletRequest; -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; -import java.util.TreeMap; - /** - *

    * Implementation of the ActionBeanPropertyBinder interface that uses Stripes' built in property * expression support to perform JavaBean property binding. Several additions/enhancements are * available above and beyond the standard JavaBean syntax. These include: - *

    - * + * *
      - *
    • The ability to instantiate and set null intermediate properties in a property chain
    • - *
    • The ability to use Lists and Maps directly for indexed properties
    • - *
    • The ability to infer type information from the generics information in classes
    • + *
    • The ability to instantiate and set null intermediate properties in a property chain + *
    • The ability to use Lists and Maps directly for indexed properties + *
    • The ability to infer type information from the generics information in classes *
    - * + * * @author Tim Fennell * @since Stripes 1.4 */ public class DefaultActionBeanPropertyBinder implements ActionBeanPropertyBinder { - private static final Log log = Log.getInstance(DefaultActionBeanPropertyBinder.class); - - /** Configuration instance passed in at initialization time. */ - private Configuration configuration; - - /** - * Looks up and caches in a useful form the metadata necessary to perform validations as - * properties are bound to the bean. - */ - public void init(Configuration configuration) throws Exception { - this.configuration = configuration; + private static final Log log = Log.getInstance(DefaultActionBeanPropertyBinder.class); + + /** Configuration instance passed in at initialization time. */ + private Configuration configuration; + + /** + * Looks up and caches in a useful form the metadata necessary to perform validations as + * properties are bound to the bean. + */ + public void init(Configuration configuration) throws Exception { + this.configuration = configuration; + } + + /** Returns the Configuration object that was passed to the init() method. */ + protected Configuration getConfiguration() { + return configuration; + } + + /** + * Loops through the parameters contained in the request and attempts to bind each one to the + * supplied ActionBean. Invokes validation for each of the properties on the bean before binding + * is attempted. Only fields which do not produce validation errors will be bound to the + * ActionBean. + * + *

    Individual property binding is delegated to the other interface method, bind(ActionBean, + * String, Object), in order to allow for easy extension of this class. + * + * @param bean the ActionBean whose properties are to be validated and bound + * @param context the ActionBeanContext of the current request + * @param validate true indicates that validation should be run, false indicates that only type + * conversion should occur + */ + public ValidationErrors bind(ActionBean bean, ActionBeanContext context, boolean validate) { + ValidationErrors fieldErrors = context.getValidationErrors(); + Map validationInfos = + this.configuration.getValidationMetadataProvider().getValidationMetadata(bean.getClass()); + + // Take the ParameterMap and turn the keys into ParameterNames + Map parameters = getParameters(bean); + + // Run the required validation first to catch fields that weren't even submitted + if (validate) { + validateRequiredFields(parameters, bean, fieldErrors); } - /** Returns the Configuration object that was passed to the init() method. */ - protected Configuration getConfiguration() { return configuration; } - - /** - *

    - * Loops through the parameters contained in the request and attempts to bind each one to the - * supplied ActionBean. Invokes validation for each of the properties on the bean before binding - * is attempted. Only fields which do not produce validation errors will be bound to the - * ActionBean. - *

    - * - *

    - * Individual property binding is delegated to the other interface method, bind(ActionBean, - * String, Object), in order to allow for easy extension of this class. - *

    - * - * @param bean the ActionBean whose properties are to be validated and bound - * @param context the ActionBeanContext of the current request - * @param validate true indicates that validation should be run, false indicates that only type - * conversion should occur - */ - public ValidationErrors bind(ActionBean bean, ActionBeanContext context, boolean validate) { - ValidationErrors fieldErrors = context.getValidationErrors(); - Map validationInfos = this.configuration - .getValidationMetadataProvider().getValidationMetadata(bean.getClass()); - - // Take the ParameterMap and turn the keys into ParameterNames - Map parameters = getParameters(bean); - - // Run the required validation first to catch fields that weren't even submitted - if (validate) { - validateRequiredFields(parameters, bean, fieldErrors); - } - - // Converted values for all fields are accumulated in this map to make post-conversion - // validation go a little easier - Map> allConvertedFields = new TreeMap>(); - - // First we bind all the regular parameters - for (Map.Entry entry : parameters.entrySet()) { - List convertedValues = null; - ParameterName name = entry.getKey(); - - try { - String pname = name.getName(); // exact name of the param in the request - if (!StripesConstants.SPECIAL_URL_KEYS.contains(pname) - && !fieldErrors.containsKey(pname)) { - log.trace("Running binding for property with name: ", name); - - // Determine the target type - ValidationMetadata validationInfo = validationInfos.get(name.getStrippedName()); - PropertyExpressionEvaluation eval; - try { - eval = new PropertyExpressionEvaluation(PropertyExpression - .getExpression(pname), bean); - } - catch (Exception e) { - if (pname.equals(context.getEventName())) - continue; - else - throw e; - } - Class type = eval.getType(); - Class scalarType = eval.getScalarType(); - - // Check to see if binding into this expression is permitted - if (!isBindingAllowed(eval)) - continue; - - if (type == null - && (validationInfo == null || validationInfo.converter() == null)) { - if (!pname.equals(context.getEventName())) { - log.trace("Could not find type for property '", name.getName(), - "' of '", bean.getClass().getSimpleName(), - "' probably because it's not ", - "a property of the bean. Skipping binding."); - } - continue; - } - String[] values = entry.getValue(); - - // Do Validation and type conversion - List errors = new ArrayList(); - - // If the property should be ignored, skip to the next property - if (validationInfo != null && validationInfo.ignore()) { - continue; - } - - if (validate && validationInfo != null) { - doPreConversionValidations(name, values, validationInfo, errors); - } - - // Only do type conversion if there aren't errors already - if (errors.isEmpty()) { - convertedValues = convert(bean, name, values, type, scalarType, validationInfo, errors); - allConvertedFields.put(name, convertedValues); - } - - // If we have errors, save them, otherwise bind the parameter to the form - if (errors.size() > 0) { - fieldErrors.addAll(name.getName(), errors); - } - else if (convertedValues.size() > 0) { - bindNonNullValue(bean, eval, convertedValues, type, scalarType); - } - else { - bindNullValue(bean, name.getName(), type); - } - } - } - catch (Exception e) { - handlePropertyBindingError(bean, name, convertedValues, e, fieldErrors); - } - } - - // Null out any values that were in the form, but values were not supplied - bindMissingValuesAsNull(bean, context); - - // Then we figure out if any files were uploaded and bind those too - StripesRequestWrapper request = StripesRequestWrapper.findStripesWrapper(context - .getRequest()); - if (request.isMultipart()) { - Enumeration fileParameterNames = request.getFileParameterNames(); - - while (fileParameterNames.hasMoreElements()) { - String fileParameterName = fileParameterNames.nextElement(); - FileBean fileBean = request.getFileParameterValue(fileParameterName); - log.trace("Attempting to bind file parameter with name [", fileParameterName, - "] and value: ", fileBean); - - if (fileBean != null) { - try { - bind(bean, fileParameterName, fileBean); - } - catch (Exception e) { - log.debug(e, "Could not bind file property with name [", fileParameterName, - "] and value: ", fileBean); - } - } + // Converted values for all fields are accumulated in this map to make post-conversion + // validation go a little easier + Map> allConvertedFields = + new TreeMap>(); + + // First we bind all the regular parameters + for (Map.Entry entry : parameters.entrySet()) { + List convertedValues = null; + ParameterName name = entry.getKey(); + + try { + String pname = name.getName(); // exact name of the param in the request + if (!StripesConstants.SPECIAL_URL_KEYS.contains(pname) && !fieldErrors.containsKey(pname)) { + log.trace("Running binding for property with name: ", name); + + // Determine the target type + ValidationMetadata validationInfo = validationInfos.get(name.getStrippedName()); + PropertyExpressionEvaluation eval; + try { + eval = new PropertyExpressionEvaluation(PropertyExpression.getExpression(pname), bean); + } catch (Exception e) { + if (pname.equals(context.getEventName())) continue; + else throw e; + } + Class type = eval.getType(); + Class scalarType = eval.getScalarType(); + + // Check to see if binding into this expression is permitted + if (!isBindingAllowed(eval)) continue; + + if (type == null && (validationInfo == null || validationInfo.converter() == null)) { + if (!pname.equals(context.getEventName())) { + log.trace( + "Could not find type for property '", + name.getName(), + "' of '", + bean.getClass().getSimpleName(), + "' probably because it's not ", + "a property of the bean. Skipping binding."); } + continue; + } + String[] values = entry.getValue(); + + // Do Validation and type conversion + List errors = new ArrayList(); + + // If the property should be ignored, skip to the next property + if (validationInfo != null && validationInfo.ignore()) { + continue; + } + + if (validate && validationInfo != null) { + doPreConversionValidations(name, values, validationInfo, errors); + } + + // Only do type conversion if there aren't errors already + if (errors.isEmpty()) { + convertedValues = convert(bean, name, values, type, scalarType, validationInfo, errors); + allConvertedFields.put(name, convertedValues); + } + + // If we have errors, save them, otherwise bind the parameter to the form + if (errors.size() > 0) { + fieldErrors.addAll(name.getName(), errors); + } else if (convertedValues.size() > 0) { + bindNonNullValue(bean, eval, convertedValues, type, scalarType); + } else { + bindNullValue(bean, name.getName(), type); + } } - - // Run post-conversion validation after absolutely everything has been bound - // and validated so that the expression validation can have access to the full - // state of the bean - if (validate) { - doPostConversionValidations(bean, allConvertedFields, fieldErrors); - } - - return fieldErrors; + } catch (Exception e) { + handlePropertyBindingError(bean, name, convertedValues, e, fieldErrors); + } } - /** - *

    - * Checks to see if binding is permitted for the provided expression evaluation. Note that the - * expression is available through the {@code getExpression()} and the ActionBean is available - * through the {@code getBean()} method on the evaluation. - *

    - * - *

    - * By default checks to ensure that the expression is not attempting to bind into the - * ActionBeanContext for security reasons. - *

    - * - * @param eval the expression evaluation to check for binding permission - * @return true if binding can/should proceed, false to veto binding - */ - protected boolean isBindingAllowed(PropertyExpressionEvaluation eval) { - boolean allowed = BindingPolicyManager.getInstance(eval.getBean().getClass()) - .isBindingAllowed(eval); - if (!allowed) { - String param = eval.getExpression().getSource(); - if (configuration.isDebugMode()) { - throw new BindingDeniedException(param); - } - log.warn("Binding denied for parameter [", param, "]. Use @Validate to allow binding in conjunction with @StrictBinding."); + // Null out any values that were in the form, but values were not supplied + bindMissingValuesAsNull(bean, context); + + // Then we figure out if any files were uploaded and bind those too + StripesRequestWrapper request = StripesRequestWrapper.findStripesWrapper(context.getRequest()); + if (request.isMultipart()) { + Enumeration fileParameterNames = request.getFileParameterNames(); + + while (fileParameterNames.hasMoreElements()) { + String fileParameterName = fileParameterNames.nextElement(); + FileBean fileBean = request.getFileParameterValue(fileParameterName); + log.trace( + "Attempting to bind file parameter with name [", + fileParameterName, + "] and value: ", + fileBean); + + if (fileBean != null) { + try { + bind(bean, fileParameterName, fileBean); + } catch (Exception e) { + log.debug( + e, + "Could not bind file property with name [", + fileParameterName, + "] and value: ", + fileBean); + } } - return allowed; + } } - /** - * Invoked whenever an exception is thrown when attempting to bind a property to an ActionBean. - * By default logs some information about the occurrence, but could be overridden to do more - * intelligent things based on the application. - * - * @param bean the ActionBean that was the subject of binding - * @param name the ParameterName object for the parameter being bound - * @param values the list of values being bound, potentially null if the error occurred when - * binding a null value - * @param e the exception raised during binding - * @param errors the validation errors object associated to the ActionBean - */ - protected void handlePropertyBindingError(ActionBean bean, ParameterName name, - List values, Exception e, ValidationErrors errors) { - if (e instanceof NoSuchPropertyException) { - NoSuchPropertyException nspe = (NoSuchPropertyException) e; - // No stack trace if it's a no such property exception - log.debug("Could not bind property with name [", name, "] to bean of type: ", bean - .getClass().getSimpleName(), " : ", nspe.getMessage()); - } - else { - log.debug(e, "Could not bind property with name [", name, "] to bean of type: ", bean - .getClass().getSimpleName()); - } + // Run post-conversion validation after absolutely everything has been bound + // and validated so that the expression validation can have access to the full + // state of the bean + if (validate) { + doPostConversionValidations(bean, allConvertedFields, fieldErrors); } - /** - * Uses a hidden field to determine what (if any) fields were present in the form but did not get - * submitted to the server. For each such field the value is "softly" set to null on the - * ActionBean. This is not uncommon for checkboxes, and also for multi-selects. - * - * @param bean the ActionBean being bound to - * @param context the current ActionBeanContext - */ - @SuppressWarnings("unchecked") - protected void bindMissingValuesAsNull(ActionBean bean, ActionBeanContext context) { - Set parametersSubmitted = context.getRequest().getParameterMap().keySet(); - - for (String name : getFieldsPresentInfo(bean)) { - if (!parametersSubmitted.contains(name)) { - try { - BeanUtil.setPropertyToNull(name, bean); - } - catch (Exception e) { - handlePropertyBindingError(bean, new ParameterName(name), null, e, context - .getValidationErrors()); - } - } - } + return fieldErrors; + } + + /** + * Checks to see if binding is permitted for the provided expression evaluation. Note that the + * expression is available through the {@code getExpression()} and the ActionBean is available + * through the {@code getBean()} method on the evaluation. + * + *

    By default checks to ensure that the expression is not attempting to bind into the + * ActionBeanContext for security reasons. + * + * @param eval the expression evaluation to check for binding permission + * @return true if binding can/should proceed, false to veto binding + */ + protected boolean isBindingAllowed(PropertyExpressionEvaluation eval) { + boolean allowed = + BindingPolicyManager.getInstance(eval.getBean().getClass()).isBindingAllowed(eval); + if (!allowed) { + String param = eval.getExpression().getSource(); + if (configuration.isDebugMode()) { + throw new BindingDeniedException(param); + } + log.warn( + "Binding denied for parameter [", + param, + "]. Use @Validate to allow binding in conjunction with @StrictBinding."); } - - /** - * In a lot of cases (and specifically during wizards) the Stripes form field writes out a - * hidden field containing a set of field names. This is encrypted to stop the user from - * monkeying with it. This method retrieves the list of field names, decrypts it and splits it - * out into a Collection of field names. - * - * @param bean the current ActionBean - * @return a non-null (though possibly empty) list of field names - */ - protected Collection getFieldsPresentInfo(ActionBean bean) { - ActionBeanContext ctx = bean.getContext(); - String fieldsPresent = ctx.getRequest().getParameter(StripesConstants.URL_KEY_FIELDS_PRESENT); - Wizard wizard = bean.getClass().getAnnotation(Wizard.class); - boolean isWizard = wizard != null; - - if (fieldsPresent == null || "".equals(fieldsPresent)) { - if (isWizard && !CollectionUtil.contains(wizard.startEvents(), ctx.getEventName())) { - throw new StripesRuntimeException( - "Submission of a wizard form in Stripes absolutely requires that " - + "the hidden field Stripes writes containing the names of the fields " - + "present on the form is present and encrypted (as Stripes write it). " - + "This is necessary to prevent a user from spoofing the system and " - + "getting around any security/data checks."); - } - else { - return Collections.emptySet(); - } - } - else { - fieldsPresent = CryptoUtil.decrypt(fieldsPresent); - return HtmlUtil.splitValues(fieldsPresent); - } + return allowed; + } + + /** + * Invoked whenever an exception is thrown when attempting to bind a property to an ActionBean. By + * default logs some information about the occurrence, but could be overridden to do more + * intelligent things based on the application. + * + * @param bean the ActionBean that was the subject of binding + * @param name the ParameterName object for the parameter being bound + * @param values the list of values being bound, potentially null if the error occurred when + * binding a null value + * @param e the exception raised during binding + * @param errors the validation errors object associated to the ActionBean + */ + protected void handlePropertyBindingError( + ActionBean bean, + ParameterName name, + List values, + Exception e, + ValidationErrors errors) { + if (e instanceof NoSuchPropertyException) { + NoSuchPropertyException nspe = (NoSuchPropertyException) e; + // No stack trace if it's a no such property exception + log.debug( + "Could not bind property with name [", + name, + "] to bean of type: ", + bean.getClass().getSimpleName(), + " : ", + nspe.getMessage()); + } else { + log.debug( + e, + "Could not bind property with name [", + name, + "] to bean of type: ", + bean.getClass().getSimpleName()); } - - /** - * Internal helper method to bind one or more values to a single property on an ActionBean. If - * the target type is an array of Collection, then all values are bound. If the target type is a - * scalar type then the first value in the List of values is bound. - * - * @param bean the ActionBean instance to which the property is being bound - * @param propertyEvaluation the property evaluation to be used to set the property - * @param valueOrValues a List containing one or more values - * @param targetType the declared type of the property on the ActionBean - * @throws Exception if the property cannot be bound for any reason - */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - protected void bindNonNullValue(ActionBean bean, - PropertyExpressionEvaluation propertyEvaluation, List valueOrValues, - Class targetType, Class scalarType) throws Exception { - Class valueType = valueOrValues.iterator().next().getClass(); - - // If the target type is an array, set it as one, otherwise set as scalar - if (targetType.isArray() && !valueType.isArray()) { - Object typedArray = Array.newInstance(scalarType, valueOrValues.size()); - for (int i = 0; i < valueOrValues.size(); ++i) { - Array.set(typedArray, i, valueOrValues.get(i)); - } - - propertyEvaluation.setValue(typedArray); - } - else if (Collection.class.isAssignableFrom(targetType) - && !Collection.class.isAssignableFrom(valueType)) { - Collection collection; - if (EnumSet.class.isAssignableFrom(targetType) && Enum.class.isAssignableFrom(scalarType)) { - collection = EnumSet.noneOf(scalarType.asSubclass(Enum.class)); - } - else { - collection = getConfiguration().getObjectFactory().newInstance( - (Class) targetType); - } - - collection.addAll(valueOrValues); - propertyEvaluation.setValue(collection); - } - else { - propertyEvaluation.setValue(valueOrValues.get(0)); + } + + /** + * Uses a hidden field to determine what (if any) fields were present in the form but did not get + * submitted to the server. For each such field the value is "softly" set to null on the + * ActionBean. This is not uncommon for checkboxes, and also for multi-selects. + * + * @param bean the ActionBean being bound to + * @param context the current ActionBeanContext + */ + @SuppressWarnings("unchecked") + protected void bindMissingValuesAsNull(ActionBean bean, ActionBeanContext context) { + Set parametersSubmitted = context.getRequest().getParameterMap().keySet(); + + for (String name : getFieldsPresentInfo(bean)) { + if (!parametersSubmitted.contains(name)) { + try { + BeanUtil.setPropertyToNull(name, bean); + } catch (Exception e) { + handlePropertyBindingError( + bean, new ParameterName(name), null, e, context.getValidationErrors()); } + } } - - /** - * Internal helper method that determines what to do when no value was supplied for a given form - * field (but the field was present on the page). In all cases if the property is already null, - * or intervening objects in a nested property are null, nothing is done. If the property is - * non-null, it will be set to null. Unless the property is a collection, in which case it will - * be clear()'d. - * - * @param bean the ActionBean to which properties are being bound - * @param property the name of the property being bound - * @param type the declared type of the property on the ActionBean - * @throws ExpressionException if the value cannot be manipulated for any reason - */ - protected void bindNullValue(ActionBean bean, String property, Class type) - throws ExpressionException { - BeanUtil.setPropertyToNull(property, bean); + } + + /** + * In a lot of cases (and specifically during wizards) the Stripes form field writes out a hidden + * field containing a set of field names. This is encrypted to stop the user from monkeying with + * it. This method retrieves the list of field names, decrypts it and splits it out into a + * Collection of field names. + * + * @param bean the current ActionBean + * @return a non-null (though possibly empty) list of field names + */ + protected Collection getFieldsPresentInfo(ActionBean bean) { + ActionBeanContext ctx = bean.getContext(); + String fieldsPresent = ctx.getRequest().getParameter(StripesConstants.URL_KEY_FIELDS_PRESENT); + Wizard wizard = bean.getClass().getAnnotation(Wizard.class); + boolean isWizard = wizard != null; + + if (fieldsPresent == null || "".equals(fieldsPresent)) { + if (isWizard && !CollectionUtil.contains(wizard.startEvents(), ctx.getEventName())) { + throw new StripesRuntimeException( + "Submission of a wizard form in Stripes absolutely requires that " + + "the hidden field Stripes writes containing the names of the fields " + + "present on the form is present and encrypted (as Stripes write it). " + + "This is necessary to prevent a user from spoofing the system and " + + "getting around any security/data checks."); + } else { + return Collections.emptySet(); + } + } else { + fieldsPresent = CryptoUtil.decrypt(fieldsPresent); + return HtmlUtil.splitValues(fieldsPresent); } - - /** - * Converts the map of parameters in the request into a Map of ParameterName to String[]. - * Returns a SortedMap so that when iterated over parameter names are accessed in order of - * length of parameter name. - */ - @SuppressWarnings("unchecked") - protected SortedMap getParameters(ActionBean bean) { - Map requestParameters = bean.getContext().getRequest().getParameterMap(); - Map validations = StripesFilter.getConfiguration() - .getValidationMetadataProvider().getValidationMetadata(bean.getClass()); - SortedMap parameters = new TreeMap(); - - for (Map.Entry entry : requestParameters.entrySet()) { - ParameterName paramName = new ParameterName(entry.getKey().trim()); - ValidationMetadata validation = validations.get(paramName.getStrippedName()); - parameters.put(paramName, trim(entry.getValue(), validation)); - } - - return parameters; + } + + /** + * Internal helper method to bind one or more values to a single property on an ActionBean. If the + * target type is an array of Collection, then all values are bound. If the target type is a + * scalar type then the first value in the List of values is bound. + * + * @param bean the ActionBean instance to which the property is being bound + * @param propertyEvaluation the property evaluation to be used to set the property + * @param valueOrValues a List containing one or more values + * @param targetType the declared type of the property on the ActionBean + * @throws Exception if the property cannot be bound for any reason + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + protected void bindNonNullValue( + ActionBean bean, + PropertyExpressionEvaluation propertyEvaluation, + List valueOrValues, + Class targetType, + Class scalarType) + throws Exception { + Class valueType = valueOrValues.iterator().next().getClass(); + + // If the target type is an array, set it as one, otherwise set as scalar + if (targetType.isArray() && !valueType.isArray()) { + Object typedArray = Array.newInstance(scalarType, valueOrValues.size()); + for (int i = 0; i < valueOrValues.size(); ++i) { + Array.set(typedArray, i, valueOrValues.get(i)); + } + + propertyEvaluation.setValue(typedArray); + } else if (Collection.class.isAssignableFrom(targetType) + && !Collection.class.isAssignableFrom(valueType)) { + Collection collection; + if (EnumSet.class.isAssignableFrom(targetType) && Enum.class.isAssignableFrom(scalarType)) { + collection = EnumSet.noneOf(scalarType.asSubclass(Enum.class)); + } else { + collection = + getConfiguration() + .getObjectFactory() + .newInstance((Class) targetType); + } + + collection.addAll(valueOrValues); + propertyEvaluation.setValue(collection); + } else { + propertyEvaluation.setValue(valueOrValues.get(0)); } - - /** - * Attempt to set the named property on the target bean. If the binding fails for any reason - * (property does not exist, type conversion not possible etc.) an exception will be thrown. - * - * @param bean the ActionBean on to which the property is to be bound - * @param propertyName the name of the property to be bound (simple or complex) - * @param propertyValue the value of the target property - * @throws Exception thrown if the property cannot be bound for any reason - */ - public void bind(ActionBean bean, String propertyName, Object propertyValue) throws Exception { - BeanUtil.setPropertyValue(propertyName, bean, propertyValue); + } + + /** + * Internal helper method that determines what to do when no value was supplied for a given form + * field (but the field was present on the page). In all cases if the property is already null, or + * intervening objects in a nested property are null, nothing is done. If the property is + * non-null, it will be set to null. Unless the property is a collection, in which case it will be + * clear()'d. + * + * @param bean the ActionBean to which properties are being bound + * @param property the name of the property being bound + * @param type the declared type of the property on the ActionBean + * @throws ExpressionException if the value cannot be manipulated for any reason + */ + protected void bindNullValue(ActionBean bean, String property, Class type) + throws ExpressionException { + BeanUtil.setPropertyToNull(property, bean); + } + + /** + * Converts the map of parameters in the request into a Map of ParameterName to String[]. Returns + * a SortedMap so that when iterated over parameter names are accessed in order of length of + * parameter name. + */ + @SuppressWarnings("unchecked") + protected SortedMap getParameters(ActionBean bean) { + Map requestParameters = bean.getContext().getRequest().getParameterMap(); + Map validations = + StripesFilter.getConfiguration() + .getValidationMetadataProvider() + .getValidationMetadata(bean.getClass()); + SortedMap parameters = new TreeMap(); + + for (Map.Entry entry : requestParameters.entrySet()) { + ParameterName paramName = new ParameterName(entry.getKey().trim()); + ValidationMetadata validation = validations.get(paramName.getStrippedName()); + parameters.put(paramName, trim(entry.getValue(), validation)); } - /** - * Validates that all required fields have been submitted. This is done by looping through the - * set of validation annotations and checking that each field marked as required was submitted - * in the request and submitted with a non-empty value. - */ - protected void validateRequiredFields(Map parameters, ActionBean bean, - ValidationErrors errors) { - - log.debug("Running required field validation on bean class ", bean.getClass().getName()); + return parameters; + } + + /** + * Attempt to set the named property on the target bean. If the binding fails for any reason + * (property does not exist, type conversion not possible etc.) an exception will be thrown. + * + * @param bean the ActionBean on to which the property is to be bound + * @param propertyName the name of the property to be bound (simple or complex) + * @param propertyValue the value of the target property + * @throws Exception thrown if the property cannot be bound for any reason + */ + public void bind(ActionBean bean, String propertyName, Object propertyValue) throws Exception { + BeanUtil.setPropertyValue(propertyName, bean, propertyValue); + } + + /** + * Validates that all required fields have been submitted. This is done by looping through the set + * of validation annotations and checking that each field marked as required was submitted in the + * request and submitted with a non-empty value. + */ + protected void validateRequiredFields( + Map parameters, ActionBean bean, ValidationErrors errors) { + + log.debug("Running required field validation on bean class ", bean.getClass().getName()); + + // Assemble a set of names that we know have indexed parameters, so we won't check + // for required-ness the regular way + Set indexedParams = new HashSet(); + for (ParameterName name : parameters.keySet()) { + if (name.isIndexed()) { + indexedParams.add(name.getStrippedName()); + } + } - // Assemble a set of names that we know have indexed parameters, so we won't check - // for required-ness the regular way - Set indexedParams = new HashSet(); - for (ParameterName name : parameters.keySet()) { - if (name.isIndexed()) { - indexedParams.add(name.getStrippedName()); + Map validationInfos = + this.configuration.getValidationMetadataProvider().getValidationMetadata(bean.getClass()); + ActionBeanContext context = bean.getContext(); + HttpServletRequest request = context.getRequest(); + StripesRequestWrapper stripesReq = StripesRequestWrapper.findStripesWrapper(request); + + if (validationInfos != null) { + boolean wizard = bean.getClass().getAnnotation(Wizard.class) != null; + Collection fieldsOnPage = getFieldsPresentInfo(bean); + + for (Map.Entry entry : validationInfos.entrySet()) { + String propertyName = entry.getKey(); + ValidationMetadata validationInfo = entry.getValue(); + + // If the field is required, and we don't have index params that collapse + // to that property name, check that it was supplied + if (validationInfo.requiredOn(context.getEventName()) + && !indexedParams.contains(propertyName)) { + + // Make the added check that if the form is a wizard, the required field is + // in the set of fields that were on the page + if (!wizard || fieldsOnPage.contains(propertyName)) { + String[] values = trim(request.getParameterValues(propertyName), validationInfo); + + // Decrypt encrypted fields before checking for null + if (validationInfo.encrypted()) { + for (int i = 0, n = values.length; i < n; i++) { + if (values[i] != null) values[i] = CryptoUtil.decrypt(values[i]); + } } - } - Map validationInfos = this.configuration - .getValidationMetadataProvider().getValidationMetadata(bean.getClass()); - ActionBeanContext context = bean.getContext(); - HttpServletRequest request = context.getRequest(); - StripesRequestWrapper stripesReq = StripesRequestWrapper.findStripesWrapper(request); - - if (validationInfos != null) { - boolean wizard = bean.getClass().getAnnotation(Wizard.class) != null; - Collection fieldsOnPage = getFieldsPresentInfo(bean); - - for (Map.Entry entry : validationInfos.entrySet()) { - String propertyName = entry.getKey(); - ValidationMetadata validationInfo = entry.getValue(); - - // If the field is required, and we don't have index params that collapse - // to that property name, check that it was supplied - if (validationInfo.requiredOn(context.getEventName()) - && !indexedParams.contains(propertyName)) { - - // Make the added check that if the form is a wizard, the required field is - // in the set of fields that were on the page - if (!wizard || fieldsOnPage.contains(propertyName)) { - String[] values = trim(request.getParameterValues(propertyName), validationInfo); - - // Decrypt encrypted fields before checking for null - if (validationInfo.encrypted()) { - for (int i = 0, n = values.length; i < n; i++) { - if (values[i] != null) - values[i] = CryptoUtil.decrypt(values[i]); - } - } - - log.debug("Checking required field: ", propertyName, ", with values: ", values); - checkSingleRequiredField(propertyName, propertyName, values, stripesReq, errors); - } - } - } + log.debug("Checking required field: ", propertyName, ", with values: ", values); + checkSingleRequiredField(propertyName, propertyName, values, stripesReq, errors); + } } + } + } - // Now the easy work is done, figure out which rows of indexed props had values submitted - // and what to flag up as failing required field validation - if (indexedParams.size() > 0) { - Map rows = new HashMap(); - - for (Map.Entry entry : parameters.entrySet()) { - ParameterName name = entry.getKey(); - String[] values = entry.getValue(); - - if (name.isIndexed()) { - String rowKey = name.getName().substring(0, name.getName().indexOf(']') + 1); - if (!rows.containsKey(rowKey)) { - rows.put(rowKey, new Row()); - } - - rows.get(rowKey).put(name, values); - } - } + // Now the easy work is done, figure out which rows of indexed props had values submitted + // and what to flag up as failing required field validation + if (indexedParams.size() > 0) { + Map rows = new HashMap(); - for (Row row : rows.values()) { - if (row.hasNonEmptyValues()) { - for (Map.Entry entry : row.entrySet()) { - ParameterName name = entry.getKey(); - String[] values = entry.getValue(); - ValidationMetadata validationInfo = validationInfos.get(name.getStrippedName()); - - if (validationInfo != null - && validationInfo.requiredOn(context.getEventName())) { - checkSingleRequiredField(name.getName(), name.getStrippedName(), - values, stripesReq, errors); - } - } - } - else { - // If the row is full of empty data, get rid of it all to - // prevent problems in downstream validation - for (ParameterName name : row.keySet()) { - parameters.remove(name); - } - } - } + for (Map.Entry entry : parameters.entrySet()) { + ParameterName name = entry.getKey(); + String[] values = entry.getValue(); - } - } + if (name.isIndexed()) { + String rowKey = name.getName().substring(0, name.getName().indexOf(']') + 1); + if (!rows.containsKey(rowKey)) { + rows.put(rowKey, new Row()); + } - /** - *

    - * Checks to see if a single field's set of values are 'present', where that is defined as - * having one or more values, and where each value is a non-empty String after it has had white - * space trimmed from each end. - *

    - * - *

    - * For any fields that fail validation, creates a ScopedLocaliableError that uses the stripped - * name of the field to find localized info (e.g. foo.bar instead of foo[1].bar). The error is - * bound to the actual field on the form though, e.g. foo[1].bar. - *

    - * - * @param name the name of the parameter verbatim from the request - * @param strippedName the name of the parameter with any indexing removed from it - * @param values the String[] of values that was submitted in the request - * @param errors a ValidationErrors object into which errors can be placed - */ - protected void checkSingleRequiredField(String name, String strippedName, String[] values, - StripesRequestWrapper req, ValidationErrors errors) { - - // Firstly if the post is a multipart request, check to see if a file was - // sent under that parameter name - FileBean file = null; - if (req.isMultipart() && (file = req.getFileParameterValue(name)) != null) { - if (file.getSize() <= 0) { - errors.add(name, new ScopedLocalizableError("validation.required", - "valueNotPresent")); - } - } - // And if not, see if any regular parameters were sent - else if (values == null || values.length == 0) { - ValidationError error = new ScopedLocalizableError("validation.required", - "valueNotPresent"); - error.setFieldValue(null); - errors.add(name, error); - } - else { - for (String value : values) { - if (value == null || value.length() == 0) { - ValidationError error = new ScopedLocalizableError("validation.required", - "valueNotPresent"); - error.setFieldValue(value); - errors.add(name, error); - } - } - } - } - - /** - * Performs several basic validations on the String value supplied in the HttpServletRequest, - * based on information provided in annotations on the ActionBean. - * - * @param propertyName the name of the property being validated (used for constructing errors) - * @param values the String[] of values from the request being validated - * @param validationInfo the ValidationMetadata for the property being validated - * @param errors a collection of errors to be populated with any validation errors discovered - */ - protected void doPreConversionValidations(ParameterName propertyName, String[] values, - ValidationMetadata validationInfo, List errors) { - - for (String value : values) { - // Only run validations when there are non-empty values - if (value != null && value.length() > 0) { - if (validationInfo.minlength() != null - && value.length() < validationInfo.minlength()) { - ValidationError error = new ScopedLocalizableError("validation.minlength", - "valueTooShort", validationInfo.minlength()); - - error.setFieldValue(value); - errors.add(error); - } - - if (validationInfo.maxlength() != null - && value.length() > validationInfo.maxlength()) { - ValidationError error = new ScopedLocalizableError("validation.maxlength", - "valueTooLong", validationInfo.maxlength()); - error.setFieldValue(value); - errors.add(error); - } - - if (validationInfo.mask() != null - && !validationInfo.mask().matcher(value).matches()) { - - ValidationError error = new ScopedLocalizableError("validation.mask", - "valueDoesNotMatch"); - - error.setFieldValue(value); - errors.add(error); - } - } + rows.get(rowKey).put(name, values); } - } - - /** - * Performs basic post-conversion validations on the properties of the ActionBean after they - * have been converted to their rich type by the type conversion system. Validates single - * properties in isolation from other properties. - * - * @param bean the ActionBean that is undergoing validation and binding - * @param convertedValues a map of ParameterName to all converted values for each field - * @param errors the validation errors object to put errors in to - */ - protected void doPostConversionValidations(ActionBean bean, - Map> convertedValues, ValidationErrors errors) { + } - Map validationInfos = this.configuration - .getValidationMetadataProvider().getValidationMetadata(bean.getClass()); - for (Map.Entry> entry : convertedValues.entrySet()) { - // Sort out what we need to validate this field + for (Row row : rows.values()) { + if (row.hasNonEmptyValues()) { + for (Map.Entry entry : row.entrySet()) { ParameterName name = entry.getKey(); - List values = entry.getValue(); + String[] values = entry.getValue(); ValidationMetadata validationInfo = validationInfos.get(name.getStrippedName()); - if (values.size() == 0 || validationInfo == null) { - continue; + if (validationInfo != null && validationInfo.requiredOn(context.getEventName())) { + checkSingleRequiredField( + name.getName(), name.getStrippedName(), values, stripesReq, errors); } - - for (Object value : values) { - // If the value is a number then we should check to see if there are range - // boundaries - // established, and check them. - if (value instanceof Number) { - Number number = (Number) value; - - if (validationInfo.minvalue() != null - && number.doubleValue() < validationInfo.minvalue()) { - ValidationError error = new ScopedLocalizableError("validation.minvalue", - "valueBelowMinimum", validationInfo.minvalue()); - error.setFieldValue(String.valueOf(value)); - errors.add(name.getName(), error); - } - - if (validationInfo.maxvalue() != null - && number.doubleValue() > validationInfo.maxvalue()) { - ValidationError error = new ScopedLocalizableError("validation.maxvalue", - "valueAboveMaximum", validationInfo.maxvalue()); - error.setFieldValue(String.valueOf(value)); - errors.add(name.getName(), error); - } - } - } - - // And then do any expression validation - doExpressionValidation(bean, name, values, validationInfo, errors); + } + } else { + // If the row is full of empty data, get rid of it all to + // prevent problems in downstream validation + for (ParameterName name : row.keySet()) { + parameters.remove(name); + } } + } } - - /** - * Performs validation of attribute values using a JSP EL expression if one is defined in the - * {@literal @}Validate annotation. The expression is evaluated once for each value converted. - * See {@link net.sourceforge.stripes.validation.expression.ExpressionValidator} for details - * on how this is implemented. - * - * @param bean the ActionBean who's property is being validated - * @param name the name of the property being validated - * @param values the non-null post-conversion values for the property - * @param validationInfo the validation metadata for the property - * @param errors the validation errors object to add errors to - */ - protected void doExpressionValidation(ActionBean bean, ParameterName name, List values, - ValidationMetadata validationInfo, ValidationErrors errors) { - - if (validationInfo.expression() != null) - ExpressionValidator.evaluate(bean, name, values, validationInfo, errors); + } + + /** + * Checks to see if a single field's set of values are 'present', where that is defined as having + * one or more values, and where each value is a non-empty String after it has had white space + * trimmed from each end. + * + *

    + * + *

    For any fields that fail validation, creates a ScopedLocaliableError that uses the stripped + * name of the field to find localized info (e.g. foo.bar instead of foo[1].bar). The error is + * bound to the actual field on the form though, e.g. foo[1].bar. + * + * @param name the name of the parameter verbatim from the request + * @param strippedName the name of the parameter with any indexing removed from it + * @param values the String[] of values that was submitted in the request + * @param errors a ValidationErrors object into which errors can be placed + */ + protected void checkSingleRequiredField( + String name, + String strippedName, + String[] values, + StripesRequestWrapper req, + ValidationErrors errors) { + + // Firstly if the post is a multipart request, check to see if a file was + // sent under that parameter name + FileBean file = null; + if (req.isMultipart() && (file = req.getFileParameterValue(name)) != null) { + if (file.getSize() <= 0) { + errors.add(name, new ScopedLocalizableError("validation.required", "valueNotPresent")); + } } - - /** - *

    - * Converts the String[] of values for a given parameter in the HttpServletRequest into the - * desired type of Object. If a converter is declared using an annotation for the property (or - * getter/setter) then that converter will be used - if it does not convert to the right type an - * exception will be logged and values will not be converted. If no Converter was specified then - * a default converter will be looked up based on the target type of the property. If there is - * no default converter, then a Constructor will be looked for on the target type which takes a - * single String parameter. If such a Constructor exists it will be invoked. - *

    - * - *

    - * Only parameter values that are non-null and do not equal the empty String will be converted - * and returned. So an input array with one entry equaling the empty string, [""], will result - * in an empty List being returned. Similarly, if a length three array is passed in with - * one item equaling the empty String, a List of length two will be returned. - *

    - * - * @param bean the ActionBean on which the property to convert exists - * @param propertyName the name of the property being converted - * @param values a String array of values to attempt conversion of - * @param declaredType the declared type of the ActionBean property - * @param scalarType if the declaredType is a collection, map or array then this will - * be the type contained within the collection/map value/array, otherwise - * the same as declaredType - * @param validationInfo the validation metadata for the property if defined - * @param errors a List into which ValidationError objects will be populated for any errors - * discovered during conversion. - * @return List a List of objects containing only objects of the desired type. It is - * not guaranteed to be the same length as the values array passed in. - */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - protected List convert(ActionBean bean, ParameterName propertyName, String[] values, - Class declaredType, Class scalarType, - ValidationMetadata validationInfo, List errors) - throws Exception { - - List returns = new ArrayList(); - Class returnType = null; - - // Dig up the type converter. This gets a bit tricky because we need to handle - // the following cases: - // 1. We need to simply find a converter for the declared type of a simple property - // 2. We need to find a converter for the element type in a list/array/map - // 3. We have a domain model object that implements List/Map and has a converter itself! - TypeConverterFactory factory = this.configuration.getTypeConverterFactory(); - TypeConverter converter = null; - Locale locale = bean.getContext().getRequest().getLocale(); - - converter = factory.getTypeConverter(declaredType, locale); - if (validationInfo != null && validationInfo.converter() != null) { - // If a specific converter was requested and it's the same type as one we'd use - // for the declared type, set the return type appropriately - if (converter != null && validationInfo.converter().isAssignableFrom(converter.getClass())) { - returnType = declaredType; - } - // Otherwise assume that it's a converter for the scalar type inside a collection - else { - returnType = scalarType; - } - converter = factory.getInstance(validationInfo.converter(), locale); + // And if not, see if any regular parameters were sent + else if (values == null || values.length == 0) { + ValidationError error = new ScopedLocalizableError("validation.required", "valueNotPresent"); + error.setFieldValue(null); + errors.add(name, error); + } else { + for (String value : values) { + if (value == null || value.length() == 0) { + ValidationError error = + new ScopedLocalizableError("validation.required", "valueNotPresent"); + error.setFieldValue(value); + errors.add(name, error); } - // Else, if we got a converter for the declared type (e.g. Foo implements List) - // then convert for the declared type - else if (converter != null) { - returnType = declaredType; + } + } + } + + /** + * Performs several basic validations on the String value supplied in the HttpServletRequest, + * based on information provided in annotations on the ActionBean. + * + * @param propertyName the name of the property being validated (used for constructing errors) + * @param values the String[] of values from the request being validated + * @param validationInfo the ValidationMetadata for the property being validated + * @param errors a collection of errors to be populated with any validation errors discovered + */ + protected void doPreConversionValidations( + ParameterName propertyName, + String[] values, + ValidationMetadata validationInfo, + List errors) { + + for (String value : values) { + // Only run validations when there are non-empty values + if (value != null && value.length() > 0) { + if (validationInfo.minlength() != null && value.length() < validationInfo.minlength()) { + ValidationError error = + new ScopedLocalizableError( + "validation.minlength", "valueTooShort", validationInfo.minlength()); + + error.setFieldValue(value); + errors.add(error); } - // Else look for a converter for the scalar type (Bar in List) - else { - converter = factory.getTypeConverter(scalarType, locale); - returnType = scalarType; + + if (validationInfo.maxlength() != null && value.length() > validationInfo.maxlength()) { + ValidationError error = + new ScopedLocalizableError( + "validation.maxlength", "valueTooLong", validationInfo.maxlength()); + error.setFieldValue(value); + errors.add(error); } - log.debug("Converting ", values.length, " value(s) using ", (converter != null ? - "converter " + converter.getClass().getName() - : "Constructor(String) if available")); + if (validationInfo.mask() != null && !validationInfo.mask().matcher(value).matches()) { - for (String value : values) { - if (validationInfo != null && validationInfo.encrypted()) { - value = CryptoUtil.decrypt(value); - } + ValidationError error = + new ScopedLocalizableError("validation.mask", "valueDoesNotMatch"); - if (value != null && value.length() > 0) { - try { - Object retval = null; - if (converter != null) { - retval = converter.convert(value, returnType, errors); - } - else { - try { - retval = getConfiguration().getObjectFactory() - .constructor(returnType, String.class).newInstance(value); - } - catch (StripesRuntimeException e) { - log.debug("Could not find a way to convert the parameter ", propertyName.getName(), - " to a ", returnType.getSimpleName(), ". No TypeConverter could be ", - "found and the class does not ", "have a constructor that takes a ", - "single String parameter."); - } - } - - // If we managed to get a non-null converted value, add it to the return set - if (retval != null) { - returns.add(retval); - } - - // Set the field name and value on the error - for (ValidationError error : errors) { - error.setFieldName(propertyName.getStrippedName()); - error.setFieldValue(value); - } - } - catch (Exception e) { - log.warn(e, "Looks like type converter ", converter, " threw an exception."); - } - } + error.setFieldValue(value); + errors.add(error); + } + } + } + } + + /** + * Performs basic post-conversion validations on the properties of the ActionBean after they have + * been converted to their rich type by the type conversion system. Validates single properties in + * isolation from other properties. + * + * @param bean the ActionBean that is undergoing validation and binding + * @param convertedValues a map of ParameterName to all converted values for each field + * @param errors the validation errors object to put errors in to + */ + protected void doPostConversionValidations( + ActionBean bean, Map> convertedValues, ValidationErrors errors) { + + Map validationInfos = + this.configuration.getValidationMetadataProvider().getValidationMetadata(bean.getClass()); + for (Map.Entry> entry : convertedValues.entrySet()) { + // Sort out what we need to validate this field + ParameterName name = entry.getKey(); + List values = entry.getValue(); + ValidationMetadata validationInfo = validationInfos.get(name.getStrippedName()); + + if (values.size() == 0 || validationInfo == null) { + continue; + } + + for (Object value : values) { + // If the value is a number then we should check to see if there are range + // boundaries + // established, and check them. + if (value instanceof Number) { + Number number = (Number) value; + + if (validationInfo.minvalue() != null + && number.doubleValue() < validationInfo.minvalue()) { + ValidationError error = + new ScopedLocalizableError( + "validation.minvalue", "valueBelowMinimum", validationInfo.minvalue()); + error.setFieldValue(String.valueOf(value)); + errors.add(name.getName(), error); + } + + if (validationInfo.maxvalue() != null + && number.doubleValue() > validationInfo.maxvalue()) { + ValidationError error = + new ScopedLocalizableError( + "validation.maxvalue", "valueAboveMaximum", validationInfo.maxvalue()); + error.setFieldValue(String.valueOf(value)); + errors.add(name.getName(), error); + } } + } - return returns; + // And then do any expression validation + doExpressionValidation(bean, name, values, validationInfo, errors); + } + } + + /** + * Performs validation of attribute values using a JSP EL expression if one is defined in the + * {@literal @}Validate annotation. The expression is evaluated once for each value converted. See + * {@link net.sourceforge.stripes.validation.expression.ExpressionValidator} for details on how + * this is implemented. + * + * @param bean the ActionBean who's property is being validated + * @param name the name of the property being validated + * @param values the non-null post-conversion values for the property + * @param validationInfo the validation metadata for the property + * @param errors the validation errors object to add errors to + */ + protected void doExpressionValidation( + ActionBean bean, + ParameterName name, + List values, + ValidationMetadata validationInfo, + ValidationErrors errors) { + + if (validationInfo.expression() != null) + ExpressionValidator.evaluate(bean, name, values, validationInfo, errors); + } + + /** + * Converts the String[] of values for a given parameter in the HttpServletRequest into the + * desired type of Object. If a converter is declared using an annotation for the property (or + * getter/setter) then that converter will be used - if it does not convert to the right type an + * exception will be logged and values will not be converted. If no Converter was specified then a + * default converter will be looked up based on the target type of the property. If there is no + * default converter, then a Constructor will be looked for on the target type which takes a + * single String parameter. If such a Constructor exists it will be invoked. + * + *

    Only parameter values that are non-null and do not equal the empty String will be converted + * and returned. So an input array with one entry equaling the empty string, [""], will result in + * an empty List being returned. Similarly, if a length three array is passed in with one + * item equaling the empty String, a List of length two will be returned. + * + * @param bean the ActionBean on which the property to convert exists + * @param propertyName the name of the property being converted + * @param values a String array of values to attempt conversion of + * @param declaredType the declared type of the ActionBean property + * @param scalarType if the declaredType is a collection, map or array then this will be the type + * contained within the collection/map value/array, otherwise the same as declaredType + * @param validationInfo the validation metadata for the property if defined + * @param errors a List into which ValidationError objects will be populated for any errors + * discovered during conversion. + * @return List a List of objects containing only objects of the desired type. It is not + * guaranteed to be the same length as the values array passed in. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + protected List convert( + ActionBean bean, + ParameterName propertyName, + String[] values, + Class declaredType, + Class scalarType, + ValidationMetadata validationInfo, + List errors) + throws Exception { + + List returns = new ArrayList(); + Class returnType = null; + + // Dig up the type converter. This gets a bit tricky because we need to handle + // the following cases: + // 1. We need to simply find a converter for the declared type of a simple property + // 2. We need to find a converter for the element type in a list/array/map + // 3. We have a domain model object that implements List/Map and has a converter itself! + TypeConverterFactory factory = this.configuration.getTypeConverterFactory(); + TypeConverter converter = null; + Locale locale = bean.getContext().getRequest().getLocale(); + + converter = factory.getTypeConverter(declaredType, locale); + if (validationInfo != null && validationInfo.converter() != null) { + // If a specific converter was requested and it's the same type as one we'd use + // for the declared type, set the return type appropriately + if (converter != null && validationInfo.converter().isAssignableFrom(converter.getClass())) { + returnType = declaredType; + } + // Otherwise assume that it's a converter for the scalar type inside a collection + else { + returnType = scalarType; + } + converter = factory.getInstance(validationInfo.converter(), locale); + } + // Else, if we got a converter for the declared type (e.g. Foo implements List) + // then convert for the declared type + else if (converter != null) { + returnType = declaredType; + } + // Else look for a converter for the scalar type (Bar in List) + else { + converter = factory.getTypeConverter(scalarType, locale); + returnType = scalarType; } - /** - * Inspects the given {@link ValidationMetadata} object to determine if the given {@code values} - * should be trimmed. If so, then the trimmed values are returned. Otherwise, the values are - * returned unchanged. If {@code meta} is null, then the default action is taken, and the values - * are trimmed. Either {@code values} or {@code meta} (or both) may be null. - */ - protected String[] trim(String[] values, ValidationMetadata meta) { - if (values != null && values.length > 0 && (meta == null || meta.trim())) { - String[] copy = new String[values.length]; - for (int i = 0; i < values.length; i++) { - if (values[i] != null) - copy[i] = values[i].trim(); + log.debug( + "Converting ", + values.length, + " value(s) using ", + (converter != null + ? "converter " + converter.getClass().getName() + : "Constructor(String) if available")); + + for (String value : values) { + if (validationInfo != null && validationInfo.encrypted()) { + value = CryptoUtil.decrypt(value); + } + + if (value != null && value.length() > 0) { + try { + Object retval = null; + if (converter != null) { + retval = converter.convert(value, returnType, errors); + } else { + try { + retval = + getConfiguration() + .getObjectFactory() + .constructor(returnType, String.class) + .newInstance(value); + } catch (StripesRuntimeException e) { + log.debug( + "Could not find a way to convert the parameter ", + propertyName.getName(), + " to a ", + returnType.getSimpleName(), + ". No TypeConverter could be ", + "found and the class does not ", + "have a constructor that takes a ", + "single String parameter."); } - return copy; - } - else { - return values; + } + + // If we managed to get a non-null converted value, add it to the return set + if (retval != null) { + returns.add(retval); + } + + // Set the field name and value on the error + for (ValidationError error : errors) { + error.setFieldName(propertyName.getStrippedName()); + error.setFieldValue(value); + } + } catch (Exception e) { + log.warn(e, "Looks like type converter ", converter, " threw an exception."); } + } } - /** - * An inner class that represents a "row" of form properties that all have the same index - * so that we can validate all those properties together. - */ - protected static class Row extends HashMap { - private static final long serialVersionUID = 1L; + return returns; + } + + /** + * Inspects the given {@link ValidationMetadata} object to determine if the given {@code values} + * should be trimmed. If so, then the trimmed values are returned. Otherwise, the values are + * returned unchanged. If {@code meta} is null, then the default action is taken, and the values + * are trimmed. Either {@code values} or {@code meta} (or both) may be null. + */ + protected String[] trim(String[] values, ValidationMetadata meta) { + if (values != null && values.length > 0 && (meta == null || meta.trim())) { + String[] copy = new String[values.length]; + for (int i = 0; i < values.length; i++) { + if (values[i] != null) copy[i] = values[i].trim(); + } + return copy; + } else { + return values; + } + } - private boolean hasNonEmptyValues = false; + /** + * An inner class that represents a "row" of form properties that all have the same index so that + * we can validate all those properties together. + */ + protected static class Row extends HashMap { + private static final long serialVersionUID = 1L; - /** - * Adds the value to the map, along the way checking to see if there are any non-null values for - * the row so far. - */ - @Override - public String[] put(ParameterName key, String[] values) { - if (!hasNonEmptyValues) { - hasNonEmptyValues = (values != null) && (values.length > 0) && (values[0] != null) - && (values[0].trim().length() > 0); + private boolean hasNonEmptyValues = false; - } - return super.put(key, values); - } + /** + * Adds the value to the map, along the way checking to see if there are any non-null values for + * the row so far. + */ + @Override + public String[] put(ParameterName key, String[] values) { + if (!hasNonEmptyValues) { + hasNonEmptyValues = + (values != null) + && (values.length > 0) + && (values[0] != null) + && (values[0].trim().length() > 0); + } + return super.put(key, values); + } - /** Returns true if the row had any non-empty values in it, otherwise false. */ - public boolean hasNonEmptyValues() { - return this.hasNonEmptyValues; - } + /** Returns true if the row had any non-empty values in it, otherwise false. */ + public boolean hasNonEmptyValues() { + return this.hasNonEmptyValues; } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultObjectFactory.java b/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultObjectFactory.java index 84e9206ed..dbc443490 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultObjectFactory.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultObjectFactory.java @@ -31,7 +31,6 @@ import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; - import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.config.TargetTypes; import net.sourceforge.stripes.exception.StripesRuntimeException; @@ -40,298 +39,288 @@ import net.sourceforge.stripes.util.TypeHandlerCache; /** - *

    * An implementation of {@link ObjectFactory} that simply calls {@link Class#newInstance()} to * obtain a new instance. - *

    - * + * * @author Ben Gunter * @since Stripes 1.5.1 */ @SuppressWarnings("unchecked") public class DefaultObjectFactory implements ObjectFactory { - /** - * An implementation of {@link ConstructorWrapper} that calls back to - * {@link DefaultObjectFactory#newInstance(Constructor, Object...)} to instantiate a class. - */ - public static class DefaultConstructorWrapper implements ConstructorWrapper { - private ObjectFactory factory; - private Constructor constructor; - - /** - * Wrap the given constructor. - * - * @param factory The object factory whose - * {@link ObjectFactory#newInstance(Constructor, Object...)} method will be - * called when invoking the constructor. - * @param constructor The constructor to wrap. - */ - public DefaultConstructorWrapper(ObjectFactory factory, Constructor constructor) { - this.factory = factory; - this.constructor = constructor; - } - - /** Get the {@link Constructor} object wrapped by this instance. */ - public Constructor getConstructor() { - return constructor; - } - - /** Invoke the constructor with the specified arguments and return the new object. */ - public T newInstance(Object... args) { - return factory.newInstance(constructor, args); - } - } - - private static final Log log = Log.getInstance(DefaultObjectFactory.class); + /** + * An implementation of {@link ConstructorWrapper} that calls back to {@link + * DefaultObjectFactory#newInstance(Constructor, Object...)} to instantiate a class. + */ + public static class DefaultConstructorWrapper implements ConstructorWrapper { + private ObjectFactory factory; + private Constructor constructor; /** - * Holds a map of commonly used interface types (mostly collections) to a class that implements - * the interface and will, by default, be instantiated when an instance of the interface is - * needed. + * Wrap the given constructor. + * + * @param factory The object factory whose {@link ObjectFactory#newInstance(Constructor, + * Object...)} method will be called when invoking the constructor. + * @param constructor The constructor to wrap. */ - protected final Map, Class> interfaceImplementations = new HashMap, Class>(); - { - interfaceImplementations.put(Collection.class, ArrayList.class); - interfaceImplementations.put(List.class, ArrayList.class); - interfaceImplementations.put(Set.class, HashSet.class); - interfaceImplementations.put(SortedSet.class, TreeSet.class); - interfaceImplementations.put(Queue.class, LinkedList.class); - interfaceImplementations.put(Map.class, HashMap.class); - interfaceImplementations.put(SortedMap.class, TreeMap.class); + public DefaultConstructorWrapper(ObjectFactory factory, Constructor constructor) { + this.factory = factory; + this.constructor = constructor; } - private Configuration configuration; - private TypeHandlerCache> postProcessors; - - /** Does nothing. */ - public void init(Configuration configuration) throws Exception { - this.configuration = configuration; + /** Get the {@link Constructor} object wrapped by this instance. */ + public Constructor getConstructor() { + return constructor; } - /** Get the {@link Configuration} that was passed into {@link #init(Configuration)}. */ - public Configuration getConfiguration() { - return configuration; + /** Invoke the constructor with the specified arguments and return the new object. */ + public T newInstance(Object... args) { + return factory.newInstance(constructor, args); } + } - /** - * Register a post-processor that will be allowed to manipulate instances of {@code targetType} - * after they are created and before they are returned. The types to which the post-processor - * will apply are determined by the value of the {@link TargetTypes} annotation on the class. If - * there is no such annotation, then the post-processor will process all instances created by - * the object factory. - * - * @param postProcessor The post-processor to use. - */ - public synchronized void addPostProcessor(ObjectPostProcessor postProcessor) { - // The cache will be null by default to indicate that there are no post-processors - if (postProcessors == null) { - postProcessors = new TypeHandlerCache>(); - } + private static final Log log = Log.getInstance(DefaultObjectFactory.class); - // Determine target types from type arguments - List> targetTypes = new ArrayList>(); - Type[] typeArguments = ReflectUtil.getActualTypeArguments(postProcessor.getClass(), - ObjectPostProcessor.class); - if ((typeArguments != null) && (typeArguments.length == 1) - && !typeArguments[0].equals(Object.class)) { - if (typeArguments[0] instanceof Class) { - targetTypes.add((Class) typeArguments[0]); - } - else { - log.warn("Type parameter for non-abstract post-processor [", postProcessor - .getClass().getName(), "] is not a class."); - } - } + /** + * Holds a map of commonly used interface types (mostly collections) to a class that implements + * the interface and will, by default, be instantiated when an instance of the interface is + * needed. + */ + protected final Map, Class> interfaceImplementations = + new HashMap, Class>(); - // Determine target types from annotation; if no annotation then process everything - TargetTypes annotation = postProcessor.getClass().getAnnotation(TargetTypes.class); - if (annotation != null) - targetTypes.addAll(Arrays.asList(annotation.value())); + { + interfaceImplementations.put(Collection.class, ArrayList.class); + interfaceImplementations.put(List.class, ArrayList.class); + interfaceImplementations.put(Set.class, HashSet.class); + interfaceImplementations.put(SortedSet.class, TreeSet.class); + interfaceImplementations.put(Queue.class, LinkedList.class); + interfaceImplementations.put(Map.class, HashMap.class); + interfaceImplementations.put(SortedMap.class, TreeMap.class); + } - // Default to Object - if (targetTypes.isEmpty()) - targetTypes.add(Object.class); + private Configuration configuration; + private TypeHandlerCache> postProcessors; - // Register post-processor for each target type - for (Class targetType : targetTypes) { - List list = postProcessors.getHandler(targetType); - if (list == null) { - list = new ArrayList(); - postProcessors.add(targetType, list); - } - log.debug("Adding post-processor of type ", postProcessor.getClass().getName(), - " for ", targetType); - list.add(postProcessor); - } + /** Does nothing. */ + public void init(Configuration configuration) throws Exception { + this.configuration = configuration; + } + + /** Get the {@link Configuration} that was passed into {@link #init(Configuration)}. */ + public Configuration getConfiguration() { + return configuration; + } - postProcessor.setObjectFactory(this); + /** + * Register a post-processor that will be allowed to manipulate instances of {@code targetType} + * after they are created and before they are returned. The types to which the post-processor will + * apply are determined by the value of the {@link TargetTypes} annotation on the class. If there + * is no such annotation, then the post-processor will process all instances created by the object + * factory. + * + * @param postProcessor The post-processor to use. + */ + public synchronized void addPostProcessor(ObjectPostProcessor postProcessor) { + // The cache will be null by default to indicate that there are no post-processors + if (postProcessors == null) { + postProcessors = new TypeHandlerCache>(); } - /** - * Calls {@link Class#newInstance()} and returns the newly created object. - * - * @param clazz The class to instantiate. - * @return The new object - */ - public T newInstance(Class clazz) { - try { - if (clazz.isInterface()) - return postProcess(newInterfaceInstance(clazz)); - else - return postProcess(clazz.newInstance()); - } - catch (InstantiationException e) { - throw new StripesRuntimeException("Could not instantiate " + clazz, e); - } - catch (IllegalAccessException e) { - throw new StripesRuntimeException("Could not instantiate " + clazz, e); - } + // Determine target types from type arguments + List> targetTypes = new ArrayList>(); + Type[] typeArguments = + ReflectUtil.getActualTypeArguments(postProcessor.getClass(), ObjectPostProcessor.class); + if ((typeArguments != null) + && (typeArguments.length == 1) + && !typeArguments[0].equals(Object.class)) { + if (typeArguments[0] instanceof Class) { + targetTypes.add((Class) typeArguments[0]); + } else { + log.warn( + "Type parameter for non-abstract post-processor [", + postProcessor.getClass().getName(), + "] is not a class."); + } } - /** - * Attempts to determine an implementing class for the interface provided and instantiate it - * using a default constructor. - * - * @param interfaceType an interface (or abstract class) to make an instance of - * @return an instance of the interface type supplied - * @throws InstantiationException if no implementation type has been configured - * @throws IllegalAccessException if thrown by the JVM during class instantiation - */ - public T newInterfaceInstance(Class interfaceType) throws InstantiationException, - IllegalAccessException { - Class impl = getImplementingClass(interfaceType); - if (impl == null) { - throw new InstantiationException( - "Stripes needed to instantiate a property who's declared type as an " + - "interface (which obviously cannot be instantiated. The interface is not " + - "one that Stripes is aware of, so no implementing class was known. The " + - "interface type was: '" + interfaceType.getName() + "'. To fix this " + - "you'll need to do one of three things. 1) Change the getter/setter methods " + - "to use a concrete type so that Stripes can instantiate it. 2) in the bean's " + - "setContext() method pre-instantiate the property so Stripes doesn't have to. " + - "3) Bug the Stripes author ;) If the interface is a JDK type it can easily be " + - "fixed. If not, if enough people ask, a generic way to handle the problem " + - "might get implemented."); - } - else { - return newInstance((Class) impl); - } + // Determine target types from annotation; if no annotation then process everything + TargetTypes annotation = postProcessor.getClass().getAnnotation(TargetTypes.class); + if (annotation != null) targetTypes.addAll(Arrays.asList(annotation.value())); + + // Default to Object + if (targetTypes.isEmpty()) targetTypes.add(Object.class); + + // Register post-processor for each target type + for (Class targetType : targetTypes) { + List list = postProcessors.getHandler(targetType); + if (list == null) { + list = new ArrayList(); + postProcessors.add(targetType, list); + } + log.debug( + "Adding post-processor of type ", + postProcessor.getClass().getName(), + " for ", + targetType); + list.add(postProcessor); } - /** - * Looks up the default implementing type for the supplied interface. This is done based on a - * static map of known common interface types and implementing classes. - * - * @param iface an interface for which an implementing class is needed - * @return a Class object representing the implementing type, or null if one is not found - */ - public Class getImplementingClass(Class iface) { - return interfaceImplementations.get(iface); + postProcessor.setObjectFactory(this); + } + + /** + * Calls {@link Class#newInstance()} and returns the newly created object. + * + * @param clazz The class to instantiate. + * @return The new object + */ + public T newInstance(Class clazz) { + try { + if (clazz.isInterface()) return postProcess(newInterfaceInstance(clazz)); + else return postProcess(clazz.newInstance()); + } catch (InstantiationException e) { + throw new StripesRuntimeException("Could not instantiate " + clazz, e); + } catch (IllegalAccessException e) { + throw new StripesRuntimeException("Could not instantiate " + clazz, e); } + } - /** - * Register a class as the default implementation of an interface. The implementation class will - * be returned from future calls to {@link #getImplementingClass(Class)} when the argument is - * {@code iface}. - * - * @param iface The interface class - * @param impl The implementation class - */ - public void addImplementingClass(Class iface, Class impl) { - if (!iface.isInterface()) - throw new IllegalArgumentException("Class " + iface.getName() + " is not an interface"); - else - interfaceImplementations.put(iface, impl); + /** + * Attempts to determine an implementing class for the interface provided and instantiate it using + * a default constructor. + * + * @param interfaceType an interface (or abstract class) to make an instance of + * @return an instance of the interface type supplied + * @throws InstantiationException if no implementation type has been configured + * @throws IllegalAccessException if thrown by the JVM during class instantiation + */ + public T newInterfaceInstance(Class interfaceType) + throws InstantiationException, IllegalAccessException { + Class impl = getImplementingClass(interfaceType); + if (impl == null) { + throw new InstantiationException( + "Stripes needed to instantiate a property who's declared type as an " + + "interface (which obviously cannot be instantiated. The interface is not " + + "one that Stripes is aware of, so no implementing class was known. The " + + "interface type was: '" + + interfaceType.getName() + + "'. To fix this " + + "you'll need to do one of three things. 1) Change the getter/setter methods " + + "to use a concrete type so that Stripes can instantiate it. 2) in the bean's " + + "setContext() method pre-instantiate the property so Stripes doesn't have to. " + + "3) Bug the Stripes author ;) If the interface is a JDK type it can easily be " + + "fixed. If not, if enough people ask, a generic way to handle the problem " + + "might get implemented."); + } else { + return newInstance((Class) impl); } + } - /** - * Create a new instance of {@code clazz} by looking up the specified constructor and passing it - * and its parameters to {@link #newInstance(Constructor, Object...)}. - * - * @param clazz The class to instantiate. - * @param constructorArgTypes The type parameters of the constructor to be invoked. (See - * {@link Class#getConstructor(Class...)}.) - * @param constructorArgs The parameters to pass to the constructor. (See - * {@link Constructor#newInstance(Object...)}.) - * @return A new instance of the class. - */ - public T newInstance(Class clazz, Class[] constructorArgTypes, - Object[] constructorArgs) { - try { - Constructor constructor = clazz.getConstructor(constructorArgTypes); - return postProcess(newInstance(constructor, constructorArgs)); - } - catch (SecurityException e) { - throw new StripesRuntimeException("Could not instantiate " + clazz, e); - } - catch (NoSuchMethodException e) { - throw new StripesRuntimeException("Could not instantiate " + clazz, e); - } - catch (IllegalArgumentException e) { - throw new StripesRuntimeException("Could not instantiate " + clazz, e); - } + /** + * Looks up the default implementing type for the supplied interface. This is done based on a + * static map of known common interface types and implementing classes. + * + * @param iface an interface for which an implementing class is needed + * @return a Class object representing the implementing type, or null if one is not found + */ + public Class getImplementingClass(Class iface) { + return interfaceImplementations.get(iface); + } + + /** + * Register a class as the default implementation of an interface. The implementation class will + * be returned from future calls to {@link #getImplementingClass(Class)} when the argument is + * {@code iface}. + * + * @param iface The interface class + * @param impl The implementation class + */ + public void addImplementingClass(Class iface, Class impl) { + if (!iface.isInterface()) + throw new IllegalArgumentException("Class " + iface.getName() + " is not an interface"); + else interfaceImplementations.put(iface, impl); + } + + /** + * Create a new instance of {@code clazz} by looking up the specified constructor and passing it + * and its parameters to {@link #newInstance(Constructor, Object...)}. + * + * @param clazz The class to instantiate. + * @param constructorArgTypes The type parameters of the constructor to be invoked. (See {@link + * Class#getConstructor(Class...)}.) + * @param constructorArgs The parameters to pass to the constructor. (See {@link + * Constructor#newInstance(Object...)}.) + * @return A new instance of the class. + */ + public T newInstance( + Class clazz, Class[] constructorArgTypes, Object[] constructorArgs) { + try { + Constructor constructor = clazz.getConstructor(constructorArgTypes); + return postProcess(newInstance(constructor, constructorArgs)); + } catch (SecurityException e) { + throw new StripesRuntimeException("Could not instantiate " + clazz, e); + } catch (NoSuchMethodException e) { + throw new StripesRuntimeException("Could not instantiate " + clazz, e); + } catch (IllegalArgumentException e) { + throw new StripesRuntimeException("Could not instantiate " + clazz, e); } + } - /** - * Calls {@link Constructor#newInstance(Object...)} with the given parameters, passes the new - * object to {@link #postProcess(Object)} and returns it. - * - * @param constructor The constructor to invoke. - * @param params The parameters to pass to the constructor. - */ - public T newInstance(Constructor constructor, Object... params) { - try { - return postProcess(constructor.newInstance(params)); - } - catch (InstantiationException e) { - throw new StripesRuntimeException("Could not invoke constructor " + constructor, e); - } - catch (IllegalAccessException e) { - throw new StripesRuntimeException("Could not invoke constructor " + constructor, e); - } - catch (InvocationTargetException e) { - throw new StripesRuntimeException("Could not invoke constructor " + constructor, e); - } + /** + * Calls {@link Constructor#newInstance(Object...)} with the given parameters, passes the new + * object to {@link #postProcess(Object)} and returns it. + * + * @param constructor The constructor to invoke. + * @param params The parameters to pass to the constructor. + */ + public T newInstance(Constructor constructor, Object... params) { + try { + return postProcess(constructor.newInstance(params)); + } catch (InstantiationException e) { + throw new StripesRuntimeException("Could not invoke constructor " + constructor, e); + } catch (IllegalAccessException e) { + throw new StripesRuntimeException("Could not invoke constructor " + constructor, e); + } catch (InvocationTargetException e) { + throw new StripesRuntimeException("Could not invoke constructor " + constructor, e); } + } - /** - * Get a {@link ConstructorWrapper} that wraps the constructor for the given class that accepts - * parameters of the given types. - * - * @param clazz The class to look up the constructor in. - * @param parameterTypes The parameter types that the constructor accepts. - */ - public DefaultConstructorWrapper constructor(Class clazz, Class... parameterTypes) { - try { - return new DefaultConstructorWrapper(this, clazz.getConstructor(parameterTypes)); - } - catch (SecurityException e) { - throw new StripesRuntimeException("Could not instantiate " + clazz, e); - } - catch (NoSuchMethodException e) { - throw new StripesRuntimeException("Could not instantiate " + clazz, e); - } + /** + * Get a {@link ConstructorWrapper} that wraps the constructor for the given class that accepts + * parameters of the given types. + * + * @param clazz The class to look up the constructor in. + * @param parameterTypes The parameter types that the constructor accepts. + */ + public DefaultConstructorWrapper constructor(Class clazz, Class... parameterTypes) { + try { + return new DefaultConstructorWrapper(this, clazz.getConstructor(parameterTypes)); + } catch (SecurityException e) { + throw new StripesRuntimeException("Could not instantiate " + clazz, e); + } catch (NoSuchMethodException e) { + throw new StripesRuntimeException("Could not instantiate " + clazz, e); } + } - /** - * Perform post-processing on objects created by {@link #newInstance(Class)} or - * {@link #newInstance(Class, Class[], Object[])}. Subclasses that do not need to change the way - * objects are instantiated but do need to do something to the objects before returning them may - * override this method to achieve that. - * - * @param object A newly created object. - * @return The given object, unchanged. - */ - protected T postProcess(T object) { - if (postProcessors != null) { - List list = postProcessors.getHandler(object.getClass()); - if (list != null) { - for (ObjectPostProcessor postProcessor : list) { - object = (T) postProcessor.postProcess(object); - } - } + /** + * Perform post-processing on objects created by {@link #newInstance(Class)} or {@link + * #newInstance(Class, Class[], Object[])}. Subclasses that do not need to change the way objects + * are instantiated but do need to do something to the objects before returning them may override + * this method to achieve that. + * + * @param object A newly created object. + * @return The given object, unchanged. + */ + protected T postProcess(T object) { + if (postProcessors != null) { + List list = postProcessors.getHandler(object.getClass()); + if (list != null) { + for (ObjectPostProcessor postProcessor : list) { + object = (T) postProcessor.postProcess(object); } - - return object; + } } + + return object; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultViewActionBean.java b/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultViewActionBean.java index e76189945..4fcbd9b7b 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultViewActionBean.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/DefaultViewActionBean.java @@ -5,25 +5,35 @@ import net.sourceforge.stripes.action.Resolution; /** - *

    A special purpose ActionBean that is used by the NameBasedActionResolver when a valid - * ActionBean cannot be found for a URL. If the URL can be successfully translated into a - * JSP URL and a JSP exists, an instance of this ActionBean is created that will forward the - * user to the appropriate JSP.

    + * A special purpose ActionBean that is used by the NameBasedActionResolver when a valid ActionBean + * cannot be found for a URL. If the URL can be successfully translated into a JSP URL and a JSP + * exists, an instance of this ActionBean is created that will forward the user to the appropriate + * JSP. * - *

    Because this ActionBean does not have a default no-arg constructor, even though it - * gets bound to a URL, if that URL is hit the ActionBean cannot be instantiated and therefore - * cannot be accessed directly by a user playing with the URL.

    + *

    Because this ActionBean does not have a default no-arg constructor, even though it gets bound + * to a URL, if that URL is hit the ActionBean cannot be instantiated and therefore cannot be + * accessed directly by a user playing with the URL. * * @author Tim Fennell, Abdullah Jibaly * @since Stripes 1.3 */ public class DefaultViewActionBean implements ActionBean { - private ActionBeanContext context; - private Resolution view; + private ActionBeanContext context; + private Resolution view; - public DefaultViewActionBean(Resolution view) { this.view = view; } - public void setContext(ActionBeanContext context) { this.context = context; } - public ActionBeanContext getContext() { return this.context; } + public DefaultViewActionBean(Resolution view) { + this.view = view; + } - public Resolution view() { return view; } -} \ No newline at end of file + public void setContext(ActionBeanContext context) { + this.context = context; + } + + public ActionBeanContext getContext() { + return this.context; + } + + public Resolution view() { + return view; + } +} diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherHelper.java b/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherHelper.java index 0f7ebde50..6b53ef03d 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherHelper.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherHelper.java @@ -14,6 +14,20 @@ */ package net.sourceforge.stripes.controller; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.jsp.PageContext; +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.WeakHashMap; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.action.DontBind; @@ -21,523 +35,533 @@ import net.sourceforge.stripes.action.Resolution; import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.exception.StripesServletException; +import net.sourceforge.stripes.util.CollectionUtil; import net.sourceforge.stripes.util.HtmlUtil; import net.sourceforge.stripes.util.Log; -import net.sourceforge.stripes.util.CollectionUtil; import net.sourceforge.stripes.validation.ValidationError; import net.sourceforge.stripes.validation.ValidationErrorHandler; import net.sourceforge.stripes.validation.ValidationErrors; import net.sourceforge.stripes.validation.ValidationMethod; import net.sourceforge.stripes.validation.ValidationState; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.jsp.PageContext; -import java.lang.reflect.Method; -import java.lang.ref.WeakReference; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.MissingResourceException; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.WeakHashMap; - /** - * Helper class that contains much of the logic used when dispatching requests in Stripes. - * Used primarily by the DispatcherSerlvet, but also by the UseActionBean tag. + * Helper class that contains much of the logic used when dispatching requests in Stripes. Used + * primarily by the DispatcherSerlvet, but also by the UseActionBean tag. * * @author Tim Fennell */ public class DispatcherHelper { - private static final Log log = Log.getInstance(DispatcherHelper.class); - - /** - * A Map that is used to cache the validation method that are discovered for each - * ActionBean. Entries are added to this map the first time that a request is made - * to a particular ActionBean. The Map will contain a zero length array for ActionBeans - * that do not have any validation methods. - */ - private static final Map, WeakReference> customValidations = - Collections.synchronizedMap(new WeakHashMap, WeakReference>()); - - /** A place to hide a page context object so that we can get access to EL classes. */ - private static ThreadLocal pageContextStash = new ThreadLocal(); - - /** - * Should be called prior to invoking validation related methods to provide the helper - * with access to a PageContext object that can be used to manufacture expression - * evaluator instances. - * - * @param ctx a page context object supplied by the container - */ - public static void setPageContext(PageContext ctx) { - if (ctx == null) pageContextStash.remove(); - else pageContextStash.set(ctx); - } - - /** - * Used by the validation subsystem to access a page context that can be used to - * create an expression evaluator for use in the expression validation code. - * - * @return a page context object if one was set with setPageContext() - */ - public static PageContext getPageContext() { - return pageContextStash.get(); - } - - /** - * Responsible for resolving the ActionBean for this request and setting it on the - * ExecutionContext. If no ActionBean can be found the ActionResolver will throw an - * exception, thereby aborting the current request. - * - * @param ctx the ExecutionContext being used to process the current request - * @return a Resolution if any interceptor determines that the request processing should - * be aborted in favor of another Resolution, null otherwise. - */ - public static Resolution resolveActionBean(final ExecutionContext ctx) throws Exception { - final Configuration config = StripesFilter.getConfiguration(); - ctx.setLifecycleStage(LifecycleStage.ActionBeanResolution); - ctx.setInterceptors(config.getInterceptors(LifecycleStage.ActionBeanResolution)); - return ctx.wrap( new Interceptor() { - public Resolution intercept(ExecutionContext ctx) throws Exception { - // Look up the ActionBean and set it on the context - ActionBeanContext context = ctx.getActionBeanContext(); - ActionBean bean = StripesFilter.getConfiguration().getActionResolver().getActionBean(context); - ctx.setActionBean(bean); - - // Prefer the context from the resolved bean if it differs from the ExecutionContext - if (context != bean.getContext()) { - ActionBeanContext other = bean.getContext(); - other.setEventName(context.getEventName()); - other.setRequest(context.getRequest()); - other.setResponse(context.getResponse()); - - context = other; - ctx.setActionBeanContext(context); - } - - // Then register it in the Request as THE ActionBean for this request - HttpServletRequest request = context.getRequest(); - request.setAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN, bean); - return null; + private static final Log log = Log.getInstance(DispatcherHelper.class); + + /** + * A Map that is used to cache the validation method that are discovered for each ActionBean. + * Entries are added to this map the first time that a request is made to a particular ActionBean. + * The Map will contain a zero length array for ActionBeans that do not have any validation + * methods. + */ + private static final Map, WeakReference> customValidations = + Collections.synchronizedMap(new WeakHashMap, WeakReference>()); + + /** A place to hide a page context object so that we can get access to EL classes. */ + private static ThreadLocal pageContextStash = new ThreadLocal(); + + /** + * Should be called prior to invoking validation related methods to provide the helper with access + * to a PageContext object that can be used to manufacture expression evaluator instances. + * + * @param ctx a page context object supplied by the container + */ + public static void setPageContext(PageContext ctx) { + if (ctx == null) pageContextStash.remove(); + else pageContextStash.set(ctx); + } + + /** + * Used by the validation subsystem to access a page context that can be used to create an + * expression evaluator for use in the expression validation code. + * + * @return a page context object if one was set with setPageContext() + */ + public static PageContext getPageContext() { + return pageContextStash.get(); + } + + /** + * Responsible for resolving the ActionBean for this request and setting it on the + * ExecutionContext. If no ActionBean can be found the ActionResolver will throw an exception, + * thereby aborting the current request. + * + * @param ctx the ExecutionContext being used to process the current request + * @return a Resolution if any interceptor determines that the request processing should be + * aborted in favor of another Resolution, null otherwise. + */ + public static Resolution resolveActionBean(final ExecutionContext ctx) throws Exception { + final Configuration config = StripesFilter.getConfiguration(); + ctx.setLifecycleStage(LifecycleStage.ActionBeanResolution); + ctx.setInterceptors(config.getInterceptors(LifecycleStage.ActionBeanResolution)); + return ctx.wrap( + new Interceptor() { + public Resolution intercept(ExecutionContext ctx) throws Exception { + // Look up the ActionBean and set it on the context + ActionBeanContext context = ctx.getActionBeanContext(); + ActionBean bean = + StripesFilter.getConfiguration().getActionResolver().getActionBean(context); + ctx.setActionBean(bean); + + // Prefer the context from the resolved bean if it differs from the ExecutionContext + if (context != bean.getContext()) { + ActionBeanContext other = bean.getContext(); + other.setEventName(context.getEventName()); + other.setRequest(context.getRequest()); + other.setResponse(context.getResponse()); + + context = other; + ctx.setActionBeanContext(context); } - }); - } - /** - * Responsible for resolving the event name for this request and setting it on the - * ActionBeanContext contained within the ExecutionContext. Once the event name has - * been determined this method must resolve the handler method. If a handler method - * cannot be determined an exception should be thrown to abort processing. - * - * @param ctx the ExecutionContext being used to process the current request - * @return a Resolution if any interceptor determines that the request processing should - * be aborted in favor of another Resolution, null otherwise. - */ - public static Resolution resolveHandler(final ExecutionContext ctx) throws Exception { - final Configuration config = StripesFilter.getConfiguration(); - ctx.setLifecycleStage(LifecycleStage.HandlerResolution); - ctx.setInterceptors(config.getInterceptors(LifecycleStage.HandlerResolution)); - - return ctx.wrap( new Interceptor() { - public Resolution intercept(ExecutionContext ctx) throws Exception { - ActionBean bean = ctx.getActionBean(); - ActionBeanContext context = ctx.getActionBeanContext(); - ActionResolver resolver = config.getActionResolver(); - - // Then lookup the event name and handler method etc. - String eventName = resolver.getEventName(bean.getClass(), context); - context.setEventName(eventName); - - final Method handler; - if (eventName != null) { - handler = resolver.getHandler(bean.getClass(), eventName); - } - else { - handler = resolver.getDefaultHandler(bean.getClass()); - if (handler != null) { - context.setEventName(resolver.getHandledEvent(handler)); - } - } + // Then register it in the Request as THE ActionBean for this request + HttpServletRequest request = context.getRequest(); + request.setAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN, bean); + return null; + } + }); + } + + /** + * Responsible for resolving the event name for this request and setting it on the + * ActionBeanContext contained within the ExecutionContext. Once the event name has been + * determined this method must resolve the handler method. If a handler method cannot be + * determined an exception should be thrown to abort processing. + * + * @param ctx the ExecutionContext being used to process the current request + * @return a Resolution if any interceptor determines that the request processing should be + * aborted in favor of another Resolution, null otherwise. + */ + public static Resolution resolveHandler(final ExecutionContext ctx) throws Exception { + final Configuration config = StripesFilter.getConfiguration(); + ctx.setLifecycleStage(LifecycleStage.HandlerResolution); + ctx.setInterceptors(config.getInterceptors(LifecycleStage.HandlerResolution)); + + return ctx.wrap( + new Interceptor() { + public Resolution intercept(ExecutionContext ctx) throws Exception { + ActionBean bean = ctx.getActionBean(); + ActionBeanContext context = ctx.getActionBeanContext(); + ActionResolver resolver = config.getActionResolver(); + + // Then lookup the event name and handler method etc. + String eventName = resolver.getEventName(bean.getClass(), context); + context.setEventName(eventName); + + final Method handler; + if (eventName != null) { + handler = resolver.getHandler(bean.getClass(), eventName); + } else { + handler = resolver.getDefaultHandler(bean.getClass()); + if (handler != null) { + context.setEventName(resolver.getHandledEvent(handler)); + } + } - // Insist that we have a handler - if (handler == null) { - throw new StripesServletException( - "No handler method found for request with ActionBean [" + - bean.getClass().getName() + "] and eventName [ " + eventName + "]"); - } + // Insist that we have a handler + if (handler == null) { + throw new StripesServletException( + "No handler method found for request with ActionBean [" + + bean.getClass().getName() + + "] and eventName [ " + + eventName + + "]"); + } - log.debug("Resolved event: ", context.getEventName(), "; will invoke: ", - bean.getClass().getSimpleName(), ".", handler.getName(), "()"); + log.debug( + "Resolved event: ", + context.getEventName(), + "; will invoke: ", + bean.getClass().getSimpleName(), + ".", + handler.getName(), + "()"); - ctx.setHandler(handler); - return null; - } + ctx.setHandler(handler); + return null; + } }); - } - - /** - * Responsible for performing binding and validation. Once properties have been bound and - * validation complete then they ValidationErrors object must be accessible through the - * ActionBeanContext contained within the ExecutionContext (regardless of whether any - * errors were generated or not). - * - * @param ctx the ExecutionContext being used to process the current request - * @return a Resolution if any interceptor determines that the request processing should - * be aborted in favor of another Resolution, null otherwise. - */ - public static Resolution doBindingAndValidation(final ExecutionContext ctx, - final boolean validate) throws Exception { - // Bind the value to the bean - this includes performing field level validation - final Method handler = ctx.getHandler(); - final boolean doBind = handler == null || handler.getAnnotation(DontBind.class) == null; - final boolean doValidate = doBind && validate && (handler == null || handler.getAnnotation(DontValidate.class) == null); - final Configuration config = StripesFilter.getConfiguration(); - - ctx.setLifecycleStage(LifecycleStage.BindingAndValidation); - ctx.setInterceptors(config.getInterceptors(LifecycleStage.BindingAndValidation)); - - return ctx.wrap(new Interceptor() { - public Resolution intercept(ExecutionContext ctx) throws Exception { - if (doBind) { - ActionBeanPropertyBinder binder = config.getActionBeanPropertyBinder(); - binder.bind(ctx.getActionBean(), ctx.getActionBeanContext(), doValidate); - fillInValidationErrors(ctx); - } - return null; + } + + /** + * Responsible for performing binding and validation. Once properties have been bound and + * validation complete then they ValidationErrors object must be accessible through the + * ActionBeanContext contained within the ExecutionContext (regardless of whether any errors were + * generated or not). + * + * @param ctx the ExecutionContext being used to process the current request + * @return a Resolution if any interceptor determines that the request processing should be + * aborted in favor of another Resolution, null otherwise. + */ + public static Resolution doBindingAndValidation( + final ExecutionContext ctx, final boolean validate) throws Exception { + // Bind the value to the bean - this includes performing field level validation + final Method handler = ctx.getHandler(); + final boolean doBind = handler == null || handler.getAnnotation(DontBind.class) == null; + final boolean doValidate = + doBind + && validate + && (handler == null || handler.getAnnotation(DontValidate.class) == null); + final Configuration config = StripesFilter.getConfiguration(); + + ctx.setLifecycleStage(LifecycleStage.BindingAndValidation); + ctx.setInterceptors(config.getInterceptors(LifecycleStage.BindingAndValidation)); + + return ctx.wrap( + new Interceptor() { + public Resolution intercept(ExecutionContext ctx) throws Exception { + if (doBind) { + ActionBeanPropertyBinder binder = config.getActionBeanPropertyBinder(); + binder.bind(ctx.getActionBean(), ctx.getActionBeanContext(), doValidate); + fillInValidationErrors(ctx); } + return null; + } }); - } - - /** - * Responsible for coordinating the invocation of any custom validation logic exposed - * by the ActionBean. Will only call the validation methods if certain conditions are - * met (there are no errors, or the always call validate flag is set etc.). - * - * @param ctx the ExecutionContext being used to process the current request - * @return a Resolution if any interceptor determines that the request processing should - * be aborted in favor of another Resolution, null otherwise. - */ - public static Resolution doCustomValidation(final ExecutionContext ctx, - final boolean alwaysInvokeValidate) throws Exception { - final ValidationErrors errors = ctx.getActionBeanContext().getValidationErrors(); - final ActionBean bean = ctx.getActionBean(); - final Method handler = ctx.getHandler(); - final boolean doBind = handler != null && handler.getAnnotation(DontBind.class) == null; - final boolean doValidate = doBind && handler.getAnnotation(DontValidate.class) == null; - Configuration config = StripesFilter.getConfiguration(); - - // Run the bean's methods annotated with @ValidateMethod if the following conditions are met: - // l. This event is not marked to bypass binding - // 2. This event is not marked to bypass validation (doValidate == true) - // 3. We have no errors so far OR alwaysInvokeValidate is true - if (doValidate) { - - ctx.setLifecycleStage(LifecycleStage.CustomValidation); - ctx.setInterceptors(config.getInterceptors(LifecycleStage.CustomValidation)); - - return ctx.wrap( new Interceptor() { - public Resolution intercept(ExecutionContext context) throws Exception { - // Run any of the annotated validation methods - Method[] validations = findCustomValidationMethods(bean.getClass()); - for (Method validation : validations) { - ValidationMethod ann = validation.getAnnotation(ValidationMethod.class); - - boolean run = (ann.when() == ValidationState.ALWAYS) - || (ann.when() == ValidationState.DEFAULT && alwaysInvokeValidate) - || errors.isEmpty(); - - if (run && applies(ann, ctx.getActionBeanContext().getEventName())) { - Class[] args = validation.getParameterTypes(); - if (args.length == 1 && args[0].equals(ValidationErrors.class)) { - validation.invoke(bean, errors); - } - else { - validation.invoke(bean); - } - } - } - - fillInValidationErrors(ctx); - return null; + } + + /** + * Responsible for coordinating the invocation of any custom validation logic exposed by the + * ActionBean. Will only call the validation methods if certain conditions are met (there are no + * errors, or the always call validate flag is set etc.). + * + * @param ctx the ExecutionContext being used to process the current request + * @return a Resolution if any interceptor determines that the request processing should be + * aborted in favor of another Resolution, null otherwise. + */ + public static Resolution doCustomValidation( + final ExecutionContext ctx, final boolean alwaysInvokeValidate) throws Exception { + final ValidationErrors errors = ctx.getActionBeanContext().getValidationErrors(); + final ActionBean bean = ctx.getActionBean(); + final Method handler = ctx.getHandler(); + final boolean doBind = handler != null && handler.getAnnotation(DontBind.class) == null; + final boolean doValidate = doBind && handler.getAnnotation(DontValidate.class) == null; + Configuration config = StripesFilter.getConfiguration(); + + // Run the bean's methods annotated with @ValidateMethod if the following conditions are met: + // l. This event is not marked to bypass binding + // 2. This event is not marked to bypass validation (doValidate == true) + // 3. We have no errors so far OR alwaysInvokeValidate is true + if (doValidate) { + + ctx.setLifecycleStage(LifecycleStage.CustomValidation); + ctx.setInterceptors(config.getInterceptors(LifecycleStage.CustomValidation)); + + return ctx.wrap( + new Interceptor() { + public Resolution intercept(ExecutionContext context) throws Exception { + // Run any of the annotated validation methods + Method[] validations = findCustomValidationMethods(bean.getClass()); + for (Method validation : validations) { + ValidationMethod ann = validation.getAnnotation(ValidationMethod.class); + + boolean run = + (ann.when() == ValidationState.ALWAYS) + || (ann.when() == ValidationState.DEFAULT && alwaysInvokeValidate) + || errors.isEmpty(); + + if (run && applies(ann, ctx.getActionBeanContext().getEventName())) { + Class[] args = validation.getParameterTypes(); + if (args.length == 1 && args[0].equals(ValidationErrors.class)) { + validation.invoke(bean, errors); + } else { + validation.invoke(bean); + } } - }); - } - else { - return null; - } - } + } - /** - *

    Determines if the ValidationMethod annotation should be applied to the named - * event. True if the list of events to apply the validation to is empty, or it - * contains the event name, or it contains event names starting with "!" but not the - * event name. Some examples to illustrate the point

    - * - *
      - *
    • info.on={}, event="save" => true
    • - *
    • info.on={"save", "update"}, event="save" => true
    • - *
    • info.on={"save", "update"}, event="delete" => false
    • - *
    • info.on={"!delete"}, event="save" => true
    • - *
    • info.on={"!delete"}, event="delete" => false
    • - *
    - * - * @param info the ValidationMethod being examined - * @param event the event being processed - * @return true if the custom validation should be applied to this event, false otherwise - */ - public static boolean applies(ValidationMethod info, String event) { - return CollectionUtil.applies(info.on(), event); + fillInValidationErrors(ctx); + return null; + } + }); + } else { + return null; } - - /** - * Finds and returns all methods in the ActionBean class and it's superclasses that - * are marked with the ValidationMethod annotation and returns them ordered by - * priority (and alphabetically within priorities). Looks first in an instance level - * cache, and if that does not contain information for an ActionBean, examines the - * ActionBean and adds the information to the cache. - * - * @param type a Class representing an ActionBean - * @return a Method[] containing all methods marked as custom validations. May return - * an empty array, but never null. - */ - public static Method[] findCustomValidationMethods(Class type) throws Exception { - Method[] validations = null; - WeakReference ref = customValidations.get(type); - if (ref != null) validations = ref.get(); - - // Lazily examine the ActionBean and collect this information - if (validations == null) { - // A sorted set with a custom comparator that will order the methods in - // the set based upon the priority in their custom validation annotation - SortedSet validationMethods = new TreeSet( new Comparator() { + } + + /** + * Determines if the ValidationMethod annotation should be applied to the named event. True if the + * list of events to apply the validation to is empty, or it contains the event name, or it + * contains event names starting with "!" but not the event name. Some examples to illustrate the + * point + * + *
      + *
    • info.on={}, event="save" => true + *
    • info.on={"save", "update"}, event="save" => true + *
    • info.on={"save", "update"}, event="delete" => false + *
    • info.on={"!delete"}, event="save" => true + *
    • info.on={"!delete"}, event="delete" => false + *
    + * + * @param info the ValidationMethod being examined + * @param event the event being processed + * @return true if the custom validation should be applied to this event, false otherwise + */ + public static boolean applies(ValidationMethod info, String event) { + return CollectionUtil.applies(info.on(), event); + } + + /** + * Finds and returns all methods in the ActionBean class and it's superclasses that are marked + * with the ValidationMethod annotation and returns them ordered by priority (and alphabetically + * within priorities). Looks first in an instance level cache, and if that does not contain + * information for an ActionBean, examines the ActionBean and adds the information to the cache. + * + * @param type a Class representing an ActionBean + * @return a Method[] containing all methods marked as custom validations. May return an empty + * array, but never null. + */ + public static Method[] findCustomValidationMethods(Class type) + throws Exception { + Method[] validations = null; + WeakReference ref = customValidations.get(type); + if (ref != null) validations = ref.get(); + + // Lazily examine the ActionBean and collect this information + if (validations == null) { + // A sorted set with a custom comparator that will order the methods in + // the set based upon the priority in their custom validation annotation + SortedSet validationMethods = + new TreeSet( + new Comparator() { public int compare(Method o1, Method o2) { - // If one of the methods overrides the others, return equal! - if (o1.getName().equals(o2.getName()) && - Arrays.equals(o1.getParameterTypes(), o2.getParameterTypes())) { - return 0; - } - - ValidationMethod ann1 = o1.getAnnotation(ValidationMethod.class); - ValidationMethod ann2 = o2.getAnnotation(ValidationMethod.class); - int returnValue = new Integer(ann1.priority()).compareTo(ann2.priority()); + // If one of the methods overrides the others, return equal! + if (o1.getName().equals(o2.getName()) + && Arrays.equals(o1.getParameterTypes(), o2.getParameterTypes())) { + return 0; + } - if (returnValue == 0) { - returnValue = o1.getName().compareTo(o2.getName()); - } + ValidationMethod ann1 = o1.getAnnotation(ValidationMethod.class); + ValidationMethod ann2 = o2.getAnnotation(ValidationMethod.class); + int returnValue = new Integer(ann1.priority()).compareTo(ann2.priority()); - return returnValue; - } - }); + if (returnValue == 0) { + returnValue = o1.getName().compareTo(o2.getName()); + } - Class temp = type; - while ( temp != null ) { - for (Method method : temp.getDeclaredMethods()) { - Class[] args = method.getParameterTypes(); - - if ((method.getAnnotation(ValidationMethod.class) != null) && - ((args.length == 0) || (args.length == 1 && args[0].equals(ValidationErrors.class)))) { - validationMethods.add(method); - } + return returnValue; } - - temp = temp.getSuperclass(); - } - - validations = validationMethods.toArray(new Method[validationMethods.size()]); - customValidations.put(type, new WeakReference(validations)); + }); + + Class temp = type; + while (temp != null) { + for (Method method : temp.getDeclaredMethods()) { + Class[] args = method.getParameterTypes(); + + if ((method.getAnnotation(ValidationMethod.class) != null) + && ((args.length == 0) + || (args.length == 1 && args[0].equals(ValidationErrors.class)))) { + validationMethods.add(method); + } } - return validations; + temp = temp.getSuperclass(); + } + + validations = validationMethods.toArray(new Method[validationMethods.size()]); + customValidations.put(type, new WeakReference(validations)); } - /** - * Responsible for checking to see if validation errors exist and if so for handling - * them appropriately. This includes ensuring that the error objects have all information - * necessary to render themselves appropriately and invoking any error handling code. - * - * @param ctx the ExecutionContext being used to process the current request - * @return a Resolution if the error handling code determines that some kind of resolution - * should be processed in favor of continuing on to handler invocation - */ - public static Resolution handleValidationErrors(ExecutionContext ctx) throws Exception { - DontValidate annotation = ctx.getHandler().getAnnotation(DontValidate.class); - boolean doValidate = annotation == null || !annotation.ignoreBindingErrors(); - - // If we have errors, add the action path to them + return validations; + } + + /** + * Responsible for checking to see if validation errors exist and if so for handling them + * appropriately. This includes ensuring that the error objects have all information necessary to + * render themselves appropriately and invoking any error handling code. + * + * @param ctx the ExecutionContext being used to process the current request + * @return a Resolution if the error handling code determines that some kind of resolution should + * be processed in favor of continuing on to handler invocation + */ + public static Resolution handleValidationErrors(ExecutionContext ctx) throws Exception { + DontValidate annotation = ctx.getHandler().getAnnotation(DontValidate.class); + boolean doValidate = annotation == null || !annotation.ignoreBindingErrors(); + + // If we have errors, add the action path to them + fillInValidationErrors(ctx); + + Resolution resolution = null; + if (doValidate) { + ActionBean bean = ctx.getActionBean(); + ActionBeanContext context = ctx.getActionBeanContext(); + ValidationErrors errors = context.getValidationErrors(); + + // Now if we have errors and the bean wants to handle them... + if (errors.size() > 0 && bean instanceof ValidationErrorHandler) { + resolution = ((ValidationErrorHandler) bean).handleValidationErrors(errors); fillInValidationErrors(ctx); + } - Resolution resolution = null; - if (doValidate) { - ActionBean bean = ctx.getActionBean(); - ActionBeanContext context = ctx.getActionBeanContext(); - ValidationErrors errors = context.getValidationErrors(); - - // Now if we have errors and the bean wants to handle them... - if (errors.size() > 0 && bean instanceof ValidationErrorHandler) { - resolution = ((ValidationErrorHandler) bean).handleValidationErrors(errors); - fillInValidationErrors(ctx); - } - - // If there are still errors see if we need to lookup the resolution - if (errors.size() > 0 && resolution == null) { - logValidationErrors(context); - resolution = context.getSourcePageResolution(); - } - } - - return resolution; + // If there are still errors see if we need to lookup the resolution + if (errors.size() > 0 && resolution == null) { + logValidationErrors(context); + resolution = context.getSourcePageResolution(); + } } - /** - * Makes sure that validation errors have all the necessary information to render - * themselves properly, including the UrlBinding of the action bean and the field - * value if it hasn't already been set. - * - * @param ctx the ExecutionContext being used to process the current request - */ - public static void fillInValidationErrors(ExecutionContext ctx) { - ActionBeanContext context = ctx.getActionBeanContext(); - ValidationErrors errors = context.getValidationErrors(); - - if (errors.size() > 0) { - String formAction = StripesFilter.getConfiguration().getActionResolver() - .getUrlBinding(ctx.getActionBean().getClass()); - HttpServletRequest request = ctx.getActionBeanContext().getRequest(); - - /** Since we don't pass form action down the stack, we add it to the errors here. */ - for (Map.Entry> entry : errors.entrySet()) { - String parameterName = entry.getKey(); - List listOfErrors = entry.getValue(); - - for (ValidationError error : listOfErrors) { - // Make sure we process each error only once, no matter how often we're called - if (error.getActionPath() == null) { - error.setActionPath(formAction); - error.setBeanclass(ctx.getActionBean().getClass()); - - // If the value isn't set, set it, otherwise encode the one that's there - if (error.getFieldValue() == null) { - error.setFieldValue(HtmlUtil.encode(request.getParameter(parameterName))); - } - else { - error.setFieldValue(HtmlUtil.encode(error.getFieldValue())); - } - } - } + return resolution; + } + + /** + * Makes sure that validation errors have all the necessary information to render themselves + * properly, including the UrlBinding of the action bean and the field value if it hasn't already + * been set. + * + * @param ctx the ExecutionContext being used to process the current request + */ + public static void fillInValidationErrors(ExecutionContext ctx) { + ActionBeanContext context = ctx.getActionBeanContext(); + ValidationErrors errors = context.getValidationErrors(); + + if (errors.size() > 0) { + String formAction = + StripesFilter.getConfiguration() + .getActionResolver() + .getUrlBinding(ctx.getActionBean().getClass()); + HttpServletRequest request = ctx.getActionBeanContext().getRequest(); + + /** Since we don't pass form action down the stack, we add it to the errors here. */ + for (Map.Entry> entry : errors.entrySet()) { + String parameterName = entry.getKey(); + List listOfErrors = entry.getValue(); + + for (ValidationError error : listOfErrors) { + // Make sure we process each error only once, no matter how often we're called + if (error.getActionPath() == null) { + error.setActionPath(formAction); + error.setBeanclass(ctx.getActionBean().getClass()); + + // If the value isn't set, set it, otherwise encode the one that's there + if (error.getFieldValue() == null) { + error.setFieldValue(HtmlUtil.encode(request.getParameter(parameterName))); + } else { + error.setFieldValue(HtmlUtil.encode(error.getFieldValue())); } + } } + } } - - /** - * Responsible for invoking the event handler identified. This method will only be - * called if an event handler was identified, and can assume that the bean and handler - * are present in the ExecutionContext. - * - * @param ctx the ExecutionContext being used to process the current request - * type conversion should occur - * @return a Resolution if the error handling code determines that some kind of resolution - * should be processed in favor of continuing on to handler invocation - */ - public static Resolution invokeEventHandler(ExecutionContext ctx) throws Exception { - final Configuration config = StripesFilter.getConfiguration(); - final Method handler = ctx.getHandler(); - final ActionBean bean = ctx.getActionBean(); - - // Finally execute the handler method! - ctx.setLifecycleStage(LifecycleStage.EventHandling); - ctx.setInterceptors(config.getInterceptors(LifecycleStage.EventHandling)); - - return ctx.wrap( new Interceptor() { - public Resolution intercept(ExecutionContext ctx) throws Exception { - Object returnValue = handler.invoke(bean); - fillInValidationErrors(ctx); - - if (returnValue != null && returnValue instanceof Resolution) { - ctx.setResolutionFromHandler(true); - return (Resolution) returnValue; - } - else if (returnValue != null) { - log.warn("Expected handler method ", handler.getName(), " on class ", - bean.getClass().getSimpleName(), " to return a Resolution. Instead it ", - "returned: ", returnValue); - } - - return null; + } + + /** + * Responsible for invoking the event handler identified. This method will only be called if an + * event handler was identified, and can assume that the bean and handler are present in the + * ExecutionContext. + * + * @param ctx the ExecutionContext being used to process the current request type conversion + * should occur + * @return a Resolution if the error handling code determines that some kind of resolution should + * be processed in favor of continuing on to handler invocation + */ + public static Resolution invokeEventHandler(ExecutionContext ctx) throws Exception { + final Configuration config = StripesFilter.getConfiguration(); + final Method handler = ctx.getHandler(); + final ActionBean bean = ctx.getActionBean(); + + // Finally execute the handler method! + ctx.setLifecycleStage(LifecycleStage.EventHandling); + ctx.setInterceptors(config.getInterceptors(LifecycleStage.EventHandling)); + + return ctx.wrap( + new Interceptor() { + public Resolution intercept(ExecutionContext ctx) throws Exception { + Object returnValue = handler.invoke(bean); + fillInValidationErrors(ctx); + + if (returnValue != null && returnValue instanceof Resolution) { + ctx.setResolutionFromHandler(true); + return (Resolution) returnValue; + } else if (returnValue != null) { + log.warn( + "Expected handler method ", + handler.getName(), + " on class ", + bean.getClass().getSimpleName(), + " to return a Resolution. Instead it ", + "returned: ", + returnValue); } - }); - } - /** - * Responsible for executing the Resolution returned by the request. Transitions the - * execution context to {@link net.sourceforge.stripes.controller.LifecycleStage#ResolutionExecution}, sets the resolution - * on the execution context and then invokes the interceptor chain to execute the - * Resolution. - * - * @param ctx the current execution context representing the request - * @param resolution the resolution to be executed unless another is substituted by an - * interceptor before calling ctx.proceed() - */ - public static void executeResolution(ExecutionContext ctx, Resolution resolution) throws Exception { - final Configuration config = StripesFilter.getConfiguration(); - - ctx.setLifecycleStage(LifecycleStage.ResolutionExecution); - ctx.setInterceptors(config.getInterceptors(LifecycleStage.ResolutionExecution)); - ctx.setResolution(resolution); - - Resolution retval = ctx.wrap( new Interceptor() { - public Resolution intercept(ExecutionContext context) throws Exception { + return null; + } + }); + } + + /** + * Responsible for executing the Resolution returned by the request. Transitions the execution + * context to {@link net.sourceforge.stripes.controller.LifecycleStage#ResolutionExecution}, sets + * the resolution on the execution context and then invokes the interceptor chain to execute the + * Resolution. + * + * @param ctx the current execution context representing the request + * @param resolution the resolution to be executed unless another is substituted by an interceptor + * before calling ctx.proceed() + */ + public static void executeResolution(ExecutionContext ctx, Resolution resolution) + throws Exception { + final Configuration config = StripesFilter.getConfiguration(); + + ctx.setLifecycleStage(LifecycleStage.ResolutionExecution); + ctx.setInterceptors(config.getInterceptors(LifecycleStage.ResolutionExecution)); + ctx.setResolution(resolution); + + Resolution retval = + ctx.wrap( + new Interceptor() { + public Resolution intercept(ExecutionContext context) throws Exception { ActionBeanContext abc = context.getActionBeanContext(); Resolution resolution = context.getResolution(); if (resolution != null) { - resolution.execute(abc.getRequest(), abc.getResponse()); + resolution.execute(abc.getRequest(), abc.getResponse()); } return null; - } - }); + } + }); - if (retval != null) { - log.warn("An interceptor wrapping LifecycleStage.ResolutionExecution returned ", - "a Resolution. This almost certainly did NOT have the desired effect. ", - "At this LifecycleStage interceptors are running *around* the actual ", - "execution of the Resolution, and so returning an alternate Resolution ", - "has the effect of stopping the original Resolution from being executed ", - "while NOT causing the alternate Resolution to get executed. Interceptor ", - "code running before the Resolution is executed (i.e. before calling ", - "ExecutionContext.proceed()) can alter the Resolution by calling ", - "ExecutionContext.setResolution() instead. Code running after the Resolution ", - "has been executed can no longer alter what Resolution is executed for ", - "what are hopefully obvious reasons!"); - } + if (retval != null) { + log.warn( + "An interceptor wrapping LifecycleStage.ResolutionExecution returned ", + "a Resolution. This almost certainly did NOT have the desired effect. ", + "At this LifecycleStage interceptors are running *around* the actual ", + "execution of the Resolution, and so returning an alternate Resolution ", + "has the effect of stopping the original Resolution from being executed ", + "while NOT causing the alternate Resolution to get executed. Interceptor ", + "code running before the Resolution is executed (i.e. before calling ", + "ExecutionContext.proceed()) can alter the Resolution by calling ", + "ExecutionContext.setResolution() instead. Code running after the Resolution ", + "has been executed can no longer alter what Resolution is executed for ", + "what are hopefully obvious reasons!"); } - - /** Log validation errors at DEBUG to help during development. */ - public static final void logValidationErrors(ActionBeanContext context) { - StringBuilder buf = new StringBuilder("The following validation errors need to be fixed:"); - - for (List list : context.getValidationErrors().values()) { - for (ValidationError error : list) { - String fieldName = error.getFieldName(); - if (ValidationErrors.GLOBAL_ERROR.equals(fieldName)) - fieldName = "GLOBAL"; - - String message; - try { - message = error.getMessage(Locale.getDefault()); - } - catch (MissingResourceException e) { - message = "(missing resource)"; - } - - buf.append("\n -> [").append(fieldName).append("] ").append(message); - } + } + + /** Log validation errors at DEBUG to help during development. */ + public static final void logValidationErrors(ActionBeanContext context) { + StringBuilder buf = new StringBuilder("The following validation errors need to be fixed:"); + + for (List list : context.getValidationErrors().values()) { + for (ValidationError error : list) { + String fieldName = error.getFieldName(); + if (ValidationErrors.GLOBAL_ERROR.equals(fieldName)) fieldName = "GLOBAL"; + + String message; + try { + message = error.getMessage(Locale.getDefault()); + } catch (MissingResourceException e) { + message = "(missing resource)"; } - log.debug(buf); + buf.append("\n -> [").append(fieldName).append("] ").append(message); + } } + + log.debug(buf); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherServlet.java b/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherServlet.java index cb8b93704..d70aa4d33 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherServlet.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherServlet.java @@ -14,6 +14,14 @@ */ package net.sourceforge.stripes.controller; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.jsp.JspFactory; +import jakarta.servlet.jsp.PageContext; +import java.lang.reflect.InvocationTargetException; +import java.util.Stack; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.action.Resolution; @@ -25,331 +33,336 @@ import net.sourceforge.stripes.validation.expression.ExpressionValidator; import net.sourceforge.stripes.validation.expression.Jsp20ExpressionExecutor; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.jsp.JspFactory; -import javax.servlet.jsp.PageContext; -import java.lang.reflect.InvocationTargetException; -import java.util.Stack; - /** - *

    Servlet that controls how requests to the Stripes framework are processed. Uses an instance of + * Servlet that controls how requests to the Stripes framework are processed. Uses an instance of * the ActionResolver interface to locate the bean and method used to handle the current request and - * then delegates processing to the bean.

    + * then delegates processing to the bean. * - *

    While the DispatcherServlet is structured so that it can be easily subclassed and - * overridden much of the processing work is delegated to the {@link DispatcherHelper} class.

    + *

    While the DispatcherServlet is structured so that it can be easily subclassed and overridden + * much of the processing work is delegated to the {@link DispatcherHelper} class. * * @author Tim Fennell */ public class DispatcherServlet extends HttpServlet { - private static final long serialVersionUID = 1L; - - /** - * Configuration key used to lookup up a property that determines whether or not beans' - * custom validate() method gets invoked when validation errors are generated during - * the binding process - */ - public static final String RUN_CUSTOM_VALIDATION_WHEN_ERRORS = - "Validation.InvokeValidateWhenErrorsExist"; - - private Boolean alwaysInvokeValidate; - - /** Log used throughout the class. */ - private static final Log log = Log.getInstance(DispatcherServlet.class); - - /** - *

    Invokes the following instance level methods in order to coordinate the processing - * of requests:

    - * - *
      - *
    • {@link #resolveActionBean(ExecutionContext)}
    • - *
    • {@link #resolveHandler(ExecutionContext)}
    • - *
    • {@link #doBindingAndValidation(ExecutionContext)}
    • - *
    • {@link #doCustomValidation(ExecutionContext)}
    • - *
    • {@link #handleValidationErrors(ExecutionContext)}
    • - *
    • {@link #invokeEventHandler(ExecutionContext)}
    • - *
    - * - *

    If any of the above methods return a {@link Resolution} the rest of the request processing - * is aborted and the resolution is executed.

    - * - * @param request the HttpServletRequest handed to the class by the container - * @param response the HttpServletResponse paired to the request - * @throws ServletException thrown when the system fails to process the request in any way - */ - @Override - protected void service(final HttpServletRequest request, final HttpServletResponse response) - throws ServletException { - - // It sucks that we have to do this here (in the request cycle), but there doesn't - // seem to be a good way to get at the Configuration from the Filter in init() - doOneTimeConfiguration(); - - /////////////////////////////////////////////////////////////////////// - // Here beings the real processing of the request! - /////////////////////////////////////////////////////////////////////// - log.trace("Dispatching request to URL: ", HttpUtil.getRequestedPath(request)); - - PageContext pageContext = null; - final ExecutionContext ctx = new ExecutionContext(); - - try { - final Configuration config = StripesFilter.getConfiguration(); - - // First manufacture an ActionBeanContext - final ActionBeanContext context = - config.getActionBeanContextFactory().getContextInstance(request, response); - context.setServletContext(getServletContext()); - - // Then setup the ExecutionContext that we'll use to process this request - ctx.setActionBeanContext(context); - - try { - ActionBeanContext abc = ctx.getActionBeanContext(); - - // It's unclear whether this usage of the JspFactory will work in all containers. It looks - // like it should, but still, we should be careful not to screw up regular request - // processing if it should fail. Why do we do this? So we can have a container-agnostic - // way of getting an ExpressionEvaluator to do expression based validation. And we only - // need it if the Jsp20 executor is used, so maybe soon we can kill it? - if (ExpressionValidator.getExecutor() instanceof Jsp20ExpressionExecutor) { - pageContext = JspFactory.getDefaultFactory().getPageContext(this, // the servlet inst - abc.getRequest(), // req - abc.getResponse(), // res - null, // error page url - (request.getSession(false) != null), // needsSession - don't force a session creation if one doesn't already exist - abc.getResponse().getBufferSize(), - true); // autoflush - DispatcherHelper.setPageContext(pageContext); - } - - } - catch (Exception e) { - // Don't even log this, this failure gets reported if action beans actually - // try and make use of expression validation, otherwise this is just noise - } - - // Resolve the ActionBean, and if an interceptor returns a resolution, bail now - saveActionBean(request); - - Resolution resolution = requestInit(ctx); - - if (resolution == null) { - resolution = resolveActionBean(ctx); - - if (resolution == null) { - resolution = resolveHandler(ctx); - - if (resolution == null) { - // Then run binding and validation - resolution = doBindingAndValidation(ctx); - - if (resolution == null) { - // Then continue on to custom validation - resolution = doCustomValidation(ctx); - - if (resolution == null) { - // And then validation error handling - resolution = handleValidationErrors(ctx); - - if (resolution == null) { - // And finally invoking of the event handler - resolution = invokeEventHandler(ctx); - } - } - } - } - } - } - - // Whatever stage it came from, execute the resolution - if (resolution != null) { - executeResolution(ctx, resolution); - } - } - catch (ServletException se) { throw se; } - catch (RuntimeException re) { throw re; } - catch (InvocationTargetException ite) { - if (ite.getTargetException() instanceof ServletException) { - throw (ServletException) ite.getTargetException(); - } - else if (ite.getTargetException() instanceof RuntimeException) { - throw (RuntimeException) ite.getTargetException(); - } - else { - throw new StripesServletException - ("ActionBean execution threw an exception.", ite.getTargetException()); - } - } - catch (Exception e) { - throw new StripesServletException("Exception encountered processing request.", e); + private static final long serialVersionUID = 1L; + + /** + * Configuration key used to lookup up a property that determines whether or not beans' custom + * validate() method gets invoked when validation errors are generated during the binding process + */ + public static final String RUN_CUSTOM_VALIDATION_WHEN_ERRORS = + "Validation.InvokeValidateWhenErrorsExist"; + + private Boolean alwaysInvokeValidate; + + /** Log used throughout the class. */ + private static final Log log = Log.getInstance(DispatcherServlet.class); + + /** + * Invokes the following instance level methods in order to coordinate the processing of requests: + * + *
      + *
    • {@link #resolveActionBean(ExecutionContext)} + *
    • {@link #resolveHandler(ExecutionContext)} + *
    • {@link #doBindingAndValidation(ExecutionContext)} + *
    • {@link #doCustomValidation(ExecutionContext)} + *
    • {@link #handleValidationErrors(ExecutionContext)} + *
    • {@link #invokeEventHandler(ExecutionContext)} + *
    + * + *

    If any of the above methods return a {@link Resolution} the rest of the request processing + * is aborted and the resolution is executed. + * + * @param request the HttpServletRequest handed to the class by the container + * @param response the HttpServletResponse paired to the request + * @throws ServletException thrown when the system fails to process the request in any way + */ + @Override + protected void service(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException { + + // It sucks that we have to do this here (in the request cycle), but there doesn't + // seem to be a good way to get at the Configuration from the Filter in init() + doOneTimeConfiguration(); + + /////////////////////////////////////////////////////////////////////// + // Here beings the real processing of the request! + /////////////////////////////////////////////////////////////////////// + log.trace("Dispatching request to URL: ", HttpUtil.getRequestedPath(request)); + + PageContext pageContext = null; + final ExecutionContext ctx = new ExecutionContext(); + + try { + final Configuration config = StripesFilter.getConfiguration(); + + // First manufacture an ActionBeanContext + final ActionBeanContext context = + config.getActionBeanContextFactory().getContextInstance(request, response); + context.setServletContext(getServletContext()); + + // Then setup the ExecutionContext that we'll use to process this request + ctx.setActionBeanContext(context); + + try { + ActionBeanContext abc = ctx.getActionBeanContext(); + + // It's unclear whether this usage of the JspFactory will work in all containers. It looks + // like it should, but still, we should be careful not to screw up regular request + // processing if it should fail. Why do we do this? So we can have a container-agnostic + // way of getting an ExpressionEvaluator to do expression based validation. And we only + // need it if the Jsp20 executor is used, so maybe soon we can kill it? + if (ExpressionValidator.getExecutor() instanceof Jsp20ExpressionExecutor) { + pageContext = + JspFactory.getDefaultFactory() + .getPageContext( + this, // the servlet inst + abc.getRequest(), // req + abc.getResponse(), // res + null, // error page url + (request.getSession(false) + != null), // needsSession - don't force a session creation if one doesn't + // already exist + abc.getResponse().getBufferSize(), + true); // autoflush + DispatcherHelper.setPageContext(pageContext); } - finally { - // Make sure to release the page context - if (pageContext != null) { - JspFactory.getDefaultFactory().releasePageContext(pageContext); - DispatcherHelper.setPageContext(null); - } - - requestComplete(ctx); - - restoreActionBean(request); - } - } - /** - * Calls interceptors listening for RequestInit. There is no Stripes code that - * executes for this lifecycle stage. - */ - private Resolution requestInit(ExecutionContext ctx) throws Exception { - ctx.setLifecycleStage(LifecycleStage.RequestInit); - ctx.setInterceptors(StripesFilter.getConfiguration().getInterceptors(LifecycleStage.RequestInit)); - return ctx.wrap(new Interceptor() {public Resolution intercept(ExecutionContext context) throws Exception {return null;}}); - } + } catch (Exception e) { + // Don't even log this, this failure gets reported if action beans actually + // try and make use of expression validation, otherwise this is just noise + } - /** - * Calls interceptors listening for RequestComplete. There is no Stripes code - * that executes for this lifecycle stage. In addition, any response from - * interceptors is ignored because it is too late to execute a Resolution at - * this point. - */ - private void requestComplete(ExecutionContext ctx) { - ctx.setLifecycleStage(LifecycleStage.RequestComplete); - ctx.setInterceptors(StripesFilter.getConfiguration().getInterceptors(LifecycleStage.RequestComplete)); - try { - Resolution resolution = ctx.wrap(new Interceptor() {public Resolution intercept(ExecutionContext context) throws Exception {return null;}}); - if (resolution != null) - log.warn("Resolutions returned from interceptors for ", ctx.getLifecycleStage(), - " are ignored because it is too late to execute them."); - } - catch (Exception e) { - log.error(e); - } - } + // Resolve the ActionBean, and if an interceptor returns a resolution, bail now + saveActionBean(request); - /** - * Responsible for resolving the ActionBean for the current request. Delegates to - * {@link DispatcherHelper#resolveActionBean(ExecutionContext)}. - */ - protected Resolution resolveActionBean(ExecutionContext ctx) throws Exception { - return DispatcherHelper.resolveActionBean(ctx); - } + Resolution resolution = requestInit(ctx); - /** - * Responsible for resolving the event handler method for the current request. Delegates to - * {@link DispatcherHelper#resolveHandler(ExecutionContext)}. - */ - protected Resolution resolveHandler(ExecutionContext ctx) throws Exception { - return DispatcherHelper.resolveHandler(ctx); - } + if (resolution == null) { + resolution = resolveActionBean(ctx); - /** - * Responsible for executing binding and validation for the current request. Delegates to - * {@link DispatcherHelper#doBindingAndValidation(ExecutionContext, boolean)}. - */ - protected Resolution doBindingAndValidation(ExecutionContext ctx) throws Exception { - return DispatcherHelper.doBindingAndValidation(ctx, true); - } + if (resolution == null) { + resolution = resolveHandler(ctx); - /** - * Responsible for executing custom validation methods for the current request. Delegates to - * {@link DispatcherHelper#doCustomValidation(ExecutionContext, boolean)}. - */ - protected Resolution doCustomValidation(ExecutionContext ctx) throws Exception { - return DispatcherHelper.doCustomValidation(ctx, alwaysInvokeValidate); - } + if (resolution == null) { + // Then run binding and validation + resolution = doBindingAndValidation(ctx); - /** - * Responsible for handling any validation errors that arise during validation. Delegates to - * {@link DispatcherHelper#handleValidationErrors(ExecutionContext)}. - */ - protected Resolution handleValidationErrors(ExecutionContext ctx) throws Exception { - return DispatcherHelper.handleValidationErrors(ctx); - } + if (resolution == null) { + // Then continue on to custom validation + resolution = doCustomValidation(ctx); - /** - * Responsible for invoking the event handler if no validation errors occur. Delegates to - * {@link DispatcherHelper#invokeEventHandler(ExecutionContext)}. - */ - protected Resolution invokeEventHandler(ExecutionContext ctx) throws Exception { - return DispatcherHelper.invokeEventHandler(ctx); - } + if (resolution == null) { + // And then validation error handling + resolution = handleValidationErrors(ctx); - /** - * Responsible for executing the Resolution for the current request. Delegates to - * {@link DispatcherHelper#executeResolution(ExecutionContext, Resolution)}. - */ - protected void executeResolution(ExecutionContext ctx, Resolution resolution) throws Exception { - DispatcherHelper.executeResolution(ctx, resolution); - } - - /** - * Performs a simple piece of one time configuration that requires access to the - * Configuration object delivered through the Stripes Filter. - */ - private void doOneTimeConfiguration() { - if (alwaysInvokeValidate == null) { - // Check to see if, in this application, validate() methods should always be run - // even when validation errors already exist - String callValidateWhenErrorsExist = StripesFilter.getConfiguration() - .getBootstrapPropertyResolver().getProperty(RUN_CUSTOM_VALIDATION_WHEN_ERRORS); - - if (callValidateWhenErrorsExist != null) { - BooleanTypeConverter c = new BooleanTypeConverter(); - this.alwaysInvokeValidate = c.convert(callValidateWhenErrorsExist, Boolean.class, null); - } - else { - this.alwaysInvokeValidate = false; // Default behaviour + if (resolution == null) { + // And finally invoking of the event handler + resolution = invokeEventHandler(ctx); + } + } } + } } + } + + // Whatever stage it came from, execute the resolution + if (resolution != null) { + executeResolution(ctx, resolution); + } + } catch (ServletException se) { + throw se; + } catch (RuntimeException re) { + throw re; + } catch (InvocationTargetException ite) { + if (ite.getTargetException() instanceof ServletException) { + throw (ServletException) ite.getTargetException(); + } else if (ite.getTargetException() instanceof RuntimeException) { + throw (RuntimeException) ite.getTargetException(); + } else { + throw new StripesServletException( + "ActionBean execution threw an exception.", ite.getTargetException()); + } + } catch (Exception e) { + throw new StripesServletException("Exception encountered processing request.", e); + } finally { + // Make sure to release the page context + if (pageContext != null) { + JspFactory.getDefaultFactory().releasePageContext(pageContext); + DispatcherHelper.setPageContext(null); + } + + requestComplete(ctx); + + restoreActionBean(request); } - - /** - * Fetches, and lazily creates if required, a Stack in the request to store ActionBeans - * should the current request involve forwards or includes to other ActionBeans. - * - * @param request the current HttpServletRequest - * @return the Stack if present, or if creation is requested - */ - @SuppressWarnings("unchecked") - protected Stack getActionBeanStack(HttpServletRequest request, boolean create) { - Stack stack = (Stack) request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN_STACK); - if (stack == null && create) { - stack = new Stack(); - request.setAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN_STACK, stack); - } - - return stack; + } + + /** + * Calls interceptors listening for RequestInit. There is no Stripes code that executes for this + * lifecycle stage. + */ + private Resolution requestInit(ExecutionContext ctx) throws Exception { + ctx.setLifecycleStage(LifecycleStage.RequestInit); + ctx.setInterceptors( + StripesFilter.getConfiguration().getInterceptors(LifecycleStage.RequestInit)); + return ctx.wrap( + new Interceptor() { + public Resolution intercept(ExecutionContext context) throws Exception { + return null; + } + }); + } + + /** + * Calls interceptors listening for RequestComplete. There is no Stripes code that executes for + * this lifecycle stage. In addition, any response from interceptors is ignored because it is too + * late to execute a Resolution at this point. + */ + private void requestComplete(ExecutionContext ctx) { + ctx.setLifecycleStage(LifecycleStage.RequestComplete); + ctx.setInterceptors( + StripesFilter.getConfiguration().getInterceptors(LifecycleStage.RequestComplete)); + try { + Resolution resolution = + ctx.wrap( + new Interceptor() { + public Resolution intercept(ExecutionContext context) throws Exception { + return null; + } + }); + if (resolution != null) + log.warn( + "Resolutions returned from interceptors for ", + ctx.getLifecycleStage(), + " are ignored because it is too late to execute them."); + } catch (Exception e) { + log.error(e); } - - /** - * Saves the current value of the 'actionBean' attribute in the request so that it - * can be restored at a later date by calling {@link #restoreActionBean(HttpServletRequest)}. - * If no ActionBean is currently stored in the request, nothing is changed. - * - * @param request the current HttpServletRequest - */ - protected void saveActionBean(HttpServletRequest request) { - if (request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN) != null) { - Stack stack = getActionBeanStack(request, true); - stack.push((ActionBean) request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN)); - } + } + + /** + * Responsible for resolving the ActionBean for the current request. Delegates to {@link + * DispatcherHelper#resolveActionBean(ExecutionContext)}. + */ + protected Resolution resolveActionBean(ExecutionContext ctx) throws Exception { + return DispatcherHelper.resolveActionBean(ctx); + } + + /** + * Responsible for resolving the event handler method for the current request. Delegates to {@link + * DispatcherHelper#resolveHandler(ExecutionContext)}. + */ + protected Resolution resolveHandler(ExecutionContext ctx) throws Exception { + return DispatcherHelper.resolveHandler(ctx); + } + + /** + * Responsible for executing binding and validation for the current request. Delegates to {@link + * DispatcherHelper#doBindingAndValidation(ExecutionContext, boolean)}. + */ + protected Resolution doBindingAndValidation(ExecutionContext ctx) throws Exception { + return DispatcherHelper.doBindingAndValidation(ctx, true); + } + + /** + * Responsible for executing custom validation methods for the current request. Delegates to + * {@link DispatcherHelper#doCustomValidation(ExecutionContext, boolean)}. + */ + protected Resolution doCustomValidation(ExecutionContext ctx) throws Exception { + return DispatcherHelper.doCustomValidation(ctx, alwaysInvokeValidate); + } + + /** + * Responsible for handling any validation errors that arise during validation. Delegates to + * {@link DispatcherHelper#handleValidationErrors(ExecutionContext)}. + */ + protected Resolution handleValidationErrors(ExecutionContext ctx) throws Exception { + return DispatcherHelper.handleValidationErrors(ctx); + } + + /** + * Responsible for invoking the event handler if no validation errors occur. Delegates to {@link + * DispatcherHelper#invokeEventHandler(ExecutionContext)}. + */ + protected Resolution invokeEventHandler(ExecutionContext ctx) throws Exception { + return DispatcherHelper.invokeEventHandler(ctx); + } + + /** + * Responsible for executing the Resolution for the current request. Delegates to {@link + * DispatcherHelper#executeResolution(ExecutionContext, Resolution)}. + */ + protected void executeResolution(ExecutionContext ctx, Resolution resolution) throws Exception { + DispatcherHelper.executeResolution(ctx, resolution); + } + + /** + * Performs a simple piece of one time configuration that requires access to the Configuration + * object delivered through the Stripes Filter. + */ + private void doOneTimeConfiguration() { + if (alwaysInvokeValidate == null) { + // Check to see if, in this application, validate() methods should always be run + // even when validation errors already exist + String callValidateWhenErrorsExist = + StripesFilter.getConfiguration() + .getBootstrapPropertyResolver() + .getProperty(RUN_CUSTOM_VALIDATION_WHEN_ERRORS); + + if (callValidateWhenErrorsExist != null) { + BooleanTypeConverter c = new BooleanTypeConverter(); + this.alwaysInvokeValidate = c.convert(callValidateWhenErrorsExist, Boolean.class, null); + } else { + this.alwaysInvokeValidate = false; // Default behaviour + } + } + } + + /** + * Fetches, and lazily creates if required, a Stack in the request to store ActionBeans should the + * current request involve forwards or includes to other ActionBeans. + * + * @param request the current HttpServletRequest + * @return the Stack if present, or if creation is requested + */ + @SuppressWarnings("unchecked") + protected Stack getActionBeanStack(HttpServletRequest request, boolean create) { + Stack stack = + (Stack) request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN_STACK); + if (stack == null && create) { + stack = new Stack(); + request.setAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN_STACK, stack); } - /** - * Restores the previous value of the 'actionBean' attribute in the request. If no - * ActionBeans have been saved using {@link #saveActionBean(HttpServletRequest)} then this - * method has no effect. - * - * @param request the current HttpServletRequest - */ - protected void restoreActionBean(HttpServletRequest request) { - Stack stack = getActionBeanStack(request, false); - if (stack != null && !stack.empty()) { - request.setAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN, stack.pop()); - } + return stack; + } + + /** + * Saves the current value of the 'actionBean' attribute in the request so that it can be restored + * at a later date by calling {@link #restoreActionBean(HttpServletRequest)}. If no ActionBean is + * currently stored in the request, nothing is changed. + * + * @param request the current HttpServletRequest + */ + protected void saveActionBean(HttpServletRequest request) { + if (request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN) != null) { + Stack stack = getActionBeanStack(request, true); + stack.push((ActionBean) request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN)); + } + } + + /** + * Restores the previous value of the 'actionBean' attribute in the request. If no ActionBeans + * have been saved using {@link #saveActionBean(HttpServletRequest)} then this method has no + * effect. + * + * @param request the current HttpServletRequest + */ + protected void restoreActionBean(HttpServletRequest request) { + Stack stack = getActionBeanStack(request, false); + if (stack != null && !stack.empty()) { + request.setAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN, stack.pop()); } -} \ No newline at end of file + } +} diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/DynamicMappingFilter.java b/stripes/src/main/java/net/sourceforge/stripes/controller/DynamicMappingFilter.java index 65c4b3ba8..90d906802 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/DynamicMappingFilter.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/DynamicMappingFilter.java @@ -14,6 +14,21 @@ */ package net.sourceforge.stripes.controller; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; @@ -29,21 +44,6 @@ import java.util.List; import java.util.Map; import java.util.UUID; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletConfig; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -51,64 +51,57 @@ import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; - import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.exception.StripesServletException; import net.sourceforge.stripes.util.HttpUtil; import net.sourceforge.stripes.util.Log; - import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** - *

    * A servlet filter that dynamically maps URLs to {@link ActionBean}s. This filter can be used to * allow Stripes to dispatch requests to {@link ActionBean}s based on their URL binding, even if the * URL to which they are bound is not explicitly mapped in {@code web.xml}. - *

    - *

    - * One caveat must be observed when using this filter. This filter MUST be the last filter - * in the filter chain. When it dynamically maps an {@link ActionBean} to a URL, the filter chain is - * interrupted. - *

    - *

    - * {@link StripesFilter} and/or {@link DispatcherServlet} may be declared in {@code web.xml}, but + * + *

    One caveat must be observed when using this filter. This filter MUST be the last + * filter in the filter chain. When it dynamically maps an {@link ActionBean} to a URL, the filter + * chain is interrupted. + * + *

    {@link StripesFilter} and/or {@link DispatcherServlet} may be declared in {@code web.xml}, but * neither is required for this filter to work. If you choose not to declare {@link StripesFilter} * in {@code web.xml}, then this filter should be configured the way you would normally configure * {@link StripesFilter}. However, some resources, such as JSPs, may require access to the Stripes * {@link Configuration} through {@link StripesFilter}. If you intend to access JSPs directly, then * {@link StripesFilter} should be explicitly mapped to {@code *.jsp}. - *

    - *

    - * This filter takes the following approach to determining when to dispatch an {@link ActionBean}: + * + *

    This filter takes the following approach to determining when to dispatch an {@link + * ActionBean}: + * *

      - *
    1. Allow the request to process normally, trapping any HTTP errors that are returned.
    2. - *
    3. If no error was returned then do nothing, allowing the request to complete successfully. If - * any error other than {@code 404} was returned then send the error through. Otherwise ...
    4. - *
    5. Check the {@link ActionResolver} to see if an {@link ActionBean} is mapped to the URL. If - * not, then send the {@code 404} error through. Otherwise...
    6. - *
    7. Invoke {@link StripesFilter} and {@link DispatcherServlet}
    8. + *
    9. Allow the request to process normally, trapping any HTTP errors that are returned. + *
    10. If no error was returned then do nothing, allowing the request to complete successfully. If + * any error other than {@code 404} was returned then send the error through. Otherwise ... + *
    11. Check the {@link ActionResolver} to see if an {@link ActionBean} is mapped to the URL. If + * not, then send the {@code 404} error through. Otherwise... + *
    12. Invoke {@link StripesFilter} and {@link DispatcherServlet} *
    - *

    - *

    - * One benefit of this approach is that static resources can be delivered from the same namespace to - * which an {@link ActionBean} is mapped using clean URLs. (For more information on clean URLs, see - * {@link UrlBinding}.) For example, if your {@code UserActionBean} is mapped to + * + *

    One benefit of this approach is that static resources can be delivered from the same namespace + * to which an {@link ActionBean} is mapped using clean URLs. (For more information on clean URLs, + * see {@link UrlBinding}.) For example, if your {@code UserActionBean} is mapped to * {@code @UrlBinding("/user/{id}/{$event}")} and you have a static file at {@code /user/icon.gif}, * then your icon will be delivered correctly because the initial request will not have returned a * {@code 404} error. - *

    - *

    - * The {@code IncludeBufferSize} initialization parameter (optional, default 1024) sets the number - * of characters to be buffered by {@link TempBufferWriter} for include requests. See - * {@link TempBufferWriter} for more information. - *

    - * This is the suggested mapping for this filter in {@code web.xml}. - *

    - * + * + *

    The {@code IncludeBufferSize} initialization parameter (optional, default 1024) sets the + * number of characters to be buffered by {@link TempBufferWriter} for include requests. See {@link + * TempBufferWriter} for more information. + * + *

    This is the suggested mapping for this filter in {@code web.xml}. + * *

      *  <filter>
      *      <description>Dynamically maps URLs to ActionBeans.</description>
    @@ -122,7 +115,7 @@
      *          <param-value>com.yourcompany.stripes.action</param-value>
      *      </init-param>
      *  </filter>
    - *  
    + *
      *  <filter-mapping>
      *      <filter-name>DynamicMappingFilter</filter-name>
      *      <url-pattern>/*</url-pattern>
    @@ -131,699 +124,730 @@
      *      <dispatcher>INCLUDE</dispatcher>
      *  </filter-mapping>
      * 
    - * + * * @author Ben Gunter - * @since Stripes 1.5 * @see UrlBinding + * @since Stripes 1.5 */ public class DynamicMappingFilter implements Filter { - /** - *

    - * A {@link Writer} that passes characters to a {@link PrintWriter}. It buffers the first - * {@code N} characters written to it and automatically overflows when the number of characters - * written exceeds the limit. The size of the buffer defaults to 1024 characters, but it can be - * changed using the {@code IncludeBufferSize} filter init-param in {@code web.xml}. If - * {@code IncludeBufferSize} is zero or negative, then a {@link TempBufferWriter} will not be - * used at all. This is only a good idea if your servlet container does not write an error - * message to output when it can't find an included resource or if you only include resources - * that do not depend on this filter to be delivered, such as other servlets, JSPs, static - * resources, ActionBeans that are mapped with a prefix ({@code /action/*}) or suffix ({@code *.action}), - * etc. - *

    - *

    - * This writer is used to partially buffer the output of includes. Some (all?) servlet - * containers write a message to the output stream indicating if an included resource is missing - * because if the response has already been committed, they cannot send a 404 error. Since the - * filter depends on getting a 404 before it attempts to dispatch an {@code ActionBean}, that - * is problematic. So in using this writer, we assume that the length of the "missing resource" - * message will be less than the buffer size and we discard that message if we're able to map - * the included URL to an {@code ActionBean}. If there is no 404 then the output will be sent - * normally. If there is a 404 and the URL does not match an ActionBean then the "missing - * resource" message is sent through. - *

    - * - * @author Ben Gunter - * @since Stripes 1.5 - */ - public static class TempBufferWriter extends Writer { - private StringWriter buffer; - private PrintWriter out; - - public TempBufferWriter(PrintWriter out) { - this.out = out; - this.buffer = new StringWriter(includeBufferSize); - } - - @Override - public void close() throws IOException { - flush(); - out.close(); - } + /** + * A {@link Writer} that passes characters to a {@link PrintWriter}. It buffers the first {@code + * N} characters written to it and automatically overflows when the number of characters written + * exceeds the limit. The size of the buffer defaults to 1024 characters, but it can be changed + * using the {@code IncludeBufferSize} filter init-param in {@code web.xml}. If {@code + * IncludeBufferSize} is zero or negative, then a {@link TempBufferWriter} will not be used at + * all. This is only a good idea if your servlet container does not write an error message to + * output when it can't find an included resource or if you only include resources that do not + * depend on this filter to be delivered, such as other servlets, JSPs, static resources, + * ActionBeans that are mapped with a prefix ({@code /action/*}) or suffix ({@code *.action}), + * etc. + * + *

    This writer is used to partially buffer the output of includes. Some (all?) servlet + * containers write a message to the output stream indicating if an included resource is missing + * because if the response has already been committed, they cannot send a 404 error. Since the + * filter depends on getting a 404 before it attempts to dispatch an {@code ActionBean}, that is + * problematic. So in using this writer, we assume that the length of the "missing resource" + * message will be less than the buffer size, and we discard that message if we're able to map the + * included URL to an {@code ActionBean}. If there is no 404 then the output will be sent + * normally. If there is a 404 and the URL does not match an ActionBean then the "missing + * resource" message is sent through. + * + * @author Ben Gunter + * @since Stripes 1.5 + */ + public static class TempBufferWriter extends Writer { + private StringWriter buffer; + private final PrintWriter out; + + public TempBufferWriter(PrintWriter out) { + this.out = out; + this.buffer = new StringWriter(includeBufferSize); + } - @Override - public void flush() throws IOException { - overflow(); - out.flush(); - } + @Override + public void close() throws IOException { + flush(); + out.close(); + } - @Override - public void write(char[] chars, int offset, int length) throws IOException { - if (buffer == null) { - out.write(chars, offset, length); - } - else if (buffer.getBuffer().length() + length > includeBufferSize) { - overflow(); - out.write(chars, offset, length); - } - else { - buffer.write(chars, offset, length); - } - } + @Override + public void flush() { + overflow(); + out.flush(); + } - /** - * Write the contents of the buffer to the underlying writer. After a call to - * {@link #overflow()}, all future writes to this writer will pass directly to the - * underlying writer. - */ - protected void overflow() { - if (buffer != null) { - out.print(buffer.toString()); - buffer = null; - } - } + @Override + public void write(char[] chars, int offset, int length) throws IOException { + if (buffer == null) { + out.write(chars, offset, length); + } else if (buffer.getBuffer().length() + length > includeBufferSize) { + overflow(); + out.write(chars, offset, length); + } else { + buffer.write(chars, offset, length); + } } /** - * An {@link HttpServletResponseWrapper} that traps HTTP errors by overriding - * {@code sendError(int, ..)}. The error code can be retrieved by calling - * {@link #getErrorCode()}. A call to {@link #proceed()} sends the error to the client. - * - * @author Ben Gunter - * @since Stripes 1.5 + * Write the contents of the buffer to the underlying writer. After a call to this method, all + * future writes to this writer will pass directly to the underlying writer. */ - public static class ErrorTrappingResponseWrapper extends HttpServletResponseWrapper { - private Integer errorCode; - private String errorMessage; - private boolean include; - private PrintWriter printWriter; - private TempBufferWriter tempBufferWriter; - - /** Wrap the given {@code response}. */ - public ErrorTrappingResponseWrapper(HttpServletResponse response) { - super(response); - } - - @Override - public void sendError(int errorCode, String errorMessage) throws IOException { - this.errorCode = errorCode; - this.errorMessage = errorMessage; - } - - @Override - public void sendError(int errorCode) throws IOException { - this.errorCode = errorCode; - this.errorMessage = null; - } - - @Override - public PrintWriter getWriter() throws IOException { - if (isInclude() && includeBufferSize > 0) { - if (printWriter == null) { - tempBufferWriter = new TempBufferWriter(super.getWriter()); - printWriter = new PrintWriter(tempBufferWriter); - } - return printWriter; - } - else { - return super.getWriter(); - } - } - - /** True if the currently executing request is an include. */ - public boolean isInclude() { - return include; - } - - /** Indicate if the currently executing request is an include. */ - public void setInclude(boolean include) { - this.include = include; - } - - /** Get the error code that was passed into {@code sendError(int, ..)} */ - public Integer getErrorCode() { - return errorCode; - } + protected void overflow() { + if (buffer != null) { + out.print(buffer); + buffer = null; + } + } + } + + /** + * An {@link HttpServletResponseWrapper} that traps HTTP errors by overriding {@code + * sendError(int, ..)}. The error code can be retrieved by calling {@link #getErrorCode()}. A call + * to {@link #proceed()} sends the error to the client. + * + * @author Ben Gunter + * @since Stripes 1.5 + */ + public static class ErrorTrappingResponseWrapper extends HttpServletResponseWrapper { + private Integer errorCode; + private String errorMessage; + private boolean include; + private PrintWriter printWriter; + private TempBufferWriter tempBufferWriter; - /** Clear error code and error message. */ - public void clearError() { - this.errorCode = null; - this.errorMessage = null; - } + /** + * Wrap the given {@code response}. + * + * @param response The response to wrap + */ + public ErrorTrappingResponseWrapper(HttpServletResponse response) { + super(response); + } - /** - * Send the error, if any, to the client. If {@code sendError(int, ..)} has not previously - * been called, then do nothing. - */ - public void proceed() throws IOException { - // Explicitly overflow the buffer so the output gets written - if (tempBufferWriter != null) - tempBufferWriter.overflow(); - - if (errorCode != null) { - if (errorMessage == null) - super.sendError(errorCode); - else - super.sendError(errorCode, errorMessage); - } - } + @Override + public void sendError(int errorCode, String errorMessage) { + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } - @Override - public void setStatus(int sc) { - this.errorCode = sc; - } + @Override + public void sendError(int errorCode) { + this.errorCode = errorCode; + this.errorMessage = null; + } - @Override - public void setStatus(int sc, String sm) { - this.errorCode = sc; - this.errorMessage = sm; - } + @Override + public PrintWriter getWriter() throws IOException { + if (isInclude() && includeBufferSize > 0) { + if (printWriter == null) { + tempBufferWriter = new TempBufferWriter(super.getWriter()); + printWriter = new PrintWriter(tempBufferWriter); + } + return printWriter; + } else { + return super.getWriter(); + } } /** - * The name of the init-param that can be used to set the size of the buffer used by - * {@link TempBufferWriter} before it overflows. + * True if the currently executing request is an include. + * + * @return True if the currently executing request is an include. */ - public static final String INCLUDE_BUFFER_SIZE_PARAM = "IncludeBufferSize"; + public boolean isInclude() { + return include; + } /** - * The attribute name used to store a reference to {@link StripesFilter} in the servlet context. + * Indicate if the currently executing request is an include. + * + * @param include True if the currently executing request is an include. */ - public static final String CONTEXT_KEY_STRIPES_FILTER = StripesFilter.class.getName(); + public void setInclude(boolean include) { + this.include = include; + } /** - * Request header that indicates that the current request is part of the process of trying to - * force initialization of {@link StripesFilter}. If this header is present then - * {@link DynamicMappingFilter} makes no attempt to map the request to an {@link ActionBean}. + * Get the error code that was passed into {@code sendError(int, ..)} + * + * @return The error code that was passed into {@code sendError(int, ..)} */ - private static final String REQ_HEADER_INIT_FLAG = "X-Dynamic-Mapping-Filter-Init"; - - /** The size of the buffer used by {@link TempBufferWriter} before it overflows. */ - private static int includeBufferSize = 1024; - - /** Logger */ - private static Log log = Log.getInstance(DynamicMappingFilter.class); - - private FilterConfig filterConfig; - private ServletContext servletContext; - private StripesFilter stripesFilter; - private DispatcherServlet stripesDispatcher; - private boolean stripesFilterIsInternal, initializing; - - public void init(final FilterConfig config) throws ServletException { - try { - String value = config.getInitParameter(INCLUDE_BUFFER_SIZE_PARAM); - if (value != null) { - includeBufferSize = Integer.valueOf(value.trim()); - log.info(getClass().getSimpleName(), " include buffer size is ", includeBufferSize); - } - } - catch (Exception e) { - log.warn(e, "Could not interpret '", - config.getInitParameter(INCLUDE_BUFFER_SIZE_PARAM), - "' as a number for init-param '", INCLUDE_BUFFER_SIZE_PARAM, - "'. Using default value ", includeBufferSize, "."); - } - - this.filterConfig = config; - this.servletContext = config.getServletContext(); - this.stripesDispatcher = new DispatcherServlet(); - this.stripesDispatcher.init(new ServletConfig() { - public String getInitParameter(String name) { - return config.getInitParameter(name); - } + public Integer getErrorCode() { + return errorCode; + } - public Enumeration getInitParameterNames() { - return config.getInitParameterNames(); - } + /** + * Send the error, if any, to the client. If {@code sendError(int, ..)} has not previously been + * called, then do nothing. + * + * @throws IOException If thrown by {@link #sendError(int)} or {@link #sendError(int, String)} + */ + public void proceed() throws IOException { + // Explicitly overflow the buffer so the output gets written + if (tempBufferWriter != null) tempBufferWriter.overflow(); + + if (errorCode != null) { + if (errorMessage == null) super.sendError(errorCode); + else super.sendError(errorCode, errorMessage); + } + } - public ServletContext getServletContext() { - return config.getServletContext(); - } + @Override + public void setStatus(int sc) { + this.errorCode = sc; + } + } + + /** + * The name of the init-param that can be used to set the size of the buffer used by {@link + * TempBufferWriter} before it overflows. + */ + public static final String INCLUDE_BUFFER_SIZE_PARAM = "IncludeBufferSize"; + + /** + * The attribute name used to store a reference to {@link StripesFilter} in the servlet context. + */ + public static final String CONTEXT_KEY_STRIPES_FILTER = StripesFilter.class.getName(); + + /** + * Request header that indicates that the current request is part of the process of trying to + * force initialization of {@link StripesFilter}. If this header is present then {@link + * DynamicMappingFilter} makes no attempt to map the request to an {@link ActionBean}. + */ + private static final String REQ_HEADER_INIT_FLAG = "X-Dynamic-Mapping-Filter-Init"; + + /** The size of the buffer used by {@link TempBufferWriter} before it overflows. */ + private static int includeBufferSize = 1024; + + /** Logger */ + private static final Log log = Log.getInstance(DynamicMappingFilter.class); + + private FilterConfig filterConfig; + private ServletContext servletContext; + private StripesFilter stripesFilter; + private DispatcherServlet stripesDispatcher; + private boolean stripesFilterIsInternal, initializing; + + public void init(final FilterConfig config) throws ServletException { + try { + String value = config.getInitParameter(INCLUDE_BUFFER_SIZE_PARAM); + if (value != null) { + includeBufferSize = Integer.parseInt(value.trim()); + log.info(getClass().getSimpleName(), " include buffer size is ", includeBufferSize); + } + } catch (Exception e) { + log.warn( + e, + "Could not interpret '", + config.getInitParameter(INCLUDE_BUFFER_SIZE_PARAM), + "' as a number for init-param '", + INCLUDE_BUFFER_SIZE_PARAM, + "'. Using default value ", + includeBufferSize, + "."); + } - public String getServletName() { - return config.getFilterName(); - } + this.filterConfig = config; + this.servletContext = config.getServletContext(); + this.stripesDispatcher = new DispatcherServlet(); + this.stripesDispatcher.init( + new ServletConfig() { + public String getInitParameter(String name) { + return config.getInitParameter(name); + } + + public Enumeration getInitParameterNames() { + return config.getInitParameterNames(); + } + + public ServletContext getServletContext() { + return config.getServletContext(); + } + + public String getServletName() { + return config.getFilterName(); + } }); + } + + public void destroy() { + try { + if (stripesDispatcher != null) stripesDispatcher.destroy(); + } finally { + stripesDispatcher = null; + + try { + if (stripesFilterIsInternal && stripesFilter != null) stripesFilter.destroy(); + } finally { + stripesFilter = null; + } } - - public void destroy() { - try { - if (stripesDispatcher != null) - stripesDispatcher.destroy(); - } - finally { - stripesDispatcher = null; - - try { - if (stripesFilterIsInternal && stripesFilter != null) - stripesFilter.destroy(); - } - finally { - stripesFilter = null; - } - } + } + + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + // Wrap the response in a wrapper that catches errors (but not exceptions) + final ErrorTrappingResponseWrapper wrapper = + new ErrorTrappingResponseWrapper((HttpServletResponse) response); + wrapper.setInclude(request.getAttribute(StripesConstants.REQ_ATTR_INCLUDE_PATH) != null); + + // Catch FileNotFoundException, which some containers (e.g. GlassFish) throw instead of setting + // SC_NOT_FOUND + boolean fileNotFoundExceptionThrown = false; + + try { + chain.doFilter(request, wrapper); + } catch (FileNotFoundException exc) { + fileNotFoundExceptionThrown = true; + } catch (ServletException e) { + // IBM Liberty basically wraps the FileNotFound into a ServletException + if (e.getRootCause() instanceof FileNotFoundException) { + fileNotFoundExceptionThrown = true; + } else { + // re-throw the exception + throw e; + } } - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - // Wrap the response in a wrapper that catches errors (but not exceptions) - final ErrorTrappingResponseWrapper wrapper = new ErrorTrappingResponseWrapper( - (HttpServletResponse) response); - wrapper.setInclude(request.getAttribute(StripesConstants.REQ_ATTR_INCLUDE_PATH) != null); - - // Catch FileNotFoundException, which some containers (e.g. GlassFish) throw instead of setting SC_NOT_FOUND - boolean fileNotFoundExceptionThrown = false; - - try { - chain.doFilter(request, wrapper); - } - catch (FileNotFoundException exc) { - fileNotFoundExceptionThrown = true; - } - catch(ServletException e) { - // IBM Liberty basically wraps the FileNotFound into a ServletException - if (e.getRootCause() instanceof FileNotFoundException) { - fileNotFoundExceptionThrown = true; - } else { - // re-throw the exception - throw e; - } - } - - // Check the instance field as well as request header for initialization request - HttpServletRequest httpServletRequest = (HttpServletRequest) request; - boolean initializing = this.initializing - || httpServletRequest.getHeader(REQ_HEADER_INIT_FLAG) != null; - - // If a FileNotFoundException or SC_NOT_FOUND error occurred, then try to match an ActionBean to the URL - boolean notFound = false; - Integer errorCode = wrapper.getErrorCode(); - if (errorCode!=null) { - if (errorCode==HttpServletResponse.SC_NOT_FOUND) { - notFound = true; + // Check the instance field as well as request header for initialization request + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + boolean initializing = + this.initializing || httpServletRequest.getHeader(REQ_HEADER_INIT_FLAG) != null; + + // If a FileNotFoundException or SC_NOT_FOUND error occurred, then try to match an ActionBean to + // the URL + boolean notFound = isNotFound(wrapper, httpServletRequest); + if (!initializing && (notFound || fileNotFoundExceptionThrown)) { + // Get a reference to a StripesFilter instance + StripesFilter sf = getStripesFilter(); + if (sf == null) { + initStripesFilter((HttpServletRequest) request, wrapper); + sf = getStripesFilter(); + } + + sf.doFilter( + request, + response, + (request1, response1) -> { + // Look for an ActionBean that is mapped to the URI + String uri = HttpUtil.getRequestedPath((HttpServletRequest) request1); + Class beanType = + getStripesFilter() + .getInstanceConfiguration() + .getActionResolver() + .getActionBeanType(uri); + + // If found then call the dispatcher directly. Otherwise, send the error. + if (beanType == null) { + wrapper.proceed(); } else { - // special handling for WildFly, - // see http://www.stripesframework.org/jira/browse/STS-916 - if ("POST".equals(httpServletRequest.getMethod()) - && errorCode == HttpServletResponse.SC_METHOD_NOT_ALLOWED) { - notFound = true; - } + stripesDispatcher.service(request1, response1); } - } - if (!initializing && (notFound || fileNotFoundExceptionThrown)) { - // Get a reference to a StripesFilter instance - StripesFilter sf = getStripesFilter(); - if (sf == null) { - initStripesFilter((HttpServletRequest) request, wrapper); - sf = getStripesFilter(); - } - - sf.doFilter(request, response, new FilterChain() { - public void doFilter(ServletRequest request, ServletResponse response) - throws IOException, ServletException { - // Look for an ActionBean that is mapped to the URI - String uri = HttpUtil.getRequestedPath((HttpServletRequest) request); - Class beanType = getStripesFilter() - .getInstanceConfiguration().getActionResolver().getActionBeanType(uri); - - // If found then call the dispatcher directly. Otherwise, send the error. - if (beanType == null) { - wrapper.proceed(); - } - else { - stripesDispatcher.service(request, response); - } - } - }); - } - else { - wrapper.proceed(); - } + }); + } else { + wrapper.proceed(); } - - /** - * Get a reference to {@link StripesFilter}. The first time this method is called, the reference - * will be looked up in the servlet context and cached in the {@link #stripesFilter} field. - */ - protected StripesFilter getStripesFilter() { - if (stripesFilter == null) { - stripesFilter = (StripesFilter) servletContext.getAttribute(CONTEXT_KEY_STRIPES_FILTER); - if (stripesFilter != null) { - log.debug("Found StripesFilter in the servlet context."); - } - } - - return stripesFilter; + } + + private static boolean isNotFound( + ErrorTrappingResponseWrapper wrapper, HttpServletRequest httpServletRequest) { + boolean notFound = false; + Integer errorCode = wrapper.getErrorCode(); + if (errorCode != null) { + if (errorCode == HttpServletResponse.SC_NOT_FOUND) { + notFound = true; + } else { + // special handling for WildFly, + // see http://www.stripesframework.org/jira/browse/STS-916 + if ("POST".equals(httpServletRequest.getMethod()) + && errorCode == HttpServletResponse.SC_METHOD_NOT_ALLOWED) { + notFound = true; + } + } + } + return notFound; + } + + /** + * Get a reference to {@link StripesFilter}. The first time this method is called, the reference + * will be looked up in the servlet context and cached in the {@link #stripesFilter} field. + * + * @return A reference to {@link StripesFilter} or {@code null} if it could not be found. + */ + protected StripesFilter getStripesFilter() { + if (stripesFilter == null) { + stripesFilter = (StripesFilter) servletContext.getAttribute(CONTEXT_KEY_STRIPES_FILTER); + if (stripesFilter != null) { + log.debug("Found StripesFilter in the servlet context."); + } } - /** - * The servlet spec allows a container to wait until a filter is required to process a request - * before it initializes the filter. Since we need to get a reference to {@link StripesFilter} - * from the servlet context, we really need {@link StripesFilter} to have been initialized at - * the time we process our first request. If that didn't happen automatically, this method does - * its best to force it to happen. - * - * @param request The current request - * @param response The current response - * @throws ServletException If anything goes wrong that simply can't be ignored. - */ - protected synchronized void initStripesFilter(HttpServletRequest request, - HttpServletResponse response) throws ServletException { - try { - // Check if another thread got into this method before the current thread - if (getStripesFilter() != null) - return; - - log.info("StripesFilter not initialized. Checking the situation in web.xml ..."); - Document document = parseWebXml(); - NodeList filterNodes = eval("/web-app/filter/filter-class[text()='" - + StripesFilter.class.getName() + "']/..", document, XPathConstants.NODESET); - if (filterNodes == null || filterNodes.getLength() != 1) { - String msg; - if (filterNodes == null || filterNodes.getLength() < 1) { - msg = "StripesFilter is not declared in web.xml. "; - } - else { - msg = "StripesFilter is declared multiple times in web.xml; refusing to use either one. "; + return stripesFilter; + } + + /** + * The servlet spec allows a container to wait until a filter is required to process a request + * before it initializes the filter. Since we need to get a reference to {@link StripesFilter} + * from the servlet context, we really need {@link StripesFilter} to have been initialized at the + * time we process our first request. If that didn't happen automatically, this method does its + * best to force it to happen. + * + * @param request The current request + * @param response The current response + * @throws ServletException If anything goes wrong that simply can't be ignored. + */ + protected synchronized void initStripesFilter( + HttpServletRequest request, HttpServletResponse response) throws ServletException { + try { + // Check if another thread got into this method before the current thread + if (getStripesFilter() != null) return; + + log.info("StripesFilter not initialized. Checking the situation in web.xml ..."); + Document document = parseWebXml(); + NodeList filterNodes = + eval( + "/web-app/filter/filter-class[text()='" + StripesFilter.class.getName() + "']/..", + document, + XPathConstants.NODESET); + if (filterNodes == null || filterNodes.getLength() != 1) { + String msg; + if (filterNodes == null || filterNodes.getLength() < 1) { + msg = "StripesFilter is not declared in web.xml. "; + } else { + msg = "StripesFilter is declared multiple times in web.xml; refusing to use either one. "; + } + + log.info(msg, "Initializing with \"", filterConfig.getFilterName(), "\" configuration."); + createStripesFilter(filterConfig); + } else { + Node filterNode = filterNodes.item(0); + final String name = eval("filter-name", filterNode, XPathConstants.STRING); + log.debug("Found StripesFilter declared as ", name, " in web.xml"); + + List patterns = getFilterUrlPatterns(filterNode); + if (patterns.isEmpty()) { + log.info( + "StripesFilter is declared but not mapped in web.xml. ", + "Initializing with \"", + name, + "\" configuration from web.xml."); + + final Map parameters = getFilterParameters(filterNode); + createStripesFilter( + new FilterConfig() { + public ServletContext getServletContext() { + return servletContext; } - log.info(msg, "Initializing with \"", filterConfig.getFilterName(), - "\" configuration."); - createStripesFilter(filterConfig); - } - else { - Node filterNode = filterNodes.item(0); - final String name = eval("filter-name", filterNode, XPathConstants.STRING); - log.debug("Found StripesFilter declared as ", name, " in web.xml"); - - List patterns = getFilterUrlPatterns(filterNode); - if (patterns.isEmpty()) { - log.info("StripesFilter is declared but not mapped in web.xml. ", - "Initializing with \"", name, "\" configuration from web.xml."); - - final Map parameters = getFilterParameters(filterNode); - createStripesFilter(new FilterConfig() { - public ServletContext getServletContext() { - return servletContext; - } - - public Enumeration getInitParameterNames() { - return Collections.enumeration(parameters.keySet()); - } - - public String getInitParameter(String name) { - return parameters.get(name); - } - - public String getFilterName() { - return name; - } - }); + public Enumeration getInitParameterNames() { + return Collections.enumeration(parameters.keySet()); } - else { - issueRequests(patterns, request, response); + + public String getInitParameter(String name) { + return parameters.get(name); } - } - } - catch (RuntimeException e) { - throw e; - } - catch (Exception e) { - throw new StripesServletException( - "Unhandled exception trying to force initialization of StripesFilter", e); - } - // Blow up if no StripesFilter instance could be acquired or created - if (getStripesFilter() == null) { - String msg = "There is no StripesFilter instance available in the servlet context, " - + "and DynamicMappingFilter was unable to initialize one. See previous log " - + "messages for more information."; - log.error(msg); - throw new StripesServletException(msg); - } + public String getFilterName() { + return name; + } + }); + } else { + issueRequests(patterns, request, response); + } + } + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new StripesServletException( + "Unhandled exception trying to force initialization of StripesFilter", e); } - /** - * Parse the application's {@code web.xml} file and return a DOM {@link Document}. - * - * @throws ParserConfigurationException If thrown by the XML parser - * @throws IOException If thrown by the XML parser - * @throws SAXException If thrown by the XML parser - */ - protected Document parseWebXml() throws SAXException, IOException, ParserConfigurationException { - return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse( - servletContext.getResourceAsStream("/WEB-INF/web.xml")); + // Blow up if no StripesFilter instance could be acquired or created + if (getStripesFilter() == null) { + String msg = + "There is no StripesFilter instance available in the servlet context, " + + "and DynamicMappingFilter was unable to initialize one. See previous log " + + "messages for more information."; + log.error(msg); + throw new StripesServletException(msg); } - - /** - * Evaluate an xpath expression against a DOM {@link Node} and return the result. - * - * @param expression The expression to evaluate - * @param source The node against which the expression will be evaluated - * @param returnType One of the constants defined in {@link XPathConstants} - * @return The result returned by {@link XPath#evaluate(String, Object, QName)} - * @throws XPathExpressionException If the xpath expression is invalid - */ - @SuppressWarnings("unchecked") - protected T eval(String expression, Node source, QName returnType) - throws XPathExpressionException { - XPath xpath = XPathFactory.newInstance().newXPath(); - return (T) xpath.evaluate(expression, source, returnType); + } + + /** + * Parse the application's {@code web.xml} file and return a DOM {@link Document}. + * + * @return The DOM {@link Document} representing {@code web.xml} + * @throws ParserConfigurationException If thrown by the XML parser + * @throws IOException If thrown by the XML parser + * @throws SAXException If thrown by the XML parser + */ + protected Document parseWebXml() throws SAXException, IOException, ParserConfigurationException { + return DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(servletContext.getResourceAsStream("/WEB-INF/web.xml")); + } + + /** + * Evaluate a xpath expression against a DOM {@link Node} and return the result. + * + * @param The type of the result + * @param expression The expression to evaluate + * @param source The node against which the expression will be evaluated + * @param returnType One of the constants defined in {@link XPathConstants} + * @return The result returned by {@link XPath#evaluate(String, Object, QName)} + * @throws XPathExpressionException If the xpath expression is invalid + */ + @SuppressWarnings("unchecked") + protected T eval(String expression, Node source, QName returnType) + throws XPathExpressionException { + XPath xpath = XPathFactory.newInstance().newXPath(); + return (T) xpath.evaluate(expression, source, returnType); + } + + /** + * Get all the URL patterns to which a filter is mapped in {@code web.xml}. This includes direct + * mappings using {@code filter-mapping/url-pattern} and indirect mappings using {@code + * filter-mapping/servlet-name} and {@code servlet-mapping/url-pattern}. + * + * @param filterNode The DOM ({@code <filter>}) {@link Node} containing the filter + * declaration from {@code web.xml} + * @return A list of all the patterns to which the filter is mapped + * @throws XPathExpressionException In case of failure evaluating a xpath expression + */ + protected List getFilterUrlPatterns(Node filterNode) throws XPathExpressionException { + String filterName = eval("filter-name", filterNode, XPathConstants.STRING); + Document document = filterNode.getOwnerDocument(); + + NodeList urlMappings = + eval( + "/web-app/filter-mapping/filter-name[text()='" + filterName + "']/../url-pattern", + document, + XPathConstants.NODESET); + NodeList servletMappings = + eval( + "/web-app/filter-mapping/filter-name[text()='" + filterName + "']/../servlet-name", + document, + XPathConstants.NODESET); + + List patterns = new ArrayList<>(); + if (urlMappings != null && urlMappings.getLength() > 0) { + for (int i = 0; i < urlMappings.getLength(); i++) { + patterns.add(urlMappings.item(i).getTextContent().trim()); + } } - /** - * Get all the URL patterns to which a filter is mapped in {@code web.xml}. This includes direct - * mappings using {@code filter-mapping/url-pattern} and indirect mappings using - * {@code filter-mapping/servlet-name} and {@code servlet-mapping/url-pattern}. - * - * @param filterNode The DOM ({@code <filter>}) {@link Node} containing the filter - * declaration from {@code web.xml} - * @return A list of all the patterns to which the filter is mapped - * @throws XPathExpressionException In case of failure evaluating an xpath expression - */ - protected List getFilterUrlPatterns(Node filterNode) throws XPathExpressionException { - String filterName = eval("filter-name", filterNode, XPathConstants.STRING); - Document document = filterNode.getOwnerDocument(); - - NodeList urlMappings = eval("/web-app/filter-mapping/filter-name[text()='" + filterName - + "']/../url-pattern", document, XPathConstants.NODESET); - NodeList servletMappings = eval("/web-app/filter-mapping/filter-name[text()='" + filterName - + "']/../servlet-name", document, XPathConstants.NODESET); - - List patterns = new ArrayList(); - if (urlMappings != null && urlMappings.getLength() > 0) { - for (int i = 0; i < urlMappings.getLength(); i++) { - patterns.add(urlMappings.item(i).getTextContent().trim()); - } - } - - if (servletMappings != null && servletMappings.getLength() > 0) { - for (int i = 0; i < servletMappings.getLength(); i++) { - String servletName = servletMappings.item(i).getTextContent().trim(); - urlMappings = eval("/web-app/servlet-mapping/servlet-name[text()='" + servletName - + "']/../url-pattern", document, XPathConstants.NODESET); - for (int j = 0; j < urlMappings.getLength(); j++) { - patterns.add(urlMappings.item(j).getTextContent().trim()); - } - } - } - - log.debug("Filter ", filterName, " maps to ", patterns); - return patterns; + if (servletMappings != null && servletMappings.getLength() > 0) { + for (int i = 0; i < servletMappings.getLength(); i++) { + String servletName = servletMappings.item(i).getTextContent().trim(); + urlMappings = + eval( + "/web-app/servlet-mapping/servlet-name[text()='" + + servletName + + "']/../url-pattern", + document, + XPathConstants.NODESET); + for (int j = 0; j < urlMappings.getLength(); j++) { + patterns.add(urlMappings.item(j).getTextContent().trim()); + } + } } - /** - * Get the initialization parameters for a filter declared in {@code web.xml}. - * - * @param filterNode The DOM ({@code <filter>}) {@link Node} containing the filter - * declaration from {@code web.xml} - * @return A map of parameter names to parameter values - * @throws XPathExpressionException In case of failure evaluation an xpath expression - */ - protected Map getFilterParameters(Node filterNode) - throws XPathExpressionException { - Map params = new LinkedHashMap(); - NodeList paramNodes = eval("init-param", filterNode, XPathConstants.NODESET); - for (int i = 0; i < paramNodes.getLength(); i++) { - Node node = paramNodes.item(i); - String key = eval("param-name", node, XPathConstants.STRING); - String value = eval("param-value", node, XPathConstants.STRING); - params.put(key, value); - } - return params; + log.debug("Filter ", filterName, " maps to ", patterns); + return patterns; + } + + /** + * Get the initialization parameters for a filter declared in {@code web.xml}. + * + * @param filterNode The DOM ({@code <filter>}) {@link Node} containing the filter + * declaration from {@code web.xml} + * @return A map of parameter names to parameter values + * @throws XPathExpressionException In case of failure evaluation a xpath expression + */ + protected Map getFilterParameters(Node filterNode) + throws XPathExpressionException { + Map params = new LinkedHashMap<>(); + NodeList paramNodes = eval("init-param", filterNode, XPathConstants.NODESET); + for (int i = 0; i < paramNodes.getLength(); i++) { + Node node = paramNodes.item(i); + String key = eval("param-name", node, XPathConstants.STRING); + String value = eval("param-value", node, XPathConstants.STRING); + params.put(key, value); } - - /** - * Create and initialize an instance of {@link StripesFilter} with the given configuration. - * - * @param config The filter configuration - * @throws ServletException If initialization of the filter fails - */ - protected void createStripesFilter(FilterConfig config) throws ServletException { - StripesFilter filter = new StripesFilter(); - filter.init(config); - this.stripesFilter = filter; - this.stripesFilterIsInternal = true; + return params; + } + + /** + * Create and initialize an instance of {@link StripesFilter} with the given configuration. + * + * @param config The filter configuration + * @throws ServletException If initialization of the filter fails + */ + protected void createStripesFilter(FilterConfig config) throws ServletException { + StripesFilter filter = new StripesFilter(); + filter.init(config); + this.stripesFilter = filter; + this.stripesFilterIsInternal = true; + } + + /** + * Issue a series of requests in an attempt to force an invocation (and initialization) of {@link + * StripesFilter} in the application context. All patterns will be requested first with an + * internal forward, then an include and finally with a brand-new request to the address and port + * returned by {@link HttpServletRequest#getLocalAddr()} and {@link + * HttpServletRequest#getLocalPort()}, respectively. + * + * @param patterns The list of patterns to request, as specified by {@code url-pattern} elements + * in {@code web.xml} + * @param request The current request, required to process a forward or include + * @param response The current response, required to process a forward or include + */ + protected void issueRequests( + List patterns, HttpServletRequest request, HttpServletResponse response) { + // Replace globs in the patterns with a random string + String random = "stripes-dmf-request-" + UUID.randomUUID(); + List uris = new ArrayList<>(patterns.size()); + for (String pattern : patterns) { + String uri = pattern.replace("*", random); + if (!uri.startsWith("/")) uri = "/" + uri; + uris.add(uri); } - /** - * Issue a series of requests in an attempt to force an invocation (and initialization) of - * {@link StripesFilter} in the application context. All patterns will be requested first with - * an internal forward, then an include and finally with a brand new request to the address and - * port returned by {@link HttpServletRequest#getLocalAddr()} and - * {@link HttpServletRequest#getLocalPort()}, respectively. - * - * @param patterns The list of patterns to request, as specified by {@code url-pattern} elements - * in {@code web.xml} - * @param request The current request, required to process a forward or include - * @param response The current response, required to process a forward or include - */ - protected void issueRequests(List patterns, HttpServletRequest request, - HttpServletResponse response) { - // Replace globs in the patterns with a random string - String random = "stripes-dmf-request-" + UUID.randomUUID(); - List uris = new ArrayList(patterns.size()); - for (String pattern : patterns) { - String uri = pattern.replace("*", random); - if (!uri.startsWith("/")) - uri = "/" + uri; - uris.add(uri); - } - - // Set the HTTP method to something generally harmless - HttpServletRequestWrapper req = new HttpServletRequestWrapper(request) { - @Override - public String getMethod() { - return "OPTIONS"; - } + // Set the HTTP method to something generally harmless + HttpServletRequestWrapper req = + new HttpServletRequestWrapper(request) { + @Override + public String getMethod() { + return "OPTIONS"; + } }; - // Response swallows all output - HttpServletResponseWrapper rsp = new HttpServletResponseWrapper(response) { - @Override - public ServletOutputStream getOutputStream() throws IOException { - return new ServletOutputStream() { - @Override - public void write(int b) throws IOException { - // No output - } - }; - } - - @Override - public PrintWriter getWriter() throws IOException { - return new PrintWriter(getOutputStream()); - } + // Response swallows all output + HttpServletResponseWrapper rsp = + new HttpServletResponseWrapper(response) { + @Override + public ServletOutputStream getOutputStream() { + return new ServletOutputStream() { + @Override + public boolean isReady() { + // Never ready + return false; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + // No write listener + } + + @Override + public void write(int b) { + // No output + } + }; + } + + @Override + public PrintWriter getWriter() { + return new PrintWriter(getOutputStream()); + } }; - // Try forward first - log.info("Found StripesFilter declared and mapped in web.xml but not yet initialized."); - Iterator iterator = uris.iterator(); - while (getStripesFilter() == null && iterator.hasNext()) { - String uri = iterator.next(); - log.info("Try to force initialization of StripesFilter with forward to ", uri); - try { - initializing = true; - RequestDispatcher dispatcher = servletContext.getRequestDispatcher(uri); - dispatcher.forward(req, rsp); - } - catch (Exception e) { - log.debug(e, "Ignored exception during forward"); - } - finally { - initializing = false; - response.reset(); - } - } - - // If forward failed, try include - iterator = uris.iterator(); - while (getStripesFilter() == null && iterator.hasNext()) { - String uri = iterator.next(); - log.info("Try to force initialization of StripesFilter with include of ", uri); - try { - initializing = true; - RequestDispatcher dispatcher = servletContext.getRequestDispatcher(uri); - dispatcher.forward(req, rsp); - } - catch (Exception e) { - log.debug(e, "Ignored exception during forward"); - } - finally { - initializing = false; - response.reset(); - } - } - - // If both forward and include failed, then do something truly abominable ... - iterator = uris.iterator(); - while (getStripesFilter() == null && iterator.hasNext()) { - try { - String uri = iterator.next(); - log.info("Try to force initialization of StripesFilter with request to ", uri); - requestRemotely(request, uri); - } - catch (Exception e) { - log.debug(e, "Ignored exception during request"); - } - } + // Try forward first + log.info("Found StripesFilter declared and mapped in web.xml but not yet initialized."); + Iterator iterator = uris.iterator(); + while (getStripesFilter() == null && iterator.hasNext()) { + String uri = iterator.next(); + log.info("Try to force initialization of StripesFilter with forward to ", uri); + doDispatcherForward(response, req, rsp, uri); } - /** - * Issue a new request to a path relative to the request's context. The connection is made to - * the address and port returned by {@link HttpServletRequest#getLocalAddr()} and - * {@link HttpServletRequest#getLocalPort()}, respectively. - * - * @param request The current request - * @param relativePath The context-relative path to request - */ - @SuppressWarnings("unchecked") - public void requestRemotely(HttpServletRequest request, String relativePath) { - HttpURLConnection cxn = null; - try { - // Create a new URL using the current request's protocol, port and context - String protocol = new URL(request.getRequestURL().toString()).getProtocol(); - String file = request.getContextPath() + relativePath; - URL url = new URL(protocol, request.getLocalAddr(), request.getLocalPort(), file); - cxn = (HttpURLConnection) url.openConnection(); - - // Set the HTTP method to something generally harmless - cxn.setRequestMethod("OPTIONS"); - - // Copy all the request headers to the new request - Enumeration headerNames = request.getHeaderNames(); - while (headerNames.hasMoreElements()) { - String hdr = headerNames.nextElement(); - cxn.setRequestProperty(hdr, request.getHeader(hdr)); - } - - // Set a flag to let DMF know not to process the request - cxn.setRequestProperty(REQ_HEADER_INIT_FLAG, "true"); + // If forward failed, try include + iterator = uris.iterator(); + while (getStripesFilter() == null && iterator.hasNext()) { + String uri = iterator.next(); + log.info("Try to force initialization of StripesFilter with include of ", uri); + doDispatcherForward(response, req, rsp, uri); + } - // Log the HTTP status - log.debug(cxn.getResponseCode(), " ", cxn.getResponseMessage(), " (", cxn - .getContentLength(), " bytes) from ", url); - } - catch (Exception e) { - log.debug(e, "Request failed trying to force initialization of StripesFilter"); - } - finally { - try { - cxn.disconnect(); - } - catch (Exception e) { - // Ignore - } - } + // If both forward and include failed, then do something truly abominable ... + iterator = uris.iterator(); + while (getStripesFilter() == null && iterator.hasNext()) { + try { + String uri = iterator.next(); + log.info("Try to force initialization of StripesFilter with request to ", uri); + requestRemotely(request, uri); + } catch (Exception e) { + log.debug(e, "Ignored exception during request"); + } + } + } + + private void doDispatcherForward( + HttpServletResponse response, + HttpServletRequestWrapper req, + HttpServletResponseWrapper rsp, + String uri) { + try { + initializing = true; + RequestDispatcher dispatcher = servletContext.getRequestDispatcher(uri); + dispatcher.forward(req, rsp); + } catch (Exception e) { + log.debug(e, "Ignored exception during forward"); + } finally { + initializing = false; + response.reset(); + } + } + + /** + * Issue a new request to a path relative to the request's context. The connection is made to the + * address and port returned by {@link HttpServletRequest#getLocalAddr()} and {@link + * HttpServletRequest#getLocalPort()}, respectively. + * + * @param request The current request + * @param relativePath The context-relative path to request + */ + public void requestRemotely(HttpServletRequest request, String relativePath) { + HttpURLConnection cxn = null; + try { + // Create a new URL using the current request's protocol, port and context + String protocol = new URL(request.getRequestURL().toString()).getProtocol(); + String file = request.getContextPath() + relativePath; + URL url = new URL(protocol, request.getLocalAddr(), request.getLocalPort(), file); + cxn = (HttpURLConnection) url.openConnection(); + + // Set the HTTP method to something generally harmless + cxn.setRequestMethod("OPTIONS"); + + // Copy all the request headers to the new request + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String hdr = headerNames.nextElement(); + cxn.setRequestProperty(hdr, request.getHeader(hdr)); + } + + // Set a flag to let DMF know not to process the request + cxn.setRequestProperty(REQ_HEADER_INIT_FLAG, "true"); + + // Log the HTTP status + log.debug( + cxn.getResponseCode(), + " ", + cxn.getResponseMessage(), + " (", + cxn.getContentLength(), + " bytes) from ", + url); + } catch (Exception e) { + log.debug(e, "Request failed trying to force initialization of StripesFilter"); + } finally { + try { + if (cxn != null) { + cxn.disconnect(); + } + } catch (Exception e) { + // Ignore + } } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/ExecutionContext.java b/stripes/src/main/java/net/sourceforge/stripes/controller/ExecutionContext.java index 4e8ddedf6..35f967ea2 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/ExecutionContext.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/ExecutionContext.java @@ -14,174 +14,183 @@ */ package net.sourceforge.stripes.controller; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Iterator; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.action.Resolution; import net.sourceforge.stripes.util.Log; -import java.util.Collection; -import java.util.Iterator; -import java.lang.reflect.Method; - /** - *

    Holds the execution context for processing a single request. The ExecutionContext is made - * available to {@link Interceptor} classes that are interleaved with the regular request - * processing lifecycle.

    + * Holds the execution context for processing a single request. The ExecutionContext is made + * available to {@link Interceptor} classes that are interleaved with the regular request processing + * lifecycle. * *

    The ExecutionContext is not populated all at once, but in pieces as the request progresses. - * Check the accessor method for each item for information on when that item becomes available - * in the request processing lifecycle.

    + * Check the accessor method for each item for information on when that item becomes available in + * the request processing lifecycle. * * @author Tim Fennell * @since Stripes 1.3 */ public class ExecutionContext { - private static final Log log = Log.getInstance(ExecutionContext.class); - private static final ThreadLocal currentContext = new ThreadLocal(); - - /** Get the execution context for the current thread. */ - public static final ExecutionContext currentContext() { - return currentContext.get(); + private static final Log log = Log.getInstance(ExecutionContext.class); + private static final ThreadLocal currentContext = + new ThreadLocal(); + + /** Get the execution context for the current thread. */ + public static final ExecutionContext currentContext() { + return currentContext.get(); + } + + private Collection interceptors; + private Iterator iterator; + private Interceptor target; + private ActionBeanContext actionBeanContext; + private ActionBean actionBean; + private Method handler; + private Resolution resolution; + private LifecycleStage lifecycleStage; + private boolean resolutionFromHandler = false; + + /** + * Used by the {@link DispatcherServlet} to initialize and/or swap out the list of {@link + * Interceptor} instances which should wrap the current {@link LifecycleStage}. + * + * @param stack a non-null (though possibly empty) ordered collection of interceptors + */ + public void setInterceptors(Collection stack) { + this.interceptors = stack; + } + + /** + * Used by the {@link DispatcherServlet} to wrap a block of lifecycle code in {@link Interceptor} + * calls. + * + * @param target a block of lifecycle/request processing code that is contained inside a class + * that implements Interceptor + * @return a Resolution instance or null depending on what is returned by the lifecycle code and + * any interceptors which intercept the execution + * @throws Exception if the lifecycle code or an interceptor throws an Exception + */ + public Resolution wrap(Interceptor target) throws Exception { + this.target = target; + this.iterator = null; + + // Before executing RequestInit, set this as the current execution context + if (lifecycleStage == LifecycleStage.RequestInit) currentContext.set(this); + + try { + return proceed(); + } finally { + // Make sure the current execution context gets cleared after RequestComplete + if (LifecycleStage.RequestComplete == getLifecycleStage()) currentContext.set(null); } - - private Collection interceptors; - private Iterator iterator; - private Interceptor target; - private ActionBeanContext actionBeanContext; - private ActionBean actionBean; - private Method handler; - private Resolution resolution; - private LifecycleStage lifecycleStage; - private boolean resolutionFromHandler = false; - - /** - * Used by the {@link DispatcherServlet} to initialize and/or swap out the list of - * {@link Interceptor} instances which should wrap the current {@link LifecycleStage}. - * - * @param stack a non-null (though possibly empty) ordered collection of interceptors - */ - public void setInterceptors(Collection stack) { - this.interceptors = stack; + } + + /** + * Retrieves the ActionBeanContext associated with the current request. Available to all + * interceptors regardless of {@link LifecycleStage}. + * + * @return the current ActionBeanContext + */ + public ActionBeanContext getActionBeanContext() { + return actionBeanContext; + } + + /** Sets the ActionBeanContext for the current request. */ + public void setActionBeanContext(ActionBeanContext actionBeanContext) { + this.actionBeanContext = actionBeanContext; + } + + /** + * Retrieves the ActionBean instance that is associated with the current request. Available to + * interceptors only after {@link LifecycleStage#ActionBeanResolution} has occurred. + * + * @return the current ActionBean instance, or null if not yet resolved + */ + public ActionBean getActionBean() { + return actionBean; + } + + /** Sets the ActionBean associated with the current request. */ + public void setActionBean(ActionBean actionBean) { + this.actionBean = actionBean; + } + + /** + * Retrieves the handler Method that is targeted by the current request. Available to interceptors + * only after {@link LifecycleStage#HandlerResolution} has occurred. + * + * @return the current handler method, or null if not yet resolved + */ + public Method getHandler() { + return handler; + } + + /** Sets the handler method that will be invoked to process the current request. */ + public void setHandler(Method handler) { + this.handler = handler; + } + + /** + * Gets the Resolution that will be executed at the end of the execution. This value is generally + * not populated until just prior to {@link LifecycleStage#ResolutionExecution}. + * + * @return the Resolution associated with this execution + */ + public Resolution getResolution() { + return resolution; + } + + /** Sets the Resolution that will be executed to terminate this execution. */ + public void setResolution(Resolution resolution) { + this.resolution = resolution; + } + + /** + * Gets the current LifecycleStage being processed. This is always set to the appropriate + * lifecycle stage before invoking any interceptors or lifecycle code, so that interceptors that + * intercept at multiple lifecycle stages can be aware of which stage is being intercepted. + * + * @return the LifecycleStage currently being processed/intercepted + */ + public LifecycleStage getLifecycleStage() { + return lifecycleStage; + } + + /** Sets the current stage in the request processing lifecycle. */ + public void setLifecycleStage(LifecycleStage lifecycleStage) { + this.lifecycleStage = lifecycleStage; + } + + /** + * Continues the flow of execution. If there are more interceptors in the stack intercepting the + * current lifecycle stage then the flow continues by calling the next interceptor. If there are + * no more interceptors then the lifecycle code is invoked. + * + * @return a Resolution if the lifecycle code or one of the interceptors returns one + * @throws Exception if the lifecycle code or one of the interceptors throws one + */ + public Resolution proceed() throws Exception { + if (this.iterator == null) { + log.debug("Transitioning to lifecycle stage ", lifecycleStage); + this.iterator = this.interceptors.iterator(); } - /** - * Used by the {@link DispatcherServlet} to wrap a block of lifecycle code in - * {@link Interceptor} calls. - * - * @param target a block of lifecycle/request processing code that is contained inside - * a class that implements Interceptor - * @return a Resolution instance or null depending on what is returned by the lifecycle - * code and any interceptors which intercept the execution - * @throws Exception if the lifecycle code or an interceptor throws an Exception - */ - public Resolution wrap(Interceptor target) throws Exception { - this.target = target; - this.iterator = null; - - // Before executing RequestInit, set this as the current execution context - if (lifecycleStage == LifecycleStage.RequestInit) - currentContext.set(this); - - try { - return proceed(); - } - finally { - // Make sure the current execution context gets cleared after RequestComplete - if (LifecycleStage.RequestComplete == getLifecycleStage()) - currentContext.set(null); - } + if (this.iterator.hasNext()) { + return this.iterator.next().intercept(this); + } else { + return this.target.intercept(this); } + } - /** - * Retrieves the ActionBeanContext associated with the current request. Available to all - * interceptors regardless of {@link LifecycleStage}. - * - * @return the current ActionBeanContext - */ - public ActionBeanContext getActionBeanContext() { - return actionBeanContext; - } + public boolean isResolutionFromHandler() { + return resolutionFromHandler; + } - /** Sets the ActionBeanContext for the current request. */ - public void setActionBeanContext(ActionBeanContext actionBeanContext) { - this.actionBeanContext = actionBeanContext; - } - - /** - * Retrieves the ActionBean instance that is associated with the current request. Available - * to interceptors only after {@link LifecycleStage#ActionBeanResolution} has occurred. - * - * @return the current ActionBean instance, or null if not yet resolved - */ - public ActionBean getActionBean() { return actionBean; } - - /** Sets the ActionBean associated with the current request. */ - public void setActionBean(ActionBean actionBean) { this.actionBean = actionBean; } - - /** - * Retrieves the handler Method that is targeted by the current request. Available - * to interceptors only after {@link LifecycleStage#HandlerResolution} has occurred. - * - * @return the current handler method, or null if not yet resolved - */ - public Method getHandler() { return handler; } - - /** Sets the handler method that will be invoked to process the current request. */ - public void setHandler(Method handler) { this.handler = handler; } - - /** - * Gets the Resolution that will be executed at the end of the execution. This value - * is generally not populated until just prior to {@link LifecycleStage#ResolutionExecution}. - * - * @return the Resolution associated with this execution - */ - public Resolution getResolution() { return resolution; } - - /** Sets the Resolution that will be executed to terminate this execution. */ - public void setResolution(Resolution resolution) { this.resolution = resolution; } - - /** - * Gets the current LifecycleStage being processed. This is always set to the appropriate - * lifecycle stage before invoking any interceptors or lifecycle code, so that interceptors - * that intercept at multiple lifecycle stages can be aware of which stage is being - * intercepted. - * - * @return the LifecycleStage currently being processed/intercepted - */ - public LifecycleStage getLifecycleStage() { return lifecycleStage; } - - /** Sets the current stage in the request processing lifecycle. */ - public void setLifecycleStage(LifecycleStage lifecycleStage) { - this.lifecycleStage = lifecycleStage; - } - - /** - * Continues the flow of execution. If there are more interceptors in the stack intercepting - * the current lifecycle stage then the flow continues by calling the next interceptor. If - * there are no more interceptors then the lifecycle code is invoked. - * - * @return a Resolution if the lifecycle code or one of the interceptors returns one - * @throws Exception if the lifecycle code or one of the interceptors throws one - */ - public Resolution proceed() throws Exception { - if (this.iterator == null) { - log.debug("Transitioning to lifecycle stage ", lifecycleStage); - this.iterator = this.interceptors.iterator(); - } - - if (this.iterator.hasNext()) { - return this.iterator.next().intercept(this); - } - else { - return this.target.intercept(this); - } - } - - public boolean isResolutionFromHandler() { - return resolutionFromHandler; - } - - public void setResolutionFromHandler(boolean resolutionFromHandler) { - this.resolutionFromHandler = resolutionFromHandler; - } + public void setResolutionFromHandler(boolean resolutionFromHandler) { + this.resolutionFromHandler = resolutionFromHandler; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/FileUploadLimitExceededException.java b/stripes/src/main/java/net/sourceforge/stripes/controller/FileUploadLimitExceededException.java index eaed7f409..332c9f738 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/FileUploadLimitExceededException.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/FileUploadLimitExceededException.java @@ -17,33 +17,37 @@ import net.sourceforge.stripes.exception.StripesServletException; /** - * Exception that is thrown when the post size of a multipart/form post used for file - * upload exceeds the configured maximum size. + * Exception that is thrown when the post size of a multipart/form post used for file upload exceeds + * the configured maximum size. * * @author Tim Fennell */ public class FileUploadLimitExceededException extends StripesServletException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private long maximum; - private long posted; + private long maximum; + private long posted; - /** - * Constructs a new exception that contains the limit that was violated, and the size - * of the post that violated it, both in bytes. - * - * @param max the current post size limit - * @param posted the size of the post - */ - public FileUploadLimitExceededException(long max, long posted) { - super("File post limit exceeded. Limit: " + max + " bytes. Posted: " + posted + " bytes."); - this.maximum = max; - this.posted = posted; - } + /** + * Constructs a new exception that contains the limit that was violated, and the size of the post + * that violated it, both in bytes. + * + * @param max the current post size limit + * @param posted the size of the post + */ + public FileUploadLimitExceededException(long max, long posted) { + super("File post limit exceeded. Limit: " + max + " bytes. Posted: " + posted + " bytes."); + this.maximum = max; + this.posted = posted; + } - /** Gets the limit in bytes for HTTP POSTs. */ - public long getMaximum() { return maximum; } + /** Gets the limit in bytes for HTTP POSTs. */ + public long getMaximum() { + return maximum; + } - /** The size in bytes of the HTTP POST. */ - public long getPosted() { return posted; } -} \ No newline at end of file + /** The size in bytes of the HTTP POST. */ + public long getPosted() { + return posted; + } +} diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/FlashRequest.java b/stripes/src/main/java/net/sourceforge/stripes/controller/FlashRequest.java index 67ae09c57..22223d8b9 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/FlashRequest.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/FlashRequest.java @@ -1,8 +1,23 @@ package net.sourceforge.stripes.controller; +import jakarta.servlet.AsyncContext; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletConnection; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpUpgradeHandler; +import jakarta.servlet.http.Part; import java.io.BufferedReader; import java.io.Serializable; import java.security.Principal; +import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; @@ -10,377 +25,442 @@ import java.util.Locale; import java.util.Map; -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletInputStream; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - /** - * Captures the state of an {@link javax.servlet.http.HttpServletRequest} so that the information - * contained therein can be carried over to the next request for use by the flash scope. There are - * several methods in here that cannot be faked and so must delegate to an active {@link - * javax.servlet.http.HttpServletRequest} object, the {@link #delegate}. If one of these methods is - * called and there is no delegate object set on the instance, they will throw a {@link - * net.sourceforge.stripes.exception.StripesRuntimeException}. Unless this class is used outside its - * intended context (during a live request processed through {@link StripesFilter}), you won't need - * to worry about that. + * Captures the state of an {@link HttpServletRequest} so that the information contained therein can + * be carried over to the next request for use by the flash scope. There are several methods in here + * that cannot be faked and so must delegate to an active {@link HttpServletRequest} object, the + * {@link #delegate}. If one of these methods is called and there is no delegate object set on the + * instance, they will throw a {@link net.sourceforge.stripes.exception.StripesRuntimeException}. + * Unless this class is used outside its intended context (during a live request processed through + * {@link StripesFilter}), you won't need to worry about that. * * @author Ben Gunter * @since Stripes 1.4.3 */ public class FlashRequest implements HttpServletRequest, Serializable { - private static final long serialVersionUID = 1L; - - private Cookie[] cookies; - private HttpServletRequest delegate; - private List locales; - private Locale locale; - private Map> headers = new HashMap>(); - private Map dateHeaders = new HashMap(); - private Map attributes = new HashMap(); - private Map parameters = new HashMap(); - private String authType; - private String characterEncoding; - private String contentType; - private String contextPath; - private String localAddr; - private String localName; - private String method; - private String pathInfo; - private String pathTranslated; - private String protocol; - private String queryString; - private String remoteAddr; - private String remoteHost; - private String remoteUser; - private String requestURI; - private String requestedSessionId; - private String scheme; - private String serverName; - private String servletPath; - private StringBuffer requestURL; - private boolean requestedSessionIdFromCookie; - private boolean requestedSessionIdFromURL; - private boolean requestedSessionIdFromUrl; - private boolean requestedSessionIdValid; - private boolean secure; - private int localPort; - private int remotePort; - private int serverPort; - - /** - * Finds the StripesRequestWrapper for the supplied request and swaps out the underlying - * request for an instance of FlashRequest. - * - * @param request the current HttpServletRequest - * @return the StripesRequestWrapper for this request with the "live" request replaced - */ - public static StripesRequestWrapper replaceRequest(HttpServletRequest request) { - StripesRequestWrapper wrapper = StripesRequestWrapper.findStripesWrapper(request); - wrapper.setRequest(new FlashRequest((HttpServletRequest) wrapper.getRequest())); - return wrapper; - } - - /** - * Creates a new FlashRequest by copying all appropriate attributes from the prototype - * request supplied. - * - * @param prototype the HttpServletRequest to create a disconnected copy of - */ - @SuppressWarnings({ "unchecked", "deprecation" }) - public FlashRequest(HttpServletRequest prototype) { - // copy properties - authType = prototype.getAuthType(); - characterEncoding = prototype.getCharacterEncoding(); - contentType = prototype.getContentType(); - contextPath = prototype.getContextPath(); - cookies = prototype.getCookies(); - localAddr = prototype.getLocalAddr(); - localName = prototype.getLocalName(); - localPort = prototype.getLocalPort(); - locale = prototype.getLocale(); - method = prototype.getMethod(); - pathInfo = prototype.getPathInfo(); - pathTranslated = prototype.getPathTranslated(); - protocol = prototype.getProtocol(); - queryString = prototype.getQueryString(); - remoteAddr = prototype.getRemoteAddr(); - remoteHost = prototype.getRemoteHost(); - remotePort = prototype.getRemotePort(); - remoteUser = prototype.getRemoteUser(); - requestURI = prototype.getRequestURI(); - requestURL = prototype.getRequestURL(); - requestedSessionId = prototype.getRequestedSessionId(); - requestedSessionIdFromCookie = prototype.isRequestedSessionIdFromCookie(); - requestedSessionIdFromURL = prototype.isRequestedSessionIdFromURL(); - requestedSessionIdFromUrl = prototype.isRequestedSessionIdFromUrl(); - requestedSessionIdValid = prototype.isRequestedSessionIdValid(); - scheme = prototype.getScheme(); - secure = prototype.isSecure(); - serverName = prototype.getServerName(); - serverPort = prototype.getServerPort(); - servletPath = prototype.getServletPath(); - - // copy attributes - for (String key : Collections.list((Enumeration) prototype.getAttributeNames())) { - attributes.put(key, prototype.getAttribute(key)); - } - - // copy headers - for (String key : Collections.list((Enumeration) prototype.getHeaderNames())) { - headers.put(key, Collections.list(prototype.getHeaders(key))); - try { - dateHeaders.put(key, prototype.getDateHeader(key)); - } - catch (Exception e) { - } - } - - // copy locales - locales = Collections.list(prototype.getLocales()); - - // copy parameters - parameters.putAll(prototype.getParameterMap()); - } - - protected HttpServletRequest getDelegate() { - if (delegate == null) { - throw new IllegalStateException( - "Attempt to access a delegate method of " + - FlashRequest.class.getName() + - " but no delegate request has been set"); - } - return delegate; - } - - public void setDelegate(HttpServletRequest delegate) { - this.delegate = delegate; - } - - public String getAuthType() { - return authType; - } - - public Cookie[] getCookies() { - return cookies; - } - - public long getDateHeader(String name) { - Long value = dateHeaders.get(name); - return value == null ? 0 : value; - } - - public String getHeader(String name) { - List values = headers.get(name); - return values != null && values.size() > 0 ? values.get(0) : null; - } - - public Enumeration getHeaders(String name) { - return Collections.enumeration(headers.get(name)); - } - - public Enumeration getHeaderNames() { - return Collections.enumeration(headers.keySet()); - } - - public int getIntHeader(String name) { - try { - return Integer.parseInt(getHeader(name)); - } - catch (Exception e) { - return 0; - } - } - - public String getMethod() { - return method; - } - - public String getPathInfo() { - return pathInfo; - } - - public String getPathTranslated() { - return pathTranslated; - } - - public String getContextPath() { - return contextPath; - } - - public String getQueryString() { - return queryString; - } - - public String getRemoteUser() { - return remoteUser; - } - - public boolean isUserInRole(String role) { - return getDelegate().isUserInRole(role); - } - - public Principal getUserPrincipal() { - return getDelegate().getUserPrincipal(); - } - - public String getRequestedSessionId() { - return requestedSessionId; - } - - public String getRequestURI() { - return requestURI; - } - - public StringBuffer getRequestURL() { - return new StringBuffer(requestURL.toString()); - } - - public String getServletPath() { - return servletPath; - } - - public HttpSession getSession(boolean create) { - return getDelegate().getSession(create); - } - - public HttpSession getSession() { - return getDelegate().getSession(); - } - - public boolean isRequestedSessionIdValid() { - return requestedSessionIdValid; - } - - public boolean isRequestedSessionIdFromCookie() { - return requestedSessionIdFromCookie; - } - - public boolean isRequestedSessionIdFromURL() { - return requestedSessionIdFromURL; - } - - @Deprecated - public boolean isRequestedSessionIdFromUrl() { - return requestedSessionIdFromUrl; - } - - public Object getAttribute(String name) { - return attributes.get(name); - } - - public Enumeration getAttributeNames() { - return Collections.enumeration(attributes.keySet()); - } - - public String getCharacterEncoding() { - return characterEncoding; - } - - public void setCharacterEncoding(String characterEncoding) { - this.characterEncoding = characterEncoding; - } - - public int getContentLength() { - return 0; - } - - public String getContentType() { - return contentType; - } - - public ServletInputStream getInputStream() { - return null; - } - - public String getParameter(String name) { - String[] values = getParameterValues(name); - return values != null && values.length > 0 ? values[0] : null; - } - - public Enumeration getParameterNames() { - return Collections.enumeration(parameters.keySet()); - } - - public String[] getParameterValues(String name) { - return parameters.get(name); - } - - public Map getParameterMap() { - return Collections.unmodifiableMap(parameters); - } - - public String getProtocol() { - return protocol; - } - - public String getScheme() { - return scheme; - } - - public String getServerName() { - return serverName; - } - - public int getServerPort() { - return serverPort; - } - - public BufferedReader getReader() { - return null; - } - - public String getRemoteAddr() { - return remoteAddr; - } - - public String getRemoteHost() { - return remoteHost; - } - - public void setAttribute(String name, Object value) { - attributes.put(name, value); - } - - public void removeAttribute(String name) { - attributes.remove(name); - } - - public Locale getLocale() { - return locale; - } - - public Enumeration getLocales() { - return Collections.enumeration(locales); - } - - public boolean isSecure() { - return secure; - } - - public RequestDispatcher getRequestDispatcher(String name) { - return getDelegate().getRequestDispatcher(name); - } - - @Deprecated - public String getRealPath(String name) { - return getDelegate().getRealPath(name); - } - - public int getRemotePort() { - return remotePort; - } - - public String getLocalName() { - return localName; - } - - public String getLocalAddr() { - return localAddr; - } - - public int getLocalPort() { - return localPort; - } + private static final long serialVersionUID = 1L; + + private final Cookie[] cookies; + private HttpServletRequest delegate; + private final List locales; + private final Locale locale; + private final Map> headers = new HashMap<>(); + private final Map dateHeaders = new HashMap<>(); + private final Map attributes = new HashMap<>(); + private final Map parameters = new HashMap<>(); + private final String authType; + private String characterEncoding; + private final String contentType; + private final String contextPath; + private final String localAddr; + private final String localName; + private final String method; + private final String pathInfo; + private final String pathTranslated; + private final String protocol; + private final String queryString; + private final String remoteAddr; + private final String remoteHost; + private final String remoteUser; + private final String requestURI; + private final String requestedSessionId; + private final String scheme; + private final String serverName; + private final String servletPath; + private final StringBuffer requestURL; + private final boolean requestedSessionIdFromCookie; + private final boolean requestedSessionIdFromURL; + private final boolean requestedSessionIdValid; + private final boolean secure; + private final int localPort; + private final int remotePort; + private final int serverPort; + + /** + * Finds the StripesRequestWrapper for the supplied request and swaps out the underlying request + * for an instance of FlashRequest. + * + * @param request the current HttpServletRequest + * @return the StripesRequestWrapper for this request with the "live" request replaced + */ + public static StripesRequestWrapper replaceRequest(HttpServletRequest request) { + StripesRequestWrapper wrapper = StripesRequestWrapper.findStripesWrapper(request); + wrapper.setRequest(new FlashRequest((HttpServletRequest) wrapper.getRequest())); + return wrapper; + } + + /** + * Creates a new FlashRequest by copying all appropriate attributes from the prototype request + * supplied. + * + * @param prototype the HttpServletRequest to create a disconnected copy of + */ + public FlashRequest(HttpServletRequest prototype) { + // copy properties + authType = prototype.getAuthType(); + characterEncoding = prototype.getCharacterEncoding(); + contentType = prototype.getContentType(); + contextPath = prototype.getContextPath(); + cookies = prototype.getCookies(); + localAddr = prototype.getLocalAddr(); + localName = prototype.getLocalName(); + localPort = prototype.getLocalPort(); + locale = prototype.getLocale(); + method = prototype.getMethod(); + pathInfo = prototype.getPathInfo(); + pathTranslated = prototype.getPathTranslated(); + protocol = prototype.getProtocol(); + queryString = prototype.getQueryString(); + remoteAddr = prototype.getRemoteAddr(); + remoteHost = prototype.getRemoteHost(); + remotePort = prototype.getRemotePort(); + remoteUser = prototype.getRemoteUser(); + requestURI = prototype.getRequestURI(); + requestURL = prototype.getRequestURL(); + requestedSessionId = prototype.getRequestedSessionId(); + requestedSessionIdFromCookie = prototype.isRequestedSessionIdFromCookie(); + requestedSessionIdFromURL = prototype.isRequestedSessionIdFromURL(); + requestedSessionIdValid = prototype.isRequestedSessionIdValid(); + scheme = prototype.getScheme(); + secure = prototype.isSecure(); + serverName = prototype.getServerName(); + serverPort = prototype.getServerPort(); + servletPath = prototype.getServletPath(); + + // copy attributes + for (String key : Collections.list(prototype.getAttributeNames())) { + attributes.put(key, prototype.getAttribute(key)); + } + + // copy headers + for (String key : Collections.list(prototype.getHeaderNames())) { + headers.put(key, Collections.list(prototype.getHeaders(key))); + try { + dateHeaders.put(key, prototype.getDateHeader(key)); + } catch (Exception ignored) { + } + } + + // copy locales + locales = Collections.list(prototype.getLocales()); + + // copy parameters + parameters.putAll(prototype.getParameterMap()); + } + + protected HttpServletRequest getDelegate() { + if (delegate == null) { + throw new IllegalStateException( + "Attempt to access a delegate method of " + + FlashRequest.class.getName() + + " but no delegate request has been set"); + } + return delegate; + } + + public void setDelegate(HttpServletRequest delegate) { + this.delegate = delegate; + } + + public String getAuthType() { + return authType; + } + + public Cookie[] getCookies() { + return cookies; + } + + public long getDateHeader(String name) { + Long value = dateHeaders.get(name); + return value == null ? 0 : value; + } + + public String getHeader(String name) { + List values = headers.get(name); + return values != null && !values.isEmpty() ? values.get(0) : null; + } + + public Enumeration getHeaders(String name) { + return Collections.enumeration(headers.get(name)); + } + + public Enumeration getHeaderNames() { + return Collections.enumeration(headers.keySet()); + } + + public int getIntHeader(String name) { + try { + return Integer.parseInt(getHeader(name)); + } catch (Exception e) { + return 0; + } + } + + public String getMethod() { + return method; + } + + public String getPathInfo() { + return pathInfo; + } + + public String getPathTranslated() { + return pathTranslated; + } + + public String getContextPath() { + return contextPath; + } + + public String getQueryString() { + return queryString; + } + + public String getRemoteUser() { + return remoteUser; + } + + public boolean isUserInRole(String role) { + return getDelegate().isUserInRole(role); + } + + public Principal getUserPrincipal() { + return getDelegate().getUserPrincipal(); + } + + public String getRequestedSessionId() { + return requestedSessionId; + } + + public String getRequestURI() { + return requestURI; + } + + public StringBuffer getRequestURL() { + return new StringBuffer(requestURL.toString()); + } + + public String getServletPath() { + return servletPath; + } + + public HttpSession getSession(boolean create) { + return getDelegate().getSession(create); + } + + public HttpSession getSession() { + return getDelegate().getSession(); + } + + @Override + public String changeSessionId() { + return null; + } + + public boolean isRequestedSessionIdValid() { + return requestedSessionIdValid; + } + + public boolean isRequestedSessionIdFromCookie() { + return requestedSessionIdFromCookie; + } + + public boolean isRequestedSessionIdFromURL() { + return requestedSessionIdFromURL; + } + + @Override + public boolean authenticate(HttpServletResponse httpServletResponse) { + return false; + } + + @Override + public void login(String s, String s1) {} + + @Override + public void logout() {} + + @Override + public Collection getParts() { + return null; + } + + @Override + public Part getPart(String s) { + return null; + } + + @Override + public T upgrade(Class aClass) { + return null; + } + + public Object getAttribute(String name) { + return attributes.get(name); + } + + public Enumeration getAttributeNames() { + return Collections.enumeration(attributes.keySet()); + } + + public String getCharacterEncoding() { + return characterEncoding; + } + + public void setCharacterEncoding(String characterEncoding) { + this.characterEncoding = characterEncoding; + } + + public int getContentLength() { + return 0; + } + + @Override + public long getContentLengthLong() { + return 0; + } + + public String getContentType() { + return contentType; + } + + public ServletInputStream getInputStream() { + return null; + } + + public String getParameter(String name) { + String[] values = getParameterValues(name); + return values != null && values.length > 0 ? values[0] : null; + } + + public Enumeration getParameterNames() { + return Collections.enumeration(parameters.keySet()); + } + + public String[] getParameterValues(String name) { + return parameters.get(name); + } + + public Map getParameterMap() { + return Collections.unmodifiableMap(parameters); + } + + public String getProtocol() { + return protocol; + } + + public String getScheme() { + return scheme; + } + + public String getServerName() { + return serverName; + } + + public int getServerPort() { + return serverPort; + } + + public BufferedReader getReader() { + return null; + } + + public String getRemoteAddr() { + return remoteAddr; + } + + public String getRemoteHost() { + return remoteHost; + } + + public void setAttribute(String name, Object value) { + attributes.put(name, value); + } + + public void removeAttribute(String name) { + attributes.remove(name); + } + + public Locale getLocale() { + return locale; + } + + public Enumeration getLocales() { + return Collections.enumeration(locales); + } + + public boolean isSecure() { + return secure; + } + + public RequestDispatcher getRequestDispatcher(String name) { + return getDelegate().getRequestDispatcher(name); + } + + public int getRemotePort() { + return remotePort; + } + + public String getLocalName() { + return localName; + } + + public String getLocalAddr() { + return localAddr; + } + + public int getLocalPort() { + return localPort; + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + return null; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) + throws IllegalStateException { + return null; + } + + @Override + public boolean isAsyncStarted() { + return false; + } + + @Override + public boolean isAsyncSupported() { + return false; + } + + @Override + public AsyncContext getAsyncContext() { + return null; + } + + @Override + public DispatcherType getDispatcherType() { + return null; + } + + @Override + public String getRequestId() { + return null; + } + + @Override + public String getProtocolRequestId() { + return null; + } + + @Override + public ServletConnection getServletConnection() { + return null; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/FlashResponseInvocationHandler.java b/stripes/src/main/java/net/sourceforge/stripes/controller/FlashResponseInvocationHandler.java index a9dad31c5..aad2b1615 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/FlashResponseInvocationHandler.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/FlashResponseInvocationHandler.java @@ -6,20 +6,22 @@ /** * Used as the {@link java.lang.reflect.InvocationHandler} for a dynamic proxy that replaces the - * {@link javax.servlet.http.HttpServletResponse} on {@link + * {@link jakarta.servlet.http.HttpServletResponse} on {@link * net.sourceforge.stripes.action.ActionBeanContext}s in the flash scope after the current request * cycle has completed. - * + * * @author Ben Gunter * @since Stripes 1.4.3 */ public class FlashResponseInvocationHandler implements InvocationHandler, Serializable { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - public Object invoke(Object object, Method method, Object[] objects) throws Throwable { - throw new IllegalStateException( - "Attempt to call " + method + " after the request cycle has completed. " + - "This is most likely due to misuse of a flashed ActionBean or ActionBeanContext " + - "on the ensuing request."); - } + public Object invoke(Object object, Method method, Object[] objects) throws Throwable { + throw new IllegalStateException( + "Attempt to call " + + method + + " after the request cycle has completed. " + + "This is most likely due to misuse of a flashed ActionBean or ActionBeanContext " + + "on the ensuing request."); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/FlashScope.java b/stripes/src/main/java/net/sourceforge/stripes/controller/FlashScope.java index 3c0038da2..5fd499964 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/FlashScope.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/FlashScope.java @@ -14,14 +14,9 @@ */ package net.sourceforge.stripes.controller; -import net.sourceforge.stripes.action.ActionBean; -import net.sourceforge.stripes.action.ActionBeanContext; -import net.sourceforge.stripes.exception.StripesRuntimeException; -import net.sourceforge.stripes.util.Log; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; import java.lang.reflect.Proxy; import java.util.Collection; import java.util.Collections; @@ -32,404 +27,393 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import net.sourceforge.stripes.action.ActionBean; +import net.sourceforge.stripes.action.ActionBeanContext; +import net.sourceforge.stripes.exception.StripesRuntimeException; +import net.sourceforge.stripes.util.Log; /** - *

    A FlashScope is an object that can be used to store objects and make them available as - * request parameters during this request cycle and the next one. It is extremely useful - * when implementing the redirect-after-post pattern in which an ActionBean receives a POST, - * does some processing and then redirects to a JSP to display the outcome. FlashScopes make - * temporary use of session to store themselves briefly between two requests.

    + * A FlashScope is an object that can be used to store objects and make them available as request + * parameters during this request cycle and the next one. It is extremely useful when implementing + * the redirect-after-post pattern in which an ActionBean receives a POST, does some processing and + * then redirects to a JSP to display the outcome. FlashScopes make temporary use of session + * to store themselves briefly between two requests. * - *

    In general, use of the FlashScope should be intermediated by the - * {@link net.sourceforge.stripes.action.ActionBeanContext}, making it transparent to the - * rest of the application. Any object that is put into a FlashScope will be immediately - * exposed in the current request as a request attribute, and under certain conditions will - * also be exposed in the subsequent request as a request attribute.

    + *

    In general, use of the FlashScope should be intermediated by the {@link + * net.sourceforge.stripes.action.ActionBeanContext}, making it transparent to the rest of the + * application. Any object that is put into a FlashScope will be immediately exposed in the current + * request as a request attribute, and under certain conditions will also be exposed in the + * subsequent request as a request attribute. * - *

    To make values available to the subsequent request a parameter must be included in - * the redirect URL that identifies the flash scope to use (this avoids collisions where two - * concurrent requests in the same session might otherwise cause problems for one another). - * The Stripes {@link net.sourceforge.stripes.action.RedirectResolution} will automatically - * insert this parameter into the URL when a flash scope is present. Should you wish to issue - * redirects using a different mechanism you will need to add the parameter using code - * similar to the following:

    + *

    To make values available to the subsequent request a parameter must be included in the + * redirect URL that identifies the flash scope to use (this avoids collisions where two concurrent + * requests in the same session might otherwise cause problems for one another). The Stripes {@link + * net.sourceforge.stripes.action.RedirectResolution} will automatically insert this parameter into + * the URL when a flash scope is present. Should you wish to issue redirects using a different + * mechanism you will need to add the parameter using code similar to the following: * - *

    - *FlashScope flash = FlashScope.getCurrent(request, false);
    - *if (flash != null) {
    + * 
    + * FlashScope flash = FlashScope.getCurrent(request, false);
    + * if (flash != null) {
      *    url.addParameter(StripesConstants.URL_KEY_FLASH_SCOPE_ID, flash.key());
    - *}
    - *
    + * } + *
    * *

    The lifecycle of a FlashScope is managed is conjunction with the {@link StripesFilter}. - * FlashScopes are manufactured using lazy instantiation when - * {@code FlashScope.getCurrent(request, true)} is called. When a request is completed, the - * StripesFilter notifies the current FlashScope that the request is over, which causes it - * to record the time when the request terminated. On the subsequent request, if the flash - * scope is referenced by a URL parameter, then it is removed from session and it's contents - * are pushed into request attributes for the current request.

    + * FlashScopes are manufactured using lazy instantiation when {@code FlashScope.getCurrent(request, + * true)} is called. When a request is completed, the StripesFilter notifies the current FlashScope + * that the request is over, which causes it to record the time when the request terminated. On the + * subsequent request, if the flash scope is referenced by a URL parameter, then it is removed from + * session and it's contents are pushed into request attributes for the current request. * *

    To ensure that orphaned FlashScopes do not consume increasing amounts of HttpSession memory, - * the StripesFilter, after each request, checks to see if any FlashScopes have recently expired. - * A FlashScope is expired when the length of time from the end of the request that created the - * FlashScope is greater than the timeout set on the FlashScope. The default timeout is 120 seconds - * (or two minutes), and can be varied by calling {@link #setTimeout(int)} Since the timer - * starts when a request completes, and FlashScopes are only meant to live from the end of one - * request to the beginning of a subsequent request this value is set quite low.

    + * the StripesFilter, after each request, checks to see if any FlashScopes have recently expired. A + * FlashScope is expired when the length of time from the end of the request that created the + * FlashScope is greater than the timeout set on the FlashScope. The default timeout is 120 seconds + * (or two minutes), and can be varied by calling {@link #setTimeout(int)} Since the timer starts + * when a request completes, and FlashScopes are only meant to live from the end of one request to + * the beginning of a subsequent request this value is set quite low. * * @author Tim Fennell * @since Stripes 1.2 */ -public class FlashScope extends HashMap { - private static final long serialVersionUID = 1L; - - /** The default timeout for a flash scope. */ - public static final int DEFAULT_TIMEOUT_IN_SECONDS = 120; - - private static final Log log = Log.getInstance(FlashScope.class); - private static final Random random = new Random(); - private long startTime; - private int timeout = DEFAULT_TIMEOUT_IN_SECONDS; - private transient HttpServletRequest request; - private Integer key; - private Semaphore semaphore; - - /** - * Protected constructor to prevent random creation of FlashScopes. Uses the request - * to generate a key under which the flash scope will be stored, and can be identified - * by later. - * - * @param request the request for which this flash scope will be used. - * @param key the key by which this flash scope can be looked up in the map - */ - protected FlashScope(HttpServletRequest request, Integer key) { - this.request = request; - this.key = key; - this.semaphore = new Semaphore(1); - this.semaphore.acquireUninterruptibly(); - } - - /** Returns the timeout in seconds after which the flash scope will be discarded. */ - public int getTimeout() { return timeout; } - - /** Sets the timeout in seconds after which the flash scope will be discarded. */ - public void setTimeout(int timeout) { this.timeout = timeout; } - - /** - * Returns the key used to store this flash scope in the collection of flash scopes. - */ - public Integer key() { - return key; - } - - /** - * Get the semaphore that is used to synchronize the calls to {@link #completeRequest()} and - * {@link #beginRequest(HttpServletRequest)} made by {@link StripesFilter}. - */ - protected Semaphore getSemaphore() { - return semaphore; - } - - /** - *

    Used by the StripesFilter to notify the flash scope that the request for which - * it is used has been completed. The FlashScope uses this notification to start a - * timer, and also to null out it's reference to the request so that it can be - * garbage collected.

    - * - *

    The timer is used to determine if a flash scope has been orphaned (i.e. the subsequent - * request was not made) after a period of time, so that it can be removed from session.

    - */ - public void completeRequest() { - // Clean up any old-age flash scopes - Map scopes = getContainer(request, false); - if (scopes != null && !scopes.isEmpty()) { - Iterator iterator = scopes.values().iterator(); - while (iterator.hasNext()) { - if (iterator.next().isExpired()) { - iterator.remove(); - } - } +public class FlashScope extends HashMap { + private static final long serialVersionUID = 1L; + + /** The default timeout for a flash scope. */ + public static final int DEFAULT_TIMEOUT_IN_SECONDS = 120; + + private static final Log log = Log.getInstance(FlashScope.class); + private static final Random random = new Random(); + private long startTime; + private int timeout = DEFAULT_TIMEOUT_IN_SECONDS; + private transient HttpServletRequest request; + private Integer key; + private Semaphore semaphore; + + /** + * Protected constructor to prevent random creation of FlashScopes. Uses the request to generate a + * key under which the flash scope will be stored, and can be identified by later. + * + * @param request the request for which this flash scope will be used. + * @param key the key by which this flash scope can be looked up in the map + */ + protected FlashScope(HttpServletRequest request, Integer key) { + this.request = request; + this.key = key; + this.semaphore = new Semaphore(1); + this.semaphore.acquireUninterruptibly(); + } + + /** Returns the timeout in seconds after which the flash scope will be discarded. */ + public int getTimeout() { + return timeout; + } + + /** Sets the timeout in seconds after which the flash scope will be discarded. */ + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + /** Returns the key used to store this flash scope in the collection of flash scopes. */ + public Integer key() { + return key; + } + + /** + * Get the semaphore that is used to synchronize the calls to {@link #completeRequest()} and + * {@link #beginRequest(HttpServletRequest)} made by {@link StripesFilter}. + */ + protected Semaphore getSemaphore() { + return semaphore; + } + + /** + * Used by the StripesFilter to notify the flash scope that the request for which it is used has + * been completed. The FlashScope uses this notification to start a timer, and also to null out + * it's reference to the request so that it can be garbage collected. + * + *

    The timer is used to determine if a flash scope has been orphaned (i.e. the subsequent + * request was not made) after a period of time, so that it can be removed from session. + */ + public void completeRequest() { + // Clean up any old-age flash scopes + Map scopes = getContainer(request, false); + if (scopes != null && !scopes.isEmpty()) { + Iterator iterator = scopes.values().iterator(); + while (iterator.hasNext()) { + if (iterator.next().isExpired()) { + iterator.remove(); } + } + } - // Replace the request and response objects for the request cycle that is ending - // with objects that are safe to use on the ensuing request. - HttpServletRequest flashRequest = FlashRequest.replaceRequest(request); - HttpServletResponse flashResponse = (HttpServletResponse) Proxy.newProxyInstance( + // Replace the request and response objects for the request cycle that is ending + // with objects that are safe to use on the ensuing request. + HttpServletRequest flashRequest = FlashRequest.replaceRequest(request); + HttpServletResponse flashResponse = + (HttpServletResponse) + Proxy.newProxyInstance( getClass().getClassLoader(), - new Class[] { HttpServletResponse.class }, + new Class[] {HttpServletResponse.class}, new FlashResponseInvocationHandler()); - for (Object o : this.values()) { - if (o instanceof ActionBean) { - ActionBeanContext context = ((ActionBean) o).getContext(); - if (context != null) { - context.setRequest(flashRequest); - context.setResponse(flashResponse); - } - } + for (Object o : this.values()) { + if (o instanceof ActionBean) { + ActionBeanContext context = ((ActionBean) o).getContext(); + if (context != null) { + context.setRequest(flashRequest); + context.setResponse(flashResponse); } - - // start timer, clear request - this.startTime = System.currentTimeMillis(); - this.request = null; - this.semaphore.release(); + } } - /** - *

    - * Called by {@link StripesFilter} to copy all the attributes from this flash scope to the given - * {@code request}. {@link #beginRequest(HttpServletRequest)} must never be called before - * {@link #completeRequest()} is called. Since the two methods are normally called by different - * threads, synchronization of the calls is accomplished through use of a {@link Semaphore}. - *

    - * - * @param request The request to copy the flash scope attributes to - */ - public void beginRequest(HttpServletRequest request) { - boolean acquired = false; - try { - // Acquire the permit from the semaphore with a 1 second timeout for safety - acquired = getSemaphore().tryAcquire(1, TimeUnit.SECONDS); - - // If no permit was acquired, then that's bad so log it as an error - if (!acquired) { - log.error("Something is amiss! A timeout occurred while trying to copy a flash " - + "scope to a new request. Only StripesFilter should call " - + "FlashScope.completeRequest() and FlashScope.beginRequest(), and the " - + "calls must be properly synchronized. The timeout likely means that " - + "completeRequest() was never called or did not complete successfully " - + "on this flash scope."); - } - - // Copy all the attributes from this scope to the request scope - for (Map.Entry entry : entrySet()) { - Object value = entry.getValue(); - if (value instanceof ActionBean) { - HttpServletRequest tmp = ((ActionBean) value).getContext().getRequest(); - if (tmp != null) { - tmp = StripesRequestWrapper.findStripesWrapper(tmp); - if (tmp != null) { - tmp = (HttpServletRequest) ((StripesRequestWrapper) tmp).getRequest(); - if (tmp instanceof FlashRequest) - ((FlashRequest) tmp).setDelegate(request); - } - } - } - request.setAttribute(entry.getKey(), value); + // start timer, clear request + this.startTime = System.currentTimeMillis(); + this.request = null; + this.semaphore.release(); + } + + /** + * Called by {@link StripesFilter} to copy all the attributes from this flash scope to the given + * {@code request}. {@link #beginRequest(HttpServletRequest)} must never be called before {@link + * #completeRequest()} is called. Since the two methods are normally called by different threads, + * synchronization of the calls is accomplished through use of a {@link Semaphore}. + * + * @param request The request to copy the flash scope attributes to + */ + public void beginRequest(HttpServletRequest request) { + boolean acquired = false; + try { + // Acquire the permit from the semaphore with a 1 second timeout for safety + acquired = getSemaphore().tryAcquire(1, TimeUnit.SECONDS); + + // If no permit was acquired, then that's bad so log it as an error + if (!acquired) { + log.error( + "Something is amiss! A timeout occurred while trying to copy a flash " + + "scope to a new request. Only StripesFilter should call " + + "FlashScope.completeRequest() and FlashScope.beginRequest(), and the " + + "calls must be properly synchronized. The timeout likely means that " + + "completeRequest() was never called or did not complete successfully " + + "on this flash scope."); + } + + // Copy all the attributes from this scope to the request scope + for (Map.Entry entry : entrySet()) { + Object value = entry.getValue(); + if (value instanceof ActionBean) { + HttpServletRequest tmp = ((ActionBean) value).getContext().getRequest(); + if (tmp != null) { + tmp = StripesRequestWrapper.findStripesWrapper(tmp); + if (tmp != null) { + tmp = (HttpServletRequest) ((StripesRequestWrapper) tmp).getRequest(); + if (tmp instanceof FlashRequest) ((FlashRequest) tmp).setDelegate(request); } + } } - catch (InterruptedException e) { - throw new StripesRuntimeException(e); - } - finally { - // Make sure the semaphore permit gets released - if (acquired) - getSemaphore().release(); - } + request.setAttribute(entry.getKey(), value); + } + } catch (InterruptedException e) { + throw new StripesRuntimeException(e); + } finally { + // Make sure the semaphore permit gets released + if (acquired) getSemaphore().release(); } - - /** - * Returns the time in seconds since the request that generated this flash scope - * completed. Will return 0 if this flash scope has not yet started to age. - */ - public long age() { - if (startTime == 0) { - return 0; - } - else { - return (System.currentTimeMillis() - this.startTime) / 1000; - } + } + + /** + * Returns the time in seconds since the request that generated this flash scope completed. Will + * return 0 if this flash scope has not yet started to age. + */ + public long age() { + if (startTime == 0) { + return 0; + } else { + return (System.currentTimeMillis() - this.startTime) / 1000; } - - /** - * Returns true if the flash scope has expired and should be dereferenced to allow - * garbage collection. Returns false if the flash scope should be retained. - * - * @return true if the flash scope has expired, false otherwise - */ - public boolean isExpired() { - return age() > this.timeout; + } + + /** + * Returns true if the flash scope has expired and should be dereferenced to allow garbage + * collection. Returns false if the flash scope should be retained. + * + * @return true if the flash scope has expired, false otherwise + */ + public boolean isExpired() { + return age() > this.timeout; + } + + /** + * Stores the provided value both in the flash scope a under the specified name, and in a + * request attribute with the specified name. Allows flash scope attributes to be accessed + * seamlessly as request attributes during both the current request and the subsequent request. + * + * @param name the name of the attribute to add to flash scope + * @param value the value to be added + * @return the previous object stored with the same name (possibly null) + */ + @Override + public Object put(String name, Object value) { + this.request.setAttribute(name, value); + return super.put(name, value); + } + + /** + * Stores an ActionBean into the flash scope. Additional checking is performed to see if the + * ActionBean is the currently resolved (main) ActionBean for the request. The result is that on + * the next request the ActionBean will appear in the request as if it was created on that + * request. + * + * @param bean an ActionBean that should be present in the next request + */ + public void put(ActionBean bean) { + String binding = + StripesFilter.getConfiguration().getActionResolver().getUrlBinding(bean.getClass()); + super.put(binding, bean); + + ActionBean main = (ActionBean) request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN); + if (main != null && main.equals(bean)) { + super.put(StripesConstants.REQ_ATTR_ACTION_BEAN, bean); } - - /** - * Stores the provided value both in the flash scope a under the specified name, and - * in a request attribute with the specified name. Allows flash scope attributes to be - * accessed seamlessly as request attributes during both the current request and the - * subsequent request. - * - * @param name the name of the attribute to add to flash scope - * @param value the value to be added - * @return the previous object stored with the same name (possibly null) - */ - @Override - public Object put(String name, Object value) { - this.request.setAttribute(name, value); - return super.put(name, value); + } + + /** + * Gets the collection of all flash scopes present in the current session. + * + * @param req the current request, needed to get access to the session + * @return a collection of flash scopes. Will return an empty collection if there are no flash + * scopes present. + */ + public static Collection getAllFlashScopes(HttpServletRequest req) { + Map scopes = getContainer(req, false); + + if (scopes == null) { + return Collections.emptySet(); + } else { + return scopes.values(); } - - /** - * Stores an ActionBean into the flash scope. Additional checking is performed to see - * if the ActionBean is the currently resolved (main) ActionBean for the request. The - * result is that on the next request the ActionBean will appear in the request as if - * it was created on that request. - * - * @param bean an ActionBean that should be present in the next request - */ - public void put(ActionBean bean) { - String binding = StripesFilter.getConfiguration() - .getActionResolver().getUrlBinding(bean.getClass()); - super.put(binding, bean); - - ActionBean main = (ActionBean) request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN); - if (main != null && main.equals(bean)) { - super.put(StripesConstants.REQ_ATTR_ACTION_BEAN, bean); - } + } + + /** + * Fetch the flash scope that was populated during the previous request, if one exists. This is + * only really intended for use by the StripesFilter and things which extend it, in order to grab + * a flash scope for a previous request and empty it's contents into request attributes. + * + *

    NOTE: calling this method has the side-affect of removing the flash scope from the set of + * managed flash scopes! + * + * @param req the current request + * @return a FlashScope if one exists with the key provided. + */ + public static FlashScope getPrevious(HttpServletRequest req) { + String keyString = req.getParameter(StripesConstants.URL_KEY_FLASH_SCOPE_ID); + + if (keyString == null) { + return null; + } else { + try { + Integer id = new Integer(keyString); + Map scopes = getContainer(req, false); + return scopes == null ? null : scopes.remove(id); + } catch (NumberFormatException e) { + return null; + } } - - /** - * Gets the collection of all flash scopes present in the current session. - * @param req the current request, needed to get access to the session - * @return a collection of flash scopes. Will return an empty collection if there are - * no flash scopes present. - */ - public static Collection getAllFlashScopes(HttpServletRequest req) { - Map scopes = getContainer(req, false); - - if (scopes == null) { - return Collections.emptySet(); - } - else { - return scopes.values(); + } + + /** + * Gets the current flash scope into which items can be stored temporarily. If create + * is true, then a new one will be created. + * + *

    It is assumed that the request object will be used by only one thread so access to the + * request is not synchronized. Access to the scopes map that is stored in the session and the + * static {@link Random} that is used to generate the keys for the map is synchronized. + * + * @param req the current request + * @param create if true then the FlashScope will be created when it does not exist already + * @return the current FlashScope, or null if it does not exist and create is false + */ + public static FlashScope getCurrent(HttpServletRequest req, boolean create) { + Map scopes = getContainer(req, create); + + if (scopes == null) { + return null; + } else { + FlashScope scope = null; + Integer key = (Integer) req.getAttribute(StripesConstants.REQ_ATTR_CURRENT_FLASH_SCOPE); + if (key != null) { + scope = scopes.get(key); + } else if (create) { + synchronized (random) { + do { + key = random.nextInt(); + } while (scopes.containsKey(key)); + scope = new FlashScope(req, key); + scopes.put(scope.key(), scope); } - } + req.setAttribute(StripesConstants.REQ_ATTR_CURRENT_FLASH_SCOPE, key); + } - /** - *

    Fetch the flash scope that was populated during the previous request, if one exists. - * This is only really intended for use by the StripesFilter and things which extend it, - * in order to grab a flash scope for a previous request and empty it's contents into request - * attributes.

    - * - *

    NOTE: calling this method has the side-affect of removing the flash scope from - * the set of managed flash scopes!

    - * - * @param req the current request - * @return a FlashScope if one exists with the key provided. - */ - public static FlashScope getPrevious(HttpServletRequest req) { - String keyString = req.getParameter(StripesConstants.URL_KEY_FLASH_SCOPE_ID); - - if (keyString == null) { - return null; - } - else { - try { - Integer id = new Integer(keyString); - Map scopes = getContainer(req, false); - return scopes == null ? null : scopes.remove(id); - } - catch (NumberFormatException e) { - return null; - } - } - } - - /** - *

    - * Gets the current flash scope into which items can be stored temporarily. If - * create is true, then a new one will be created. - *

    - *

    - * It is assumed that the request object will be used by only one thread so access to the - * request is not synchronized. Access to the scopes map that is stored in the session and the - * static {@link Random} that is used to generate the keys for the map is synchronized. - *

    - * - * @param req the current request - * @param create if true then the FlashScope will be created when it does not exist already - * @return the current FlashScope, or null if it does not exist and create is false - */ - public static FlashScope getCurrent(HttpServletRequest req, boolean create) { - Map scopes = getContainer(req, create); - - if (scopes == null) { - return null; - } - else { - FlashScope scope = null; - Integer key = (Integer) req.getAttribute(StripesConstants.REQ_ATTR_CURRENT_FLASH_SCOPE); - if (key != null) { - scope = scopes.get(key); - } - else if (create) { - synchronized (random) { - do { - key = random.nextInt(); - } while (scopes.containsKey(key)); - scope = new FlashScope(req, key); - scopes.put(scope.key(), scope); - } - req.setAttribute(StripesConstants.REQ_ATTR_CURRENT_FLASH_SCOPE, key); - } - - return scope; - } + return scope; } - - /** - * Internal helper method to retrieve (and selectively create) the container for all - * the flash scopes. Will return null if the container does not exist and create is - * false. Will also return null if the current session has been invalidated, regardless - * of the value of create. - * - * @param req the current request - * @param create if true, create the container when it doesn't exist. - * @return a Map of integer keys to FlashScope objects - */ - private static Map getContainer(HttpServletRequest req, boolean create) { - try { - HttpSession session = req.getSession(create); - Map scopes = null; - if (session != null) { - scopes = getContainer(session); - - if (scopes == null && create) { - synchronized (FlashScope.class) { - // after obtaining a lock, try looking it up again - scopes = getContainer(session); - - // if still not there, then create and save it - if (scopes == null) { - scopes = new ConcurrentHashMap(); - session.setAttribute(StripesConstants.REQ_ATTR_FLASH_SCOPE_LOCATION, scopes); - } - } - } + } + + /** + * Internal helper method to retrieve (and selectively create) the container for all the flash + * scopes. Will return null if the container does not exist and create is false. Will also + * return null if the current session has been invalidated, regardless of the value of + * create. + * + * @param req the current request + * @param create if true, create the container when it doesn't exist. + * @return a Map of integer keys to FlashScope objects + */ + private static Map getContainer(HttpServletRequest req, boolean create) { + try { + HttpSession session = req.getSession(create); + Map scopes = null; + if (session != null) { + scopes = getContainer(session); + + if (scopes == null && create) { + synchronized (FlashScope.class) { + // after obtaining a lock, try looking it up again + scopes = getContainer(session); + + // if still not there, then create and save it + if (scopes == null) { + scopes = new ConcurrentHashMap(); + session.setAttribute(StripesConstants.REQ_ATTR_FLASH_SCOPE_LOCATION, scopes); } - - return scopes; - } - catch (IllegalStateException ise) { - // If the session has been invalidated we'll get this exception, but there's no - // way to know this without try and getting the exception :( - log.warn("An IllegalStateException got thrown trying to create a flash scope. ", - "This happens when add something to flash scope for the first time ", - "causes creation of the HttpSession, but for some other reason the ", - "response is already committed!"); - return null; + } } + } + + return scopes; + } catch (IllegalStateException ise) { + // If the session has been invalidated we'll get this exception, but there's no + // way to know this without try and getting the exception :( + log.warn( + "An IllegalStateException got thrown trying to create a flash scope. ", + "This happens when add something to flash scope for the first time ", + "causes creation of the HttpSession, but for some other reason the ", + "response is already committed!"); + return null; } - - /** - * Internal helper method to retrieve the container for all the flash scopes. Will return null - * if the container does not exist. - * - * @param session - * @return a Map of integer keys to FlashScope objects - * @throws IllegalStateException if the session has been invalidated - */ - @SuppressWarnings("unchecked") - private static Map getContainer(HttpSession session) - throws IllegalStateException { - return (Map) session - .getAttribute(StripesConstants.REQ_ATTR_FLASH_SCOPE_LOCATION); - } + } + + /** + * Internal helper method to retrieve the container for all the flash scopes. Will return null if + * the container does not exist. + * + * @param session + * @return a Map of integer keys to FlashScope objects + * @throws IllegalStateException if the session has been invalidated + */ + @SuppressWarnings("unchecked") + private static Map getContainer(HttpSession session) + throws IllegalStateException { + return (Map) + session.getAttribute(StripesConstants.REQ_ATTR_FLASH_SCOPE_LOCATION); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/HttpCacheInterceptor.java b/stripes/src/main/java/net/sourceforge/stripes/controller/HttpCacheInterceptor.java index c18f2a6a0..7005a6d42 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/HttpCacheInterceptor.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/HttpCacheInterceptor.java @@ -14,12 +14,10 @@ */ package net.sourceforge.stripes.controller; +import jakarta.servlet.http.HttpServletResponse; import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; - -import javax.servlet.http.HttpServletResponse; - import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.HttpCache; import net.sourceforge.stripes.action.Resolution; @@ -29,126 +27,145 @@ * Looks for an {@link HttpCache} annotation on the event handler method, the {@link ActionBean} * class or the {@link ActionBean}'s superclasses. If an {@link HttpCache} is found, then the * appropriate response headers are set to control client-side caching. - * + * * @author Ben Gunter * @since Stripes 1.5 */ @Intercepts(LifecycleStage.ResolutionExecution) public class HttpCacheInterceptor implements Interceptor { - private static final Log logger = Log.getInstance(HttpCacheInterceptor.class); - - @HttpCache - private static final class CacheKey { - final Method method; - final Class beanClass; - final int hashCode; - - /** Create a cache key for the given event handler method and {@link ActionBean} class. */ - public CacheKey(Method method, Class beanClass) { - this.method = method; - this.beanClass = beanClass; - this.hashCode = method.hashCode() * 37 + beanClass.hashCode(); - } - - @Override - public boolean equals(Object obj) { - final CacheKey that = (CacheKey) obj; - return this.method.equals(that.method) && this.beanClass.equals(that.beanClass); - } - - @Override - public int hashCode() { - return hashCode; - } - - @Override - public String toString() { - return beanClass.getName() + "." + method.getName() + "()"; - } + private static final Log logger = Log.getInstance(HttpCacheInterceptor.class); + + @HttpCache + private static final class CacheKey { + final Method method; + final Class beanClass; + final int hashCode; + + /** Create a cache key for the given event handler method and {@link ActionBean} class. */ + public CacheKey(Method method, Class beanClass) { + this.method = method; + this.beanClass = beanClass; + this.hashCode = method.hashCode() * 37 + beanClass.hashCode(); } - private Map cache = new ConcurrentHashMap(128); - - /** Null values are not allowed by {@link ConcurrentHashMap} so use this reference instead. */ - private static final HttpCache NULL_CACHE = CacheKey.class.getAnnotation(HttpCache.class); - - public Resolution intercept(ExecutionContext ctx) throws Exception { - final ActionBean actionBean = ctx.getActionBean(); - final Method handler = ctx.getHandler(); - if (actionBean != null && handler != null) { - final Class beanClass = actionBean.getClass(); - // if caching is disabled, then set the appropriate response headers - logger.debug("Looking for ", HttpCache.class.getSimpleName(), " on ", - beanClass.getName(), ".", handler.getName(), "()"); - final HttpCache annotation = getAnnotation(handler, beanClass); - if (annotation != null) { - final HttpServletResponse response = ctx.getActionBeanContext().getResponse(); - if (annotation.allow()) { - long expires = annotation.expires(); - if (expires != HttpCache.DEFAULT_EXPIRES) { - logger.debug("Response expires in ", expires, " seconds"); - expires = expires * 1000 + System.currentTimeMillis(); - response.setDateHeader("Expires", expires); - } - } - else { - logger.debug("Disabling client-side caching for response"); - response.setDateHeader("Expires", 0); - response.setHeader("Cache-control", "no-store, no-cache, must-revalidate"); - response.setHeader("Pragma", "no-cache"); - } - } - } + @Override + public boolean equals(Object obj) { + final CacheKey that = (CacheKey) obj; + return this.method.equals(that.method) && this.beanClass.equals(that.beanClass); + } - return ctx.proceed(); + @Override + public int hashCode() { + return hashCode; } - /** - * Look for a {@link HttpCache} annotation on the method first and then on the class and its - * superclasses. - * - * @param method an event handler method - * @param beanClass the class to inspect for annotations if none is found on the method - * @return The first {@link HttpCache} annotation found. If none is found then null. - */ - protected HttpCache getAnnotation(Method method, Class beanClass) { - // check cache first - final CacheKey cacheKey = new CacheKey(method, beanClass); - HttpCache annotation = cache.get(cacheKey); - if (annotation != null) { - return annotation == NULL_CACHE ? null : annotation; + @Override + public String toString() { + return beanClass.getName() + "." + method.getName() + "()"; + } + } + + private Map cache = new ConcurrentHashMap(128); + + /** Null values are not allowed by {@link ConcurrentHashMap} so use this reference instead. */ + private static final HttpCache NULL_CACHE = CacheKey.class.getAnnotation(HttpCache.class); + + public Resolution intercept(ExecutionContext ctx) throws Exception { + final ActionBean actionBean = ctx.getActionBean(); + final Method handler = ctx.getHandler(); + if (actionBean != null && handler != null) { + final Class beanClass = actionBean.getClass(); + // if caching is disabled, then set the appropriate response headers + logger.debug( + "Looking for ", + HttpCache.class.getSimpleName(), + " on ", + beanClass.getName(), + ".", + handler.getName(), + "()"); + final HttpCache annotation = getAnnotation(handler, beanClass); + if (annotation != null) { + final HttpServletResponse response = ctx.getActionBeanContext().getResponse(); + if (annotation.allow()) { + long expires = annotation.expires(); + if (expires != HttpCache.DEFAULT_EXPIRES) { + logger.debug("Response expires in ", expires, " seconds"); + expires = expires * 1000 + System.currentTimeMillis(); + response.setDateHeader("Expires", expires); + } + } else { + logger.debug("Disabling client-side caching for response"); + response.setDateHeader("Expires", 0); + response.setHeader("Cache-control", "no-store, no-cache, must-revalidate"); + response.setHeader("Pragma", "no-cache"); } + } + } - // not found in cache so figure it out - annotation = method.getAnnotation(HttpCache.class); - if (annotation == null) { - // search the method's class and its superclasses - Class clazz = beanClass; - do { - annotation = clazz.getAnnotation(HttpCache.class); - clazz = clazz.getSuperclass(); - } while (clazz != null && annotation == null); - } + return ctx.proceed(); + } + + /** + * Look for a {@link HttpCache} annotation on the method first and then on the class and its + * superclasses. + * + * @param method an event handler method + * @param beanClass the class to inspect for annotations if none is found on the method + * @return The first {@link HttpCache} annotation found. If none is found then null. + */ + protected HttpCache getAnnotation(Method method, Class beanClass) { + // check cache first + final CacheKey cacheKey = new CacheKey(method, beanClass); + HttpCache annotation = cache.get(cacheKey); + if (annotation != null) { + return annotation == NULL_CACHE ? null : annotation; + } - // check for weirdness - if (annotation != null) { - logger.debug("Found ", HttpCache.class.getSimpleName(), " for ", beanClass.getName(), - ".", method.getName(), "()"); - final int expires = annotation.expires(); - if (annotation.allow() && expires != HttpCache.DEFAULT_EXPIRES && expires < 0) { - logger.warn(HttpCache.class.getSimpleName(), " for ", beanClass.getName(), ".", - method.getName(), "() allows caching but expires in the past"); - } - else if (!annotation.allow() && expires != HttpCache.DEFAULT_EXPIRES) { - logger.warn(HttpCache.class.getSimpleName(), " for ", beanClass.getName(), ".", - method.getName(), "() disables caching but explicitly sets expires"); - } - } - else { - annotation = NULL_CACHE; - } + // not found in cache so figure it out + annotation = method.getAnnotation(HttpCache.class); + if (annotation == null) { + // search the method's class and its superclasses + Class clazz = beanClass; + do { + annotation = clazz.getAnnotation(HttpCache.class); + clazz = clazz.getSuperclass(); + } while (clazz != null && annotation == null); + } - cache.put(cacheKey, annotation); - return annotation; + // check for weirdness + if (annotation != null) { + logger.debug( + "Found ", + HttpCache.class.getSimpleName(), + " for ", + beanClass.getName(), + ".", + method.getName(), + "()"); + final int expires = annotation.expires(); + if (annotation.allow() && expires != HttpCache.DEFAULT_EXPIRES && expires < 0) { + logger.warn( + HttpCache.class.getSimpleName(), + " for ", + beanClass.getName(), + ".", + method.getName(), + "() allows caching but expires in the past"); + } else if (!annotation.allow() && expires != HttpCache.DEFAULT_EXPIRES) { + logger.warn( + HttpCache.class.getSimpleName(), + " for ", + beanClass.getName(), + ".", + method.getName(), + "() disables caching but explicitly sets expires"); + } + } else { + annotation = NULL_CACHE; } + + cache.put(cacheKey, annotation); + return annotation; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/Interceptor.java b/stripes/src/main/java/net/sourceforge/stripes/controller/Interceptor.java index 1a3284ee0..fe0fc4074 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/Interceptor.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/Interceptor.java @@ -17,72 +17,70 @@ import net.sourceforge.stripes.action.Resolution; /** - *

    Interface for classes which wish to intercept the processing of a request at various - * stages in the Stripes lifecycle. To denote the {@link LifecycleStage} (or stages) at - * which an interceptor should run, the class should be marked with an {@link Intercepts} - * annotation declaring one or more lifecycle stages.

    + * Interface for classes which wish to intercept the processing of a request at various stages in + * the Stripes lifecycle. To denote the {@link LifecycleStage} (or stages) at which an interceptor + * should run, the class should be marked with an {@link Intercepts} annotation declaring one or + * more lifecycle stages. * *

    {@code Interceptors} execute around the intercepted lifecycle stage. Assuming for - * simplicity's sake that a single interceptor is configured, any code in the - * {@link #intercept(ExecutionContext)} method prior to calling {@code context.proceed()} will - * be executed immediately prior to the lifecycle code. Any code after calling - * {@code context.proceed()} will be executed immediately after the lifecycle code. For example - * the following implementation would print out a message before and after validation and - * binding occur:

    + * simplicity's sake that a single interceptor is configured, any code in the {@link + * #intercept(ExecutionContext)} method prior to calling {@code context.proceed()} will be executed + * immediately prior to the lifecycle code. Any code after calling {@code context.proceed()} will be + * executed immediately after the lifecycle code. For example the following implementation would + * print out a message before and after validation and binding occur: * - *
    - *{@literal @}Intercepts(LifecycleStage.BindingAndValidation)
    - *public class NoisyInterceptor implements Interceptor {
    + * 
    + * {@literal @}Intercepts(LifecycleStage.BindingAndValidation)
    + * public class NoisyInterceptor implements Interceptor {
      *    public Resolution intercept(ExecutionContext context) {
      *        System.out.println("Before validation and binding!");
      *        Resolution r = context.proceed();
      *        System.out.println("After validation and binding!");
      *        return r;
      *    }
    - *}
    - *
    + * } + *
    * - *

    Interceptors can, in addition to adding behaviour, divert the flow of execution. They do - * this by returning a {@link Resolution}. If an interceptor returns a Resolution Stripes will - * abort processing of the current request and immediately execute the Resolution.

    + *

    Interceptors can, in addition to adding behaviour, divert the flow of execution. They do this + * by returning a {@link Resolution}. If an interceptor returns a Resolution Stripes will abort + * processing of the current request and immediately execute the Resolution. * *

    Interceptor developers must be careful to ensure that interceptors are well behaved. To * continue normal processing interceptors must invoke {@code context.proceed()}. Since a - * given interceptor may be part of a stack of interceptors, or the lifecycle code may return - * a resolution, the interceptor must return the Resolution produced by {@code context.proceed()} - * unless it explicitly wishes to alter the flow of execution.

    + * given interceptor may be part of a stack of interceptors, or the lifecycle code may return a + * resolution, the interceptor must return the Resolution produced by {@code context.proceed()} + * unless it explicitly wishes to alter the flow of execution. * - *

    Interceptors gain access to information about the current execution environment through - * the {@link ExecutionContext}. Through this they can access the ActionBean, the handler - * Method, the lifecycle stage etc. Care must be taken to ensure that information is available - * before using it. For example interceptors which execute around ActionBeanResolution will not - * have access to the current ActionBean until after calling context.proceed() and will not have - * access to the event name or handler method at all (HandlerResolution occurs after - * ActionBeanResolution).

    + *

    Interceptors gain access to information about the current execution environment through the + * {@link ExecutionContext}. Through this they can access the ActionBean, the handler Method, the + * lifecycle stage etc. Care must be taken to ensure that information is available before using it. + * For example interceptors which execute around ActionBeanResolution will not have access to the + * current ActionBean until after calling context.proceed() and will not have access to the event + * name or handler method at all (HandlerResolution occurs after ActionBeanResolution). * - *

    Optionally, Interceptor classes may implement the - * {@link net.sourceforge.stripes.config.ConfigurableComponent} interface. If implemented, - * the Interceptor will have it's {@code init(Configuration)} method called after instantiation - * and before being placed into service.

    + *

    Optionally, Interceptor classes may implement the {@link + * net.sourceforge.stripes.config.ConfigurableComponent} interface. If implemented, the Interceptor + * will have it's {@code init(Configuration)} method called after instantiation and before being + * placed into service. * - *

    Interceptors are located by Stripes through it's - * {@link net.sourceforge.stripes.config.Configuration}. To configure interceptors you can either - * implement your own Configuration (probably by subclassing - * {@link net.sourceforge.stripes.config.RuntimeConfiguration}), or more likely by listing out - * the interceptors desired in the web.xml as specified in the documentation for - * {@link net.sourceforge.stripes.config.RuntimeConfiguration#initInterceptors()}.

    + *

    Interceptors are located by Stripes through it's {@link + * net.sourceforge.stripes.config.Configuration}. To configure interceptors you can either implement + * your own Configuration (probably by subclassing {@link + * net.sourceforge.stripes.config.RuntimeConfiguration}), or more likely by listing out the + * interceptors desired in the web.xml as specified in the documentation for {@link + * net.sourceforge.stripes.config.RuntimeConfiguration#initInterceptors()}. * * @author Tim Fennell * @since Stripes 1.3 */ public interface Interceptor { - /** - * Invoked when intercepting the flow of execution. - * - * @param context the ExecutionContext of the request currently being processed - * @return the result of calling context.proceed(), or if the interceptor wishes to change - * the flow of execution, a Resolution - * @throws Exception if any non-recoverable errors occur - */ - Resolution intercept(ExecutionContext context) throws Exception; + /** + * Invoked when intercepting the flow of execution. + * + * @param context the ExecutionContext of the request currently being processed + * @return the result of calling context.proceed(), or if the interceptor wishes to change the + * flow of execution, a Resolution + * @throws Exception if any non-recoverable errors occur + */ + Resolution intercept(ExecutionContext context) throws Exception; } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/Intercepts.java b/stripes/src/main/java/net/sourceforge/stripes/controller/Intercepts.java index 6b043d0d2..e5af74df3 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/Intercepts.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/Intercepts.java @@ -14,16 +14,16 @@ */ package net.sourceforge.stripes.controller; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.lang.annotation.ElementType; -import java.lang.annotation.Documented; /** - * Annotation that declares the lifecycle stages that an interceptor should intercept. In - * most cases this will probably be a single stage, but an array of stages can be specified. - * Only valid for annotating classes that implement {@link Interceptor}. + * Annotation that declares the lifecycle stages that an interceptor should intercept. In most cases + * this will probably be a single stage, but an array of stages can be specified. Only valid for + * annotating classes that implement {@link Interceptor}. * * @author Tim Fennell * @since Stripes 1.3 @@ -32,8 +32,6 @@ @Target({ElementType.TYPE}) @Documented public @interface Intercepts { - /** - * One or more lifecycle stages at which interception should occur. - */ - LifecycleStage[] value(); + /** One or more lifecycle stages at which interception should occur. */ + LifecycleStage[] value(); } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/LifecycleStage.java b/stripes/src/main/java/net/sourceforge/stripes/controller/LifecycleStage.java index 15b9791cc..d3c0a4ee6 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/LifecycleStage.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/LifecycleStage.java @@ -15,64 +15,62 @@ package net.sourceforge.stripes.controller; /** - *

    Describes the major stages that form the Stripes request processing lifecycle. These stages - * are enumerated here primarily because they are the points around which execution can be - * intercepted using the Stripes {@link Interceptor} system.

    + * Describes the major stages that form the Stripes request processing lifecycle. These stages are + * enumerated here primarily because they are the points around which execution can be intercepted + * using the Stripes {@link Interceptor} system. * * @author Tim Fennell * @since Stripes 1.3 */ public enum LifecycleStage { - /** - * Executed before any processing occurs on the request. No Stripes processing is - * associated with this stage. It is simply provided as a hook for interceptors. - */ - RequestInit, + /** + * Executed before any processing occurs on the request. No Stripes processing is associated with + * this stage. It is simply provided as a hook for interceptors. + */ + RequestInit, - /** - * First major lifecycle stage. Involves the location of the ActionBean class that - * is bound to the URL being requested, and usually also the creation of a new instance - * of that class. - */ - ActionBeanResolution, + /** + * First major lifecycle stage. Involves the location of the ActionBean class that is bound to the + * URL being requested, and usually also the creation of a new instance of that class. + */ + ActionBeanResolution, - /** - * Second major lifecycle stage. Involves the determination of the event name in the - * request (if there is one), and the location of the Method which handles the even. - */ - HandlerResolution, + /** + * Second major lifecycle stage. Involves the determination of the event name in the request (if + * there is one), and the location of the Method which handles the even. + */ + HandlerResolution, - /** - * Third major lifecycle stage. Involves the processing of all validations specified through - * {@literal} @Validate annotations as well as the type conversion of request parameters - * and their binding to the ActionBean. - */ - BindingAndValidation, + /** + * Third major lifecycle stage. Involves the processing of all validations specified through + * {@literal} @Validate annotations as well as the type conversion of request parameters and their + * binding to the ActionBean. + */ + BindingAndValidation, - /** - * Fourth major lifecycle stage. Involves the execution of any custom validation logic - * exposed by the ActionBean. - */ - CustomValidation, + /** + * Fourth major lifecycle stage. Involves the execution of any custom validation logic exposed by + * the ActionBean. + */ + CustomValidation, - /** - * Fifth major lifecycle stage. The actual execution of the event handler method. Only - * occurs when the prior stages have produced no persistent validation errors. - */ - EventHandling, + /** + * Fifth major lifecycle stage. The actual execution of the event handler method. Only occurs when + * the prior stages have produced no persistent validation errors. + */ + EventHandling, - /** - * Sixth major lifecycle stage. Is executed any time a Resolution is executed, either - * as the outcome of an event handler, or because some other mechanism short circuits - * processing by returning a Resolution. - */ - ResolutionExecution, - - /** - * Final lifecycle stage. Executes in the finally block of the request so it will - * always be called when a request terminates regardless of any other conditions. - * This is only useful for cleaning up because Resolution execution has already - * occurred. - */ - RequestComplete + /** + * Sixth major lifecycle stage. Is executed any time a Resolution is executed, either as the + * outcome of an event handler, or because some other mechanism short circuits processing by + * returning a Resolution. + */ + ResolutionExecution, + + /** + * Final lifecycle stage. Executes in the finally block of the request so it will always be called + * when a request terminates regardless of any other conditions. This is only useful for cleaning + * up because Resolution execution has already occurred. + */ + RequestComplete } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/NameBasedActionResolver.java b/stripes/src/main/java/net/sourceforge/stripes/controller/NameBasedActionResolver.java index 6d5f6a69f..01567203e 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/NameBasedActionResolver.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/NameBasedActionResolver.java @@ -14,16 +14,7 @@ */ package net.sourceforge.stripes.controller; -import net.sourceforge.stripes.action.ActionBean; -import net.sourceforge.stripes.action.ActionBeanContext; -import net.sourceforge.stripes.action.Resolution; -import net.sourceforge.stripes.action.ForwardResolution; -import net.sourceforge.stripes.config.Configuration; -import net.sourceforge.stripes.exception.StripesServletException; -import net.sourceforge.stripes.util.Literal; -import net.sourceforge.stripes.util.Log; - -import javax.servlet.ServletContext; +import jakarta.servlet.ServletContext; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.MalformedURLException; @@ -32,399 +23,408 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import net.sourceforge.stripes.action.ActionBean; +import net.sourceforge.stripes.action.ActionBeanContext; +import net.sourceforge.stripes.action.ForwardResolution; +import net.sourceforge.stripes.action.Resolution; +import net.sourceforge.stripes.config.Configuration; +import net.sourceforge.stripes.exception.StripesServletException; +import net.sourceforge.stripes.util.Literal; +import net.sourceforge.stripes.util.Log; /** - *

    An ActionResolver that uses the names of classes and methods to generate sensible default - * URL bindings and event names respectively. Extends the default - * {@link AnnotatedClassActionResolver}, and is fully backward compatible. Any classes and - * methods that are annotated with {@link net.sourceforge.stripes.action.UrlBinding} and - * {@link net.sourceforge.stripes.action.HandlesEvent} will retain the bindings specified in - * those annotations. In the case when an annotation is absent then a default binding is - * generated.

    + * An ActionResolver that uses the names of classes and methods to generate sensible default URL + * bindings and event names respectively. Extends the default {@link AnnotatedClassActionResolver}, + * and is fully backward compatible. Any classes and methods that are annotated with {@link + * net.sourceforge.stripes.action.UrlBinding} and {@link + * net.sourceforge.stripes.action.HandlesEvent} will retain the bindings specified in those + * annotations. In the case when an annotation is absent then a default binding is generated. * - *

    The generation of ActionBean URL bindings is done by taking the class name and removing - * any extraneous packages at the front of the name, removing the strings "Action" and "Bean" - * from the end of the name, substituting slashes for periods and appending a suffix (.action by - * default). The set of packages that are trimmed is specified by the - * {@code getBasePackages()} method. By default this method returns the set - * [web, www, stripes, action]. These packages (and their parents) are removed from the - * class name. E.g. {@code com.myco.web.foo.BarActionBean} would become {@code foo.BarActionBean}. - * Continuing on, the list of Action Bean suffixes, specified by the {@code getActionBeanSuffixes()} - * method, are trimmed from the end of the Action Bean class name. With the defaults, - * [Bean, Action], we would trim {@code foo.BarActionBean} further to {@code foo.Bar}, and then - * translate it to {@code /foo/Bar}. Lastly the suffix returned by {@code getBindingSuffix()} - * is appended, giving the binding {@code /foo/Bar.action}.

    + *

    The generation of ActionBean URL bindings is done by taking the class name and removing any + * extraneous packages at the front of the name, removing the strings "Action" and "Bean" from the + * end of the name, substituting slashes for periods and appending a suffix (.action by default). + * The set of packages that are trimmed is specified by the {@code getBasePackages()} method. By + * default this method returns the set [web, www, stripes, action]. These packages (and their + * parents) are removed from the class name. E.g. {@code com.myco.web.foo.BarActionBean} would + * become {@code foo.BarActionBean}. Continuing on, the list of Action Bean suffixes, specified by + * the {@code getActionBeanSuffixes()} method, are trimmed from the end of the Action Bean class + * name. With the defaults, [Bean, Action], we would trim {@code foo.BarActionBean} further to + * {@code foo.Bar}, and then translate it to {@code /foo/Bar}. Lastly the suffix returned by {@code + * getBindingSuffix()} is appended, giving the binding {@code /foo/Bar.action}. * *

    The translation of class names into URL bindings is designed to be easy to override and - * customize. To that end you can easily change how this translation is done by overriding - * {@code getBasePackages()} and/or {@code getBindingSuffix()}, or completely customize the - * behaviour by overriding {@code getUrlBinding(String)}.

    + * customize. To that end you can easily change how this translation is done by overriding {@code + * getBasePackages()} and/or {@code getBindingSuffix()}, or completely customize the behaviour by + * overriding {@code getUrlBinding(String)}. * - *

    Mapping of method names to event names is simpler. Again the parent class is delegated to - * in case the method is annotated. If it is not, and the method is a concrete public method that + *

    Mapping of method names to event names is simpler. Again the parent class is delegated to in + * case the method is annotated. If it is not, and the method is a concrete public method that * returns a Resolution (or subclass thereof) it is mapped to an event of the same name as the - * method. So an un-annotated method "{@code public Resolution view()}" is mapped to an event - * called "view". It should be noted that there is no special method name that signifies the - * default handler. If there is more than one handler and you require a default handler you - * must still mark the default method with {@code @DefaultHandler}.

    + * method. So an un-annotated method "{@code public Resolution view()}" is mapped to an event called + * "view". It should be noted that there is no special method name that signifies the default + * handler. If there is more than one handler and you require a default handler you must still mark + * the default method with {@code @DefaultHandler}. * - *

    Another useful feature of the NameBasedActionResolver is that when a request arrives for a - * URL that is not bound to an ActionBean the resolver will attempt to map the request to a view - * and return a 'dummy' ActionBean that will take the user to the view. The exact behaviour is - * modifiable by overriding one or more of - * {@link #handleActionBeanNotFound(ActionBeanContext, String)}, {@link #findView(String)} or - * {@link #getFindViewAttempts(String)}. The default behaviour is to map the URL being requested - * to three potential JSP names/paths, check for the existence of a JSP at those locations and if - * one exists then to return an ActionBean that will render the view. For example if a user - * requested '/account/ViewAccount.action' but an ActionBean does not yet exist bound to that URL, - * the resolver will check for JSPs in the following order:

    + *

    Another useful feature of the NameBasedActionResolver is that when a request arrives for a URL + * that is not bound to an ActionBean the resolver will attempt to map the request to a view and + * return a 'dummy' ActionBean that will take the user to the view. The exact behaviour is + * modifiable by overriding one or more of {@link #handleActionBeanNotFound(ActionBeanContext, + * String)}, {@link #findView(String)} or {@link #getFindViewAttempts(String)}. The default + * behaviour is to map the URL being requested to three potential JSP names/paths, check for the + * existence of a JSP at those locations and if one exists then to return an ActionBean that will + * render the view. For example if a user requested '/account/ViewAccount.action' but an ActionBean + * does not yet exist bound to that URL, the resolver will check for JSPs in the following order: * *

      - *
    • /account/ViewAccount.jsp
    • - *
    • /account/viewAccount.jsp
    • - *
    • /account/view_account.jsp
    • + *
    • /account/ViewAccount.jsp + *
    • /account/viewAccount.jsp + *
    • /account/view_account.jsp *
    * - *

    The value of this approach comes from the fact that by default all pages can appear to have - * a pre-action whether they actually have one or not. In the above can you might chose to link - * to {@code /account/ViewAccount.action} even though you know that no action exists and you want - * to navigate directly to a page. This way, if you later decide you do need a pre-action for any - * reason you can simply code the ActionBean and be done. No URLs or links need to be modified - * and all requests to {@code /account/ViewAccount.action} will flow through the ActionBean.

    + *

    The value of this approach comes from the fact that by default all pages can appear to have a + * pre-action whether they actually have one or not. In the above can you might chose to link to + * {@code /account/ViewAccount.action} even though you know that no action exists and you want to + * navigate directly to a page. This way, if you later decide you do need a pre-action for any + * reason you can simply code the ActionBean and be done. No URLs or links need to be modified and + * all requests to {@code /account/ViewAccount.action} will flow through the ActionBean. * * @author Tim Fennell * @since Stripes 1.2 */ public class NameBasedActionResolver extends AnnotatedClassActionResolver { - /** - * Default set of packages (web, www, stripes, action) to be removed from the front - * of class names when translating them to URL bindings. - */ - public static final Set BASE_PACKAGES = - Collections.unmodifiableSet(Literal.set("web", "www", "stripes", "action")); - - /** Default suffix (.action) to add to URL bindings.*/ - public static final String DEFAULT_BINDING_SUFFIX = ".action"; - - /** - * Default list of suffixes (Bean, Action) to remove to the end of the Action Bean class name. - */ - public static final List DEFAULT_ACTION_BEAN_SUFFIXES = - Collections.unmodifiableList(Literal.list("Bean", "Action")); - - /** Log instance used to log information from this class. */ - private static final Log log = Log.getInstance(NameBasedActionResolver.class); - - /** - * First invokes the parent classes init() method and then quietly adds a specialized - * ActionBean to the set of ActionBeans the resolver is managing. The "specialized" bean - * is one that is used when a bean is not bound to a URL, to then forward the user to - * an appropriate view if one exists. - */ - @Override - public void init(Configuration configuration) throws Exception { - super.init(configuration); - addActionBean(DefaultViewActionBean.class); + /** + * Default set of packages (web, www, stripes, action) to be removed from the front of class names + * when translating them to URL bindings. + */ + public static final Set BASE_PACKAGES = + Collections.unmodifiableSet(Literal.set("web", "www", "stripes", "action")); + + /** Default suffix (.action) to add to URL bindings. */ + public static final String DEFAULT_BINDING_SUFFIX = ".action"; + + /** Default list of suffixes (Bean, Action) to remove to the end of the Action Bean class name. */ + public static final List DEFAULT_ACTION_BEAN_SUFFIXES = + Collections.unmodifiableList(Literal.list("Bean", "Action")); + + /** Log instance used to log information from this class. */ + private static final Log log = Log.getInstance(NameBasedActionResolver.class); + + /** + * First invokes the parent classes init() method and then quietly adds a specialized ActionBean + * to the set of ActionBeans the resolver is managing. The "specialized" bean is one that is used + * when a bean is not bound to a URL, to then forward the user to an appropriate view if one + * exists. + */ + @Override + public void init(Configuration configuration) throws Exception { + super.init(configuration); + addActionBean(DefaultViewActionBean.class); + } + + /** + * Finds or generates the URL binding for the class supplied. First delegates to the parent class + * to see if an annotated url binding is present. If not, the class name is taken and translated + * into a URL binding using {@code getUrlBinding(String name)}. + * + * @param clazz a Class representing an ActionBean + * @return the String URL binding for the ActionBean + */ + @Override + public String getUrlBinding(Class clazz) { + String binding = super.getUrlBinding(clazz); + + // If there's no annotated binding, and the class is concrete + if (binding == null && !Modifier.isAbstract(clazz.getModifiers())) { + binding = getUrlBinding(clazz.getName()); } - /** - *

    Finds or generates the URL binding for the class supplied. First delegates to the parent - * class to see if an annotated url binding is present. If not, the class name is taken - * and translated into a URL binding using {@code getUrlBinding(String name)}.

    - * - * @param clazz a Class representing an ActionBean - * @return the String URL binding for the ActionBean - */ - @Override - public String getUrlBinding(Class clazz) { - String binding = super.getUrlBinding(clazz); - - // If there's no annotated binding, and the class is concrete - if (binding == null && !Modifier.isAbstract(clazz.getModifiers())) { - binding = getUrlBinding(clazz.getName()); - } - - return binding; + return binding; + } + + /** + * Takes a class name and translates it into a URL binding by removing extraneous package names, + * removing Action, Bean, or ActionBean from the end of the class name if present, replacing + * periods with slashes, and appending a standard suffix as supplied by {@link + * net.sourceforge.stripes.controller.NameBasedActionResolver#getBindingSuffix()}. + * + *

    For example the class {@code com.myco.web.action.user.RegisterActionBean} would be + * translated to {@code /user/Register.action}. The behaviour of this method can be overridden + * either directly or by overriding the methods {@code getBindingSuffix()} and {@code + * getBasePackages()} which are used by this method. + * + * @param name the name of the class to create a binding for + * @return a String URL binding for the class + */ + protected String getUrlBinding(String name) { + // Chop off the packages up until (and including) any base package + for (String base : getBasePackages()) { + int i = name.indexOf("." + base + "."); + if (i != -1) { + name = name.substring(i + base.length() + 1); + } else if (name.startsWith(base + ".")) { + name = name.substring(base.length()); + } } - /** - * Takes a class name and translates it into a URL binding by removing extraneous package names, - * removing Action, Bean, or ActionBean from the end of the class name if present, replacing - * periods with slashes, and appending a standard suffix as supplied by - * {@link net.sourceforge.stripes.controller.NameBasedActionResolver#getBindingSuffix()}.

    - * - *

    For example the class {@code com.myco.web.action.user.RegisterActionBean} would be - * translated to {@code /user/Register.action}. The behaviour of this method can be - * overridden either directly or by overriding the methods {@code getBindingSuffix()} and - * {@code getBasePackages()} which are used by this method.

    - * - * @param name the name of the class to create a binding for - * @return a String URL binding for the class - * - */ - protected String getUrlBinding(String name) { - // Chop off the packages up until (and including) any base package - for (String base : getBasePackages()) { - int i = name.indexOf("." + base + "."); - if (i != -1) { - name = name.substring(i + base.length() + 1); - } - else if (name.startsWith(base + ".")) { - name = name.substring(base.length()); - } - } - - // If it ends in any of the Action Bean suffixes, remove them - for (String suffix : getActionBeanSuffixes()) { - if (name.endsWith(suffix)) { - name = name.substring(0, name.length() - suffix.length()); - } - } - - // Replace periods with slashes and make sure it starts with one - name = name.replace('.', '/'); - if (!name.startsWith("/")) { - name = "/" + name; - } - - // Lastly add the suffix - name += getBindingSuffix(); - return name; + // If it ends in any of the Action Bean suffixes, remove them + for (String suffix : getActionBeanSuffixes()) { + if (name.endsWith(suffix)) { + name = name.substring(0, name.length() - suffix.length()); + } } - /** - * Returns a set of package names (fully qualified or not) that should be removed - * from the start of a classname before translating the name into a URL Binding. By default - * returns "web", "www", "stripes" and "action". - * - * @return a non-null set of String package names. - */ - protected Set getBasePackages() { - return BASE_PACKAGES; + // Replace periods with slashes and make sure it starts with one + name = name.replace('.', '/'); + if (!name.startsWith("/")) { + name = "/" + name; } - /** - * Returns a non-null String suffix to be used when constructing URL bindings. The - * default is ".action". - */ - protected String getBindingSuffix() { - return DEFAULT_BINDING_SUFFIX; + // Lastly add the suffix + name += getBindingSuffix(); + return name; + } + + /** + * Returns a set of package names (fully qualified or not) that should be removed from the start + * of a classname before translating the name into a URL Binding. By default returns "web", "www", + * "stripes" and "action". + * + * @return a non-null set of String package names. + */ + protected Set getBasePackages() { + return BASE_PACKAGES; + } + + /** + * Returns a non-null String suffix to be used when constructing URL bindings. The default is + * ".action". + */ + protected String getBindingSuffix() { + return DEFAULT_BINDING_SUFFIX; + } + + /** + * Returns a list of suffixes to be removed from the end of the Action Bean class name, if + * present. The defaults are ["Bean", "Action"]. + * + * @since Stripes 1.5 + */ + protected List getActionBeanSuffixes() { + return DEFAULT_ACTION_BEAN_SUFFIXES; + } + + /** + * First checks with the super class to see if an annotated event name is present, and if not then + * returns the name of the handler method itself. Will return null for methods that do not return + * a resolution or are non-public or abstract. + * + * @param handler a method which may or may not be a handler method + * @return the name of the event handled, or null + */ + @Override + public String getHandledEvent(Method handler) { + String name = super.getHandledEvent(handler); + + // If the method isn't annotated, but does return a resolution and is + // not abstract (we already know it's public) then use the method name + if (name == null + && !Modifier.isAbstract(handler.getModifiers()) + && Resolution.class.isAssignableFrom(handler.getReturnType()) + && handler.getParameterTypes().length == 0) { + + name = handler.getName(); } - /** - * Returns a list of suffixes to be removed from the end of the Action Bean class name, if present. - * The defaults are ["Bean", "Action"]. - * - * @since Stripes 1.5 - */ - protected List getActionBeanSuffixes() { - return DEFAULT_ACTION_BEAN_SUFFIXES; + return name; + } + + /** + * Overridden to trap the exception that is thrown when a URL cannot be mapped to an ActionBean + * and then attempt to construct a dummy ActionBean that will forward the user to an appropriate + * view. In an exception is caught then the method {@link + * #handleActionBeanNotFound(ActionBeanContext, String)} is invoked to handle the exception. + * + * @param context the ActionBeanContext of the current request + * @param urlBinding the urlBinding determined for the current request + * @return an ActionBean if there is an appropriate way to handle the request + * @throws StripesServletException if no ActionBean or alternate strategy can be found + */ + @Override + public ActionBean getActionBean(ActionBeanContext context, String urlBinding) + throws StripesServletException { + try { + return super.getActionBean(context, urlBinding); + } catch (StripesServletException sse) { + ActionBean bean = handleActionBeanNotFound(context, urlBinding); + if (bean != null) { + setActionBeanContext(bean, context); + assertGetContextWorks(bean); + return bean; + } else { + throw sse; + } } - - /** - * First checks with the super class to see if an annotated event name is present, and if - * not then returns the name of the handler method itself. Will return null for methods - * that do not return a resolution or are non-public or abstract. - * - * @param handler a method which may or may not be a handler method - * @return the name of the event handled, or null - */ - @Override - public String getHandledEvent(Method handler) { - String name = super.getHandledEvent(handler); - - // If the method isn't annotated, but does return a resolution and is - // not abstract (we already know it's public) then use the method name - if ( name == null - && !Modifier.isAbstract(handler.getModifiers()) - && Resolution.class.isAssignableFrom(handler.getReturnType()) - && handler.getParameterTypes().length == 0) { - - name = handler.getName(); - } - - return name; + } + + /** + * Invoked when no appropriate ActionBean can be located. Attempts to locate a view that is + * appropriate for this request by calling {@link #findView(String)}. If a view is found then a + * dummy ActionBean is constructed that will send the user to the view. If no appropriate view is + * found then null is returned. + * + * @param context the ActionBeanContext of the current request + * @param urlBinding the urlBinding determined for the current request + * @return an ActionBean that will render a view for the user, or null + * @since Stripes 1.3 + */ + protected ActionBean handleActionBeanNotFound(ActionBeanContext context, String urlBinding) { + ActionBean bean = null; + Resolution view = findView(urlBinding); + + if (view != null) { + log.debug( + "Could not find an ActionBean bound to '", + urlBinding, + "', but found a view ", + "at '", + view, + "'. Forwarding the user there instead."); + bean = new DefaultViewActionBean(view); } - /** - *

    Overridden to trap the exception that is thrown when a URL cannot be mapped to an - * ActionBean and then attempt to construct a dummy ActionBean that will forward the - * user to an appropriate view. In an exception is caught then the method - * {@link #handleActionBeanNotFound(ActionBeanContext, String)} is invoked to handle - * the exception.

    - * - * @param context the ActionBeanContext of the current request - * @param urlBinding the urlBinding determined for the current request - * @return an ActionBean if there is an appropriate way to handle the request - * @throws StripesServletException if no ActionBean or alternate strategy can be found - */ - @Override - public ActionBean getActionBean(ActionBeanContext context, - String urlBinding) throws StripesServletException { - try { - return super.getActionBean(context, urlBinding); - } - catch (StripesServletException sse) { - ActionBean bean = handleActionBeanNotFound(context, urlBinding); - if (bean != null) { - setActionBeanContext(bean, context); - assertGetContextWorks(bean); - return bean; - } - else { - throw sse; - } + return bean; + } + + /** + * Attempts to locate a default view for the urlBinding provided and return a ForwardResolution + * that will take the user to the view. Looks for views by using the list of attempts returned by + * {@link #getFindViewAttempts(String)}. + * + *

    For each view name derived a check is performed using {@link + * ServletContext#getResource(String)} to see if there is a file located at that URL. Only if a + * file actually exists will a Resolution be returned. + * + *

    Can be overridden to provide a different kind of resolution. It is strongly recommended when + * overriding this method to check for the actual existence of views prior to manufacturing a + * resolution in order not to cause confusion when URLs are mistyped. + * + * @param urlBinding the url being accessed by the client in the current request + * @return a Resolution if a default view can be found, or null otherwise + * @since Stripes 1.3 + */ + protected Resolution findView(String urlBinding) { + List attempts = getFindViewAttempts(urlBinding); + + ServletContext ctx = + StripesFilter.getConfiguration() + .getBootstrapPropertyResolver() + .getFilterConfig() + .getServletContext(); + + for (String jsp : attempts) { + try { + // This will try /account/ViewAccount.jsp + if (ctx.getResource(jsp) != null) { + return new ForwardResolution(jsp); } + } catch (MalformedURLException mue) { + } } - - /** - * Invoked when no appropriate ActionBean can be located. Attempts to locate a view that is - * appropriate for this request by calling {@link #findView(String)}. If a view is found - * then a dummy ActionBean is constructed that will send the user to the view. If no appropriate - * view is found then null is returned. - * - * @param context the ActionBeanContext of the current request - * @param urlBinding the urlBinding determined for the current request - * @return an ActionBean that will render a view for the user, or null - * @since Stripes 1.3 - */ - protected ActionBean handleActionBeanNotFound(ActionBeanContext context, String urlBinding) { - ActionBean bean = null; - Resolution view = findView(urlBinding); - - if (view != null) { - log.debug("Could not find an ActionBean bound to '", urlBinding, "', but found a view ", - "at '", view, "'. Forwarding the user there instead."); - bean = new DefaultViewActionBean(view); + return null; + } + + /** + * Returns the list of attempts to locate a default view for the urlBinding provided. Generates + * attempts for views by converting the incoming urlBinding with the following rules. For example + * if the urlBinding is '/account/ViewAccount.action' the following views will be returned in + * order: + * + *

      + *
    • /account/ViewAccount.jsp + *
    • /account/viewAccount.jsp + *
    • /account/view_account.jsp + *
    + * + *

    Can be overridden to look for views with a different pattern. + * + * @param urlBinding the url being accessed by the client in the current request + * @since Stripes 1.5 + */ + protected List getFindViewAttempts(String urlBinding) { + List attempts = new ArrayList(3); + + int lastPeriod = urlBinding.lastIndexOf('.'); + String path = urlBinding.substring(0, urlBinding.lastIndexOf("/") + 1); + String name = + (lastPeriod >= path.length()) + ? urlBinding.substring(path.length(), lastPeriod) + : urlBinding.substring(path.length()); + + if (name.length() > 0) { + // This will try /account/ViewAccount.jsp + attempts.add(path + name + ".jsp"); + + // This will try /account/viewAccount.jsp + name = Character.toLowerCase(name.charAt(0)) + name.substring(1); + attempts.add(path + name + ".jsp"); + + // And finally this will try /account/view_account.jsp + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < name.length(); ++i) { + char ch = name.charAt(i); + if (Character.isUpperCase(ch)) { + builder.append("_"); + builder.append(Character.toLowerCase(ch)); + } else { + builder.append(ch); } - - return bean; + } + attempts.add(path + builder.toString() + ".jsp"); } - /** - *

    Attempts to locate a default view for the urlBinding provided and return a - * ForwardResolution that will take the user to the view. Looks for views by using the - * list of attempts returned by {@link #getFindViewAttempts(String)}. - * - *

    For each view name derived a check is performed using - * {@link ServletContext#getResource(String)} to see if there is a file located at that URL. - * Only if a file actually exists will a Resolution be returned.

    - * - *

    Can be overridden to provide a different kind of resolution. It is strongly recommended - * when overriding this method to check for the actual existence of views prior to manufacturing - * a resolution in order not to cause confusion when URLs are mistyped.

    - * - * @param urlBinding the url being accessed by the client in the current request - * @return a Resolution if a default view can be found, or null otherwise - * @since Stripes 1.3 - */ - protected Resolution findView(String urlBinding) { - List attempts = getFindViewAttempts(urlBinding); - - ServletContext ctx = StripesFilter.getConfiguration() - .getBootstrapPropertyResolver().getFilterConfig().getServletContext(); - - for (String jsp : attempts) { - try { - // This will try /account/ViewAccount.jsp - if (ctx.getResource(jsp) != null) { - return new ForwardResolution(jsp); - } - } - catch (MalformedURLException mue) { - } + return attempts; + } + + /** + * In addition to the {@link net.sourceforge.stripes.action.ActionBean} class simple name, also + * add aliases for short hand names. For instance, ManageUsersActionBean would get: + * + *
      + *
    • ManageUsersActionBean (simple name) + *
    • ManageUsersAction + *
    • ManageUsers + *
    + */ + @Override + protected void addBeanNameMappings() { + super.addBeanNameMappings(); + + Set generatedAliases = new HashSet(); + Set duplicateAliases = new HashSet(); + for (Class clazz : getActionBeanClasses()) { + String name = clazz.getSimpleName(); + for (String suffix : getActionBeanSuffixes()) { + if (name.endsWith(suffix)) { + name = name.substring(0, name.length() - suffix.length()); + if (generatedAliases.contains(name)) { + log.warn( + "Found multiple action beans with same bean name ", + name, + ". You will need to " + + "reference these action beans by their fully qualified names"); + duplicateAliases.add(name); + continue; + } + + generatedAliases.add(name); + actionBeansByName.put(name, clazz); } - return null; + } } - /** - *

    Returns the list of attempts to locate a default view for the urlBinding provided. - * Generates attempts for views by converting the incoming urlBinding with the following rules. - * For example if the urlBinding is '/account/ViewAccount.action' the following views will be - * returned in order:

    - * - *
      - *
    • /account/ViewAccount.jsp
    • - *
    • /account/viewAccount.jsp
    • - *
    • /account/view_account.jsp
    • - *
    - * - *

    Can be overridden to look for views with a different pattern.

    - * - * @param urlBinding the url being accessed by the client in the current request - * @since Stripes 1.5 - */ - protected List getFindViewAttempts(String urlBinding) { - List attempts = new ArrayList(3); - - int lastPeriod = urlBinding.lastIndexOf('.'); - String path = urlBinding.substring(0, urlBinding.lastIndexOf("/") + 1); - String name = (lastPeriod >= path.length()) ? urlBinding.substring(path.length(), lastPeriod) - : urlBinding.substring(path.length()); - - if (name.length() > 0) { - // This will try /account/ViewAccount.jsp - attempts.add(path + name + ".jsp"); - - // This will try /account/viewAccount.jsp - name = Character.toLowerCase(name.charAt(0)) + name.substring(1); - attempts.add(path + name + ".jsp"); - - // And finally this will try /account/view_account.jsp - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < name.length(); ++i) { - char ch = name.charAt(i); - if (Character.isUpperCase(ch)) { - builder.append("_"); - builder.append(Character.toLowerCase(ch)); - } - else { - builder.append(ch); - } - } - attempts.add(path + builder.toString() + ".jsp"); - } - - return attempts; - } - - /** - * In addition to the {@link net.sourceforge.stripes.action.ActionBean} class simple name, also add aliases for - * short hand names. For instance, ManageUsersActionBean would get: - *
      - *
    • ManageUsersActionBean (simple name)
    • - *
    • ManageUsersAction
    • - *
    • ManageUsers
    • - *
    - */ - @Override - protected void addBeanNameMappings() { - super.addBeanNameMappings(); - - Set generatedAliases = new HashSet(); - Set duplicateAliases = new HashSet(); - for(Class clazz : getActionBeanClasses()) { - String name = clazz.getSimpleName(); - for (String suffix : getActionBeanSuffixes()) { - if (name.endsWith(suffix)) { - name = name.substring(0, name.length() - suffix.length()); - if (generatedAliases.contains(name)) { - log.warn("Found multiple action beans with same bean name ", name, ". You will need to " + - "reference these action beans by their fully qualified names"); - duplicateAliases.add(name); - continue; - } - - generatedAliases.add(name); - actionBeansByName.put(name, clazz); - } - } - } - - // Remove any duplicate aliases that were found - for(String duplicateAlias : duplicateAliases) - { - actionBeansByName.remove(duplicateAlias); - } + // Remove any duplicate aliases that were found + for (String duplicateAlias : duplicateAliases) { + actionBeansByName.remove(duplicateAlias); } -} \ No newline at end of file + } +} diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/ObjectFactory.java b/stripes/src/main/java/net/sourceforge/stripes/controller/ObjectFactory.java index ebf5f1fbc..20db432df 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/ObjectFactory.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/ObjectFactory.java @@ -15,82 +15,75 @@ package net.sourceforge.stripes.controller; import java.lang.reflect.Constructor; - import net.sourceforge.stripes.config.ConfigurableComponent; /** - * Used throughout Stripes to instantiate classes. The default implementation is - * {@link DefaultObjectFactory}. You can specify an alternate implementation to use by setting the - * {@code ObjectFactory.Class} initialization parameter for {@link StripesFilter} or by placing your + * Used throughout Stripes to instantiate classes. The default implementation is {@link + * DefaultObjectFactory}. You can specify an alternate implementation to use by setting the {@code + * ObjectFactory.Class} initialization parameter for {@link StripesFilter} or by placing your * implementation in one of the packages named in {@code Extension.Packages}. - * + * *
      * <init-param>
      *  <param-name>ObjectFactory.Class</param-name>
      *  <param-value>com.mycompany.stripes.ext.MyObjectFactory</param-value>
      * </init-param>
      * 
    - * + * * @author Ben Gunter * @since Stripes 1.5.1 */ public interface ObjectFactory extends ConfigurableComponent { - /** - *

    - * A wrapper for a {@link Constructor}. This interface provides a builder-style API for - * instantiating classes by invoking a specific constructor. Typical usage might look like: - *

    - * - * configuration.getObjectFactory().constructor(targetType, String.class).newInstance("FOO"); - * - */ - public static interface ConstructorWrapper { - /** Get the {@link Constructor} object wrapped by this instance. */ - public Constructor getConstructor(); + /** + * A wrapper for a {@link Constructor}. This interface provides a builder-style API for + * instantiating classes by invoking a specific constructor. Typical usage might look like: + * configuration.getObjectFactory().constructor(targetType, String.class).newInstance("FOO"); + * + */ + public static interface ConstructorWrapper { + /** Get the {@link Constructor} object wrapped by this instance. */ + public Constructor getConstructor(); - /** Invoke the constructor with the specified arguments and return the new object. */ - public T newInstance(Object... args); - } + /** Invoke the constructor with the specified arguments and return the new object. */ + public T newInstance(Object... args); + } - /** - * Create a new instance of {@code clazz} and return it. - * - * @param clazz The class to instantiate. - * @return A new instances of the class. - */ - T newInstance(Class clazz); + /** + * Create a new instance of {@code clazz} and return it. + * + * @param clazz The class to instantiate. + * @return A new instances of the class. + */ + T newInstance(Class clazz); - /** - * Create a new instances of {@code T} by invoking the given constructor. - * - * @return A new object instantiated by invoking the constructor. - */ - T newInstance(Constructor constructor, Object... args); + /** + * Create a new instances of {@code T} by invoking the given constructor. + * + * @return A new object instantiated by invoking the constructor. + */ + T newInstance(Constructor constructor, Object... args); - /** - * Create a new instance of {@code clazz} by calling a specific constructor. - * - * @param clazz The class to instantiate. - * @param constructorArgTypes The type arguments of the constructor to be invoked. (See - * {@link Class#getConstructor(Class...)}.) - * @param constructorArgs The arguments to pass to the constructor. (See - * {@link Constructor#newInstance(Object...)}.) - * @return A new instance of the class. - */ - T newInstance(Class clazz, Class[] constructorArgTypes, Object[] constructorArgs); + /** + * Create a new instance of {@code clazz} by calling a specific constructor. + * + * @param clazz The class to instantiate. + * @param constructorArgTypes The type arguments of the constructor to be invoked. (See {@link + * Class#getConstructor(Class...)}.) + * @param constructorArgs The arguments to pass to the constructor. (See {@link + * Constructor#newInstance(Object...)}.) + * @return A new instance of the class. + */ + T newInstance(Class clazz, Class[] constructorArgTypes, Object[] constructorArgs); - /** - *

    - * Provides a builder-style interface for instantiating objects by calling specific - * constructors. Typical usage might look like: - *

    - * - * configuration.getObjectFactory().constructor(targetType, String.class).newInstance("FOO"); - * - * - * @param clazz The class whose constructor is to be looked up. - * @param parameterTypes The types of the parameters to the constructor. - * @return A {@link ConstructorWrapper} that allows for invoking the constructor. - */ - ConstructorWrapper constructor(Class clazz, Class... parameterTypes); -} \ No newline at end of file + /** + * Provides a builder-style interface for instantiating objects by calling specific constructors. + * Typical usage might look like: + * configuration.getObjectFactory().constructor(targetType, String.class).newInstance("FOO"); + * + * + * @param clazz The class whose constructor is to be looked up. + * @param parameterTypes The types of the parameters to the constructor. + * @return A {@link ConstructorWrapper} that allows for invoking the constructor. + */ + ConstructorWrapper constructor(Class clazz, Class... parameterTypes); +} diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/ObjectPostProcessor.java b/stripes/src/main/java/net/sourceforge/stripes/controller/ObjectPostProcessor.java index 2225e67cd..3b100f5f1 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/ObjectPostProcessor.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/ObjectPostProcessor.java @@ -18,32 +18,29 @@ * Allows for post-processing of objects created by {@link DefaultObjectFactory}. To register a * post-processor with the {@link ObjectFactory}, you must pass it to $$$. Implementations of this * interface must be thread-safe, as instances will be reused. - * + * * @author Ben Gunter */ public interface ObjectPostProcessor { - /** - *

    - * Accept a reference to a {@link DefaultObjectFactory} instance that is using this - * post-processor. This method is called by the object factory when the post-processor is passed - * to {@link DefaultObjectFactory#addPostProcessor(ObjectPostProcessor)}. - *

    - *

    - * In normal usage, this method will never be called more than once. However, implementations - * should guard against multiple calls if that would cause a problem. - *

    - * - * @param factory The object factory that is now using this post-processor. - */ - void setObjectFactory(DefaultObjectFactory factory); + /** + * Accept a reference to a {@link DefaultObjectFactory} instance that is using this + * post-processor. This method is called by the object factory when the post-processor is passed + * to {@link DefaultObjectFactory#addPostProcessor(ObjectPostProcessor)}. + * + *

    In normal usage, this method will never be called more than once. However, implementations + * should guard against multiple calls if that would cause a problem. + * + * @param factory The object factory that is now using this post-processor. + */ + void setObjectFactory(DefaultObjectFactory factory); - /** - * Do whatever post-processing is necessary on the object and return it. It is not absolutely - * required that this method return exactly the same object that was passed to it, but it is - * strongly recommended. - * - * @param object The object to be processed. - * @return The object that was passed in. - */ - T postProcess(T object); + /** + * Do whatever post-processing is necessary on the object and return it. It is not absolutely + * required that this method return exactly the same object that was passed to it, but it is + * strongly recommended. + * + * @param object The object to be processed. + * @return The object that was passed in. + */ + T postProcess(T object); } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/ParameterName.java b/stripes/src/main/java/net/sourceforge/stripes/controller/ParameterName.java index 057e7c3f1..2ff7e4341 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/ParameterName.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/ParameterName.java @@ -14,115 +14,110 @@ */ package net.sourceforge.stripes.controller; -import java.util.regex.Pattern; import java.util.regex.Matcher; +import java.util.regex.Pattern; /** - * Encapsulates the name of a parameter in the HttpServletRequest. Detects whether or - * not the name refers to an indexed or mapped property. + * Encapsulates the name of a parameter in the HttpServletRequest. Detects whether or not the name + * refers to an indexed or mapped property. * * @author Tim Fennell */ public class ParameterName implements Comparable { - /** Stores the regular expression that will remove all [] segments. */ - public static final Pattern pattern = Pattern.compile("\\[.*?\\]"); + /** Stores the regular expression that will remove all [] segments. */ + public static final Pattern pattern = Pattern.compile("\\[.*?\\]"); - /** Stores the name passed in at construction time. */ - private String name; + /** Stores the name passed in at construction time. */ + private String name; - /** Stores the name with all indexing and mapping stripped out of it. */ - private String strippedName; + /** Stores the name with all indexing and mapping stripped out of it. */ + private String strippedName; - /** True if the name has indexing or mapping in it. */ - private boolean indexed; + /** True if the name has indexing or mapping in it. */ + private boolean indexed; - /** - * Constructs a ParameterName for a given name from the HttpServletRequest. As it is - * constructed, detects whether or not the name contains indexing or mapping components, - * and if it does, also creates and stores the stripped name. - * - * @param name a name that may or may not contain indexing or mapping - */ - public ParameterName(String name) { - this.name = name; - Matcher matcher = pattern.matcher(this.name); - this.indexed = matcher.find(); + /** + * Constructs a ParameterName for a given name from the HttpServletRequest. As it is constructed, + * detects whether or not the name contains indexing or mapping components, and if it does, also + * creates and stores the stripped name. + * + * @param name a name that may or may not contain indexing or mapping + */ + public ParameterName(String name) { + this.name = name; + Matcher matcher = pattern.matcher(this.name); + this.indexed = matcher.find(); - if (this.indexed) { - this.strippedName = matcher.replaceAll(""); - } - else { - this.strippedName = this.name; - } + if (this.indexed) { + this.strippedName = matcher.replaceAll(""); + } else { + this.strippedName = this.name; } + } - /** Returns true if the name has indexing or mapping components, otherwise false. */ - public boolean isIndexed() { - return this.indexed; - } - - /** - * Always returns the parameter name as passed in to the constructor. If it contained - * indexing or mapping components (e.g. [3] or (foo)) they will be present in the - * String returned. - * - * @return String the name as supplied in the request - */ - public String getName() { - return this.name; - } + /** Returns true if the name has indexing or mapping components, otherwise false. */ + public boolean isIndexed() { + return this.indexed; + } - /** - * Returns the name with all indexing and mapping components stripped. E.g. if the name - * in the request was 'foo[1].bar', this method will return 'foo.bar'. - * - * @return String the name minus indexing and mapping - */ - public String getStrippedName() { - return this.strippedName; - } + /** + * Always returns the parameter name as passed in to the constructor. If it contained indexing or + * mapping components (e.g. [3] or (foo)) they will be present in the String returned. + * + * @return String the name as supplied in the request + */ + public String getName() { + return this.name; + } - /** - * Orders ParameterNames so that those with shorter (unstripped) names come first. Two - * names of the same length are then ordered alphabetically by String.compareTo(). - * - * @param that another ParameterName to compare to - * @return -1 if this value sorts first, 0 if the values are identical and +1 if the - * parameter passed in sorts first. - */ - public int compareTo(ParameterName that) { - int result = new Integer(this.name.length()).compareTo(that.name.length()); - if (result == 0) { - result = this.name.compareTo(that.name); - } + /** + * Returns the name with all indexing and mapping components stripped. E.g. if the name in the + * request was 'foo[1].bar', this method will return 'foo.bar'. + * + * @return String the name minus indexing and mapping + */ + public String getStrippedName() { + return this.strippedName; + } - return result; + /** + * Orders ParameterNames so that those with shorter (unstripped) names come first. Two names of + * the same length are then ordered alphabetically by String.compareTo(). + * + * @param that another ParameterName to compare to + * @return -1 if this value sorts first, 0 if the values are identical and +1 if the parameter + * passed in sorts first. + */ + public int compareTo(ParameterName that) { + int result = new Integer(this.name.length()).compareTo(that.name.length()); + if (result == 0) { + result = this.name.compareTo(that.name); } - /** - * Checks for equality as efficiently as possible. First checks for JVM equality to - * see if we can short circuit, and then checks for equality of the name attribute for - * a real test. - */ - @Override - public boolean equals(Object obj) { - return (obj instanceof ParameterName) && - (this == obj || compareTo((ParameterName) obj) == 0); - } + return result; + } - /** Simple hashcode method based on the name of the parameter. */ - @Override - public int hashCode() { - return this.name.hashCode(); - } + /** + * Checks for equality as efficiently as possible. First checks for JVM equality to see if we can + * short circuit, and then checks for equality of the name attribute for a real test. + */ + @Override + public boolean equals(Object obj) { + return (obj instanceof ParameterName) && (this == obj || compareTo((ParameterName) obj) == 0); + } - /** - * Uses the original name as the string representation of the class. This is probably - * the most useful thing to see in log messages, which is the only place toString will - * be used. - */ - @Override - public String toString() { - return this.name; - } + /** Simple hashcode method based on the name of the parameter. */ + @Override + public int hashCode() { + return this.name.hashCode(); + } + + /** + * Uses the original name as the string representation of the class. This is probably the most + * useful thing to see in log messages, which is the only place toString will be used. + */ + @Override + public String toString() { + return this.name; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/StripesConstants.java b/stripes/src/main/java/net/sourceforge/stripes/controller/StripesConstants.java index 4b9c79135..c8988558e 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/StripesConstants.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/StripesConstants.java @@ -14,10 +14,9 @@ */ package net.sourceforge.stripes.controller; -import net.sourceforge.stripes.util.Literal; - -import java.util.Set; import java.util.Collections; +import java.util.Set; +import net.sourceforge.stripes.util.Literal; /** * Container for constant values that are used across more than one class in Stripes. @@ -25,90 +24,90 @@ * @author Tim Fennell */ public interface StripesConstants { - /** - * The name of a URL parameter that is used to hold the path (relative to the web application - * root) from which the current form submission was made. - */ - String URL_KEY_SOURCE_PAGE = "_sourcePage"; + /** + * The name of a URL parameter that is used to hold the path (relative to the web application + * root) from which the current form submission was made. + */ + String URL_KEY_SOURCE_PAGE = "_sourcePage"; - /** - * The name of a URL parameter that is used to hold the names of certain inputs that are present - * in a form if those inputs might not be submitted with the form, such as checkboxes. - */ - String URL_KEY_FIELDS_PRESENT = "__fp"; + /** + * The name of a URL parameter that is used to hold the names of certain inputs that are present + * in a form if those inputs might not be submitted with the form, such as checkboxes. + */ + String URL_KEY_FIELDS_PRESENT = "__fp"; - /** - * The name of a URL parameter that is used as a last resort to attempt to determine - * the name of the event that is being fired by the browser. - */ - String URL_KEY_EVENT_NAME = "_eventName"; + /** + * The name of a URL parameter that is used as a last resort to attempt to determine the name of + * the event that is being fired by the browser. + */ + String URL_KEY_EVENT_NAME = "_eventName"; - /** - * The name of a URL parameter that is used to tell Stripes that a flash scope exists - * for the current request. - */ - String URL_KEY_FLASH_SCOPE_ID = "__fsk"; + /** + * The name of a URL parameter that is used to tell Stripes that a flash scope exists for the + * current request. + */ + String URL_KEY_FLASH_SCOPE_ID = "__fsk"; - /** - * An immutable set of URL keys or request parameters that have special meaning to Stripes and - * as a result should not be referenced in binding, validation or other other places that - * work on the full set of request parameters. - */ - Set SPECIAL_URL_KEYS = Collections.unmodifiableSet( - Literal.set(StripesConstants.URL_KEY_SOURCE_PAGE, - StripesConstants.URL_KEY_FIELDS_PRESENT, - StripesConstants.URL_KEY_FLASH_SCOPE_ID, - StripesConstants.URL_KEY_EVENT_NAME)); - /** - * The name under which the ActionBean for a request is stored as a request attribute before - * forwarding to the JSP. - */ - String REQ_ATTR_ACTION_BEAN = "actionBean"; + /** + * An immutable set of URL keys or request parameters that have special meaning to Stripes and as + * a result should not be referenced in binding, validation or other other places that work on the + * full set of request parameters. + */ + Set SPECIAL_URL_KEYS = + Collections.unmodifiableSet( + Literal.set( + StripesConstants.URL_KEY_SOURCE_PAGE, + StripesConstants.URL_KEY_FIELDS_PRESENT, + StripesConstants.URL_KEY_FLASH_SCOPE_ID, + StripesConstants.URL_KEY_EVENT_NAME)); + /** + * The name under which the ActionBean for a request is stored as a request attribute before + * forwarding to the JSP. + */ + String REQ_ATTR_ACTION_BEAN = "actionBean"; - /** - * The name of a request attribute in which a Stack of action beans is some times stored - * when a single request involves includes of action beans. - */ - String REQ_ATTR_ACTION_BEAN_STACK = "__stripes_actionBeanStack"; + /** + * The name of a request attribute in which a Stack of action beans is some times stored when a + * single request involves includes of action beans. + */ + String REQ_ATTR_ACTION_BEAN_STACK = "__stripes_actionBeanStack"; - /** - * The attribute key that is used to store the default set of non-error messages for - * display to the user. - */ - String REQ_ATTR_MESSAGES = "_stripes_defaultMessages"; + /** + * The attribute key that is used to store the default set of non-error messages for display to + * the user. + */ + String REQ_ATTR_MESSAGES = "_stripes_defaultMessages"; - /** - * The attribute key that is used to store the tag stack during page processing. The tag - * stack is a Stack of Stripes tags so that parent tag relationships can work across - * includes etc. - */ - String REQ_ATTR_TAG_STACK = "__stripes_tag_stack"; + /** + * The attribute key that is used to store the tag stack during page processing. The tag stack is + * a Stack of Stripes tags so that parent tag relationships can work across includes etc. + */ + String REQ_ATTR_TAG_STACK = "__stripes_tag_stack"; - /** - * The name of a request parameter that holds a Map of flash scopes keyed by the - * hash code of the request that generated them. - */ - String REQ_ATTR_FLASH_SCOPE_LOCATION = "__flash_scopes"; - - /** The name of a request attribute that holds the lookup key of the current flash scope. */ - String REQ_ATTR_CURRENT_FLASH_SCOPE = "__current_flash_scope"; + /** + * The name of a request parameter that holds a Map of flash scopes keyed by the hash code of the + * request that generated them. + */ + String REQ_ATTR_FLASH_SCOPE_LOCATION = "__flash_scopes"; - /** - * The name of a request attribute that is checked first to determine the - * name of the event that should fire. - */ - String REQ_ATTR_EVENT_NAME = "__stripes_event_name"; + /** The name of a request attribute that holds the lookup key of the current flash scope. */ + String REQ_ATTR_CURRENT_FLASH_SCOPE = "__current_flash_scope"; - /** - * Request attribute key defined by the servlet spec for storing the included servlet - * path when processing a server side include. - */ - String REQ_ATTR_INCLUDE_PATH = "javax.servlet.include.servlet_path"; + /** + * The name of a request attribute that is checked first to determine the name of the event that + * should fire. + */ + String REQ_ATTR_EVENT_NAME = "__stripes_event_name"; - /** - * Request attribute key defined by the servlet spec for storing the included path - * info when processing a server side include. - */ - String REQ_ATTR_INCLUDE_PATH_INFO = "javax.servlet.include.path_info"; + /** + * Request attribute key defined by the servlet spec for storing the included servlet path when + * processing a server side include. + */ + String REQ_ATTR_INCLUDE_PATH = "javax.servlet.include.servlet_path"; + /** + * Request attribute key defined by the servlet spec for storing the included path info when + * processing a server side include. + */ + String REQ_ATTR_INCLUDE_PATH_INFO = "javax.servlet.include.path_info"; } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/StripesFilter.java b/stripes/src/main/java/net/sourceforge/stripes/controller/StripesFilter.java index b17c5fe3b..2c681498e 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/StripesFilter.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/StripesFilter.java @@ -14,24 +14,16 @@ */ package net.sourceforge.stripes.controller; -import net.sourceforge.stripes.config.BootstrapPropertyResolver; -import net.sourceforge.stripes.config.Configuration; -import net.sourceforge.stripes.config.RuntimeConfiguration; -import net.sourceforge.stripes.exception.StripesRuntimeException; -import net.sourceforge.stripes.exception.StripesServletException; -import net.sourceforge.stripes.util.HttpUtil; -import net.sourceforge.stripes.util.Log; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; import java.beans.Introspector; import java.io.IOException; import java.lang.ref.WeakReference; @@ -39,295 +31,300 @@ import java.util.Iterator; import java.util.Locale; import java.util.Set; +import net.sourceforge.stripes.config.BootstrapPropertyResolver; +import net.sourceforge.stripes.config.Configuration; +import net.sourceforge.stripes.config.RuntimeConfiguration; +import net.sourceforge.stripes.exception.StripesRuntimeException; +import net.sourceforge.stripes.exception.StripesServletException; +import net.sourceforge.stripes.util.HttpUtil; +import net.sourceforge.stripes.util.Log; /** - * The Stripes filter is used to ensure that all requests coming to a Stripes application - * are handled in the same way. It detects and wraps any requests that contain multipart/form - * data, so that they may be treated much like any other request. Also ensures that - * all downstream components have access to essential configuration and services whether - * the request goes through the dispatcher, or straight to a JSP. + * The Stripes filter is used to ensure that all requests coming to a Stripes application are + * handled in the same way. It detects and wraps any requests that contain multipart/form data, so + * that they may be treated much like any other request. Also ensures that all downstream components + * have access to essential configuration and services whether the request goes through the + * dispatcher, or straight to a JSP. * * @author Tim Fennell */ public class StripesFilter implements Filter { - /** Key used to lookup the name of the Configuration class used to configure Stripes. */ - public static final String CONFIG_CLASS = "Configuration.Class"; - - /** Log used throughout the class. */ - private static final Log log = Log.getInstance(StripesFilter.class); - - /** The configuration instance for Stripes. */ - private Configuration configuration; - - /** The servlet context */ - private ServletContext servletContext; - - /** - * A place to stash the Configuration object so that other classes in Stripes can access it - * without resorting to ferrying it, or the request, to every class that needs access to the - * Configuration. Doing this allows multiple Stripes Configurations to exist in a single - * Classloader since the Configuration is not located statically. - */ - private static final ThreadLocal configurationStash = new ThreadLocal(); - - /** - * A set of weak references to all the Configuration objects that this class has ever - * seen. Uses weak references to allow garbage collection to reap these objects if this - * is the only reference left. Used to determine if there is only one active Configuration - * for the VM, and if so return it even when the Configuration isn't set in the thread local. - */ - private static final Set> configurations = - new HashSet>(); - - /** - * Some operations should only be done if the current invocation of - * {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} is the - * first in the filter chain. This {@link ThreadLocal} keeps track of - * whether such operations should be done or not. - */ - private static final ThreadLocal initialInvocation = new ThreadLocal() { + /** Key used to lookup the name of the Configuration class used to configure Stripes. */ + public static final String CONFIG_CLASS = "Configuration.Class"; + + /** Log used throughout the class. */ + private static final Log log = Log.getInstance(StripesFilter.class); + + /** The configuration instance for Stripes. */ + private Configuration configuration; + + /** The servlet context */ + private ServletContext servletContext; + + /** + * A place to stash the Configuration object so that other classes in Stripes can access it + * without resorting to ferrying it, or the request, to every class that needs access to the + * Configuration. Doing this allows multiple Stripes Configurations to exist in a single + * Classloader since the Configuration is not located statically. + */ + private static final ThreadLocal configurationStash = + new ThreadLocal(); + + /** + * A set of weak references to all the Configuration objects that this class has ever seen. Uses + * weak references to allow garbage collection to reap these objects if this is the only reference + * left. Used to determine if there is only one active Configuration for the VM, and if so return + * it even when the Configuration isn't set in the thread local. + */ + private static final Set> configurations = + new HashSet>(); + + /** + * Some operations should only be done if the current invocation of {@link + * #doFilter(ServletRequest, ServletResponse, FilterChain)} is the first in the filter chain. This + * {@link ThreadLocal} keeps track of whether such operations should be done or not. + */ + private static final ThreadLocal initialInvocation = + new ThreadLocal() { @Override protected Boolean initialValue() { - return true; - } - }; - - /** - * Performs the necessary initialization for the StripesFilter. Mainly this involves deciding - * what configuration class to use, and then instantiating and initializing the chosen - * Configuration. - * - * @throws ServletException thrown if a problem is encountered initializing Stripes - */ - public void init(FilterConfig filterConfig) throws ServletException { - this.configuration = createConfiguration(filterConfig); - StripesFilter.configurations.add(new WeakReference(this.configuration)); - - this.servletContext = filterConfig.getServletContext(); - this.servletContext.setAttribute(StripesFilter.class.getName(), this); - - Package pkg = getClass().getPackage(); - log.info("Stripes Initialization Complete. Version: ", pkg.getSpecificationVersion(), - ", Build: ", pkg.getImplementationVersion()); - } - - /** - * Create and configure a new {@link Configuration} instance using the suppied - * {@link FilterConfig}. - * - * @param filterConfig The filter configuration supplied by the container. - * @return The new configuration instance. - * @throws ServletException If the configuration cannot be created. - */ - protected static Configuration createConfiguration(FilterConfig filterConfig) - throws ServletException { - BootstrapPropertyResolver bootstrap = new BootstrapPropertyResolver(filterConfig); - - // Set up the Configuration - if one isn't found by the bootstrapper then - // we'll just use the default: RuntimeConfiguration - Class clazz = bootstrap.getClassProperty(CONFIG_CLASS, - Configuration.class); - - if (clazz == null) - clazz = RuntimeConfiguration.class; - - try { - Configuration configuration = clazz.newInstance(); - configuration.setBootstrapPropertyResolver(bootstrap); - configuration.init(); - return configuration; - } - catch (Exception e) { - log.fatal(e, - "Could not instantiate specified Configuration. Class name specified was ", - "[", clazz.getName(), "]."); - throw new StripesServletException("Could not instantiate specified Configuration. " - + "Class name specified was [" + clazz.getName() + "].", e); + return true; } + }; + + /** + * Performs the necessary initialization for the StripesFilter. Mainly this involves deciding what + * configuration class to use, and then instantiating and initializing the chosen Configuration. + * + * @throws ServletException thrown if a problem is encountered initializing Stripes + */ + public void init(FilterConfig filterConfig) throws ServletException { + this.configuration = createConfiguration(filterConfig); + StripesFilter.configurations.add(new WeakReference(this.configuration)); + + this.servletContext = filterConfig.getServletContext(); + this.servletContext.setAttribute(StripesFilter.class.getName(), this); + + Package pkg = getClass().getPackage(); + log.info( + "Stripes Initialization Complete. Version: ", pkg.getSpecificationVersion(), + ", Build: ", pkg.getImplementationVersion()); + } + + /** + * Create and configure a new {@link Configuration} instance using the suppied {@link + * FilterConfig}. + * + * @param filterConfig The filter configuration supplied by the container. + * @return The new configuration instance. + * @throws ServletException If the configuration cannot be created. + */ + protected static Configuration createConfiguration(FilterConfig filterConfig) + throws ServletException { + BootstrapPropertyResolver bootstrap = new BootstrapPropertyResolver(filterConfig); + + // Set up the Configuration - if one isn't found by the bootstrapper then + // we'll just use the default: RuntimeConfiguration + Class clazz = + bootstrap.getClassProperty(CONFIG_CLASS, Configuration.class); + + if (clazz == null) clazz = RuntimeConfiguration.class; + + try { + Configuration configuration = clazz.newInstance(); + configuration.setBootstrapPropertyResolver(bootstrap); + configuration.init(); + return configuration; + } catch (Exception e) { + log.fatal( + e, + "Could not instantiate specified Configuration. Class name specified was ", + "[", + clazz.getName(), + "]."); + throw new StripesServletException( + "Could not instantiate specified Configuration. " + + "Class name specified was [" + + clazz.getName() + + "].", + e); } - - /** - * Returns the Configuration that is being used to process the current request. - */ - public static Configuration getConfiguration() { - Configuration configuration = StripesFilter.configurationStash.get(); - - // If the configuration wasn't available in thread local, check to see if we only - // know about one configuration in total, and if so use that one - if (configuration == null) { - synchronized (StripesFilter.configurations) { - // Remove any references from the set that have been cleared - Iterator> iterator = StripesFilter.configurations.iterator(); - while (iterator.hasNext()) { - WeakReference ref = iterator.next(); - if (ref.get() == null) iterator.remove(); - } - - // If there is one and only one Configuration active, take it - if (StripesFilter.configurations.size() == 1) { - configuration = StripesFilter.configurations.iterator().next().get(); - } - } + } + + /** Returns the Configuration that is being used to process the current request. */ + public static Configuration getConfiguration() { + Configuration configuration = StripesFilter.configurationStash.get(); + + // If the configuration wasn't available in thread local, check to see if we only + // know about one configuration in total, and if so use that one + if (configuration == null) { + synchronized (StripesFilter.configurations) { + // Remove any references from the set that have been cleared + Iterator> iterator = StripesFilter.configurations.iterator(); + while (iterator.hasNext()) { + WeakReference ref = iterator.next(); + if (ref.get() == null) iterator.remove(); } - if (configuration == null) { - StripesRuntimeException sre = new StripesRuntimeException( - "Something is trying to access the current Stripes configuration but the " + - "current request was never routed through the StripesFilter! As a result " + - "the appropriate Configuration object cannot be located. Please take a look " + - "at the exact URL in your browser's address bar and ensure that any " + - "requests to that URL will be filtered through the StripesFilter according " + - "to the filter mappings in your web.xml." - ); - log.error(sre); // log through an exception so that users get a stracktrace + // If there is one and only one Configuration active, take it + if (StripesFilter.configurations.size() == 1) { + configuration = StripesFilter.configurations.iterator().next().get(); } - - return configuration; + } } - /** - * Returns the configuration for this instance of the StripesFilter for any class - * that has a reference to the filter. For normal runtime access to the configuration - * during a request cycle, call getConfiguration() instead. - * - * @return the Configuration of this instance of the StripesFilter - */ - public Configuration getInstanceConfiguration() { - return this.configuration; + if (configuration == null) { + StripesRuntimeException sre = + new StripesRuntimeException( + "Something is trying to access the current Stripes configuration but the " + + "current request was never routed through the StripesFilter! As a result " + + "the appropriate Configuration object cannot be located. Please take a look " + + "at the exact URL in your browser's address bar and ensure that any " + + "requests to that URL will be filtered through the StripesFilter according " + + "to the filter mappings in your web.xml."); + log.error(sre); // log through an exception so that users get a stracktrace } - /** - * Performs the primary work of the filter, including constructing a StripesRequestWrapper to - * wrap the HttpServletRequest, and using the configured LocalePicker to decide which - * Locale will be used to process the request. - */ - public void doFilter(ServletRequest servletRequest, - ServletResponse servletResponse, - FilterChain filterChain) throws IOException, ServletException { - - HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; - HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; - - // check the flag that indicates if this is the initial invocation - boolean initial = initialInvocation.get(); - if (initial) { - initialInvocation.set(false); - } - - // Wrap pretty much everything in a try/catch so that we can funnel even the most - // bizarre or unexpected exceptions into the exception handler - try { - log.trace("Intercepting request to URL: ", HttpUtil.getRequestedPath(httpRequest)); - - if (initial) { - // Pop the configuration into thread local - StripesFilter.configurationStash.set(this.configuration); - - // Figure out the locale and character encoding to use. The ordering of things here - // is very important!! We pick the locale first since picking the encoding is - // locale dependent, but the encoding *must* be set on the request before any - // parameters or parts are accessed, and wrapping the request accesses stuff. - Locale locale = this.configuration.getLocalePicker().pickLocale(httpRequest); - log.debug("LocalePicker selected locale: ", locale); - - String encoding = this.configuration.getLocalePicker().pickCharacterEncoding( - httpRequest, locale); - if (encoding != null) { - httpRequest.setCharacterEncoding(encoding); - log.debug("LocalePicker selected character encoding: ", encoding); - } - else { - log.debug("LocalePicker did not pick a character encoding, using default: ", - httpRequest.getCharacterEncoding()); - } - - StripesRequestWrapper request = wrapRequest(httpRequest); - request.setLocale(locale); - httpResponse.setLocale(locale); - if (encoding != null) { - httpResponse.setCharacterEncoding(encoding); - } - httpRequest = request; - } - else { - // process URI parameters on subsequent invocations - StripesRequestWrapper.findStripesWrapper(httpRequest).pushUriParameters( - (HttpServletRequestWrapper) httpRequest); - } - - // Execute the rest of the chain - flashInbound(httpRequest); - filterChain.doFilter(httpRequest, servletResponse); - } - catch (Throwable t) { - this.configuration.getExceptionHandler().handle(t, httpRequest, httpResponse); - } - finally { - // reset the flag that indicates if this is the initial invocation - if (initial) { - // Once the request is processed, clean up thread locals - StripesFilter.initialInvocation.remove(); - StripesFilter.configurationStash.remove(); - - flashOutbound(httpRequest); - } - else { - // restore URI parameters to their previous state - StripesRequestWrapper.findStripesWrapper(httpRequest).popUriParameters(); - } - } + return configuration; + } + + /** + * Returns the configuration for this instance of the StripesFilter for any class that has a + * reference to the filter. For normal runtime access to the configuration during a request cycle, + * call getConfiguration() instead. + * + * @return the Configuration of this instance of the StripesFilter + */ + public Configuration getInstanceConfiguration() { + return this.configuration; + } + + /** + * Performs the primary work of the filter, including constructing a StripesRequestWrapper to wrap + * the HttpServletRequest, and using the configured LocalePicker to decide which Locale will be + * used to process the request. + */ + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + + HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; + + // check the flag that indicates if this is the initial invocation + boolean initial = initialInvocation.get(); + if (initial) { + initialInvocation.set(false); } - /** - * Wraps the HttpServletRequest with a StripesServletRequest. This is done to ensure that any - * form posts that contain file uploads get handled appropriately. - * - * @param servletRequest the HttpServletRequest handed to the dispatcher by the container - * @return an instance of StripesRequestWrapper, which is an HttpServletRequestWrapper - * @throws StripesServletException if the wrapper cannot be constructed - */ - protected StripesRequestWrapper wrapRequest(HttpServletRequest servletRequest) - throws StripesServletException { - try { - return StripesRequestWrapper.findStripesWrapper(servletRequest); + // Wrap pretty much everything in a try/catch so that we can funnel even the most + // bizarre or unexpected exceptions into the exception handler + try { + log.trace("Intercepting request to URL: ", HttpUtil.getRequestedPath(httpRequest)); + + if (initial) { + // Pop the configuration into thread local + StripesFilter.configurationStash.set(this.configuration); + + // Figure out the locale and character encoding to use. The ordering of things here + // is very important!! We pick the locale first since picking the encoding is + // locale dependent, but the encoding *must* be set on the request before any + // parameters or parts are accessed, and wrapping the request accesses stuff. + Locale locale = this.configuration.getLocalePicker().pickLocale(httpRequest); + log.debug("LocalePicker selected locale: ", locale); + + String encoding = + this.configuration.getLocalePicker().pickCharacterEncoding(httpRequest, locale); + if (encoding != null) { + httpRequest.setCharacterEncoding(encoding); + log.debug("LocalePicker selected character encoding: ", encoding); + } else { + log.debug( + "LocalePicker did not pick a character encoding, using default: ", + httpRequest.getCharacterEncoding()); } - catch (IllegalStateException e) { - return new StripesRequestWrapper(servletRequest); - } - } - /** - *

    Checks to see if there is a flash scope identified by a parameter to the current - * request, and if there is, retrieves items from the flash scope and moves them - * back to request attributes.

    - */ - protected void flashInbound(HttpServletRequest req) { - // Copy the attributes from the previous flash scope to the new request - FlashScope flash = FlashScope.getPrevious(req); - if (flash != null) { - flash.beginRequest(req); + StripesRequestWrapper request = wrapRequest(httpRequest); + request.setLocale(locale); + httpResponse.setLocale(locale); + if (encoding != null) { + httpResponse.setCharacterEncoding(encoding); } + httpRequest = request; + } else { + // process URI parameters on subsequent invocations + StripesRequestWrapper.findStripesWrapper(httpRequest) + .pushUriParameters((HttpServletRequestWrapper) httpRequest); + } + + // Execute the rest of the chain + flashInbound(httpRequest); + filterChain.doFilter(httpRequest, servletResponse); + } catch (Throwable t) { + this.configuration.getExceptionHandler().handle(t, httpRequest, httpResponse); + } finally { + // reset the flag that indicates if this is the initial invocation + if (initial) { + // Once the request is processed, clean up thread locals + StripesFilter.initialInvocation.remove(); + StripesFilter.configurationStash.remove(); + + flashOutbound(httpRequest); + } else { + // restore URI parameters to their previous state + StripesRequestWrapper.findStripesWrapper(httpRequest).popUriParameters(); + } } - - /** - * Manages the work that ensures that flash scopes get cleaned up properly when - * requests go missing. Firstly timestamps the current flash scope (if one exists) - * to record the time that the request exited the container. Then checks all - * flash scopes to make sure none have been hanging out for more than a minute. - */ - protected void flashOutbound(HttpServletRequest req) { - // Start the timer on the current flash scope - FlashScope flash = FlashScope.getCurrent(req, false); - if (flash != null) { - flash.completeRequest(); - } + } + + /** + * Wraps the HttpServletRequest with a StripesServletRequest. This is done to ensure that any form + * posts that contain file uploads get handled appropriately. + * + * @param servletRequest the HttpServletRequest handed to the dispatcher by the container + * @return an instance of StripesRequestWrapper, which is an HttpServletRequestWrapper + * @throws StripesServletException if the wrapper cannot be constructed + */ + protected StripesRequestWrapper wrapRequest(HttpServletRequest servletRequest) + throws StripesServletException { + try { + return StripesRequestWrapper.findStripesWrapper(servletRequest); + } catch (IllegalStateException e) { + return new StripesRequestWrapper(servletRequest); } - - /** Calls the cleanup() method on the log to release resources held by commons logging. */ - public void destroy() { - this.servletContext.removeAttribute(StripesFilter.class.getName()); - Log.cleanup(); - Introspector.flushCaches(); // Not 100% sure this is necessary, but it doesn't hurt - StripesFilter.configurations.clear(); + } + + /** + * Checks to see if there is a flash scope identified by a parameter to the current request, and + * if there is, retrieves items from the flash scope and moves them back to request attributes. + */ + protected void flashInbound(HttpServletRequest req) { + // Copy the attributes from the previous flash scope to the new request + FlashScope flash = FlashScope.getPrevious(req); + if (flash != null) { + flash.beginRequest(req); + } + } + + /** + * Manages the work that ensures that flash scopes get cleaned up properly when requests go + * missing. Firstly timestamps the current flash scope (if one exists) to record the time that the + * request exited the container. Then checks all flash scopes to make sure none have been hanging + * out for more than a minute. + */ + protected void flashOutbound(HttpServletRequest req) { + // Start the timer on the current flash scope + FlashScope flash = FlashScope.getCurrent(req, false); + if (flash != null) { + flash.completeRequest(); } + } + + /** Calls the cleanup() method on the log to release resources held by commons logging. */ + public void destroy() { + this.servletContext.removeAttribute(StripesFilter.class.getName()); + Log.cleanup(); + Introspector.flushCaches(); // Not 100% sure this is necessary, but it doesn't hurt + StripesFilter.configurations.clear(); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/StripesRequestWrapper.java b/stripes/src/main/java/net/sourceforge/stripes/controller/StripesRequestWrapper.java index bb6feda1e..a17951218 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/StripesRequestWrapper.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/StripesRequestWrapper.java @@ -14,6 +14,9 @@ */ package net.sourceforge.stripes.controller; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -27,551 +30,527 @@ import java.util.Map; import java.util.Set; import java.util.Stack; - -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; - import net.sourceforge.stripes.action.FileBean; import net.sourceforge.stripes.controller.multipart.MultipartWrapper; import net.sourceforge.stripes.exception.StripesServletException; import net.sourceforge.stripes.exception.UrlBindingConflictException; /** - * HttpServletRequestWrapper that is used to make the file upload functionality transparent. - * Every request handled by Stripes is wrapped. Those containing multipart form file uploads - * are parsed and treated differently, while normal requests are silently wrapped and all calls - * are delegated to the real request. + * HttpServletRequestWrapper that is used to make the file upload functionality transparent. Every + * request handled by Stripes is wrapped. Those containing multipart form file uploads are parsed + * and treated differently, while normal requests are silently wrapped and all calls are delegated + * to the real request. * * @author Tim Fennell */ public class StripesRequestWrapper extends HttpServletRequestWrapper { - /** The Multipart Request that parses out all the pieces. */ - private MultipartWrapper multipart; - - /** The Locale that is going to be used to process the request. */ - private Locale locale; - - /** Local copy of the parameter map, into which URI-embedded parameters will be merged. */ - private MergedParameterMap parameterMap; - - /** - * Looks for the StripesRequesetWrapper for the specific request and returns it. This is done - * by checking to see if the request is a StripesRequestWrapper, and if not, successively - * unwrapping the request until the StripesRequestWrapper is found. - * - * @param request the ServletRequest that is wrapped by a StripesRequestWrapper - * @return the StripesRequestWrapper that is wrapping the supplied request - * @throws IllegalStateException if the request is not wrapped by Stripes - */ - public static StripesRequestWrapper findStripesWrapper(ServletRequest request) { - // Loop through any request wrappers looking for the stripes one - while ( !(request instanceof StripesRequestWrapper) - && request != null - && request instanceof HttpServletRequestWrapper) { - request = ((HttpServletRequestWrapper) request).getRequest(); - } - - // If we have our wrapper after the loop exits, we're good; otherwise... - if (request instanceof StripesRequestWrapper) { - return (StripesRequestWrapper) request; - } - - else { - throw new IllegalStateException("A request made it through to some part of Stripes " + - "without being wrapped in a StripesRequestWrapper. The StripesFilter is " + - "responsible for wrapping the request, so it is likely that either the " + - "StripesFilter is not deployed, or that its mappings do not include the " + - "DispatcherServlet _and_ *.jsp. Stripes does not require that the Stripes " + - "wrapper is the only request wrapper, or the outermost; only that it is present."); - } - } - - /** - * Constructor that will, if the POST is multi-part, parse the POST data and make it - * available through the normal channels. If the request is not a multi-part post then it is - * just wrapped and the behaviour is unchanged. - * - * @param request the HttpServletRequest to wrap - * this is not a file size limit, but a post size limit. - * @throws FileUploadLimitExceededException if the total post size is larger than the limit - * @throws StripesServletException if any other error occurs constructing the wrapper + /** The Multipart Request that parses out all the pieces. */ + private MultipartWrapper multipart; + + /** The Locale that is going to be used to process the request. */ + private Locale locale; + + /** Local copy of the parameter map, into which URI-embedded parameters will be merged. */ + private MergedParameterMap parameterMap; + + /** + * Looks for the StripesRequesetWrapper for the specific request and returns it. This is done by + * checking to see if the request is a StripesRequestWrapper, and if not, successively unwrapping + * the request until the StripesRequestWrapper is found. + * + * @param request the ServletRequest that is wrapped by a StripesRequestWrapper + * @return the StripesRequestWrapper that is wrapping the supplied request + * @throws IllegalStateException if the request is not wrapped by Stripes + */ + public static StripesRequestWrapper findStripesWrapper(ServletRequest request) { + // Loop through any request wrappers looking for the stripes one + while (!(request instanceof StripesRequestWrapper) + && request != null + && request instanceof HttpServletRequestWrapper) { + request = ((HttpServletRequestWrapper) request).getRequest(); + } + + // If we have our wrapper after the loop exits, we're good; otherwise... + if (request instanceof StripesRequestWrapper) { + return (StripesRequestWrapper) request; + } else { + throw new IllegalStateException( + "A request made it through to some part of Stripes " + + "without being wrapped in a StripesRequestWrapper. The StripesFilter is " + + "responsible for wrapping the request, so it is likely that either the " + + "StripesFilter is not deployed, or that its mappings do not include the " + + "DispatcherServlet _and_ *.jsp. Stripes does not require that the Stripes " + + "wrapper is the only request wrapper, or the outermost; only that it is present."); + } + } + + /** + * Constructor that will, if the POST is multi-part, parse the POST data and make it available + * through the normal channels. If the request is not a multi-part post then it is just wrapped + * and the behaviour is unchanged. + * + * @param request the HttpServletRequest to wrap this is not a file size limit, but a post size + * limit. + * @throws FileUploadLimitExceededException if the total post size is larger than the limit + * @throws StripesServletException if any other error occurs constructing the wrapper + */ + public StripesRequestWrapper(HttpServletRequest request) throws StripesServletException { + super(request); + + String contentType = request.getContentType(); + boolean isPost = "POST".equalsIgnoreCase(request.getMethod()); + if (isPost && contentType != null && contentType.startsWith("multipart/form-data")) { + constructMultipartWrapper(request); + } + + // Create a parameter map that merges the URI parameters with the others + if (isMultipart()) this.parameterMap = new MergedParameterMap(this, this.multipart); + else this.parameterMap = new MergedParameterMap(this); + } + + /** + * Responsible for constructing the MultipartWrapper object and setting it on to the instance + * variable 'multipart'. + * + * @param request the HttpServletRequest to wrap this is not a file size limit, but a post size + * limit. + * @throws StripesServletException if any other error occurs constructing the wrapper + */ + protected void constructMultipartWrapper(HttpServletRequest request) + throws StripesServletException { + try { + this.multipart = StripesFilter.getConfiguration().getMultipartWrapperFactory().wrap(request); + } catch (IOException e) { + throw new StripesServletException("Could not construct request wrapper.", e); + } + } + + /** Returns true if this request is wrapping a multipart request, false otherwise. */ + public boolean isMultipart() { + return this.multipart != null; + } + + /** + * Fetches just the names of regular parameters and does not include file upload parameters. If + * the request is multipart then the information is sourced from the parsed multipart object + * otherwise it is just pulled out of the request in the usual manner. + */ + @Override + public Enumeration getParameterNames() { + return Collections.enumeration(getParameterMap().keySet()); + } + + /** + * Returns all values sent in the request for a given parameter name. If the request is multipart + * then the information is sourced from the parsed multipart object otherwise it is just pulled + * out of the request in the usual manner. Values are consistent with + * HttpServletRequest.getParameterValues(String). Values for file uploads cannot be retrieved in + * this way (though parameters sent along with file uploads can). + */ + @Override + public String[] getParameterValues(String name) { + /* + * When determining whether to provide a URI parameter's default value, the merged parameter + * map needs to know if the parameter is otherwise defined in the request. It calls this + * method to do that, so if the parameter map is not defined (which it won't be during its + * construction), we delegate to the multipart wrapper or superclass. */ - public StripesRequestWrapper(HttpServletRequest request) throws StripesServletException { - super(request); - - String contentType = request.getContentType(); - boolean isPost = "POST".equalsIgnoreCase(request.getMethod()); - if (isPost && contentType != null && contentType.startsWith("multipart/form-data")) { - constructMultipartWrapper(request); - } - - // Create a parameter map that merges the URI parameters with the others - if (isMultipart()) - this.parameterMap = new MergedParameterMap(this, this.multipart); - else - this.parameterMap = new MergedParameterMap(this); - } - /** - * Responsible for constructing the MultipartWrapper object and setting it on to - * the instance variable 'multipart'. - * - * @param request the HttpServletRequest to wrap - * this is not a file size limit, but a post size limit. - * @throws StripesServletException if any other error occurs constructing the wrapper - */ - protected void constructMultipartWrapper(HttpServletRequest request) throws StripesServletException { - try { - this.multipart = - StripesFilter.getConfiguration().getMultipartWrapperFactory().wrap(request); - } - catch (IOException e) { - throw new StripesServletException("Could not construct request wrapper.", e); - } - } - - /** Returns true if this request is wrapping a multipart request, false otherwise. */ - public boolean isMultipart() { - return this.multipart != null; - } - - /** - * Fetches just the names of regular parameters and does not include file upload parameters. If - * the request is multipart then the information is sourced from the parsed multipart object - * otherwise it is just pulled out of the request in the usual manner. - */ - @Override - public Enumeration getParameterNames() { - return Collections.enumeration(getParameterMap().keySet()); - } - - /** - * Returns all values sent in the request for a given parameter name. If the request is - * multipart then the information is sourced from the parsed multipart object otherwise it is - * just pulled out of the request in the usual manner. Values are consistent with - * HttpServletRequest.getParameterValues(String). Values for file uploads cannot be retrieved - * in this way (though parameters sent along with file uploads can). - */ - @Override - public String[] getParameterValues(String name) { - /* - * When determining whether to provide a URI parameter's default value, the merged parameter - * map needs to know if the parameter is otherwise defined in the request. It calls this - * method to do that, so if the parameter map is not defined (which it won't be during its - * construction), we delegate to the multipart wrapper or superclass. - */ - - MergedParameterMap map = getParameterMap(); - if (map == null) { - if (isMultipart()) - return this.multipart.getParameterValues(name); - else - return super.getParameterValues(name); - } - else { - return map.get(name); - } - } - - /** - * Retrieves the first value of the specified parameter from the request. If the parameter was - * not sent, null will be returned. - */ - @Override - public String getParameter(String name) { - String[] values = getParameterValues(name); - if (values != null && values.length > 0) { - return values[0]; - } - else { - return null; - } - } - - /** - * If the request is a clean URL, then extract the parameters from the URI and merge with the - * parameters from the query string and/or request body. - */ - @Override - public MergedParameterMap getParameterMap() { - return this.parameterMap; - } - - /** - * Extract new URI parameters from the URI of the given {@code request} and merge them with the - * previous URI parameters. - */ - public void pushUriParameters(HttpServletRequestWrapper request) { - getParameterMap().pushUriParameters(request); - } - - /** - * Restore the URI parameters to the state they were in before the previous call to - * {@link #pushUriParameters(HttpServletRequestWrapper)}. - */ - public void popUriParameters() { - getParameterMap().popUriParameters(); - } - - /** - * Provides access to the Locale being used to process the request. - * @return a Locale object representing the chosen locale for the request. - * @see net.sourceforge.stripes.localization.LocalePicker - */ - @Override - public Locale getLocale() { - return locale; - } - - /** - * Returns a single element enumeration containing the selected Locale for this request. - * @see net.sourceforge.stripes.localization.LocalePicker - */ - @Override - public Enumeration getLocales() { - List list = new ArrayList(); - list.add(this.locale); - return Collections.enumeration(list); - } - - /////////////////////////////////////////////////////////////////////////// - // The following methods are specific to the StripesRequestWrapper and are - // not present in the HttpServletRequest interface. - /////////////////////////////////////////////////////////////////////////// - - /** Used by the dispatcher to set the Locale chosen by the configured LocalePicker. */ - protected void setLocale(Locale locale) { - this.locale = locale; - } - - /** - * Returns the names of request parameters that represent files being uploaded by the user. If - * no file upload parameters are submitted returns an empty enumeration. - */ - public Enumeration getFileParameterNames() { - return this.multipart.getFileParameterNames(); - } - - /** - * Returns a FileBean representing an uploaded file with the form field name = "name". - * If the form field was present in the request, but no file was uploaded, this method will - * return null. - * - * @param name the form field name of type file - * @return a FileBean if a file was actually submitted by the user, otherwise null - */ - public FileBean getFileParameterValue(String name) { - if (this.multipart != null) { - return this.multipart.getFileParameterValue(name); - } - else { - return null; - } - } + MergedParameterMap map = getParameterMap(); + if (map == null) { + if (isMultipart()) return this.multipart.getParameterValues(name); + else return super.getParameterValues(name); + } else { + return map.get(name); + } + } + + /** + * Retrieves the first value of the specified parameter from the request. If the parameter was not + * sent, null will be returned. + */ + @Override + public String getParameter(String name) { + String[] values = getParameterValues(name); + if (values != null && values.length > 0) { + return values[0]; + } else { + return null; + } + } + + /** + * If the request is a clean URL, then extract the parameters from the URI and merge with the + * parameters from the query string and/or request body. + */ + @Override + public MergedParameterMap getParameterMap() { + return this.parameterMap; + } + + /** + * Extract new URI parameters from the URI of the given {@code request} and merge them with the + * previous URI parameters. + */ + public void pushUriParameters(HttpServletRequestWrapper request) { + getParameterMap().pushUriParameters(request); + } + + /** + * Restore the URI parameters to the state they were in before the previous call to {@link + * #pushUriParameters(HttpServletRequestWrapper)}. + */ + public void popUriParameters() { + getParameterMap().popUriParameters(); + } + + /** + * Provides access to the Locale being used to process the request. + * + * @return a Locale object representing the chosen locale for the request. + * @see net.sourceforge.stripes.localization.LocalePicker + */ + @Override + public Locale getLocale() { + return locale; + } + + /** + * Returns a single element enumeration containing the selected Locale for this request. + * + * @see net.sourceforge.stripes.localization.LocalePicker + */ + @Override + public Enumeration getLocales() { + List list = new ArrayList(); + list.add(this.locale); + return Collections.enumeration(list); + } + + /////////////////////////////////////////////////////////////////////////// + // The following methods are specific to the StripesRequestWrapper and are + // not present in the HttpServletRequest interface. + /////////////////////////////////////////////////////////////////////////// + + /** Used by the dispatcher to set the Locale chosen by the configured LocalePicker. */ + protected void setLocale(Locale locale) { + this.locale = locale; + } + + /** + * Returns the names of request parameters that represent files being uploaded by the user. If no + * file upload parameters are submitted returns an empty enumeration. + */ + public Enumeration getFileParameterNames() { + return this.multipart.getFileParameterNames(); + } + + /** + * Returns a FileBean representing an uploaded file with the form field name = "name". + * If the form field was present in the request, but no file was uploaded, this method will return + * null. + * + * @param name the form field name of type file + * @return a FileBean if a file was actually submitted by the user, otherwise null + */ + public FileBean getFileParameterValue(String name) { + if (this.multipart != null) { + return this.multipart.getFileParameterValue(name); + } else { + return null; + } + } } /** * A {@link Map} implementation that is used by {@link StripesRequestWrapper} to merge URI parameter * values with request parameter values on the fly. - * + * * @author Ben Gunter */ class MergedParameterMap implements Map { - class Entry implements Map.Entry { - private String key; - - Entry(String key) { - this.key = key; - } - - public String getKey() { - return key; - } - - public String[] getValue() { - return get(key); - } - - public String[] setValue(String[] value) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean equals(Object obj) { - Entry that = (Entry) obj; - return this.key == that.key; - } - - @Override - public int hashCode() { - return key.hashCode(); - } + class Entry implements Map.Entry { + private String key; - @Override - public String toString() { - return "" + key + "=" + Arrays.deepToString(getValue()); - } + Entry(String key) { + this.key = key; } - private HttpServletRequestWrapper request; - private Map uriParams; - private Stack> uriParamStack; - - MergedParameterMap(HttpServletRequestWrapper request) { - this.request = request; - this.uriParams = getUriParameters(request); - if (this.uriParams == null) { - this.uriParams = Collections.emptyMap(); - } + public String getKey() { + return key; } - MergedParameterMap(HttpServletRequestWrapper request, MultipartWrapper multipart) { - this.request = request; - - // extract URI parameters - Map uriParams = getUriParameters(request); - - /* - * Special handling of parameters if this is a multipart request. The parameters will be - * pulled from the MultipartWrapper and merged in with the URI parameters. - */ - Map multipartParams = null; - Enumeration names = multipart.getParameterNames(); - if (names != null && names.hasMoreElements()) { - multipartParams = new LinkedHashMap(); - while (names.hasMoreElements()) { - String name = (String) names.nextElement(); - multipartParams.put(name, multipart.getParameterValues(name)); - } - } - - // if no multipart params and no URI params then use empty map - if (uriParams == null && multipartParams == null) { - this.uriParams = Collections.emptyMap(); - } - else { - this.uriParams = mergeParameters(uriParams, multipartParams); - } + public String[] getValue() { + return get(key); } - public void clear() { - throw new UnsupportedOperationException(); + public String[] setValue(String[] value) { + throw new UnsupportedOperationException(); } - public boolean containsKey(Object key) { - return getParameterMap().containsKey(key) || uriParams.containsKey(key); - } - - public boolean containsValue(Object value) { - return getParameterMap().containsValue(value) || uriParams.containsValue(value); - } - - public Set> entrySet() { - Set> entries = new LinkedHashSet>(); - for (String key : keySet()) { - entries.add(new Entry(key)); - } - return entries; - } - - public String[] get(Object key) { - if (key == null) - return null; - else - return mergeParameters(getParameterMap().get(key), uriParams.get(key)); - } - - public boolean isEmpty() { - return getParameterMap().isEmpty() && uriParams.isEmpty(); - } - - public Set keySet() { - Set merged = new LinkedHashSet(); - merged.addAll(uriParams.keySet()); - merged.addAll(getParameterMap().keySet()); - return merged; - } - - public String[] put(String key, String[] value) { - throw new UnsupportedOperationException(); - } - - public void putAll(Map m) { - throw new UnsupportedOperationException(); - } - - public String[] remove(Object key) { - throw new UnsupportedOperationException(); - } - - public int size() { - return keySet().size(); + @Override + public boolean equals(Object obj) { + Entry that = (Entry) obj; + return this.key == that.key; } - public Collection values() { - Set keys = keySet(); - List merged = new ArrayList(keys.size()); - for (String key : keys) { - merged.add(mergeParameters(getParameterMap().get(key), uriParams.get(key))); - } - return merged; + @Override + public int hashCode() { + return key.hashCode(); } @Override public String toString() { - StringBuilder buf = new StringBuilder("{ "); - for (Map.Entry entry : entrySet()) { - buf.append(entry).append(", "); - } - if (buf.toString().endsWith(", ")) - buf.setLength(buf.length() - 2); - buf.append(" }"); - return buf.toString(); + return "" + key + "=" + Arrays.deepToString(getValue()); } + } - /** Get the parameter map from the request that is wrapped by the {@link StripesRequestWrapper}. */ - @SuppressWarnings("unchecked") - Map getParameterMap() { - return request == null ? Collections.emptyMap() : request.getRequest().getParameterMap(); - } + private HttpServletRequestWrapper request; + private Map uriParams; + private Stack> uriParamStack; - /** - * Extract new URI parameters from the URI of the given {@code request} and merge them with the - * previous URI parameters. - */ - void pushUriParameters(HttpServletRequestWrapper request) { - if (this.uriParamStack == null) { - this.uriParamStack = new Stack>(); - } - Map map = getUriParameters(request); - this.uriParamStack.push(this.uriParams); - this.uriParams = mergeParameters(new LinkedHashMap(this.uriParams), map); + MergedParameterMap(HttpServletRequestWrapper request) { + this.request = request; + this.uriParams = getUriParameters(request); + if (this.uriParams == null) { + this.uriParams = Collections.emptyMap(); } + } - /** - * Restore the URI parameters to the state they were in before the previous call to - * {@link #pushUriParameters(HttpServletRequestWrapper)}. - */ - void popUriParameters() { - if (this.uriParamStack == null || this.uriParamStack.isEmpty()) { - this.uriParams = null; - } - else { - this.uriParams = this.uriParamStack.pop(); - } - } + MergedParameterMap(HttpServletRequestWrapper request, MultipartWrapper multipart) { + this.request = request; - /** - * Extract any parameters embedded in the URI of the given {@code request} and return them in a - * {@link Map}. If no parameters are present in the URI, then return null. - */ - Map getUriParameters(HttpServletRequest request) { - ActionResolver resolver = StripesFilter.getConfiguration().getActionResolver(); - if (!(resolver instanceof AnnotatedClassActionResolver)) - return null; - - UrlBinding binding = null; - try { - binding = ((AnnotatedClassActionResolver) resolver).getUrlBindingFactory() - .getBinding(request); - } - catch (UrlBindingConflictException e) { - // This can be safely ignored - } + // extract URI parameters + Map uriParams = getUriParameters(request); - Map params = null; - if (binding != null && binding.getParameters().size() > 0) { - for (UrlBindingParameter p : binding.getParameters()) { - String name = p.getName(); - if (name != null) { - String value = p.getValue(); - if (UrlBindingParameter.PARAMETER_NAME_EVENT.equals(name)) { - if (value == null) { - // Don't provide the default event name. The dispatcher will handle that - // automatically, and there's no way of knowing at this point if another - // event name is provided by a different parameter. - continue; - } - else { - name = value; - value = ""; - } - } - if (value == null && request.getParameterValues(name) == null) { - value = p.getDefaultValue(); - } - if (value != null) { - if (params == null) { - params = new LinkedHashMap(); - } - String[] values = params.get(name); - if (values == null) { - values = new String[] { value }; - } - else { - String[] tmp = new String[values.length + 1]; - System.arraycopy(values, 0, tmp, 0, values.length); - tmp[tmp.length - 1] = value; - values = tmp; - } - params.put(name, values); - } - } + /* + * Special handling of parameters if this is a multipart request. The parameters will be + * pulled from the MultipartWrapper and merged in with the URI parameters. + */ + Map multipartParams = null; + Enumeration names = multipart.getParameterNames(); + if (names != null && names.hasMoreElements()) { + multipartParams = new LinkedHashMap(); + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + multipartParams.put(name, multipart.getParameterValues(name)); + } + } + + // if no multipart params and no URI params then use empty map + if (uriParams == null && multipartParams == null) { + this.uriParams = Collections.emptyMap(); + } else { + this.uriParams = mergeParameters(uriParams, multipartParams); + } + } + + public void clear() { + throw new UnsupportedOperationException(); + } + + public boolean containsKey(Object key) { + return getParameterMap().containsKey(key) || uriParams.containsKey(key); + } + + public boolean containsValue(Object value) { + return getParameterMap().containsValue(value) || uriParams.containsValue(value); + } + + public Set> entrySet() { + Set> entries = new LinkedHashSet>(); + for (String key : keySet()) { + entries.add(new Entry(key)); + } + return entries; + } + + public String[] get(Object key) { + if (key == null) return null; + else return mergeParameters(getParameterMap().get(key), uriParams.get(key)); + } + + public boolean isEmpty() { + return getParameterMap().isEmpty() && uriParams.isEmpty(); + } + + public Set keySet() { + Set merged = new LinkedHashSet(); + merged.addAll(uriParams.keySet()); + merged.addAll(getParameterMap().keySet()); + return merged; + } + + public String[] put(String key, String[] value) { + throw new UnsupportedOperationException(); + } + + public void putAll(Map m) { + throw new UnsupportedOperationException(); + } + + public String[] remove(Object key) { + throw new UnsupportedOperationException(); + } + + public int size() { + return keySet().size(); + } + + public Collection values() { + Set keys = keySet(); + List merged = new ArrayList(keys.size()); + for (String key : keys) { + merged.add(mergeParameters(getParameterMap().get(key), uriParams.get(key))); + } + return merged; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder("{ "); + for (Map.Entry entry : entrySet()) { + buf.append(entry).append(", "); + } + if (buf.toString().endsWith(", ")) buf.setLength(buf.length() - 2); + buf.append(" }"); + return buf.toString(); + } + + /** + * Get the parameter map from the request that is wrapped by the {@link StripesRequestWrapper}. + */ + @SuppressWarnings("unchecked") + Map getParameterMap() { + return request == null ? Collections.emptyMap() : request.getRequest().getParameterMap(); + } + + /** + * Extract new URI parameters from the URI of the given {@code request} and merge them with the + * previous URI parameters. + */ + void pushUriParameters(HttpServletRequestWrapper request) { + if (this.uriParamStack == null) { + this.uriParamStack = new Stack>(); + } + Map map = getUriParameters(request); + this.uriParamStack.push(this.uriParams); + this.uriParams = mergeParameters(new LinkedHashMap(this.uriParams), map); + } + + /** + * Restore the URI parameters to the state they were in before the previous call to {@link + * #pushUriParameters(HttpServletRequestWrapper)}. + */ + void popUriParameters() { + if (this.uriParamStack == null || this.uriParamStack.isEmpty()) { + this.uriParams = null; + } else { + this.uriParams = this.uriParamStack.pop(); + } + } + + /** + * Extract any parameters embedded in the URI of the given {@code request} and return them in a + * {@link Map}. If no parameters are present in the URI, then return null. + */ + Map getUriParameters(HttpServletRequest request) { + ActionResolver resolver = StripesFilter.getConfiguration().getActionResolver(); + if (!(resolver instanceof AnnotatedClassActionResolver)) return null; + + UrlBinding binding = null; + try { + binding = + ((AnnotatedClassActionResolver) resolver).getUrlBindingFactory().getBinding(request); + } catch (UrlBindingConflictException e) { + // This can be safely ignored + } + + Map params = null; + if (binding != null && binding.getParameters().size() > 0) { + for (UrlBindingParameter p : binding.getParameters()) { + String name = p.getName(); + if (name != null) { + String value = p.getValue(); + if (UrlBindingParameter.PARAMETER_NAME_EVENT.equals(name)) { + if (value == null) { + // Don't provide the default event name. The dispatcher will handle that + // automatically, and there's no way of knowing at this point if another + // event name is provided by a different parameter. + continue; + } else { + name = value; + value = ""; } - } - - return params; - } - - /** Merge the values from {@code source} into {@code target}. */ - Map mergeParameters(Map target, Map source) { - // target must not be null and we must not modify source - if (target == null) - target = new LinkedHashMap(); - - // nothing to do if source is null or empty - if (source == null || source.isEmpty()) - return target; - - // merge the values from source that already exist in target - for (Map.Entry entry : target.entrySet()) { - entry.setValue(mergeParameters(entry.getValue(), source.get(entry.getKey()))); - } - - // copy the values from source that do not exist in target - for (Map.Entry entry : source.entrySet()) { - if (!target.containsKey(entry.getKey())) { - target.put(entry.getKey(), entry.getValue()); + } + if (value == null && request.getParameterValues(name) == null) { + value = p.getDefaultValue(); + } + if (value != null) { + if (params == null) { + params = new LinkedHashMap(); } - } - - return target; - } - - /** - * Merge request parameter values from the original request with the parameters that are - * embedded in the URI. Either or both arguments may be empty or null. - * - * @param requestParams the parameters from the original request - * @param uriParams parameters extracted from the URI - * @return the merged parameter values - */ - String[] mergeParameters(String[] requestParams, String[] uriParams) { - if (requestParams == null || requestParams.length == 0) { - if (uriParams == null || uriParams.length == 0) - return null; - else - return uriParams; - } - else if (uriParams == null || uriParams.length == 0) { - return requestParams; - } - else { - String[] merged = new String[uriParams.length + requestParams.length]; - System.arraycopy(uriParams, 0, merged, 0, uriParams.length); - System.arraycopy(requestParams, 0, merged, uriParams.length, requestParams.length); - return merged; - } - } + String[] values = params.get(name); + if (values == null) { + values = new String[] {value}; + } else { + String[] tmp = new String[values.length + 1]; + System.arraycopy(values, 0, tmp, 0, values.length); + tmp[tmp.length - 1] = value; + values = tmp; + } + params.put(name, values); + } + } + } + } + + return params; + } + + /** Merge the values from {@code source} into {@code target}. */ + Map mergeParameters( + Map target, Map source) { + // target must not be null and we must not modify source + if (target == null) target = new LinkedHashMap(); + + // nothing to do if source is null or empty + if (source == null || source.isEmpty()) return target; + + // merge the values from source that already exist in target + for (Map.Entry entry : target.entrySet()) { + entry.setValue(mergeParameters(entry.getValue(), source.get(entry.getKey()))); + } + + // copy the values from source that do not exist in target + for (Map.Entry entry : source.entrySet()) { + if (!target.containsKey(entry.getKey())) { + target.put(entry.getKey(), entry.getValue()); + } + } + + return target; + } + + /** + * Merge request parameter values from the original request with the parameters that are embedded + * in the URI. Either or both arguments may be empty or null. + * + * @param requestParams the parameters from the original request + * @param uriParams parameters extracted from the URI + * @return the merged parameter values + */ + String[] mergeParameters(String[] requestParams, String[] uriParams) { + if (requestParams == null || requestParams.length == 0) { + if (uriParams == null || uriParams.length == 0) return null; + else return uriParams; + } else if (uriParams == null || uriParams.length == 0) { + return requestParams; + } else { + String[] merged = new String[uriParams.length + requestParams.length]; + System.arraycopy(uriParams, 0, merged, 0, uriParams.length); + System.arraycopy(requestParams, 0, merged, uriParams.length, requestParams.length); + return merged; + } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/UrlBinding.java b/stripes/src/main/java/net/sourceforge/stripes/controller/UrlBinding.java index 11c95f508..e29ad0e7d 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/UrlBinding.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/UrlBinding.java @@ -17,144 +17,136 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; - import net.sourceforge.stripes.action.ActionBean; /** * Represents a URL binding as declared by a {@link net.sourceforge.stripes.action.UrlBinding} * annotation on an {@link ActionBean} class. - * + * * @author Ben Gunter * @since Stripes 1.5 */ public class UrlBinding { - protected Class beanType; - protected String path, suffix; - protected List components; - protected List parameters; - - /** - * Create a new instance with all its members. Collections passed in will be made immutable. - * - * @param beanType the {@link ActionBean} class to which this binding applies - * @param path the path to which the action is mapped - * @param components list of literal strings that separate the parameters - */ - public UrlBinding(Class beanType, String path, List components) { - this.beanType = beanType; - this.path = path; - - if (components != null && !components.isEmpty()) { - this.components = Collections.unmodifiableList(components); - this.parameters = new ArrayList(components.size()); - - for (Object component : components) { - if (component instanceof UrlBindingParameter) { - this.parameters.add((UrlBindingParameter) component); - } - } - - if (!this.parameters.isEmpty()) { - Object last = components.get(components.size() - 1); - if (last instanceof String) { - this.suffix = (String) last; - } - } - } - else { - this.components = Collections.emptyList(); - this.parameters = Collections.emptyList(); + protected Class beanType; + protected String path, suffix; + protected List components; + protected List parameters; + + /** + * Create a new instance with all its members. Collections passed in will be made immutable. + * + * @param beanType the {@link ActionBean} class to which this binding applies + * @param path the path to which the action is mapped + * @param components list of literal strings that separate the parameters + */ + public UrlBinding(Class beanType, String path, List components) { + this.beanType = beanType; + this.path = path; + + if (components != null && !components.isEmpty()) { + this.components = Collections.unmodifiableList(components); + this.parameters = new ArrayList(components.size()); + + for (Object component : components) { + if (component instanceof UrlBindingParameter) { + this.parameters.add((UrlBindingParameter) component); } - } - - /** - * Create a new instance that takes no parameters. - * - * @param beanType - * @param path - */ - public UrlBinding(Class beanType, String path) { - this.beanType = beanType; - this.path = path; - this.components = Collections.emptyList(); - this.parameters = Collections.emptyList(); - } - - /** - * Get the {@link ActionBean} class to which this binding applies. - */ - public Class getBeanType() { - return beanType; - } - - /** - * Get the list of components that comprise this binding. The components are returned in the - * order in which they appear in the binding definition. - */ - public List getComponents() { - return components; - } - - /** - * Get the list of parameters for this binding. - */ - public List getParameters() { - return parameters; - } + } - /** - * Get the path for this binding. The path is the string of literal characters in the pattern up - * to the first parameter definition. - */ - public String getPath() { - return path; - } - - /** - * If this binding includes one or more parameters and the last component is a {@link String}, - * then this method will return that last component. Otherwise, it returns null. - */ - public String getSuffix() { - return suffix; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof UrlBinding)) - return false; - - UrlBinding that = (UrlBinding) obj; - return this.getBeanType() == that.getBeanType() - && this.getComponents().equals(that.getComponents()); - } - - @Override - public int hashCode() { - return getPath() == null ? 0 : getPath().hashCode(); - } - - @Override - public String toString() { - StringBuilder buf = new StringBuilder(64).append(getPath()); - for (Object component : getComponents()) { - if (component instanceof UrlBindingParameter) { - buf.append('{').append(component).append('}'); - } - else { - buf.append(component); - } + if (!this.parameters.isEmpty()) { + Object last = components.get(components.size() - 1); + if (last instanceof String) { + this.suffix = (String) last; } - return buf.toString(); + } + } else { + this.components = Collections.emptyList(); + this.parameters = Collections.emptyList(); } - - /** - * Ensure the default event name is set if the binding uses the $event parameter. - * Can only be done safely after the event mappings have been processed. - * see http://www.stripesframework.org/jira/browse/STS-803 - */ - public void initDefaultValueWithDefaultHandlerIfNeeded(ActionResolver actionResolver) { - for(UrlBindingParameter parameter : parameters) { - parameter.initDefaultValueWithDefaultHandlerIfNeeded(actionResolver); - } + } + + /** + * Create a new instance that takes no parameters. + * + * @param beanType + * @param path + */ + public UrlBinding(Class beanType, String path) { + this.beanType = beanType; + this.path = path; + this.components = Collections.emptyList(); + this.parameters = Collections.emptyList(); + } + + /** Get the {@link ActionBean} class to which this binding applies. */ + public Class getBeanType() { + return beanType; + } + + /** + * Get the list of components that comprise this binding. The components are returned in the order + * in which they appear in the binding definition. + */ + public List getComponents() { + return components; + } + + /** Get the list of parameters for this binding. */ + public List getParameters() { + return parameters; + } + + /** + * Get the path for this binding. The path is the string of literal characters in the pattern up + * to the first parameter definition. + */ + public String getPath() { + return path; + } + + /** + * If this binding includes one or more parameters and the last component is a {@link String}, + * then this method will return that last component. Otherwise, it returns null. + */ + public String getSuffix() { + return suffix; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof UrlBinding)) return false; + + UrlBinding that = (UrlBinding) obj; + return this.getBeanType() == that.getBeanType() + && this.getComponents().equals(that.getComponents()); + } + + @Override + public int hashCode() { + return getPath() == null ? 0 : getPath().hashCode(); + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(64).append(getPath()); + for (Object component : getComponents()) { + if (component instanceof UrlBindingParameter) { + buf.append('{').append(component).append('}'); + } else { + buf.append(component); + } + } + return buf.toString(); + } + + /** + * Ensure the default event name is set if the binding uses the $event parameter. Can only be done + * safely after the event mappings have been processed. see + * http://www.stripesframework.org/jira/browse/STS-803 + */ + public void initDefaultValueWithDefaultHandlerIfNeeded(ActionResolver actionResolver) { + for (UrlBindingParameter parameter : parameters) { + parameter.initDefaultValueWithDefaultHandlerIfNeeded(actionResolver); } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/UrlBindingFactory.java b/stripes/src/main/java/net/sourceforge/stripes/controller/UrlBindingFactory.java index 31729376c..9af7acbfe 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/UrlBindingFactory.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/UrlBindingFactory.java @@ -14,6 +14,7 @@ */ package net.sourceforge.stripes.controller; +import jakarta.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -23,13 +24,10 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; -import java.util.Map.Entry; - -import javax.servlet.http.HttpServletRequest; - import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.exception.UrlBindingConflictException; import net.sourceforge.stripes.util.HttpUtil; @@ -37,690 +35,675 @@ import net.sourceforge.stripes.util.bean.ParseException; /** - *

    * Provides access to {@link UrlBinding} objects. Bindings are used in two contexts: + * *

      - *
    • As a prototype: Binding prototypes provide static information about the - * binding, such as the URI path, string literals, parameter names and default values. However, the - * parameters associated with a prototype do not have a value since they are not evaluated against a - * live request.
    • - *
    • "Live": Bindings that have been evaluated against a live servlet request - * or request URI are exactly like their prototypes except that the parameter values associated with - * them contain the values (if any) that were extracted from the URI.
    • + *
    • As a prototype: Binding prototypes provide static information about the + * binding, such as the URI path, string literals, parameter names and default values. + * However, the parameters associated with a prototype do not have a value since they are not + * evaluated against a live request. + *
    • "Live": Bindings that have been evaluated against a live servlet request + * or request URI are exactly like their prototypes except that the parameter values + * associated with them contain the values (if any) that were extracted from the URI. *
    - *

    - * + * * @author Ben Gunter * @since Stripes 1.5 * @see UrlBinding * @see UrlBindingParameter */ public class UrlBindingFactory { - private static final Log log = Log.getInstance(UrlBindingFactory.class); - - /** Maps {@link ActionBean} classes to {@link UrlBinding}s */ - private final Map, UrlBinding> classCache = new HashMap, UrlBinding>(); - - /** Maps simple paths to {@link UrlBinding}s */ - private final Map pathCache = new HashMap(); - - /** Keeps a list of all the paths that could not be cached due to conflicts between URL bindings */ - private final Map> pathConflicts = new HashMap>(); - - /** Holds the set of paths that are cached, sorted from longest to shortest */ - private final Map> prefixCache = new TreeMap>( - new Comparator() { - public int compare(String a, String b) { - int cmp = b.length() - a.length(); - return cmp == 0 ? a.compareTo(b) : cmp; - } - }); - - /** - * Get all the classes implementing {@link ActionBean} - */ - public Collection> getActionBeanClasses() { - return Collections.unmodifiableSet(classCache.keySet()); + private static final Log log = Log.getInstance(UrlBindingFactory.class); + + /** Maps {@link ActionBean} classes to {@link UrlBinding}s */ + private final Map, UrlBinding> classCache = + new HashMap, UrlBinding>(); + + /** Maps simple paths to {@link UrlBinding}s */ + private final Map pathCache = new HashMap(); + + /** + * Keeps a list of all the paths that could not be cached due to conflicts between URL bindings + */ + private final Map> pathConflicts = + new HashMap>(); + + /** Holds the set of paths that are cached, sorted from longest to shortest */ + private final Map> prefixCache = + new TreeMap>( + new Comparator() { + public int compare(String a, String b) { + int cmp = b.length() - a.length(); + return cmp == 0 ? a.compareTo(b) : cmp; + } + }); + + /** Get all the classes implementing {@link ActionBean} */ + public Collection> getActionBeanClasses() { + return Collections.unmodifiableSet(classCache.keySet()); + } + + /** + * Get the {@link UrlBinding} prototype associated with the given {@link ActionBean} type. This + * method may return null if no binding is associated with the given type. + * + * @param type a class that implements {@link ActionBean} + * @return a binding object if one is defined or null if not + */ + public UrlBinding getBindingPrototype(Class type) { + UrlBinding binding = classCache.get(type); + if (binding != null) return binding; + + binding = parseUrlBinding(type); + if (binding != null) addBinding(type, binding); + return binding; + } + + /** + * Examines a URI (as returned by {@link HttpUtil#getRequestedPath(HttpServletRequest)}) and + * returns the associated binding prototype, if any. No attempt is made to extract parameter + * values from the URI. This is intended as a fast means to get static information associated with + * a given request URI. + * + * @param uri a request URI + * @return a binding prototype, or null if the URI does not match + */ + public UrlBinding getBindingPrototype(String uri) { + // Look for an exact match to the URI first + UrlBinding prototype = pathCache.get(uri); + if (prototype != null) { + log.debug("Matched ", uri, " to ", prototype); + return prototype; + } else if (pathConflicts.containsKey(uri)) { + List strings = new ArrayList(); + for (UrlBinding conflict : pathConflicts.get(uri)) strings.add(conflict.toString()); + throw new UrlBindingConflictException(uri, strings); } - /** - * Get the {@link UrlBinding} prototype associated with the given {@link ActionBean} type. This - * method may return null if no binding is associated with the given type. - * - * @param type a class that implements {@link ActionBean} - * @return a binding object if one is defined or null if not - */ - public UrlBinding getBindingPrototype(Class type) { - UrlBinding binding = classCache.get(type); - if (binding != null) - return binding; - - binding = parseUrlBinding(type); - if (binding != null) - addBinding(type, binding); - return binding; + // Get all the bindings whose prefix matches the URI + Set candidates = null; + for (Entry> entry : prefixCache.entrySet()) { + if (uri.startsWith(entry.getKey())) { + candidates = entry.getValue(); + break; + } } - /** - * Examines a URI (as returned by {@link HttpUtil#getRequestedPath(HttpServletRequest)}) and - * returns the associated binding prototype, if any. No attempt is made to extract parameter - * values from the URI. This is intended as a fast means to get static information associated - * with a given request URI. - * - * @param uri a request URI - * @return a binding prototype, or null if the URI does not match - */ - public UrlBinding getBindingPrototype(String uri) { - // Look for an exact match to the URI first - UrlBinding prototype = pathCache.get(uri); - if (prototype != null) { - log.debug("Matched ", uri, " to ", prototype); - return prototype; - } - else if (pathConflicts.containsKey(uri)) { - List strings = new ArrayList(); - for (UrlBinding conflict : pathConflicts.get(uri)) - strings.add(conflict.toString()); - throw new UrlBindingConflictException(uri, strings); - } - - // Get all the bindings whose prefix matches the URI - Set candidates = null; - for (Entry> entry : prefixCache.entrySet()) { - if (uri.startsWith(entry.getKey())) { - candidates = entry.getValue(); - break; - } - } - - // If none matched or exactly one matched then return now - if (candidates == null) { - log.debug("No URL binding matches ", uri); - return null; - } - else if (candidates.size() == 1) { - log.debug("Matched ", uri, " to ", candidates); - return candidates.iterator().next(); - } - - // Now find the one that matches deepest into the URI with the fewest components - int maxIndex = 0, minComponentCount = Integer.MAX_VALUE, maxComponentMatch = 0; - List conflicts = null; - for (UrlBinding binding : candidates) { - int idx = binding.getPath().length(); - List components = binding.getComponents(); - int componentCount = components.size(), componentMatch = 0; - - for (Object component : components) { - if (!(component instanceof String)) - continue; - - String string = (String) component; - int at = uri.indexOf(string, idx); - if (at >= 0) { - idx = at + string.length(); - ++componentMatch; - } - else if (binding.getSuffix() != null) { - // Prefer suffix matches - string = binding.getSuffix(); - at = uri.indexOf(string, idx); - if (at >= 0) { - idx = at + string.length(); - ++componentMatch; - } - break; - } - else { - break; - } - } - - boolean betterMatch = idx > maxIndex - || (idx == maxIndex && (componentCount < minComponentCount || componentMatch > maxComponentMatch)); + // If none matched or exactly one matched then return now + if (candidates == null) { + log.debug("No URL binding matches ", uri); + return null; + } else if (candidates.size() == 1) { + log.debug("Matched ", uri, " to ", candidates); + return candidates.iterator().next(); + } - if (betterMatch) { - if (conflicts != null) - conflicts.clear(); - prototype = binding; - maxIndex = idx; - minComponentCount = componentCount; - maxComponentMatch = componentMatch; - } - else if (idx == maxIndex && componentCount == minComponentCount) { - if (conflicts == null) { - conflicts = new ArrayList(candidates.size()); - conflicts.add(prototype.toString()); - } - conflicts.add(binding.toString()); - prototype = null; - } + // Now find the one that matches deepest into the URI with the fewest components + int maxIndex = 0, minComponentCount = Integer.MAX_VALUE, maxComponentMatch = 0; + List conflicts = null; + for (UrlBinding binding : candidates) { + int idx = binding.getPath().length(); + List components = binding.getComponents(); + int componentCount = components.size(), componentMatch = 0; + + for (Object component : components) { + if (!(component instanceof String)) continue; + + String string = (String) component; + int at = uri.indexOf(string, idx); + if (at >= 0) { + idx = at + string.length(); + ++componentMatch; + } else if (binding.getSuffix() != null) { + // Prefer suffix matches + string = binding.getSuffix(); + at = uri.indexOf(string, idx); + if (at >= 0) { + idx = at + string.length(); + ++componentMatch; + } + break; + } else { + break; } - - log.debug("Matched @", maxIndex, " ", uri, " to ", prototype == null ? conflicts : prototype); - if (prototype == null) { - throw new UrlBindingConflictException(uri, conflicts); + } + + boolean betterMatch = + idx > maxIndex + || (idx == maxIndex + && (componentCount < minComponentCount || componentMatch > maxComponentMatch)); + + if (betterMatch) { + if (conflicts != null) conflicts.clear(); + prototype = binding; + maxIndex = idx; + minComponentCount = componentCount; + maxComponentMatch = componentMatch; + } else if (idx == maxIndex && componentCount == minComponentCount) { + if (conflicts == null) { + conflicts = new ArrayList(candidates.size()); + conflicts.add(prototype.toString()); } - - return prototype; + conflicts.add(binding.toString()); + prototype = null; + } } - /** - * Examines a servlet request and returns the associated binding prototype, if any. No attempt - * is made to extract parameter values from the URI. This is intended as a fast means to get - * static information associated with a given request. - * - * @param request a servlet request - * @return a binding prototype, or null if the request URI does not match - */ - public UrlBinding getBindingPrototype(HttpServletRequest request) { - return getBindingPrototype(HttpUtil.getRequestedPath(request)); + log.debug("Matched @", maxIndex, " ", uri, " to ", prototype == null ? conflicts : prototype); + if (prototype == null) { + throw new UrlBindingConflictException(uri, conflicts); } - /** - * Examines a URI (as returned by {@link HttpUtil#getRequestedPath(HttpServletRequest)}) and - * returns the associated binding, if any. Parameters will be extracted from the URI, and the - * {@link UrlBindingParameter} objects returned by {@link UrlBinding#getParameters()} will - * contain the values that are present in the URI. - * - * @param uri a request URI - * @return a binding prototype, or null if the URI does not match - */ - public UrlBinding getBinding(String uri) { - UrlBinding prototype = getBindingPrototype(uri); - if (prototype == null) - return null; - - // check for literal suffix in prototype and ignore it if found - int length = uri.length(); - String suffix = prototype.getSuffix(); - if (suffix != null && uri.endsWith(suffix)) { - length -= suffix.length(); - } - - // ignore trailing slashes in the URI - while (length > 0 && uri.charAt(length - 1) == '/') - --length; - - // extract the request parameters and add to new binding object - ArrayList components = new ArrayList(prototype.getComponents().size()); - int index = prototype.getPath().length(); - UrlBindingParameter current = null; - String value = null; - Iterator iter = prototype.getComponents().iterator(); - while (index < length && iter.hasNext()) { - Object component = iter.next(); - if (component instanceof String) { - // extract the parameter value from the URI - String literal = (String) component; - int end = uri.indexOf(literal, index); - if (end >= 0) { - value = uri.substring(index, end); - index = end + literal.length(); - } - else { - value = uri.substring(index, length); - index = length; - } - - // add to the binding - if (current != null && value != null && value.length() > 0) { - components.add(new UrlBindingParameter(current, value)); - components.add(component); - current = null; - value = null; - } - } - else if (component instanceof UrlBindingParameter) { - current = (UrlBindingParameter) component; - } - } + return prototype; + } + + /** + * Examines a servlet request and returns the associated binding prototype, if any. No attempt is + * made to extract parameter values from the URI. This is intended as a fast means to get static + * information associated with a given request. + * + * @param request a servlet request + * @return a binding prototype, or null if the request URI does not match + */ + public UrlBinding getBindingPrototype(HttpServletRequest request) { + return getBindingPrototype(HttpUtil.getRequestedPath(request)); + } + + /** + * Examines a URI (as returned by {@link HttpUtil#getRequestedPath(HttpServletRequest)}) and + * returns the associated binding, if any. Parameters will be extracted from the URI, and the + * {@link UrlBindingParameter} objects returned by {@link UrlBinding#getParameters()} will contain + * the values that are present in the URI. + * + * @param uri a request URI + * @return a binding prototype, or null if the URI does not match + */ + public UrlBinding getBinding(String uri) { + UrlBinding prototype = getBindingPrototype(uri); + if (prototype == null) return null; + + // check for literal suffix in prototype and ignore it if found + int length = uri.length(); + String suffix = prototype.getSuffix(); + if (suffix != null && uri.endsWith(suffix)) { + length -= suffix.length(); + } - // if component iterator ended before end of string, then grab remainder of string - if (index < length) { - value = uri.substring(index, length); + // ignore trailing slashes in the URI + while (length > 0 && uri.charAt(length - 1) == '/') --length; + + // extract the request parameters and add to new binding object + ArrayList components = new ArrayList(prototype.getComponents().size()); + int index = prototype.getPath().length(); + UrlBindingParameter current = null; + String value = null; + Iterator iter = prototype.getComponents().iterator(); + while (index < length && iter.hasNext()) { + Object component = iter.next(); + if (component instanceof String) { + // extract the parameter value from the URI + String literal = (String) component; + int end = uri.indexOf(literal, index); + if (end >= 0) { + value = uri.substring(index, end); + index = end + literal.length(); + } else { + value = uri.substring(index, length); + index = length; } - // parameter was last component in list + // add to the binding if (current != null && value != null && value.length() > 0) { - components.add(new UrlBindingParameter(current, value)); - } - - // ensure all components are included so default parameter values are available - while (iter.hasNext()) { - Object component = iter.next(); - if (component instanceof UrlBindingParameter) { - components.add(new UrlBindingParameter((UrlBindingParameter) component)); - } - else { - components.add(component); - } + components.add(new UrlBindingParameter(current, value)); + components.add(component); + current = null; + value = null; } - - return new UrlBinding(prototype.getBeanType(), prototype.getPath(), components); + } else if (component instanceof UrlBindingParameter) { + current = (UrlBindingParameter) component; + } } - /** - * Examines a servlet request and returns the associated binding, if any. Parameters will be - * extracted from the request, and the {@link UrlBindingParameter} objects returned by - * {@link UrlBinding#getParameters()} will contain the values that are present in the request. - * - * @param request a servlet request - * @return if the request matches a defined binding, then this method should return that - * binding. Otherwise, this method should return null. - */ - public UrlBinding getBinding(HttpServletRequest request) { - return getBinding(HttpUtil.getRequestedPath(request)); + // if component iterator ended before end of string, then grab remainder of string + if (index < length) { + value = uri.substring(index, length); } - /** - * Get all the {@link ActionBean}s classes that have been found. - * - * @return an immutable collection of {@link ActionBean} classes - */ - public HashMap> getPathMap() { - HashMap> map = new HashMap>(); - for (Entry entry : pathCache.entrySet()) { - if (entry.getValue() != null) { - map.put(entry.getKey(), entry.getValue().getBeanType()); - } - } - return map; + // parameter was last component in list + if (current != null && value != null && value.length() > 0) { + components.add(new UrlBindingParameter(current, value)); } - /** - * Map an {@link ActionBean} to a URL. - * - * @param beanType the {@link ActionBean} class - * @param binding the URL binding - */ - public void addBinding(Class beanType, UrlBinding binding) { - /* - * Search for a class that has already been added with the same name as the class being - * added now. If one is found then remove its information first and then proceed with adding - * it. I know this is not technically correct because two classes from two different class - * loaders can have the same name, but this feature is valuable for extensions that reload - * classes and I consider it highly unlikely to be a problem in practice. - */ - Class existing = null; - for (Class c : classCache.keySet()) { - if (c.getName().equals(beanType.getName())) { - existing = c; - break; - } - } - if (existing != null) - removeBinding(existing); - - // And now we can safely add the class - for (String path : getCachedPaths(binding)) { - cachePath(path, binding); - } - for (String prefix : getCachedPrefixes(binding)) { - cachePrefix(prefix, binding); - } - classCache.put(beanType, binding); + // ensure all components are included so default parameter values are available + while (iter.hasNext()) { + Object component = iter.next(); + if (component instanceof UrlBindingParameter) { + components.add(new UrlBindingParameter((UrlBindingParameter) component)); + } else { + components.add(component); + } } - /** - * Removes an {@link ActionBean}'s URL binding. - * - * @param beanType the {@link ActionBean} class + return new UrlBinding(prototype.getBeanType(), prototype.getPath(), components); + } + + /** + * Examines a servlet request and returns the associated binding, if any. Parameters will be + * extracted from the request, and the {@link UrlBindingParameter} objects returned by {@link + * UrlBinding#getParameters()} will contain the values that are present in the request. + * + * @param request a servlet request + * @return if the request matches a defined binding, then this method should return that binding. + * Otherwise, this method should return null. + */ + public UrlBinding getBinding(HttpServletRequest request) { + return getBinding(HttpUtil.getRequestedPath(request)); + } + + /** + * Get all the {@link ActionBean}s classes that have been found. + * + * @return an immutable collection of {@link ActionBean} classes + */ + public HashMap> getPathMap() { + HashMap> map = + new HashMap>(); + for (Entry entry : pathCache.entrySet()) { + if (entry.getValue() != null) { + map.put(entry.getKey(), entry.getValue().getBeanType()); + } + } + return map; + } + + /** + * Map an {@link ActionBean} to a URL. + * + * @param beanType the {@link ActionBean} class + * @param binding the URL binding + */ + public void addBinding(Class beanType, UrlBinding binding) { + /* + * Search for a class that has already been added with the same name as the class being + * added now. If one is found then remove its information first and then proceed with adding + * it. I know this is not technically correct because two classes from two different class + * loaders can have the same name, but this feature is valuable for extensions that reload + * classes and I consider it highly unlikely to be a problem in practice. */ - public synchronized void removeBinding(Class beanType) { - UrlBinding binding = classCache.get(beanType); - if (binding == null) - return; - - Set resolvedConflicts = null; - for (String path : getCachedPaths(binding)) { - log.debug("Clearing cached path ", path, " for ", binding); - pathCache.remove(path); - - List conflicts = pathConflicts.get(path); - if (conflicts != null) { - log.debug("Removing ", binding, " from conflicts list ", conflicts); - conflicts.remove(binding); - - if (conflicts.size() == 1) { - if (resolvedConflicts == null) { - resolvedConflicts = new LinkedHashSet(); - } - - resolvedConflicts.add(pathCache.get(conflicts.get(0))); - conflicts.clear(); - } - - if (conflicts.isEmpty()) - pathConflicts.remove(path); - } - } + Class existing = null; + for (Class c : classCache.keySet()) { + if (c.getName().equals(beanType.getName())) { + existing = c; + break; + } + } + if (existing != null) removeBinding(existing); - for (String prefix : getCachedPrefixes(binding)) { - Set bindings = prefixCache.get(prefix); - if (bindings != null) { - log.debug("Clearing cached prefix ", prefix, " for ", binding); - bindings.remove(binding); - if (bindings.isEmpty()) - prefixCache.remove(prefix); - } + // And now we can safely add the class + for (String path : getCachedPaths(binding)) { + cachePath(path, binding); + } + for (String prefix : getCachedPrefixes(binding)) { + cachePrefix(prefix, binding); + } + classCache.put(beanType, binding); + } + + /** + * Removes an {@link ActionBean}'s URL binding. + * + * @param beanType the {@link ActionBean} class + */ + public synchronized void removeBinding(Class beanType) { + UrlBinding binding = classCache.get(beanType); + if (binding == null) return; + + Set resolvedConflicts = null; + for (String path : getCachedPaths(binding)) { + log.debug("Clearing cached path ", path, " for ", binding); + pathCache.remove(path); + + List conflicts = pathConflicts.get(path); + if (conflicts != null) { + log.debug("Removing ", binding, " from conflicts list ", conflicts); + conflicts.remove(binding); + + if (conflicts.size() == 1) { + if (resolvedConflicts == null) { + resolvedConflicts = new LinkedHashSet(); + } + + resolvedConflicts.add(pathCache.get(conflicts.get(0))); + conflicts.clear(); } - classCache.remove(beanType); - - if (resolvedConflicts != null) { - log.debug("Resolved conflicts with ", resolvedConflicts); - - for (UrlBinding conflict : resolvedConflicts) { - removeBinding(conflict.getBeanType()); - addBinding(conflict.getBeanType(), conflict); - } - } + if (conflicts.isEmpty()) pathConflicts.remove(path); + } } - /** - * Get a list of the request paths that will be wired directly to an ActionBean. In some cases, - * a single path might be valid for more than one ActionBean. In such a case, a warning will be - * logged at startup and an exception will be thrown if the conflicting path is requested. - */ - protected Set getCachedPaths(UrlBinding binding) { - Set paths = new TreeSet(); - - // Wire some paths directly to the ActionBean (path, path + /, path + suffix, etc.) - paths.add(binding.getPath()); - paths.add(binding.toString()); - if (!binding.getPath().endsWith("/")) - paths.add(binding.getPath() + '/'); - if (binding.getSuffix() != null) - paths.add(binding.getPath() + binding.getSuffix()); - - return paths; + for (String prefix : getCachedPrefixes(binding)) { + Set bindings = prefixCache.get(prefix); + if (bindings != null) { + log.debug("Clearing cached prefix ", prefix, " for ", binding); + bindings.remove(binding); + if (bindings.isEmpty()) prefixCache.remove(prefix); + } } - /** - * Get a list of the request path prefixes that could map to an ActionBean. A single - * prefix may map to multiple ActionBeans. In such a case, we attempt to determine the best - * match based on the literal strings and parameters defined in the ActionBeans' URL bindings. - * If no single ActionBean is determined to be a best match, then an exception is thrown to - * report the conflict. - */ - protected Set getCachedPrefixes(UrlBinding binding) { - Set prefixes = new TreeSet(); - - // Add binding as a candidate for some prefixes (path + /, path + leading literal, etc.) - if (binding.getPath().endsWith("/")) - prefixes.add(binding.getPath()); - else - prefixes.add(binding.getPath() + '/'); - - List components = binding.getComponents(); - if (components != null && !components.isEmpty() && components.get(0) instanceof String) { - prefixes.add(binding.getPath() + components.get(0)); - } + classCache.remove(beanType); - return prefixes; - } + if (resolvedConflicts != null) { + log.debug("Resolved conflicts with ", resolvedConflicts); - /** - * Map a path directly to a binding. If the path matches more than one binding, then a warning - * will be logged indicating such a condition, and the path will not be cached for any binding. - * - * @param path The path to cache - * @param binding The binding to which the path should map - */ - protected void cachePath(String path, UrlBinding binding) { - if (pathCache.containsKey(path)) { - // Put a null value in the map to indicate a conflict - UrlBinding conflict = pathCache.put(path, null); - - // Construct a list of conflicting bindings - List conflicts = pathConflicts.get(path); - if (conflicts == null) { - conflicts = new ArrayList(); - conflicts.add(conflict); - pathConflicts.put(path, conflicts); - } - conflicts.add(binding); - - // If there is exactly one binding for this path that declares no parameters, then it is - // a static binding and should take precedence over dynamic ones. - UrlBinding statik = null; - if (conflicts.size() > 1) { - for (UrlBinding ub : conflicts) { - if (ub.getParameters().isEmpty()) { - if (statik == null) { - statik = ub; - } - else { - statik = null; - break; - } - } - } - } + for (UrlBinding conflict : resolvedConflicts) { + removeBinding(conflict.getBeanType()); + addBinding(conflict.getBeanType(), conflict); + } + } + } + + /** + * Get a list of the request paths that will be wired directly to an ActionBean. In some cases, a + * single path might be valid for more than one ActionBean. In such a case, a warning will be + * logged at startup and an exception will be thrown if the conflicting path is requested. + */ + protected Set getCachedPaths(UrlBinding binding) { + Set paths = new TreeSet(); + + // Wire some paths directly to the ActionBean (path, path + /, path + suffix, etc.) + paths.add(binding.getPath()); + paths.add(binding.toString()); + if (!binding.getPath().endsWith("/")) paths.add(binding.getPath() + '/'); + if (binding.getSuffix() != null) paths.add(binding.getPath() + binding.getSuffix()); + + return paths; + } + + /** + * Get a list of the request path prefixes that could map to an ActionBean. A single + * prefix may map to multiple ActionBeans. In such a case, we attempt to determine the best match + * based on the literal strings and parameters defined in the ActionBeans' URL bindings. If no + * single ActionBean is determined to be a best match, then an exception is thrown to report the + * conflict. + */ + protected Set getCachedPrefixes(UrlBinding binding) { + Set prefixes = new TreeSet(); + + // Add binding as a candidate for some prefixes (path + /, path + leading literal, etc.) + if (binding.getPath().endsWith("/")) prefixes.add(binding.getPath()); + else prefixes.add(binding.getPath() + '/'); + + List components = binding.getComponents(); + if (components != null && !components.isEmpty() && components.get(0) instanceof String) { + prefixes.add(binding.getPath() + components.get(0)); + } - // Replace the path cache entry if necessary and log a warning + return prefixes; + } + + /** + * Map a path directly to a binding. If the path matches more than one binding, then a warning + * will be logged indicating such a condition, and the path will not be cached for any binding. + * + * @param path The path to cache + * @param binding The binding to which the path should map + */ + protected void cachePath(String path, UrlBinding binding) { + if (pathCache.containsKey(path)) { + // Put a null value in the map to indicate a conflict + UrlBinding conflict = pathCache.put(path, null); + + // Construct a list of conflicting bindings + List conflicts = pathConflicts.get(path); + if (conflicts == null) { + conflicts = new ArrayList(); + conflicts.add(conflict); + pathConflicts.put(path, conflicts); + } + conflicts.add(binding); + + // If there is exactly one binding for this path that declares no parameters, then it is + // a static binding and should take precedence over dynamic ones. + UrlBinding statik = null; + if (conflicts.size() > 1) { + for (UrlBinding ub : conflicts) { + if (ub.getParameters().isEmpty()) { if (statik == null) { - log.debug("The path ", path, " for ", binding.getBeanType().getName(), " @ ", - binding, " conflicts with ", conflicts); + statik = ub; + } else { + statik = null; + break; } - else { - log.debug("For path ", path, ", static binding ", statik, - " supersedes conflicting bindings ", conflicts); - pathCache.put(path, statik); - } - } - else { - log.debug("Wiring path ", path, " to ", binding.getBeanType().getName(), " @ ", binding); - pathCache.put(path, binding); + } } + } + + // Replace the path cache entry if necessary and log a warning + if (statik == null) { + log.debug( + "The path ", + path, + " for ", + binding.getBeanType().getName(), + " @ ", + binding, + " conflicts with ", + conflicts); + } else { + log.debug( + "For path ", + path, + ", static binding ", + statik, + " supersedes conflicting bindings ", + conflicts); + pathCache.put(path, statik); + } + } else { + log.debug("Wiring path ", path, " to ", binding.getBeanType().getName(), " @ ", binding); + pathCache.put(path, binding); } - - /** - * Add a binding to the set of bindings associated with a prefix. - * - * @param prefix The prefix to cache - * @param binding The binding to map to the prefix - */ - protected void cachePrefix(String prefix, UrlBinding binding) { - log.debug("Wiring prefix ", prefix, "* to ", binding.getBeanType().getName(), " @ ", binding); - - // Look up existing set of bindings to which the prefix maps - Set bindings = prefixCache.get(prefix); - - // If necessary, create and store a new set of bindings - if (bindings == null) { - bindings = new TreeSet(new Comparator() { + } + + /** + * Add a binding to the set of bindings associated with a prefix. + * + * @param prefix The prefix to cache + * @param binding The binding to map to the prefix + */ + protected void cachePrefix(String prefix, UrlBinding binding) { + log.debug("Wiring prefix ", prefix, "* to ", binding.getBeanType().getName(), " @ ", binding); + + // Look up existing set of bindings to which the prefix maps + Set bindings = prefixCache.get(prefix); + + // If necessary, create and store a new set of bindings + if (bindings == null) { + bindings = + new TreeSet( + new Comparator() { public int compare(UrlBinding o1, UrlBinding o2) { - int cmp = o1.getComponents().size() - o2.getComponents().size(); - if (cmp == 0) - cmp = o1.toString().compareTo(o2.toString()); - return cmp; + int cmp = o1.getComponents().size() - o2.getComponents().size(); + if (cmp == 0) cmp = o1.toString().compareTo(o2.toString()); + return cmp; } - }); - prefixCache.put(prefix, bindings); - } - - // Add the binding to the set - bindings.add(binding); + }); + prefixCache.put(prefix, bindings); } - /** - * Look for a binding pattern for the given {@link ActionBean} class, specified by the - * {@link net.sourceforge.stripes.action.UrlBinding} annotation. If the annotation is found, - * create and return a {@link UrlBinding} object for the class. Otherwise, return null. - * - * @param beanType The {@link ActionBean} type whose binding is to be parsed - * @return A {@link UrlBinding} if one is specified, or null if not. - * @throws ParseException If the pattern cannot be parsed - */ - public static UrlBinding parseUrlBinding(Class beanType) { - // check that class is annotated - net.sourceforge.stripes.action.UrlBinding annotation = beanType - .getAnnotation(net.sourceforge.stripes.action.UrlBinding.class); - if (annotation == null) - return null; - else - return parseUrlBinding(beanType, annotation.value()); + // Add the binding to the set + bindings.add(binding); + } + + /** + * Look for a binding pattern for the given {@link ActionBean} class, specified by the {@link + * net.sourceforge.stripes.action.UrlBinding} annotation. If the annotation is found, create and + * return a {@link UrlBinding} object for the class. Otherwise, return null. + * + * @param beanType The {@link ActionBean} type whose binding is to be parsed + * @return A {@link UrlBinding} if one is specified, or null if not. + * @throws ParseException If the pattern cannot be parsed + */ + public static UrlBinding parseUrlBinding(Class beanType) { + // check that class is annotated + net.sourceforge.stripes.action.UrlBinding annotation = + beanType.getAnnotation(net.sourceforge.stripes.action.UrlBinding.class); + if (annotation == null) return null; + else return parseUrlBinding(beanType, annotation.value()); + } + + /** + * Parse the binding pattern and create a {@link UrlBinding} object for the {@link ActionBean} + * class. If pattern is null, then return null. + * + * @param beanType The {@link ActionBean} type to be mapped to the pattern. + * @param pattern The URL binding pattern to parse. + * @return A {@link UrlBinding} or null if the pattern is null + * @throws ParseException If the pattern cannot be parsed + */ + public static UrlBinding parseUrlBinding(Class beanType, String pattern) { + // check that value is not null + if (pattern == null) return null; + + // make sure it starts with / + if (!pattern.startsWith("/")) { + throw new ParseException(pattern, "A URL binding must begin with /"); } - /** - * Parse the binding pattern and create a {@link UrlBinding} object for the {@link ActionBean} - * class. If pattern is null, then return null. - * - * @param beanType The {@link ActionBean} type to be mapped to the pattern. - * @param pattern The URL binding pattern to parse. - * @return A {@link UrlBinding} or null if the pattern is null - * @throws ParseException If the pattern cannot be parsed - */ - public static UrlBinding parseUrlBinding(Class beanType, String pattern) { - // check that value is not null - if (pattern == null) - return null; - - // make sure it starts with / - if (!pattern.startsWith("/")) { - throw new ParseException(pattern, "A URL binding must begin with /"); - } - - // parse the pattern - String path = null; - List components = new ArrayList(); - boolean brace = false, escape = false; - char[] chars = pattern.toCharArray(); - StringBuilder buf = new StringBuilder(pattern.length()); - char c = 0; - for (int i = 0; i < chars.length; i++) { - c = chars[i]; - if (!escape) { - switch (c) { - case '{': - if (!brace) { - brace = true; - if (path == null) { - // extract trailing non-alphanum chars as a literal to trim the path - int end = buf.length() - 1; - while (end >= 0 && !Character.isJavaIdentifierPart(buf.charAt(end))) - --end; - if (end < 0) { - path = buf.toString(); - } - else { - ++end; - path = buf.substring(0, end); - components.add(buf.substring(end)); - } - } - else { - components.add(buf.toString()); - } - buf.setLength(0); - continue; - } - break; - case '}': - if (brace) { - brace = false; - components.add(parseUrlBindingParameter(beanType, buf.toString())); - buf.setLength(0); - continue; - } - break; - case '\\': - escape = true; - - // Preserve escape characters for parameter name parser - if (brace) { - buf.append(c); - } - - continue; + // parse the pattern + String path = null; + List components = new ArrayList(); + boolean brace = false, escape = false; + char[] chars = pattern.toCharArray(); + StringBuilder buf = new StringBuilder(pattern.length()); + char c = 0; + for (int i = 0; i < chars.length; i++) { + c = chars[i]; + if (!escape) { + switch (c) { + case '{': + if (!brace) { + brace = true; + if (path == null) { + // extract trailing non-alphanum chars as a literal to trim the path + int end = buf.length() - 1; + while (end >= 0 && !Character.isJavaIdentifierPart(buf.charAt(end))) --end; + if (end < 0) { + path = buf.toString(); + } else { + ++end; + path = buf.substring(0, end); + components.add(buf.substring(end)); } + } else { + components.add(buf.toString()); + } + buf.setLength(0); + continue; + } + break; + case '}': + if (brace) { + brace = false; + components.add(parseUrlBindingParameter(beanType, buf.toString())); + buf.setLength(0); + continue; } + break; + case '\\': + escape = true; - // append the char - buf.append(c); - escape = false; - } + // Preserve escape characters for parameter name parser + if (brace) { + buf.append(c); + } - // Were we led to expect more characters? - if (escape) - throw new ParseException(pattern, "Expression must not end with escape character"); - else if (brace) - throw new ParseException(pattern, "Unterminated left brace ('{') in expression"); - - // handle whatever is left - if (buf.length() > 0) { - if (path == null) - path = buf.toString(); - else if (c == '}') - components.add(parseUrlBindingParameter(beanType, buf.toString())); - else - components.add(buf.toString()); + continue; } + } - return new UrlBinding(beanType, path, components); + // append the char + buf.append(c); + escape = false; } - /** - * Parses a parameter specification into name and default value and returns a - * {@link UrlBindingParameter} with the corresponding name and default value properties set - * accordingly. - * - * @param beanClass the bean class to which the binding applies - * @param string the parameter string - * @return a parameter object - * @throws ParseException if the pattern cannot be parsed - */ - public static UrlBindingParameter parseUrlBindingParameter( - Class beanClass, String string) { - char[] chars = string.toCharArray(); - char c = 0; - boolean escape = false; - StringBuilder name = new StringBuilder(); - StringBuilder defaultValue = new StringBuilder(); - StringBuilder current = name; - for (int i = 0; i < chars.length; i++) { - c = chars[i]; - if (!escape) { - switch (c) { - case '\\': - escape = true; - continue; - case '=': - current = defaultValue; - continue; - } - } + // Were we led to expect more characters? + if (escape) throw new ParseException(pattern, "Expression must not end with escape character"); + else if (brace) + throw new ParseException(pattern, "Unterminated left brace ('{') in expression"); - current.append(c); - escape = false; - } + // handle whatever is left + if (buf.length() > 0) { + if (path == null) path = buf.toString(); + else if (c == '}') components.add(parseUrlBindingParameter(beanType, buf.toString())); + else components.add(buf.toString()); + } - // Parameter name must not be empty - if (name.length() < 1) { - throw new ParseException(string, "Empty parameter name in URL binding for " - + beanClass.getName()); + return new UrlBinding(beanType, path, components); + } + + /** + * Parses a parameter specification into name and default value and returns a {@link + * UrlBindingParameter} with the corresponding name and default value properties set accordingly. + * + * @param beanClass the bean class to which the binding applies + * @param string the parameter string + * @return a parameter object + * @throws ParseException if the pattern cannot be parsed + */ + public static UrlBindingParameter parseUrlBindingParameter( + Class beanClass, String string) { + char[] chars = string.toCharArray(); + char c = 0; + boolean escape = false; + StringBuilder name = new StringBuilder(); + StringBuilder defaultValue = new StringBuilder(); + StringBuilder current = name; + for (int i = 0; i < chars.length; i++) { + c = chars[i]; + if (!escape) { + switch (c) { + case '\\': + escape = true; + continue; + case '=': + current = defaultValue; + continue; } + } - String dflt = defaultValue.length() < 1 ? null : defaultValue.toString(); - if (dflt != null && UrlBindingParameter.PARAMETER_NAME_EVENT.equals(name.toString())) { - throw new ParseException(string, "In ActionBean class " + beanClass.getName() - + ", the " + UrlBindingParameter.PARAMETER_NAME_EVENT - + " parameter may not be assigned a default value. Its default value is" - + " determined by the @DefaultHandler annotation."); - } - return new UrlBindingParameter(beanClass, name.toString(), null, dflt) { - @Override - public String getValue() { - throw new UnsupportedOperationException( - "getValue() is not implemented for URL parameter prototypes"); - } - }; + current.append(c); + escape = false; + } + + // Parameter name must not be empty + if (name.length() < 1) { + throw new ParseException( + string, "Empty parameter name in URL binding for " + beanClass.getName()); } - @Override - public String toString() { - return String.valueOf(classCache); + String dflt = defaultValue.length() < 1 ? null : defaultValue.toString(); + if (dflt != null && UrlBindingParameter.PARAMETER_NAME_EVENT.equals(name.toString())) { + throw new ParseException( + string, + "In ActionBean class " + + beanClass.getName() + + ", the " + + UrlBindingParameter.PARAMETER_NAME_EVENT + + " parameter may not be assigned a default value. Its default value is" + + " determined by the @DefaultHandler annotation."); } + return new UrlBindingParameter(beanClass, name.toString(), null, dflt) { + @Override + public String getValue() { + throw new UnsupportedOperationException( + "getValue() is not implemented for URL parameter prototypes"); + } + }; + } + + @Override + public String toString() { + return String.valueOf(classCache); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/UrlBindingParameter.java b/stripes/src/main/java/net/sourceforge/stripes/controller/UrlBindingParameter.java index 9ca04546c..37abbb5d5 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/UrlBindingParameter.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/UrlBindingParameter.java @@ -15,7 +15,6 @@ package net.sourceforge.stripes.controller; import java.lang.reflect.Method; - import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.HandlesEvent; import net.sourceforge.stripes.exception.StripesRuntimeException; @@ -23,138 +22,139 @@ /** * A parameter to a clean URL. - * + * * @author Ben Gunter * @since Stripes 1.5 */ public class UrlBindingParameter { - /** The special parameter name for the event to execute */ - public static final String PARAMETER_NAME_EVENT = "$event"; - - protected Class beanClass; - protected String name; - protected String value; - protected String defaultValue; - - /** - * Create a new {@link UrlBindingParameter} with the given name and value. The - * {@link #defaultValue} will be null. - * - * @param name parameter name - * @param value parameter value - */ - public UrlBindingParameter(Class beanClass, String name, String value) { - this(beanClass, name, value, null); - } - - /** - * Create a new {@link UrlBindingParameter} with the given name, value and default value. - * - * @param name parameter name - * @param value parameter value - * @param defaultValue default value to use if value is null - */ - public UrlBindingParameter(Class beanClass, String name, String value, String defaultValue) { - this.beanClass = beanClass; - this.name = name; - this.value = value; - this.defaultValue = defaultValue; - } - - /** - * Make an exact copy of the given {@link UrlBindingParameter}. - * - * @param prototype a parameter - */ - public UrlBindingParameter(UrlBindingParameter prototype) { - this(prototype.beanClass, prototype.name, prototype.value, prototype.defaultValue); - } - - /** - * Make a copy of the given {@link UrlBindingParameter} except that the parameter's value will - * be set to value. - * - * @param prototype a parameter - * @param value the new parameter value - */ - public UrlBindingParameter(UrlBindingParameter prototype, String value) { - this(prototype.beanClass, prototype.name, value, prototype.defaultValue); - } - - /** Get the {@link ActionBean} class to which the {@link UrlBinding} applies. */ - public Class getBeanClass() { - return beanClass; - } - - /** - * Get the parameter's default value, which may be null. - * - * @return the default value - */ - public String getDefaultValue() { - return defaultValue; - } - - /** - * Ensure the default event name is set if the binding uses the $event parameter. - * Can only be done safely after the event mappings have been processed. - * see http://www.stripesframework.org/jira/browse/STS-803 - */ - void initDefaultValueWithDefaultHandlerIfNeeded(ActionResolver actionResolver) { - if (PARAMETER_NAME_EVENT.equals(name)) { - Method defaultHandler; - try { - defaultHandler = actionResolver.getDefaultHandler(beanClass); - } catch (StripesServletException e) { - throw new StripesRuntimeException("Caught an exception trying to get default handler for ActionBean '" + beanClass.getName() + - "'. Make sure this ActionBean has a default handler.", e); - } - HandlesEvent annotation = defaultHandler.getAnnotation(HandlesEvent.class); - if (annotation != null) { - this.defaultValue = annotation.value(); - } else { - this.defaultValue = defaultHandler.getName(); - } - } - } - - /** - * Get the parameter name. - * - * @return parameter name - */ - public String getName() { - return name; - } - - /** - * Return the parameter value that was extracted from a URI. - * - * @return parameter value - */ - public String getValue() { - return value; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof UrlBindingParameter)) - return false; - - UrlBindingParameter that = (UrlBindingParameter) o; - return this.value == null ? that.value == null : this.value.equals(that.value); - } - - @Override - public int hashCode() { - return getName().hashCode(); - } - - @Override - public String toString() { - if (defaultValue == null) - return name; - else - return name + "=" + defaultValue; + /** The special parameter name for the event to execute */ + public static final String PARAMETER_NAME_EVENT = "$event"; + + protected Class beanClass; + protected String name; + protected String value; + protected String defaultValue; + + /** + * Create a new {@link UrlBindingParameter} with the given name and value. The {@link + * #defaultValue} will be null. + * + * @param name parameter name + * @param value parameter value + */ + public UrlBindingParameter(Class beanClass, String name, String value) { + this(beanClass, name, value, null); + } + + /** + * Create a new {@link UrlBindingParameter} with the given name, value and default value. + * + * @param name parameter name + * @param value parameter value + * @param defaultValue default value to use if value is null + */ + public UrlBindingParameter( + Class beanClass, String name, String value, String defaultValue) { + this.beanClass = beanClass; + this.name = name; + this.value = value; + this.defaultValue = defaultValue; + } + + /** + * Make an exact copy of the given {@link UrlBindingParameter}. + * + * @param prototype a parameter + */ + public UrlBindingParameter(UrlBindingParameter prototype) { + this(prototype.beanClass, prototype.name, prototype.value, prototype.defaultValue); + } + + /** + * Make a copy of the given {@link UrlBindingParameter} except that the parameter's value will be + * set to value. + * + * @param prototype a parameter + * @param value the new parameter value + */ + public UrlBindingParameter(UrlBindingParameter prototype, String value) { + this(prototype.beanClass, prototype.name, value, prototype.defaultValue); + } + + /** Get the {@link ActionBean} class to which the {@link UrlBinding} applies. */ + public Class getBeanClass() { + return beanClass; + } + + /** + * Get the parameter's default value, which may be null. + * + * @return the default value + */ + public String getDefaultValue() { + return defaultValue; + } + + /** + * Ensure the default event name is set if the binding uses the $event parameter. Can only be done + * safely after the event mappings have been processed. see + * http://www.stripesframework.org/jira/browse/STS-803 + */ + void initDefaultValueWithDefaultHandlerIfNeeded(ActionResolver actionResolver) { + if (PARAMETER_NAME_EVENT.equals(name)) { + Method defaultHandler; + try { + defaultHandler = actionResolver.getDefaultHandler(beanClass); + } catch (StripesServletException e) { + throw new StripesRuntimeException( + "Caught an exception trying to get default handler for ActionBean '" + + beanClass.getName() + + "'. Make sure this ActionBean has a default handler.", + e); + } + HandlesEvent annotation = defaultHandler.getAnnotation(HandlesEvent.class); + if (annotation != null) { + this.defaultValue = annotation.value(); + } else { + this.defaultValue = defaultHandler.getName(); + } } + } + + /** + * Get the parameter name. + * + * @return parameter name + */ + public String getName() { + return name; + } + + /** + * Return the parameter value that was extracted from a URI. + * + * @return parameter value + */ + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof UrlBindingParameter)) return false; + + UrlBindingParameter that = (UrlBindingParameter) o; + return this.value == null ? that.value == null : this.value.equals(that.value); + } + + @Override + public int hashCode() { + return getName().hashCode(); + } + + @Override + public String toString() { + if (defaultValue == null) return name; + else return name + "=" + defaultValue; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/CommonsMultipartWrapper.java b/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/CommonsMultipartWrapper.java index 2b06cf47e..cc6e11e33 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/CommonsMultipartWrapper.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/CommonsMultipartWrapper.java @@ -1,4 +1,5 @@ /* Copyright 2005-2006 Tim Fennell + * Copyright 2023 Rick Grashel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,204 +15,214 @@ */ package net.sourceforge.stripes.controller.multipart; -import net.sourceforge.stripes.action.FileBean; -import net.sourceforge.stripes.controller.FileUploadLimitExceededException; -import org.apache.commons.fileupload.disk.DiskFileItemFactory; -import org.apache.commons.fileupload.servlet.ServletFileUpload; -import org.apache.commons.fileupload.FileItem; -import org.apache.commons.fileupload.FileUploadException; -import org.apache.commons.fileupload.FileUploadBase; +import static org.apache.commons.fileupload2.core.DiskFileItemFactory.DEFAULT_THRESHOLD; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Enumeration; -import java.util.List; -import java.util.Map; import java.util.HashMap; -import java.util.ArrayList; import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.regex.Pattern; +import net.sourceforge.stripes.action.FileBean; +import net.sourceforge.stripes.controller.FileUploadLimitExceededException; +import org.apache.commons.fileupload2.core.DiskFileItemFactory; +import org.apache.commons.fileupload2.core.FileItem; +import org.apache.commons.fileupload2.core.FileUploadException; +import org.apache.commons.fileupload2.core.FileUploadSizeException; +import org.apache.commons.fileupload2.jakarta.JakartaServletFileUpload; /** - * An implementation of MultipartWrapper that uses Jakarta Commons FileUpload (from apache) - * to parse the request parts. This implementation requires that both commons-fileupload and - * commons-io be present in the classpath. While this implementation does introduce additional - * dependencies, it's licensing (ASL 2.0) is significantly less restrictive than the licensing - * for COS - the other alternative provided by Stripes. + * An implementation of MultipartWrapper that uses Jakarta Commons FileUpload (from apache) to parse + * the request parts. This implementation requires that both commons-fileupload and commons-io be + * present in the classpath. While this implementation does introduce additional dependencies, it's + * licensing (ASL 2.0) is significantly less restrictive than the licensing for COS - the other + * alternative provided by Stripes. * * @author Tim Fennell + * @author Rick Grashel * @since Stripes 1.4 + * @since Stripes 1.6.1 */ public class CommonsMultipartWrapper implements MultipartWrapper { - private static final Pattern WINDOWS_PATH_PREFIX_PATTERN = Pattern.compile("(?i:^[A-Z]:\\\\)"); - - /** Ensure this class will not load unless Commons FileUpload is on the classpath. */ - static { - FileUploadException.class.getName(); + private static final Pattern WINDOWS_PATH_PREFIX_PATTERN = Pattern.compile("(?i:^[A-Z]:\\\\)"); + + /* + * Ensure this class will not load unless Commons FileUpload is on the classpath. + */ + static { + //noinspection ResultOfMethodCallIgnored + FileUploadException.class.getName(); + } + + @SuppressWarnings("rawtypes") + private final Map files = new HashMap<>(); + + private final Map parameters = new HashMap<>(); + private String charset; + + /** + * Pseudo-constructor that allows the class to perform any initialization necessary. + * + * @param request an HttpServletRequest that has a content-type of multipart. + * @param tempDir a File representing the temporary directory that can be used to store file parts + * as they are uploaded if this is desirable + * @param maxPostSize the size in bytes beyond which the request should not be read, and a + * FileUploadLimitExceeded exception should be thrown + * @throws IOException if a problem occurs processing the request of storing temporary files + * @throws FileUploadLimitExceededException if the POST content is longer than the maxPostSize + * supplied. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public void build(HttpServletRequest request, File tempDir, long maxPostSize) + throws IOException, FileUploadLimitExceededException { + try { + this.charset = request.getCharacterEncoding(); + DiskFileItemFactory factory = + DiskFileItemFactory.builder() + .setPath(tempDir.toPath()) + .setBufferSize(DEFAULT_THRESHOLD) + .get(); + + JakartaServletFileUpload upload = new JakartaServletFileUpload(factory); + upload.setSizeMax(maxPostSize); + List items = upload.parseRequest(request); + Map> params = new HashMap<>(); + + for (FileItem item : items) { + // If it's a form field, add the string value to the list + if (item.isFormField()) { + List values = params.computeIfAbsent(item.getFieldName(), k -> new ArrayList<>()); + values.add(charset == null ? item.getString() : item.getString(Charset.forName(charset))); + } + // Else store the file param + else { + files.put(item.getFieldName(), item); + } + } + + // Now convert them down into the usual map of String->String[] + for (Map.Entry> entry : params.entrySet()) { + List values = entry.getValue(); + this.parameters.put(entry.getKey(), values.toArray(new String[0])); + } + } catch (FileUploadSizeException fuse) { + throw new FileUploadLimitExceededException(maxPostSize, fuse.getActualSize()); + } catch (FileUploadException fue) { + throw new IOException("Could not parse and cache file upload data.", fue); } + } + + /** + * Fetches the names of all non-file parameters in the request. Directly analogous to the method + * of the same name in HttpServletRequest when the request is non-multipart. + * + * @return an Enumeration of all non-file parameter names in the request + */ + public Enumeration getParameterNames() { + return new IteratorEnumeration(this.parameters.keySet().iterator()); + } + + /** + * Fetches all values of a specific parameter in the request. To simulate the HTTP request style, + * the array should be null for non-present parameters, and values in the array should never be + * null - the empty String should be used when there is value. + * + * @param name the name of the request parameter + * @return an array of non-null parameters or null + */ + public String[] getParameterValues(String name) { + return this.parameters.get(name); + } + + /** + * Fetches the names of all file parameters in the request. Note that these are not the file + * names, but the names given to the form fields in which the files are specified. + * + * @return the names of all file parameters in the request. + */ + public Enumeration getFileParameterNames() { + return new IteratorEnumeration(this.files.keySet().iterator()); + } + + /** + * Responsible for constructing a FileBean object for the named file parameter. If there is no + * file parameter with the specified name this method should return null. + * + * @param name the name of the file parameter + * @return a FileBean object wrapping the uploaded file + */ + @SuppressWarnings("rawtypes") + public FileBean getFileParameterValue(String name) { + final FileItem item = this.files.get(name); + if (item == null + || ((item.getName() == null || item.getName().isEmpty()) && item.getSize() == 0)) { + return null; + } else { + // Attempt to ensure the file name is just the basename with no path included + String filename = item.getName(); + int index; + if (WINDOWS_PATH_PREFIX_PATTERN.matcher(filename).find()) index = filename.lastIndexOf('\\'); + else index = filename.lastIndexOf('/'); + if (index >= 0 && index + 1 < filename.length() - 1) filename = filename.substring(index + 1); + + // Use an anonymous inner subclass of FileBean that overrides all the + // methods that rely on having a File present, to use the FileItem + // created by commons upload instead. + return new FileBean(null, item.getContentType(), filename, this.charset) { + @Override + public long getSize() { + return item.getSize(); + } - private Map files = new HashMap(); - private Map parameters = new HashMap(); - private String charset; - - /** - * Pseudo-constructor that allows the class to perform any initialization necessary. - * - * @param request an HttpServletRequest that has a content-type of multipart. - * @param tempDir a File representing the temporary directory that can be used to store - * file parts as they are uploaded if this is desirable - * @param maxPostSize the size in bytes beyond which the request should not be read, and a - * FileUploadLimitExceeded exception should be thrown - * @throws IOException if a problem occurs processing the request of storing temporary - * files - * @throws FileUploadLimitExceededException if the POST content is longer than the - * maxPostSize supplied. - */ - @SuppressWarnings("unchecked") - public void build(HttpServletRequest request, File tempDir, long maxPostSize) - throws IOException, FileUploadLimitExceededException { - try { - this.charset = request.getCharacterEncoding(); - DiskFileItemFactory factory = new DiskFileItemFactory(); - factory.setRepository(tempDir); - ServletFileUpload upload = new ServletFileUpload(factory); - upload.setSizeMax(maxPostSize); - List items = upload.parseRequest(request); - Map> params = new HashMap>(); - - for (FileItem item : items) { - // If it's a form field, add the string value to the list - if (item.isFormField()) { - List values = params.get(item.getFieldName()); - if (values == null) { - values = new ArrayList(); - params.put(item.getFieldName(), values); - } - values.add(charset == null ? item.getString() : item.getString(charset)); - } - // Else store the file param - else { - files.put(item.getFieldName(), item); - } - } + @Override + public InputStream getInputStream() throws IOException { + return item.getInputStream(); + } - // Now convert them down into the usual map of String->String[] - for (Map.Entry> entry : params.entrySet()) { - List values = entry.getValue(); - this.parameters.put(entry.getKey(), values.toArray(new String[values.size()])); + @Override + public void save(File toFile) throws IOException { + try { + item.write(toFile.toPath()); + delete(); + } catch (Exception e) { + if (e instanceof IOException) throw (IOException) e; + else { + throw new IOException("Problem saving uploaded file.", e); } - } - catch (FileUploadBase.SizeLimitExceededException slee) { - throw new FileUploadLimitExceededException(maxPostSize, slee.getActualSize()); - } - catch (FileUploadException fue) { - IOException ioe = new IOException("Could not parse and cache file upload data."); - ioe.initCause(fue); - throw ioe; + } } + @Override + public void delete() throws IOException { + item.delete(); + } + }; } + } - /** - * Fetches the names of all non-file parameters in the request. Directly analogous to the - * method of the same name in HttpServletRequest when the request is non-multipart. - * - * @return an Enumeration of all non-file parameter names in the request - */ - public Enumeration getParameterNames() { - return new IteratorEnumeration(this.parameters.keySet().iterator()); - } - - /** - * Fetches all values of a specific parameter in the request. To simulate the HTTP request - * style, the array should be null for non-present parameters, and values in the array should - * never be null - the empty String should be used when there is value. - * - * @param name the name of the request parameter - * @return an array of non-null parameters or null - */ - public String[] getParameterValues(String name) { - return this.parameters.get(name); - } + /** Little helper class to create an enumeration as per the interface. */ + private static class IteratorEnumeration implements Enumeration { + Iterator iterator; - /** - * Fetches the names of all file parameters in the request. Note that these are not the file - * names, but the names given to the form fields in which the files are specified. - * - * @return the names of all file parameters in the request. - */ - public Enumeration getFileParameterNames() { - return new IteratorEnumeration(this.files.keySet().iterator()); + /** Constructs an enumeration that consumes from the underlying iterator. */ + IteratorEnumeration(Iterator iterator) { + this.iterator = iterator; } - /** - * Responsible for constructing a FileBean object for the named file parameter. If there is no - * file parameter with the specified name this method should return null. - * - * @param name the name of the file parameter - * @return a FileBean object wrapping the uploaded file - */ - public FileBean getFileParameterValue(String name) { - final FileItem item = this.files.get(name); - if (item == null - || ((item.getName() == null || item.getName().length() == 0) && item.getSize() == 0)) { - return null; - } - else { - // Attempt to ensure the file name is just the basename with no path included - String filename = item.getName(); - int index; - if (WINDOWS_PATH_PREFIX_PATTERN.matcher(filename).find()) - index = filename.lastIndexOf('\\'); - else - index = filename.lastIndexOf('/'); - if (index >= 0 && index + 1 < filename.length() - 1) - filename = filename.substring(index + 1); - - // Use an anonymous inner subclass of FileBean that overrides all the - // methods that rely on having a File present, to use the FileItem - // created by commons upload instead. - return new FileBean(null, item.getContentType(), filename, this.charset) { - @Override public long getSize() { return item.getSize(); } - - @Override public InputStream getInputStream() throws IOException { - return item.getInputStream(); - } - - @Override public void save(File toFile) throws IOException { - try { - item.write(toFile); - delete(); - } - catch (Exception e) { - if (e instanceof IOException) throw (IOException) e; - else { - IOException ioe = new IOException("Problem saving uploaded file."); - ioe.initCause(e); - throw ioe; - } - } - } - - @Override - public void delete() throws IOException { item.delete(); } - }; - } + /** Returns true if more elements can be consumed, false otherwise. */ + public boolean hasMoreElements() { + return this.iterator.hasNext(); } - /** Little helper class to create an enumeration as per the interface. */ - private static class IteratorEnumeration implements Enumeration { - Iterator iterator; - - /** Constructs an enumeration that consumes from the underlying iterator. */ - IteratorEnumeration(Iterator iterator) { this.iterator = iterator; } - - /** Returns true if more elements can be consumed, false otherwise. */ - public boolean hasMoreElements() { return this.iterator.hasNext(); } - - /** Gets the next element out of the iterator. */ - public String nextElement() { return this.iterator.next(); } + /** Gets the next element out of the iterator. */ + public String nextElement() { + return this.iterator.next(); } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/CosMultipartWrapper.java b/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/CosMultipartWrapper.java deleted file mode 100644 index 663efa55f..000000000 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/CosMultipartWrapper.java +++ /dev/null @@ -1,167 +0,0 @@ -/* Copyright 2005-2006 Tim Fennell - * - * 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 net.sourceforge.stripes.controller.multipart; - -import com.oreilly.servlet.MultipartRequest; -import com.oreilly.servlet.multipart.FileRenamePolicy; - -import net.sourceforge.stripes.action.FileBean; -import net.sourceforge.stripes.controller.FileUploadLimitExceededException; -import net.sourceforge.stripes.exception.StripesRuntimeException; - -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.io.File; -import java.util.Enumeration; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Implementation of MultipartWrapper that uses Jason Hunter's COS (com.oreilly.servlet) - * multipart parser implementation. This is the default implementation in Stripes and is - * generally preferred as it is a) free for use and b) has no other dependencies! However, - * commercial redistribution of the COS library requires licensing from Jason Hunter, so - * this implementation may not be applicable for commercial products that are distributed/sold - * (though it is fine for commercial applications that are simply developed and hosted by the - * company developing them). - * - * @author Tim Fennell - * @since Stripes 1.4 - */ -public class CosMultipartWrapper implements MultipartWrapper { - /** Pattern used to parse useful info out of the IOException cos throws. */ - private static Pattern EXCEPTION_PATTERN = - Pattern.compile("Posted content length of (\\d*) exceeds limit of (\\d*)"); - - /** Ensure this class will not load unless COS is on the classpath. */ - static { - MultipartRequest.class.getName(); - } - - private MultipartRequest multipart; - private String charset; - /** - * Pseudo-constructor that allows the class to perform any initialization necessary. - * - * @param request an HttpServletRequest that has a content-type of multipart. - * @param tempDir a File representing the temporary directory that can be used to store - * file parts as they are uploaded if this is desirable - * @param maxPostSize the size in bytes beyond which the request should not be read, and a - * FileUploadLimitExceeded exception should be thrown - * @throws IOException if a problem occurs processing the request of storing temporary - * files - * @throws FileUploadLimitExceededException if the POST content is longer than the - * maxPostSize supplied. - */ - public void build(HttpServletRequest request, final File tempDir, long maxPostSize) - throws IOException, FileUploadLimitExceededException { - - try { - // Create a new file in the temp directory in case of file name conflict - FileRenamePolicy renamePolicy = new FileRenamePolicy() { - public File rename(File arg0) { - try { - return File.createTempFile("cos", "", tempDir); - } - catch (IOException e) { - throw new StripesRuntimeException( - "Caught an exception while trying to rename an uploaded file", e); - } - } - }; - - this.charset = request.getCharacterEncoding(); - this.multipart = new MultipartRequest(request, - tempDir.getAbsolutePath(), - (int) maxPostSize, - this.charset, - renamePolicy); - } - catch (IOException ioe) { - Matcher matcher = EXCEPTION_PATTERN.matcher(ioe.getMessage()); - - if (matcher.matches()) { - throw new FileUploadLimitExceededException(Long.parseLong(matcher.group(2)), - Long.parseLong(matcher.group(1))); - } - else { - throw ioe; - } - } - } - - /** - * Fetches the names of all non-file parameters in the request. Directly analogous to the method - * of the same name in HttpServletRequest when the request is non-multipart. - * - * @return an Enumeration of all non-file parameter names in the request - */ - @SuppressWarnings("unchecked") - public Enumeration getParameterNames() { - return this.multipart.getParameterNames(); - } - - /** - * Fetches all values of a specific parameter in the request. To simulate the HTTP request - * style, the array should be null for non-present parameters, and values in the array should - * never be null - the empty String should be used when there is value. - * - * @param name the name of the request parameter - * @return an array of non-null parameters or null - */ - public String[] getParameterValues(String name) { - String[] values = this.multipart.getParameterValues(name); - if (values != null) { - for (int i=0; i getFileParameterNames() { - return this.multipart.getFileNames(); - } - - /** - * Responsible for constructing a FileBean object for the named file parameter. If there is no - * file parameter with the specified name this method should return null. - * - * @param name the name of the file parameter - * @return a FileBean object wrapping the uploaded file - */ - public FileBean getFileParameterValue(String name) { - File file = this.multipart.getFile(name); - if (file != null) { - return new FileBean(file, - this.multipart.getContentType(name), - this.multipart.getOriginalFileName(name), - this.charset); - } - else { - return null; - } - } -} diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/DefaultMultipartWrapperFactory.java b/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/DefaultMultipartWrapperFactory.java index bc91e6de6..4b9293492 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/DefaultMultipartWrapperFactory.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/DefaultMultipartWrapperFactory.java @@ -14,158 +14,173 @@ */ package net.sourceforge.stripes.controller.multipart; -import net.sourceforge.stripes.controller.FileUploadLimitExceededException; -import net.sourceforge.stripes.config.Configuration; -import net.sourceforge.stripes.util.Log; -import net.sourceforge.stripes.exception.StripesRuntimeException; - -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; +import jakarta.servlet.http.HttpServletRequest; import java.io.File; -import java.util.regex.Pattern; +import java.io.IOException; import java.util.regex.Matcher; +import java.util.regex.Pattern; +import net.sourceforge.stripes.config.Configuration; +import net.sourceforge.stripes.controller.FileUploadLimitExceededException; +import net.sourceforge.stripes.exception.StripesRuntimeException; +import net.sourceforge.stripes.util.Log; /** - *

    Default implementation of a factory for MultipartWrappers. Looks up a class name in - * Configuration under the key specified by {@link #WRAPPER_CLASS_NAME}. If no class - * name is configured, defaults to the {@link CosMultipartWrapper}. An additional configuration - * parameter is supported to specify the maximum post size allowable.

    - * + * Default implementation of a factory for MultipartWrappers. Looks up a class name in Configuration + * under the key specified by {@link #WRAPPER_CLASS_NAME}. If no class name is configured, defaults + * to the {@link CosMultipartWrapper}. An additional configuration parameter is supported to specify + * the maximum post size allowable. + * * @author Tim Fennell * @since Stripes 1.4 */ public class DefaultMultipartWrapperFactory implements MultipartWrapperFactory { - /** The configuration key used to lookup the implementation of MultipartWrapper. */ - public static final String WRAPPER_CLASS_NAME = "MultipartWrapper.Class"; - - /** The names of the MultipartWrapper classes that will be tried if no other is specified. */ - public static final String[] BUNDLED_IMPLEMENTATIONS = { - "net.sourceforge.stripes.controller.multipart.CommonsMultipartWrapper", - "net.sourceforge.stripes.controller.multipart.CosMultipartWrapper" }; - - /** Key used to lookup the name of the maximum post size. */ - public static final String MAX_POST = "FileUpload.MaximumPostSize"; - - private static final Log log = Log.getInstance(DefaultMultipartWrapperFactory.class); - - // Instance level fields - private Configuration configuration; - private Class multipartClass; - private long maxPostSizeInBytes = 1024 * 1024 * 10; // Defaults to 10MB - private File temporaryDirectory; - - /** Get the configuration object that was passed into {@link #init(Configuration)}. */ - protected Configuration getConfiguration() { - return configuration; + /** The configuration key used to lookup the implementation of MultipartWrapper. */ + public static final String WRAPPER_CLASS_NAME = "MultipartWrapper.Class"; + + /** The names of the MultipartWrapper classes that will be tried if no other is specified. */ + public static final String[] BUNDLED_IMPLEMENTATIONS = { + "net.sourceforge.stripes.controller.multipart.CommonsMultipartWrapper" + }; + + /** Key used to lookup the name of the maximum post size. */ + public static final String MAX_POST = "FileUpload.MaximumPostSize"; + + private static final Log log = Log.getInstance(DefaultMultipartWrapperFactory.class); + + // Instance level fields + private Configuration configuration; + private Class multipartClass; + private long maxPostSizeInBytes = 1024 * 1024 * 10; // Defaults to 10MB + private File temporaryDirectory; + + /** Get the configuration object that was passed into {@link #init(Configuration)}. */ + protected Configuration getConfiguration() { + return configuration; + } + + /** + * Invoked directly after instantiation to allow the configured component to perform one time + * initialization. Components are expected to fail loudly if they are not going to be in a valid + * state after initialization. + * + * @param config the Configuration object being used by Stripes + * @throws Exception should be thrown if the component cannot be configured well enough to use. + */ + @SuppressWarnings("unchecked") + public void init(Configuration config) throws Exception { + this.configuration = config; + + // Determine which class we're using + this.multipartClass = + config + .getBootstrapPropertyResolver() + .getClassProperty(WRAPPER_CLASS_NAME, MultipartWrapper.class); + + if (this.multipartClass == null) { + // It wasn't defined in web.xml so we'll try the bundled MultipartWrappers + for (String className : BUNDLED_IMPLEMENTATIONS) { + try { + this.multipartClass = ((Class) Class.forName(className)); + break; + } catch (Throwable t) { + log.debug( + getClass().getSimpleName(), + " not using ", + className, + " because it failed to load. This likely means the supporting ", + "file upload library is not present on the classpath."); + } + } } - /** - * Invoked directly after instantiation to allow the configured component to perform one time - * initialization. Components are expected to fail loudly if they are not going to be in a - * valid state after initialization. - * - * @param config the Configuration object being used by Stripes - * @throws Exception should be thrown if the component cannot be configured well enough to use. - */ - @SuppressWarnings("unchecked") - public void init(Configuration config) throws Exception { - this.configuration = config; - - // Determine which class we're using - this.multipartClass = config.getBootstrapPropertyResolver().getClassProperty(WRAPPER_CLASS_NAME, MultipartWrapper.class); - - if (this.multipartClass == null) { - // It wasn't defined in web.xml so we'll try the bundled MultipartWrappers - for (String className : BUNDLED_IMPLEMENTATIONS) { - try { - this.multipartClass = ((Class) Class - .forName(className)); - break; - } - catch (Throwable t) { - log.debug(getClass().getSimpleName(), " not using ", className, - " because it failed to load. This likely means the supporting ", - "file upload library is not present on the classpath."); - } - } - } + // Log the name of the class we'll be using or a warning if none could be loaded + if (this.multipartClass == null) { + log.warn("No ", MultipartWrapper.class.getSimpleName(), " implementation could be loaded"); + } else { + log.info( + "Using ", + this.multipartClass.getName(), + " as ", + MultipartWrapper.class.getSimpleName(), + " implementation."); + } - // Log the name of the class we'll be using or a warning if none could be loaded - if (this.multipartClass == null) { - log.warn("No ", MultipartWrapper.class.getSimpleName(), - " implementation could be loaded"); - } - else { - log.info("Using ", this.multipartClass.getName(), " as ", MultipartWrapper.class - .getSimpleName(), " implementation."); - } + // Figure out where the temp directory is, and store that info + File tempDir = (File) config.getServletContext().getAttribute("javax.servlet.context.tempdir"); + if (tempDir != null) { + this.temporaryDirectory = tempDir; + } else { + String tmpDir = System.getProperty("java.io.tmpdir"); + + if (tmpDir != null) { + this.temporaryDirectory = new File(tmpDir).getAbsoluteFile(); + } else { + log.warn( + "The tmpdir system property was null! File uploads will probably fail. ", + "This is normal if you are running on Google App Engine as it doesn't allow ", + "file system write access."); + } + } - // Figure out where the temp directory is, and store that info - File tempDir = (File) config.getServletContext().getAttribute("javax.servlet.context.tempdir"); - if (tempDir != null) { - this.temporaryDirectory = tempDir; - } - else { - String tmpDir = System.getProperty("java.io.tmpdir"); - - if (tmpDir != null) { - this.temporaryDirectory = new File(tmpDir).getAbsoluteFile(); - } - else { - log.warn("The tmpdir system property was null! File uploads will probably fail. ", - "This is normal if you are running on Google App Engine as it doesn't allow ", - "file system write access."); - } + // See if a maximum post size was configured + String limit = config.getBootstrapPropertyResolver().getProperty(MAX_POST); + if (limit != null) { + Pattern pattern = Pattern.compile("([\\d,]+)([kKmMgG]?).*"); + Matcher matcher = pattern.matcher(limit); + if (!matcher.matches()) { + log.error( + "Did not understand value of configuration parameter ", + MAX_POST, + " You supplied: ", + limit, + ". Valid values are any string of numbers ", + "optionally followed by (case insensitive) [k|kb|m|mb|g|gb]. ", + "Default value of ", + this.maxPostSizeInBytes, + " bytes will be used instead."); + } else { + String digits = matcher.group(1); + String suffix = matcher.group(2).toLowerCase(); + long number = Long.parseLong(digits); + + if ("k".equals(suffix)) { + number = number * 1024; + } else if ("m".equals(suffix)) { + number = number * 1024 * 1024; + } else if ("g".equals(suffix)) { + number = number * 1024 * 1024 * 1024; } - // See if a maximum post size was configured - String limit = config.getBootstrapPropertyResolver().getProperty(MAX_POST); - if (limit != null) { - Pattern pattern = Pattern.compile("([\\d,]+)([kKmMgG]?).*"); - Matcher matcher = pattern.matcher(limit); - if (!matcher.matches()) { - log.error("Did not understand value of configuration parameter ", MAX_POST, - " You supplied: ", limit, ". Valid values are any string of numbers ", - "optionally followed by (case insensitive) [k|kb|m|mb|g|gb]. ", - "Default value of ", this.maxPostSizeInBytes, " bytes will be used instead."); - } - else { - String digits = matcher.group(1); - String suffix = matcher.group(2).toLowerCase(); - long number = Long.parseLong(digits); - - if ("k".equals(suffix)) { number = number * 1024; } - else if ("m".equals(suffix)) { number = number * 1024 * 1024; } - else if ("g".equals(suffix)) { number = number * 1024 * 1024 * 1024; } - - this.maxPostSizeInBytes = number; - log.info("Configured file upload post size limit: ", number, " bytes."); - } - } + this.maxPostSizeInBytes = number; + log.info("Configured file upload post size limit: ", number, " bytes."); + } } - - /** - * Wraps the request in an appropriate implementation of MultipartWrapper that is capable of - * providing access to request parameters and any file parts contained within the request. - * - * @param request an active HttpServletRequest - * @return an implementation of the appropriate wrapper - * @throws IOException if encountered when consuming the contents of the request - * @throws FileUploadLimitExceededException if the post size of the request exceeds any - * configured limits - */ - public MultipartWrapper wrap(HttpServletRequest request) throws IOException, FileUploadLimitExceededException { - try { - MultipartWrapper wrapper = getConfiguration().getObjectFactory().newInstance( - this.multipartClass); - wrapper.build(request, this.temporaryDirectory, this.maxPostSizeInBytes); - return wrapper; - } - catch (IOException ioe) { throw ioe; } - catch (FileUploadLimitExceededException fulee) { throw fulee; } - catch (Exception e) { - throw new StripesRuntimeException - ("Could not construct a MultipartWrapper for the current request.", e); - } + } + + /** + * Wraps the request in an appropriate implementation of MultipartWrapper that is capable of + * providing access to request parameters and any file parts contained within the request. + * + * @param request an active HttpServletRequest + * @return an implementation of the appropriate wrapper + * @throws IOException if encountered when consuming the contents of the request + * @throws FileUploadLimitExceededException if the post size of the request exceeds any configured + * limits + */ + public MultipartWrapper wrap(HttpServletRequest request) + throws IOException, FileUploadLimitExceededException { + try { + MultipartWrapper wrapper = + getConfiguration().getObjectFactory().newInstance(this.multipartClass); + wrapper.build(request, this.temporaryDirectory, this.maxPostSizeInBytes); + return wrapper; + } catch (IOException ioe) { + throw ioe; + } catch (FileUploadLimitExceededException fulee) { + throw fulee; + } catch (Exception e) { + throw new StripesRuntimeException( + "Could not construct a MultipartWrapper for the current request.", e); } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/MultipartWrapper.java b/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/MultipartWrapper.java index 3d6036ad4..699458cbe 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/MultipartWrapper.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/MultipartWrapper.java @@ -14,70 +14,69 @@ */ package net.sourceforge.stripes.controller.multipart; -import net.sourceforge.stripes.controller.FileUploadLimitExceededException; -import net.sourceforge.stripes.action.FileBean; - -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; +import jakarta.servlet.http.HttpServletRequest; import java.io.File; +import java.io.IOException; import java.util.Enumeration; +import net.sourceforge.stripes.action.FileBean; +import net.sourceforge.stripes.controller.FileUploadLimitExceededException; /** - * Interface which must be implemented by classes which provide the ability to parse - * the POST content when it is of type multipart/form-data. Provides a single, pluggable - * wrapper interface around third party libraries which provide this capability. + * Interface which must be implemented by classes which provide the ability to parse the POST + * content when it is of type multipart/form-data. Provides a single, pluggable wrapper interface + * around third party libraries which provide this capability. * * @author Tim Fennell * @since Stripes 1.4 */ public interface MultipartWrapper { - /** - * Pseudo-constructor that allows the class to perform any initialization necessary. - * - * @param request an HttpServletRequest that has a content-type of multipart. - * @param tempDir a File representing the temporary directory that can be used to store - * file parts as they are uploaded if this is desirable - * @param maxPostSize the size in bytes beyond which the request should not be read, - * and a FileUploadLimitExceeded exception should be thrown - * @throws IOException if a problem occurs processing the request of storing temporary files - * @throws FileUploadLimitExceededException if the POST content is longer than the maxPostSize - * supplied. - */ - void build(HttpServletRequest request, File tempDir, long maxPostSize) - throws IOException, FileUploadLimitExceededException; + /** + * Pseudo-constructor that allows the class to perform any initialization necessary. + * + * @param request an HttpServletRequest that has a content-type of multipart. + * @param tempDir a File representing the temporary directory that can be used to store file parts + * as they are uploaded if this is desirable + * @param maxPostSize the size in bytes beyond which the request should not be read, and a + * FileUploadLimitExceeded exception should be thrown + * @throws IOException if a problem occurs processing the request of storing temporary files + * @throws FileUploadLimitExceededException if the POST content is longer than the maxPostSize + * supplied. + */ + void build(HttpServletRequest request, File tempDir, long maxPostSize) + throws IOException, FileUploadLimitExceededException; - /** - * Fetches the names of all non-file parameters in the request. Directly analogous to the - * method of the same name in HttpServletRequest when the request is non-multipart. - * - * @return an Enumeration of all non-file parameter names in the request - */ - Enumeration getParameterNames(); + /** + * Fetches the names of all non-file parameters in the request. Directly analogous to the method + * of the same name in HttpServletRequest when the request is non-multipart. + * + * @return an Enumeration of all non-file parameter names in the request + */ + Enumeration getParameterNames(); - /** - * Fetches all values of a specific parameter in the request. To simulate the HTTP - * request style, the array should be null for non-present parameters, and values in the - * array should never be null - the empty String should be used when there is value. - * - * @param name the name of the request parameter - * @return an array of non-null parameters or null - */ - String[] getParameterValues(String name); + /** + * Fetches all values of a specific parameter in the request. To simulate the HTTP request style, + * the array should be null for non-present parameters, and values in the array should never be + * null - the empty String should be used when there is value. + * + * @param name the name of the request parameter + * @return an array of non-null parameters or null + */ + String[] getParameterValues(String name); - /** - * Fetches the names of all file parameters in the request. Note that these are not the - * file names, but the names given to the form fields in which the files are specified. - * - * @return the names of all file parameters in the request. - */ - Enumeration getFileParameterNames(); + /** + * Fetches the names of all file parameters in the request. Note that these are not the file + * names, but the names given to the form fields in which the files are specified. + * + * @return the names of all file parameters in the request. + */ + Enumeration getFileParameterNames(); - /** - * Responsible for constructing a FileBean object for the named file parameter. If there is - * no file parameter with the specified name this method should return null. - * - * @param name the name of the file parameter - * @return a FileBean object wrapping the uploaded file - */ - FileBean getFileParameterValue(String name); + /** + * Responsible for constructing a FileBean object for the named file parameter. If there is no + * file parameter with the specified name this method should return null. + * + * @param name the name of the file parameter + * @return a FileBean object wrapping the uploaded file + */ + FileBean getFileParameterValue(String name); } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/MultipartWrapperFactory.java b/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/MultipartWrapperFactory.java index c61d146d7..d3b892ae6 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/MultipartWrapperFactory.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/multipart/MultipartWrapperFactory.java @@ -14,31 +14,29 @@ */ package net.sourceforge.stripes.controller.multipart; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; import net.sourceforge.stripes.config.ConfigurableComponent; import net.sourceforge.stripes.controller.FileUploadLimitExceededException; -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; - /** - * Factory for classes that implement {@link MultipartWrapper}. The factory may chose to - * always supply the same kind of wrapper, or vary the implementation request by request - * as it sees fit. + * Factory for classes that implement {@link MultipartWrapper}. The factory may chose to always + * supply the same kind of wrapper, or vary the implementation request by request as it sees fit. * * @author Tim Fennell * @since Stripes 1.4 */ public interface MultipartWrapperFactory extends ConfigurableComponent { - /** - * Wraps the request in an appropriate implementation of MultipartWrapper that is capable - * of providing access to request parameters and any file parts contained within the - * request. - * - * @param request an active HttpServletRequest - * @return an implementation of the appropriate wrapper - * @throws IOException if encountered when consuming the contents of the request - * @throws FileUploadLimitExceededException if the post size of the request exceeds - * any configured limits - */ - MultipartWrapper wrap(HttpServletRequest request) throws IOException, FileUploadLimitExceededException; + /** + * Wraps the request in an appropriate implementation of MultipartWrapper that is capable of + * providing access to request parameters and any file parts contained within the request. + * + * @param request an active HttpServletRequest + * @return an implementation of the appropriate wrapper + * @throws IOException if encountered when consuming the contents of the request + * @throws FileUploadLimitExceededException if the post size of the request exceeds any configured + * limits + */ + MultipartWrapper wrap(HttpServletRequest request) + throws IOException, FileUploadLimitExceededException; } diff --git a/stripes/src/main/java/net/sourceforge/stripes/exception/ActionBeanNotFoundException.java b/stripes/src/main/java/net/sourceforge/stripes/exception/ActionBeanNotFoundException.java index f68bbfb40..dafede825 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/exception/ActionBeanNotFoundException.java +++ b/stripes/src/main/java/net/sourceforge/stripes/exception/ActionBeanNotFoundException.java @@ -15,48 +15,48 @@ package net.sourceforge.stripes.exception; import java.util.Map; - import net.sourceforge.stripes.action.ActionBean; /** * Thrown when the action resolver can not find an {@link ActionBean} bound to the requested URL. - * + * * @author John Newman * @since Stripes 1.5 */ public class ActionBeanNotFoundException extends StripesServletException { - private static final long serialVersionUID = 1L; - - public ActionBeanNotFoundException(String message) { - super(message); - } - - public ActionBeanNotFoundException(String message, Throwable cause) { - super(message, cause); - } - - public ActionBeanNotFoundException(Throwable cause) { - super(cause); - } - - public ActionBeanNotFoundException(String requestedUrl, - Map> registeredBeans) { - super(buildMessage(requestedUrl, registeredBeans)); - } - - public ActionBeanNotFoundException(String requestedUrl, - Map> registeredBeans, Throwable cause) { - super(buildMessage(requestedUrl, registeredBeans), cause); - } - - /** - * Static method to build the message from the requested bean and the map of registered beans. - */ - private static String buildMessage(String requestedUrl, - Map> registeredBeans) { - return "Could not locate an ActionBean that is bound to the URL [" + requestedUrl - + "]. Commons reasons for this include mis-matched URLs and forgetting " - + "to implement ActionBean in your class. Registered ActionBeans are: " - + registeredBeans; - } -} \ No newline at end of file + private static final long serialVersionUID = 1L; + + public ActionBeanNotFoundException(String message) { + super(message); + } + + public ActionBeanNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public ActionBeanNotFoundException(Throwable cause) { + super(cause); + } + + public ActionBeanNotFoundException( + String requestedUrl, Map> registeredBeans) { + super(buildMessage(requestedUrl, registeredBeans)); + } + + public ActionBeanNotFoundException( + String requestedUrl, + Map> registeredBeans, + Throwable cause) { + super(buildMessage(requestedUrl, registeredBeans), cause); + } + + /** Static method to build the message from the requested bean and the map of registered beans. */ + private static String buildMessage( + String requestedUrl, Map> registeredBeans) { + return "Could not locate an ActionBean that is bound to the URL [" + + requestedUrl + + "]. Commons reasons for this include mis-matched URLs and forgetting " + + "to implement ActionBean in your class. Registered ActionBeans are: " + + registeredBeans; + } +} diff --git a/stripes/src/main/java/net/sourceforge/stripes/exception/AutoExceptionHandler.java b/stripes/src/main/java/net/sourceforge/stripes/exception/AutoExceptionHandler.java index 77fcdb2c5..01b0b5e9d 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/exception/AutoExceptionHandler.java +++ b/stripes/src/main/java/net/sourceforge/stripes/exception/AutoExceptionHandler.java @@ -14,26 +14,26 @@ */ package net.sourceforge.stripes.exception; - /** - *

    A marker interface for delegate exception handlers to be used with the - * {@link DelegatingExceptionHandler}. Note that the DelegatingExceptionHandler must be - * configured as the {@link ExceptionHandler} for the application in order for AutoExceptionHandlers - * to be discovered and used.

    + * A marker interface for delegate exception handlers to be used with the {@link + * DelegatingExceptionHandler}. Note that the DelegatingExceptionHandler must be configured as the + * {@link ExceptionHandler} for the application in order for AutoExceptionHandlers to be discovered + * and used. * *

    AutoExceptionHandlers can define one or more methods to handle different kinds of exceptions. - * Each method must have the following signature:

    + * Each method must have the following signature: * - *
    public Resolution handle(Type exception, HttpServletRequest req, HttpServletResponse res);
    + *
    public Resolution handle(Type exception, HttpServletRequest req, HttpServletResponse res);
    + * 
    * - *

    where Type can be any subclass of {@link java.lang.Throwable}. Handler methods do - * not have to follow any naming convention. In the above example 'handle' is used, but any - * other name, e.g. 'run', 'handleException' etc. would have worked as well. The return type is only - * loosely enforced; if the method returns an object and it is a - * {@link net.sourceforge.stripes.action.Resolution} then it will be executed, otherwise it - * will be ignored.

    + *

    where Type can be any subclass of {@link java.lang.Throwable}. Handler methods do not + * have to follow any naming convention. In the above example 'handle' is used, but any other name, + * e.g. 'run', 'handleException' etc. would have worked as well. The return type is only loosely + * enforced; if the method returns an object and it is a {@link + * net.sourceforge.stripes.action.Resolution} then it will be executed, otherwise it will be + * ignored. * * @author Jeppe Cramon * @since Stripes 1.3 */ -public interface AutoExceptionHandler { } +public interface AutoExceptionHandler {} diff --git a/stripes/src/main/java/net/sourceforge/stripes/exception/BindingDeniedException.java b/stripes/src/main/java/net/sourceforge/stripes/exception/BindingDeniedException.java index 23f7b8d56..1a4416341 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/exception/BindingDeniedException.java +++ b/stripes/src/main/java/net/sourceforge/stripes/exception/BindingDeniedException.java @@ -6,20 +6,21 @@ import net.sourceforge.stripes.validation.Validate; /** - * Exception thrown when a client attempts to bind to an {@link ActionBean} property that is not allowed. This will - * occur when using the {@link StrictBinding} annotation. If you intend to bind to the property, you should apply a - * naked {@link Validate} annotation to the property. + * Exception thrown when a client attempts to bind to an {@link ActionBean} property that is not + * allowed. This will occur when using the {@link StrictBinding} annotation. If you intend to bind + * to the property, you should apply a naked {@link Validate} annotation to the property. * - *

    - * Currently, this will only be thrown if the Stripes configuration is in debug mode (see {@link Configuration}). When - * not in debug, a warning is logged. - *

    + *

    Currently, this will only be thrown if the Stripes configuration is in debug mode (see {@link + * Configuration}). When not in debug, a warning is logged. * * @since Stripes 1.6 */ public class BindingDeniedException extends RuntimeException { - public BindingDeniedException(String parameterName) { - super("Binding denied for parameter [" + parameterName + "]. If you want to allow binding to this parameter, " + - "use the @Validate annotation."); - } + public BindingDeniedException(String parameterName) { + super( + "Binding denied for parameter [" + + parameterName + + "]. If you want to allow binding to this parameter, " + + "use the @Validate annotation."); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/exception/DefaultExceptionHandler.java b/stripes/src/main/java/net/sourceforge/stripes/exception/DefaultExceptionHandler.java index 97a99e9f7..3398234e5 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/exception/DefaultExceptionHandler.java +++ b/stripes/src/main/java/net/sourceforge/stripes/exception/DefaultExceptionHandler.java @@ -14,7 +14,9 @@ */ package net.sourceforge.stripes.exception; - +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.beans.PropertyDescriptor; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -24,11 +26,6 @@ import java.text.DecimalFormat; import java.util.HashMap; import java.util.Map; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.action.FileBean; @@ -46,29 +43,29 @@ import net.sourceforge.stripes.validation.LocalizableError; /** - *

    Default ExceptionHandler implementation that makes it easy for users to extend and - * add custom handling for different types of exception. When extending this class methods - * can be added that meet the following requirements:

    + * Default ExceptionHandler implementation that makes it easy for users to extend and add custom + * handling for different types of exception. When extending this class methods can be added that + * meet the following requirements: * *
      - *
    • Methods must be public
    • - *
    • Methods must be non-abstract
    • - *
    • Methods must have exactly three parameters
    • - *
    • The first parameter type must be Throwable or a subclass thereof
    • - *
    • The second and third arguments must be of type HttpServletRequest and - * HttpServletResponse respectively
    • - *
    • Methods may optionally return a Resolution in which case the resolution - * will be executed
    • + *
    • Methods must be public + *
    • Methods must be non-abstract + *
    • Methods must have exactly three parameters + *
    • The first parameter type must be Throwable or a subclass thereof + *
    • The second and third arguments must be of type HttpServletRequest and HttpServletResponse + * respectively + *
    • Methods may optionally return a Resolution in which case the resolution will be + * executed *
    * - *

    When an exception is caught the exception handler attempts to find a method that - * can handle that type of exception. If none is found the exception's super-types are - * iterated through and methods looked for which match the super-types. If a matching - * method is found it will be invoked. Otherwise the exception will simply be rethrown - * by the exception handler - though first it will be wrapped in a StripesServletException - * if necessary in order to make it acceptable to the container.

    + *

    When an exception is caught the exception handler attempts to find a method that can handle + * that type of exception. If none is found the exception's super-types are iterated through and + * methods looked for which match the super-types. If a matching method is found it will be invoked. + * Otherwise the exception will simply be rethrown by the exception handler - though first it will + * be wrapped in a StripesServletException if necessary in order to make it acceptable to the + * container. * - *

    The following are examples of method signatures that might be added by subclasses:

    + *

    The following are examples of method signatures that might be added by subclasses: * *

      * public Resolution handle(FileUploadLimitExceededException ex, HttpServletRequest req, HttpServletResponse resp) { ... }
    @@ -80,373 +77,372 @@
      * @since Stripes 1.3
      */
     public class DefaultExceptionHandler implements ExceptionHandler {
    -    private static final Log log = Log.getInstance(DefaultExceptionHandler.class);
    -    private Configuration configuration;
    -
    -    /** A cache of exception types handled mapped to proxy objects that can do the handling. */
    -    private Map, HandlerProxy> handlers =
    -            new HashMap, HandlerProxy>();
    -
    -    /**
    -     * Inner class that ties a class and method together an invokable object.
    -     * @author Tim Fennell
    -     * @since Stripes 1.3
    -     */
    -    protected static class HandlerProxy {
    -        private Object handler;
    -        private Method handlerMethod;
    -
    -        /** Constructs a new HandlerProxy that will tie together the instance and method used. */
    -        public HandlerProxy(Object handler, Method handlerMethod) {
    -            this.handler = handler;
    -            this.handlerMethod = handlerMethod;
    -        }
    -
    -        /** Invokes the handler and executes the resolution if one is returned. */
    -        public void handle(Throwable t, HttpServletRequest req, HttpServletResponse res) throws Exception {
    -            try {
    -                Object resolution = handlerMethod.invoke(this.handler, t, req, res);
    -                if (resolution != null && resolution instanceof Resolution) {
    -                    ((Resolution) resolution).execute(req, res);
    -                }
    -            }
    -            catch (InvocationTargetException e) {
    -                Throwable cause = e.getCause();
    -                if (cause instanceof Exception)
    -                    throw (Exception) cause;
    -                else if (cause instanceof Error)
    -                    throw (Error) cause;
    -                else
    -                    throw e;
    -            }
    -        }
    -
    -        Method getHandlerMethod() {
    -            return handlerMethod;
    -        }
    +  private static final Log log = Log.getInstance(DefaultExceptionHandler.class);
    +  private Configuration configuration;
    +
    +  /** A cache of exception types handled mapped to proxy objects that can do the handling. */
    +  private Map, HandlerProxy> handlers =
    +      new HashMap, HandlerProxy>();
    +
    +  /**
    +   * Inner class that ties a class and method together an invokable object.
    +   *
    +   * @author Tim Fennell
    +   * @since Stripes 1.3
    +   */
    +  protected static class HandlerProxy {
    +    private Object handler;
    +    private Method handlerMethod;
    +
    +    /** Constructs a new HandlerProxy that will tie together the instance and method used. */
    +    public HandlerProxy(Object handler, Method handlerMethod) {
    +      this.handler = handler;
    +      this.handlerMethod = handlerMethod;
         }
     
    -    /**
    -     * Implementation of the ExceptionHandler interface that attempts to find a method
    -     * that is capable of handing the exception. If it finds one then it is delegated to, and if
    -     * it returns a resolution it will be executed. Otherwise rethrows any unhandled exceptions,
    -     * wrapped in a StripesServletException if necessary.
    -     *
    -     * @param throwable the exception being handled
    -     * @param request the current request being processed
    -     * @param response the response paired with the current request
    -     */
    -    public void handle(Throwable throwable,
    -                       HttpServletRequest request,
    -                       HttpServletResponse response) throws ServletException, IOException {
    -        try {
    -            Throwable actual = unwrap(throwable);
    -            Class type = actual.getClass();
    -            HandlerProxy proxy = null;
    -
    -            while (type != null && proxy == null) {
    -                proxy = this.handlers.get(type);
    -                type = type.getSuperclass();
    -            }
    -
    -            if (proxy != null) {
    -                proxy.handle(actual, request, response);
    -            }
    -            else if (throwable instanceof FileUploadLimitExceededException) {
    -                Resolution resolution = handle((FileUploadLimitExceededException) throwable,
    -                        request, response);
    -                if (resolution != null)
    -                    resolution.execute(request, response);
    -            }
    -            else if (throwable instanceof SourcePageNotFoundException) {
    -                Resolution resolution = handle((SourcePageNotFoundException) throwable, request,
    -                        response);
    -                if (resolution != null)
    -                    resolution.execute(request, response);
    -            }
    -            else {
    -                // If there's no sensible proxy, rethrow the original throwable,
    -                // NOT the unwrapped one since they may add extra information
    -                log.warn(throwable, "Unhandled exception caught by the Stripes default exception handler.");
    -                throw throwable;
    -            }
    -        }
    -        catch (ServletException se) { throw se; }
    -        catch (IOException ioe) { throw ioe; }
    -        catch (Throwable t) {
    -            String message = "Unhandled exception in exception handler.";
    -            log.error(t, message);
    -            throw new StripesServletException(message, t);
    +    /** Invokes the handler and executes the resolution if one is returned. */
    +    public void handle(Throwable t, HttpServletRequest req, HttpServletResponse res)
    +        throws Exception {
    +      try {
    +        Object resolution = handlerMethod.invoke(this.handler, t, req, res);
    +        if (resolution != null && resolution instanceof Resolution) {
    +          ((Resolution) resolution).execute(req, res);
             }
    +      } catch (InvocationTargetException e) {
    +        Throwable cause = e.getCause();
    +        if (cause instanceof Exception) throw (Exception) cause;
    +        else if (cause instanceof Error) throw (Error) cause;
    +        else throw e;
    +      }
         }
     
    -    /**
    -     * 

    - * A default handler for {@link SourcePageNotFoundException}. That exception is thrown when - * validation errors occur on a request but the source page cannot be determined from the - * request parameters. Such a condition generally arises during application development when, - * for example, a parameter is accidentally omitted from a generated hyperlink or AJAX request. - *

    - *

    - * In the past, it was very difficult to determine what validation errors triggered the - * exception. This method returns a {@link ValidationErrorReportResolution}, which sends a - * simple HTML response to the client that very clearly details the validation errors. - *

    - *

    - * In production, most applications will provide their own handler for - * {@link SourcePageNotFoundException} by extending this class and overriding this method. - *

    - * - * @param exception The exception. - * @param request The servlet request. - * @param response The servlet response. - * @return A {@link ValidationErrorReportResolution} - */ - protected Resolution handle(SourcePageNotFoundException exception, HttpServletRequest request, - HttpServletResponse response) throws Exception { - return new ValidationErrorReportResolution(exception.getActionBeanContext()); + Method getHandlerMethod() { + return handlerMethod; } - - /** - *

    - * {@link FileUploadLimitExceededException} is notoriously difficult to handle for several - * reasons: - *

      - *
    • The exception is thrown during construction of the {@link StripesRequestWrapper}. Many - * Stripes components rely on the presence of this wrapper, yet it cannot be created normally.
    • - *
    • It happens before the request lifecycle has begun. There is no {@link ExecutionContext}, - * {@link ActionBeanContext}, or {@link ActionBean} associated with the request yet.
    • - *
    • None of the request parameters in the POST body can be read without risking denial of - * service. That includes the {@code _sourcePage} parameter that indicates the page from which - * the request was submitted.
    • - *
    - *

    - *

    - * This exception handler makes an attempt to handle the exception as gracefully as possible. It - * relies on the HTTP Referer header to determine where the request was submitted from. It uses - * introspection to guess the field name of the {@link FileBean} field that exceeded the POST - * limit. It instantiates an {@link ActionBean} and {@link ActionBeanContext} and adds a - * validation error to report the field name, maximum POST size, and actual POST size. Finally, - * it forwards to the referer. - *

    - *

    - * While this is a best effort, it won't be ideal for all situations. If this method is unable - * to handle the exception properly for any reason, it rethrows the exception. Subclasses can - * call this method in a {@code try} block, providing additional processing in the {@code catch} - * block. - *

    - *

    - * A simple way to provide a single, global error page for this type of exception is to override - * {@link #getFileUploadExceededExceptionPath(HttpServletRequest)} to return the path to your - * global error page. - *

    - * - * @param exception The exception that needs to be handled - * @param request The servlet request - * @param response The servlet response - * @return A {@link Resolution} to forward to the path returned by - * {@link #getFileUploadExceededExceptionPath(HttpServletRequest)} - * @throws FileUploadLimitExceededException If - * {@link #getFileUploadExceededExceptionPath(HttpServletRequest)} returns null or - * this method is unable for any other reason to forward to the error page - */ - protected Resolution handle(FileUploadLimitExceededException exception, - HttpServletRequest request, HttpServletResponse response) - throws FileUploadLimitExceededException { - // Get the path to which we will forward to display the message - final String path = getFileUploadExceededExceptionPath(request); - if (path == null) - throw exception; - - final StripesRequestWrapper wrapper; - final ActionBeanContext context; - final ActionBean actionBean; - try { - // Create a new request wrapper, avoiding the pitfalls of multipart - wrapper = new StripesRequestWrapper(request) { - @Override - protected void constructMultipartWrapper(HttpServletRequest request) - throws StripesServletException { - setLocale(configuration.getLocalePicker().pickLocale(request)); - } - }; - - // Create the ActionBean and ActionBeanContext - context = configuration.getActionBeanContextFactory().getContextInstance(wrapper, - response); - actionBean = configuration.getActionResolver().getActionBean(context); - wrapper.setAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN, actionBean); - } - catch (ServletException e) { - log.error(e); - throw exception; - } - - // Try to guess the field name by finding exactly one FileBean field - String fieldName = null; - try { - PropertyDescriptor[] pds = ReflectUtil.getPropertyDescriptors(actionBean.getClass()); - for (PropertyDescriptor pd : pds) { - if (FileBean.class.isAssignableFrom(pd.getPropertyType())) { - if (fieldName == null) { - // First FileBean field found so set the field name - fieldName = pd.getName(); - } - else { - // There's more than one FileBean field so don't use a field name - fieldName = null; - break; - } - } - } - } - catch (Exception e) { - // Not a big deal if we can't determine the field name - } - - // Add validation error with parameters for max post size and actual posted size (KB) - DecimalFormat format = new DecimalFormat("0.00"); - double max = (double) exception.getMaximum() / 1024; - double posted = (double) exception.getPosted() / 1024; - LocalizableError error = new LocalizableError("validation.file.postBodyTooBig", format - .format(max), format.format(posted)); - if (fieldName == null) - context.getValidationErrors().addGlobalError(error); - else - context.getValidationErrors().add(fieldName, error); - - // Create an ExecutionContext so that the validation errors can be filled in - ExecutionContext exectx = new ExecutionContext(); - exectx.setActionBean(actionBean); - exectx.setActionBeanContext(context); - DispatcherHelper.fillInValidationErrors(exectx); - - // Forward back to referer, using the wrapped request - return new ForwardResolution(path) { + } + + /** + * Implementation of the ExceptionHandler interface that attempts to find a method that is capable + * of handing the exception. If it finds one then it is delegated to, and if it returns a + * resolution it will be executed. Otherwise rethrows any unhandled exceptions, wrapped in a + * StripesServletException if necessary. + * + * @param throwable the exception being handled + * @param request the current request being processed + * @param response the response paired with the current request + */ + public void handle(Throwable throwable, HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + try { + Throwable actual = unwrap(throwable); + Class type = actual.getClass(); + HandlerProxy proxy = null; + + while (type != null && proxy == null) { + proxy = this.handlers.get(type); + type = type.getSuperclass(); + } + + if (proxy != null) { + proxy.handle(actual, request, response); + } else if (throwable instanceof FileUploadLimitExceededException) { + Resolution resolution = + handle((FileUploadLimitExceededException) throwable, request, response); + if (resolution != null) resolution.execute(request, response); + } else if (throwable instanceof SourcePageNotFoundException) { + Resolution resolution = handle((SourcePageNotFoundException) throwable, request, response); + if (resolution != null) resolution.execute(request, response); + } else { + // If there's no sensible proxy, rethrow the original throwable, + // NOT the unwrapped one since they may add extra information + log.warn(throwable, "Unhandled exception caught by the Stripes default exception handler."); + throw throwable; + } + } catch (ServletException se) { + throw se; + } catch (IOException ioe) { + throw ioe; + } catch (Throwable t) { + String message = "Unhandled exception in exception handler."; + log.error(t, message); + throw new StripesServletException(message, t); + } + } + + /** + * A default handler for {@link SourcePageNotFoundException}. That exception is thrown when + * validation errors occur on a request but the source page cannot be determined from the request + * parameters. Such a condition generally arises during application development when, for example, + * a parameter is accidentally omitted from a generated hyperlink or AJAX request. + * + *

    In the past, it was very difficult to determine what validation errors triggered the + * exception. This method returns a {@link ValidationErrorReportResolution}, which sends a simple + * HTML response to the client that very clearly details the validation errors. + * + *

    In production, most applications will provide their own handler for {@link + * SourcePageNotFoundException} by extending this class and overriding this method. + * + * @param exception The exception. + * @param request The servlet request. + * @param response The servlet response. + * @return A {@link ValidationErrorReportResolution} + */ + protected Resolution handle( + SourcePageNotFoundException exception, + HttpServletRequest request, + HttpServletResponse response) + throws Exception { + return new ValidationErrorReportResolution(exception.getActionBeanContext()); + } + + /** + * {@link FileUploadLimitExceededException} is notoriously difficult to handle for several + * reasons: + * + *

      + *
    • The exception is thrown during construction of the {@link StripesRequestWrapper}. Many + * Stripes components rely on the presence of this wrapper, yet it cannot be created + * normally. + *
    • It happens before the request lifecycle has begun. There is no {@link ExecutionContext}, + * {@link ActionBeanContext}, or {@link ActionBean} associated with the request yet. + *
    • None of the request parameters in the POST body can be read without risking denial of + * service. That includes the {@code _sourcePage} parameter that indicates the page from + * which the request was submitted. + *
    + * + *

    This exception handler makes an attempt to handle the exception as gracefully as possible. + * It relies on the HTTP Referer header to determine where the request was submitted from. It uses + * introspection to guess the field name of the {@link FileBean} field that exceeded the POST + * limit. It instantiates an {@link ActionBean} and {@link ActionBeanContext} and adds a + * validation error to report the field name, maximum POST size, and actual POST size. Finally, it + * forwards to the referer. + * + *

    While this is a best effort, it won't be ideal for all situations. If this method is unable + * to handle the exception properly for any reason, it rethrows the exception. Subclasses can call + * this method in a {@code try} block, providing additional processing in the {@code catch} block. + * + *

    A simple way to provide a single, global error page for this type of exception is to + * override {@link #getFileUploadExceededExceptionPath(HttpServletRequest)} to return the path to + * your global error page. + * + *

    + * + * @param exception The exception that needs to be handled + * @param request The servlet request + * @param response The servlet response + * @return A {@link Resolution} to forward to the path returned by {@link + * #getFileUploadExceededExceptionPath(HttpServletRequest)} + * @throws FileUploadLimitExceededException If {@link + * #getFileUploadExceededExceptionPath(HttpServletRequest)} returns null or this method is + * unable for any other reason to forward to the error page + */ + protected Resolution handle( + FileUploadLimitExceededException exception, + HttpServletRequest request, + HttpServletResponse response) + throws FileUploadLimitExceededException { + // Get the path to which we will forward to display the message + final String path = getFileUploadExceededExceptionPath(request); + if (path == null) throw exception; + + final StripesRequestWrapper wrapper; + final ActionBeanContext context; + final ActionBean actionBean; + try { + // Create a new request wrapper, avoiding the pitfalls of multipart + wrapper = + new StripesRequestWrapper(request) { @Override - public void execute(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - super.execute(wrapper, response); + protected void constructMultipartWrapper(HttpServletRequest request) + throws StripesServletException { + setLocale(configuration.getLocalePicker().pickLocale(request)); } - }; + }; + + // Create the ActionBean and ActionBeanContext + context = configuration.getActionBeanContextFactory().getContextInstance(wrapper, response); + actionBean = configuration.getActionResolver().getActionBean(context); + wrapper.setAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN, actionBean); + } catch (ServletException e) { + log.error(e); + throw exception; } - /** - * Get the path to which the {@link Resolution} returned by - * {@link #handle(FileUploadLimitExceededException, HttpServletRequest, HttpServletResponse)} - * should forward to report the error. The default implementation attempts to determine this - * from the HTTP Referer header. If it is unable to do so, it returns null. Subclasses may - * override this method to return whatever they wish. The return value must be relative to the - * application context root. - * - * @param request The request that generated the exception - * @return The context-relative path from which the request was submitted - */ - protected String getFileUploadExceededExceptionPath(HttpServletRequest request) { - // Get the referer URL so we can bounce back to it - URL referer = null; - try { - referer = new URL(request.getHeader("referer")); - } - catch (Exception e) { - // Header not found? Invalid? Can't do anything with it :( - return null; - } - - // Convert the referer path to a context-relative path - String path = referer.getFile(); - String contextPath = request.getContextPath(); - if (contextPath.length() > 1) { - // We can't handle it if the POST came from outside our app - if (!path.startsWith(contextPath + "/")) - return null; - - path = path.replace(contextPath, ""); + // Try to guess the field name by finding exactly one FileBean field + String fieldName = null; + try { + PropertyDescriptor[] pds = ReflectUtil.getPropertyDescriptors(actionBean.getClass()); + for (PropertyDescriptor pd : pds) { + if (FileBean.class.isAssignableFrom(pd.getPropertyType())) { + if (fieldName == null) { + // First FileBean field found so set the field name + fieldName = pd.getName(); + } else { + // There's more than one FileBean field so don't use a field name + fieldName = null; + break; + } } - - return path; + } + } catch (Exception e) { + // Not a big deal if we can't determine the field name } - /** Stores the configuration and examines the handler for usable delegate methods. */ - public void init(Configuration configuration) throws Exception { - this.configuration = configuration; - addHandler(this); + // Add validation error with parameters for max post size and actual posted size (KB) + DecimalFormat format = new DecimalFormat("0.00"); + double max = (double) exception.getMaximum() / 1024; + double posted = (double) exception.getPosted() / 1024; + LocalizableError error = + new LocalizableError( + "validation.file.postBodyTooBig", format.format(max), format.format(posted)); + if (fieldName == null) context.getValidationErrors().addGlobalError(error); + else context.getValidationErrors().add(fieldName, error); + + // Create an ExecutionContext so that the validation errors can be filled in + ExecutionContext exectx = new ExecutionContext(); + exectx.setActionBean(actionBean); + exectx.setActionBeanContext(context); + DispatcherHelper.fillInValidationErrors(exectx); + + // Forward back to referer, using the wrapped request + return new ForwardResolution(path) { + @Override + public void execute(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + super.execute(wrapper, response); + } + }; + } + + /** + * Get the path to which the {@link Resolution} returned by {@link + * #handle(FileUploadLimitExceededException, HttpServletRequest, HttpServletResponse)} should + * forward to report the error. The default implementation attempts to determine this from the + * HTTP Referer header. If it is unable to do so, it returns null. Subclasses may override this + * method to return whatever they wish. The return value must be relative to the application + * context root. + * + * @param request The request that generated the exception + * @return The context-relative path from which the request was submitted + */ + protected String getFileUploadExceededExceptionPath(HttpServletRequest request) { + // Get the referer URL so we can bounce back to it + URL referer = null; + try { + referer = new URL(request.getHeader("referer")); + } catch (Exception e) { + // Header not found? Invalid? Can't do anything with it :( + return null; } - /** - * Adds a class to the set of configured delegate handlers. Examines all the methods on the - * class looking for public non-abstract methods with a signature matching that described in - * the class level javadoc. Each method is wrapped in a HandlerProxy and stored in a cache - * by the exception type it takes. - * - * @param handlerClass the class being configured - * @throws Exception if the handler class cannot be instantiated - */ - protected void addHandler(Class handlerClass) throws Exception { - addHandler(getConfiguration().getObjectFactory().newInstance(handlerClass)); - } + // Convert the referer path to a context-relative path + String path = referer.getFile(); + String contextPath = request.getContextPath(); + if (contextPath.length() > 1) { + // We can't handle it if the POST came from outside our app + if (!path.startsWith(contextPath + "/")) return null; - /** - * Adds an object instance to the set of configured handles. Examines - * all the methods on the class looking for public non-abstract methods with a signature - * matching that described in the class level javadoc. Each method is wrapped in a - * HandlerProxy and stored in a cache by the exception type it takes. - * - * @param handler the handler instance being configured - */ - @SuppressWarnings("unchecked") - protected void addHandler(Object handler) throws Exception { - Method[] methods = handler.getClass().getMethods(); - for (Method method : methods) { - // Check the method Signature - Class[] parameters = method.getParameterTypes(); - int mods = method.getModifiers(); - - // Check all the reasons not to add it! - if (!Modifier.isPublic(mods)) continue; - if (Modifier.isAbstract(mods)) continue; - if (parameters.length != 3) continue; - if (!Throwable.class.isAssignableFrom(parameters[0])) continue; - if (!HttpServletRequest.class.equals(parameters[1])) continue; - if (!HttpServletResponse.class.equals(parameters[2])) continue; - if (handler == this && method.getName().equals("handle") && - Throwable.class.equals(parameters[0])) continue; - - // And if we made it this far, add it! - Class type = parameters[0]; - HandlerProxy proxy = new HandlerProxy(handler, method); - HandlerProxy previous = handlers.get(type); - if (previous != null) { - log.warn("More than one exception handler for exception type ", type, " in ", - handler.getClass().getSimpleName(), ". '", method.getName(), - "()' will be used instead of '", previous.getHandlerMethod().getName(), "()'."); - } - handlers.put(type, proxy); - - log.debug("Added exception handler '", handler.getClass().getSimpleName(), ".", - method.getName(), "()' for exception type: ", type); - } + path = path.replace(contextPath, ""); } - /** Provides subclasses with access to the configuration. */ - protected Configuration getConfiguration() { return configuration; } - - /** - * Unwraps the throwable passed in. If the throwable is a ServletException and has - * a root case, the root cause is returned, otherwise the throwable is returned as is. - * - * @param throwable a throwable - * @return another thowable, either the root cause of the one passed in - */ - protected Throwable unwrap(Throwable throwable) { - if (throwable instanceof ServletException) { - Throwable t = ((ServletException) throwable).getRootCause(); - - if (t != null) { - throwable = t; - } - } - - return throwable; + return path; + } + + /** Stores the configuration and examines the handler for usable delegate methods. */ + public void init(Configuration configuration) throws Exception { + this.configuration = configuration; + addHandler(this); + } + + /** + * Adds a class to the set of configured delegate handlers. Examines all the methods on the class + * looking for public non-abstract methods with a signature matching that described in the class + * level javadoc. Each method is wrapped in a HandlerProxy and stored in a cache by the exception + * type it takes. + * + * @param handlerClass the class being configured + * @throws Exception if the handler class cannot be instantiated + */ + protected void addHandler(Class handlerClass) throws Exception { + addHandler(getConfiguration().getObjectFactory().newInstance(handlerClass)); + } + + /** + * Adds an object instance to the set of configured handles. Examines all the methods on the class + * looking for public non-abstract methods with a signature matching that described in the class + * level javadoc. Each method is wrapped in a HandlerProxy and stored in a cache by the exception + * type it takes. + * + * @param handler the handler instance being configured + */ + @SuppressWarnings("unchecked") + protected void addHandler(Object handler) throws Exception { + Method[] methods = handler.getClass().getMethods(); + for (Method method : methods) { + // Check the method Signature + Class[] parameters = method.getParameterTypes(); + int mods = method.getModifiers(); + + // Check all the reasons not to add it! + if (!Modifier.isPublic(mods)) continue; + if (Modifier.isAbstract(mods)) continue; + if (parameters.length != 3) continue; + if (!Throwable.class.isAssignableFrom(parameters[0])) continue; + if (!HttpServletRequest.class.equals(parameters[1])) continue; + if (!HttpServletResponse.class.equals(parameters[2])) continue; + if (handler == this + && method.getName().equals("handle") + && Throwable.class.equals(parameters[0])) continue; + + // And if we made it this far, add it! + Class type = parameters[0]; + HandlerProxy proxy = new HandlerProxy(handler, method); + HandlerProxy previous = handlers.get(type); + if (previous != null) { + log.warn( + "More than one exception handler for exception type ", + type, + " in ", + handler.getClass().getSimpleName(), + ". '", + method.getName(), + "()' will be used instead of '", + previous.getHandlerMethod().getName(), + "()'."); + } + handlers.put(type, proxy); + + log.debug( + "Added exception handler '", + handler.getClass().getSimpleName(), + ".", + method.getName(), + "()' for exception type: ", + type); } + } + + /** Provides subclasses with access to the configuration. */ + protected Configuration getConfiguration() { + return configuration; + } + + /** + * Unwraps the throwable passed in. If the throwable is a ServletException and has a root case, + * the root cause is returned, otherwise the throwable is returned as is. + * + * @param throwable a throwable + * @return another thowable, either the root cause of the one passed in + */ + protected Throwable unwrap(Throwable throwable) { + if (throwable instanceof ServletException) { + Throwable t = ((ServletException) throwable).getRootCause(); + + if (t != null) { + throwable = t; + } + } + + return throwable; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/exception/DelegatingExceptionHandler.java b/stripes/src/main/java/net/sourceforge/stripes/exception/DelegatingExceptionHandler.java index ba19b918f..c101a10f7 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/exception/DelegatingExceptionHandler.java +++ b/stripes/src/main/java/net/sourceforge/stripes/exception/DelegatingExceptionHandler.java @@ -14,6 +14,13 @@ */ package net.sourceforge.stripes.exception; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import net.sourceforge.stripes.config.BootstrapPropertyResolver; import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.controller.AnnotatedClassActionResolver; @@ -21,120 +28,111 @@ import net.sourceforge.stripes.util.ResolverUtil; import net.sourceforge.stripes.util.StringUtil; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.lang.reflect.Modifier; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - /** - *

    An alternative implementation of {@link ExceptionHandler} that discovers and automatically + * An alternative implementation of {@link ExceptionHandler} that discovers and automatically * configures individual {@link AutoExceptionHandler} classes to handle specific types of - * exceptions. This implementation is most useful when ActionBeans may produce many different - * types of exceptions and it is desirable to separate exception handling logic for different - * groups or classes of exceptions. Using this approach multiple AutoExceptionHandlers can be - * configured simultaneously but do not have to be co-located.

    + * exceptions. This implementation is most useful when ActionBeans may produce many different types + * of exceptions and it is desirable to separate exception handling logic for different groups or + * classes of exceptions. Using this approach multiple AutoExceptionHandlers can be configured + * simultaneously but do not have to be co-located. * - *

    Searches for implementations of AutoExceptionHandler using the same mechanism as is used - * to discover ActionBean implementations - a search of the classpath for classes that implement - * the interface. The search requires one parameter, DelegatingExceptionHandler.Packages, which - * should contain a comma separated list of root packages to search for AutoExceptionHandler - * classes. If this parameter is not specified, the DelegatingExceptionHandler will use - * the configuration parameter that is used for discovering ActionBean instances - * (ActionResolver.Packages). The configuration parameter is usually specified as an - * init-param for the Stripes Filter, e.g.:

    + *

    Searches for implementations of AutoExceptionHandler using the same mechanism as is used to + * discover ActionBean implementations - a search of the classpath for classes that implement the + * interface. The search requires one parameter, DelegatingExceptionHandler.Packages, which should + * contain a comma separated list of root packages to search for AutoExceptionHandler classes. If + * this parameter is not specified, the DelegatingExceptionHandler will use the configuration + * parameter that is used for discovering ActionBean instances (ActionResolver.Packages). The + * configuration parameter is usually specified as an init-param for the Stripes Filter, e.g.: * - *

    - *<init-param>
    + * 
    + * <init-param>
      *    <param-name>DelegatingExceptionHandler.Packages</param-name>
      *    <param-value>com.myco.web,com.myco.shared</param-value>
    - *</init-param>
    - *
    + * </init-param> + *
    * - *

    When the {@link #handle(Throwable, HttpServletRequest, HttpServletResponse)} is invoked - * the set of AutoExceptionHandlers is examined to find the handler with the most specific - * signature that is capable of handling the exception. If no handler is available to handle the - * exception type supplied then the exception will be rethrown; if the exception is not a - * ServletException it will be wrapped in a StripesServletException before being rethrown.

    + *

    When the {@link #handle(Throwable, HttpServletRequest, HttpServletResponse)} is invoked the + * set of AutoExceptionHandlers is examined to find the handler with the most specific signature + * that is capable of handling the exception. If no handler is available to handle the exception + * type supplied then the exception will be rethrown; if the exception is not a ServletException it + * will be wrapped in a StripesServletException before being rethrown. * *

    If it is desirable to ensure that all exceptions are handled simply create an - * AutoExceptionHandler that takes with {@link java.lang.Exception} (preferable) or - * {@link java.lang.Throwable} (this may catch unhandlable errors like OutOfMemoryError).

    + * AutoExceptionHandler that takes with {@link java.lang.Exception} (preferable) or {@link + * java.lang.Throwable} (this may catch unhandlable errors like OutOfMemoryError). * * @author Jeppe Cramon, Tim Fennell * @since Stripes 1.3 */ public class DelegatingExceptionHandler extends DefaultExceptionHandler { - /** Log instance for use within in this class. */ - private static final Log log = Log.getInstance(DelegatingExceptionHandler.class); + /** Log instance for use within in this class. */ + private static final Log log = Log.getInstance(DelegatingExceptionHandler.class); - /** - * Configuration key used to lookup the list of packages to scan for auto handlers. - * @since Stripes 1.5 - */ - public static final String PACKAGES = "DelegatingExceptionHandler.Packages"; + /** + * Configuration key used to lookup the list of packages to scan for auto handlers. + * + * @since Stripes 1.5 + */ + public static final String PACKAGES = "DelegatingExceptionHandler.Packages"; - /** - * Looks up the filters as defined in the Configuration and then invokes the - * {@link ResolverUtil} to find implementations of AutoExceptionHandler. Each - * implementation found is then examined and cached by calling - * {@link #addHandler(Class)} - * - * @param configuration the Configuration for this Stripes application - * @throws Exception thrown if any of the discovered handler types cannot be safely - * instantiated - */ - @Override - public void init(Configuration configuration) throws Exception { - super.init(configuration); + /** + * Looks up the filters as defined in the Configuration and then invokes the {@link ResolverUtil} + * to find implementations of AutoExceptionHandler. Each implementation found is then examined and + * cached by calling {@link #addHandler(Class)} + * + * @param configuration the Configuration for this Stripes application + * @throws Exception thrown if any of the discovered handler types cannot be safely instantiated + */ + @Override + public void init(Configuration configuration) throws Exception { + super.init(configuration); - // Fetch the AutoExceptionHandler implementations and add them to the cache - Set> handlers = findClasses(); - for (Class handler : handlers) { - if (!Modifier.isAbstract(handler.getModifiers())) { - log.debug("Processing class ", handler, " looking for exception handling methods."); - addHandler(handler); - } - } + // Fetch the AutoExceptionHandler implementations and add them to the cache + Set> handlers = findClasses(); + for (Class handler : handlers) { + if (!Modifier.isAbstract(handler.getModifiers())) { + log.debug("Processing class ", handler, " looking for exception handling methods."); + addHandler(handler); + } } + } - /** - * Helper method to find implementations of AutoExceptionHandler in the packages specified in - * Configuration using the {@link ResolverUtil} class. - * - * @return a set of Class objects that represent subclasses of AutoExceptionHandler - */ - protected Set> findClasses() { - BootstrapPropertyResolver bootstrap = getConfiguration().getBootstrapPropertyResolver(); + /** + * Helper method to find implementations of AutoExceptionHandler in the packages specified in + * Configuration using the {@link ResolverUtil} class. + * + * @return a set of Class objects that represent subclasses of AutoExceptionHandler + */ + protected Set> findClasses() { + BootstrapPropertyResolver bootstrap = getConfiguration().getBootstrapPropertyResolver(); - // Try the config param that is specific to this class - String[] packages = StringUtil.standardSplit(bootstrap.getProperty(PACKAGES)); - if (packages == null || packages.length == 0) { - // Config param not found so try autodiscovery - log.info("No config parameter '", PACKAGES, "' found. Trying autodiscovery instead."); - List> classes = bootstrap - .getClassPropertyList(AutoExceptionHandler.class); - if (!classes.isEmpty()) { - return new HashSet>(classes); - } - else { - // Autodiscovery found nothing so resort to looking at the ActionBean packages - log.info("Autodiscovery found no implementations of AutoExceptionHandler. Using ", - "the value of '", AnnotatedClassActionResolver.PACKAGES, "' instead."); - packages = StringUtil.standardSplit(bootstrap - .getProperty(AnnotatedClassActionResolver.PACKAGES)); - } - } + // Try the config param that is specific to this class + String[] packages = StringUtil.standardSplit(bootstrap.getProperty(PACKAGES)); + if (packages == null || packages.length == 0) { + // Config param not found so try autodiscovery + log.info("No config parameter '", PACKAGES, "' found. Trying autodiscovery instead."); + List> classes = + bootstrap.getClassPropertyList(AutoExceptionHandler.class); + if (!classes.isEmpty()) { + return new HashSet>(classes); + } else { + // Autodiscovery found nothing so resort to looking at the ActionBean packages + log.info( + "Autodiscovery found no implementations of AutoExceptionHandler. Using ", + "the value of '", + AnnotatedClassActionResolver.PACKAGES, + "' instead."); + packages = + StringUtil.standardSplit(bootstrap.getProperty(AnnotatedClassActionResolver.PACKAGES)); + } + } - if (packages != null && packages.length > 0) { - ResolverUtil resolver = new ResolverUtil(); - resolver.findImplementations(AutoExceptionHandler.class, packages); - return resolver.getClasses(); - } - else { - return Collections.emptySet(); - } + if (packages != null && packages.length > 0) { + ResolverUtil resolver = new ResolverUtil(); + resolver.findImplementations(AutoExceptionHandler.class, packages); + return resolver.getClasses(); + } else { + return Collections.emptySet(); } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/exception/ExceptionHandler.java b/stripes/src/main/java/net/sourceforge/stripes/exception/ExceptionHandler.java index d8fa90208..933b18bf7 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/exception/ExceptionHandler.java +++ b/stripes/src/main/java/net/sourceforge/stripes/exception/ExceptionHandler.java @@ -14,48 +14,44 @@ */ package net.sourceforge.stripes.exception; -import net.sourceforge.stripes.config.ConfigurableComponent; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.ServletException; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import net.sourceforge.stripes.config.ConfigurableComponent; /** - *

    Component that is delegated to in order to handle any exceptions that are raised - * during the processing of a request which is processed through the Stripes Filter. - * Implementations have two options for handling an exception:

    + * Component that is delegated to in order to handle any exceptions that are raised during the + * processing of a request which is processed through the Stripes Filter. Implementations have two + * options for handling an exception: * *
      - *
    • Handle the exception and return
    • - *
    • Rethrown the exception if it cannot be handled
    • + *
    • Handle the exception and return + *
    • Rethrown the exception if it cannot be handled *
    * - *

    In the first case it is up to the exception handler to provide an appropriate response - * to the user. This might involve forwarding or redirecting the user to an error page, or - * providing a streaming response in the case of an AJAX client.

    + *

    In the first case it is up to the exception handler to provide an appropriate response to the + * user. This might involve forwarding or redirecting the user to an error page, or providing a + * streaming response in the case of an AJAX client. * - *

    If the ExceptionHandler elects not to handle an Exception and re-throws it then the - * exception will percolate up and the container will handle it using whatever error pages - * are configured.

    + *

    If the ExceptionHandler elects not to handle an Exception and re-throws it then the exception + * will percolate up and the container will handle it using whatever error pages are configured. * * @author Tim Fennell * @since Stripes 1.3 */ public interface ExceptionHandler extends ConfigurableComponent { - /** - * Responsible for handling any exceptions that arise as described in the class - * level javadoc. - * - * @param throwable the exception/throwable being handled - * @param request the current request. Notably, if the request progressed as far as - * ActionBeanResolution the ActionBean can be retreived by calling - * {@code request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN)}. - * @param response the current response. - * @throws ServletException if the exception passed in cannot be handled - */ - void handle(Throwable throwable, - HttpServletRequest request, - HttpServletResponse response) throws ServletException, IOException ; + /** + * Responsible for handling any exceptions that arise as described in the class level javadoc. + * + * @param throwable the exception/throwable being handled + * @param request the current request. Notably, if the request progressed as far as + * ActionBeanResolution the ActionBean can be retreived by calling {@code + * request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN)}. + * @param response the current response. + * @throws ServletException if the exception passed in cannot be handled + */ + void handle(Throwable throwable, HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException; } diff --git a/stripes/src/main/java/net/sourceforge/stripes/exception/SourcePageNotFoundException.java b/stripes/src/main/java/net/sourceforge/stripes/exception/SourcePageNotFoundException.java index b2075f3ae..117904ade 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/exception/SourcePageNotFoundException.java +++ b/stripes/src/main/java/net/sourceforge/stripes/exception/SourcePageNotFoundException.java @@ -20,35 +20,37 @@ /** * A subclass of {@link IllegalStateException} that is thrown when validation errors are present on * a request and the source page cannot be determined. - * + * * @author Ben Gunter * @since Stripes 1.5.5 */ public class SourcePageNotFoundException extends IllegalStateException { - private ActionBeanContext actionBeanContext; + private ActionBeanContext actionBeanContext; - /** - * Construct a new instance for the given action bean context. - * - * @param actionBeanContext The context. - */ - public SourcePageNotFoundException(ActionBeanContext actionBeanContext) { - // @formatter:off - super( - "Here's how it is. Someone (quite possibly the Stripes Dispatcher) needed " + - "to get the source page resolution. But no source page was supplied in the " + - "request, and unless you override ActionBeanContext.getSourcePageResolution() " + - "you're going to need that value. When you use a tag a hidden " + - "field called '" + StripesConstants.URL_KEY_SOURCE_PAGE + "' is included. " + - "If you write your own forms or links that could generate validation errors, " + - "you must include a value for this parameter. This can be done by calling " + - "request.getServletPath()."); - // @formatter:on - this.actionBeanContext = actionBeanContext; - } + /** + * Construct a new instance for the given action bean context. + * + * @param actionBeanContext The context. + */ + public SourcePageNotFoundException(ActionBeanContext actionBeanContext) { + // @formatter:off + super( + "Here's how it is. Someone (quite possibly the Stripes Dispatcher) needed " + + "to get the source page resolution. But no source page was supplied in the " + + "request, and unless you override ActionBeanContext.getSourcePageResolution() " + + "you're going to need that value. When you use a tag a hidden " + + "field called '" + + StripesConstants.URL_KEY_SOURCE_PAGE + + "' is included. " + + "If you write your own forms or links that could generate validation errors, " + + "you must include a value for this parameter. This can be done by calling " + + "request.getServletPath()."); + // @formatter:on + this.actionBeanContext = actionBeanContext; + } - /** Get the action bean context in which this exception occurred. */ - public ActionBeanContext getActionBeanContext() { - return actionBeanContext; - } + /** Get the action bean context in which this exception occurred. */ + public ActionBeanContext getActionBeanContext() { + return actionBeanContext; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/exception/StripesJspException.java b/stripes/src/main/java/net/sourceforge/stripes/exception/StripesJspException.java index c7245f3b6..91556c646 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/exception/StripesJspException.java +++ b/stripes/src/main/java/net/sourceforge/stripes/exception/StripesJspException.java @@ -14,26 +14,25 @@ */ package net.sourceforge.stripes.exception; -import javax.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspException; /** - * Stripes' version of the JspException that is used where only JspExceptions - * can be thrown. + * Stripes' version of the JspException that is used where only JspExceptions can be thrown. * * @author Tim Fennell */ public class StripesJspException extends JspException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - public StripesJspException(String string) { - super(string); - } + public StripesJspException(String string) { + super(string); + } - public StripesJspException(String string, Throwable throwable) { - super(string, throwable); - } + public StripesJspException(String string, Throwable throwable) { + super(string, throwable); + } - public StripesJspException(Throwable throwable) { - super(throwable); - } + public StripesJspException(Throwable throwable) { + super(throwable); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/exception/StripesRuntimeException.java b/stripes/src/main/java/net/sourceforge/stripes/exception/StripesRuntimeException.java index 2a6d7af56..5da8ea007 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/exception/StripesRuntimeException.java +++ b/stripes/src/main/java/net/sourceforge/stripes/exception/StripesRuntimeException.java @@ -15,23 +15,23 @@ package net.sourceforge.stripes.exception; /** - * Stripes' version of a RuntimeException that is to be preferred in Stripes - * code to throwing plain RuntimeExceptions. + * Stripes' version of a RuntimeException that is to be preferred in Stripes code to throwing plain + * RuntimeExceptions. * * @author Tim Fennell */ public class StripesRuntimeException extends RuntimeException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - public StripesRuntimeException(String message) { - super(message); - } + public StripesRuntimeException(String message) { + super(message); + } - public StripesRuntimeException(String message, Throwable cause) { - super(message, cause); - } + public StripesRuntimeException(String message, Throwable cause) { + super(message, cause); + } - public StripesRuntimeException(Throwable cause) { - super(cause); - } + public StripesRuntimeException(Throwable cause) { + super(cause); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/exception/StripesServletException.java b/stripes/src/main/java/net/sourceforge/stripes/exception/StripesServletException.java index 92e28cfdc..e098082ad 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/exception/StripesServletException.java +++ b/stripes/src/main/java/net/sourceforge/stripes/exception/StripesServletException.java @@ -14,26 +14,26 @@ */ package net.sourceforge.stripes.exception; -import javax.servlet.ServletException; +import jakarta.servlet.ServletException; /** - * Stripes' implementation of a ServletException that is thrown by Stripes - * wherever throws clauses are limited to ServletExceptions. + * Stripes' implementation of a ServletException that is thrown by Stripes wherever throws clauses + * are limited to ServletExceptions. * * @author Tim Fennell */ public class StripesServletException extends ServletException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - public StripesServletException(String string) { - super(string); - } + public StripesServletException(String string) { + super(string); + } - public StripesServletException(String string, Throwable throwable) { - super(string, throwable); - } + public StripesServletException(String string, Throwable throwable) { + super(string, throwable); + } - public StripesServletException(Throwable throwable) { - super(throwable); - } + public StripesServletException(Throwable throwable) { + super(throwable); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/exception/UrlBindingConflictException.java b/stripes/src/main/java/net/sourceforge/stripes/exception/UrlBindingConflictException.java index 5f75244e2..850597315 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/exception/UrlBindingConflictException.java +++ b/stripes/src/main/java/net/sourceforge/stripes/exception/UrlBindingConflictException.java @@ -15,116 +15,119 @@ package net.sourceforge.stripes.exception; import java.util.Collection; - import net.sourceforge.stripes.action.ActionBean; /** - *

    * This exception indicates that a URL does not contain enough information to map it to a single * {@link ActionBean} class. In some cases, a URL may match more than one URL binding. - *

    - *

    - * For example, suppose you have two ActionBeans with the URL bindings /foo/{param}/bar - * and /foo/{param}/blah. The paths {@code /foo} and {@code /foo/X} -- while legal, - * since any number of parameters or literals may be omitted from the end of a clean URL -- match - * both of the URL bindings. Since Stripes cannot determine from the URL the ActionBean to which to - * dispatch the request, it throws this exception to indicate the conflict. - *

    - * + * + *

    For example, suppose you have two ActionBeans with the URL bindings /foo/{param}/bar + * and /foo/{param}/blah. The paths {@code /foo} and {@code /foo/X} -- while + * legal, since any number of parameters or literals may be omitted from the end of a clean URL -- + * match both of the URL bindings. Since Stripes cannot determine from the URL the ActionBean to + * which to dispatch the request, it throws this exception to indicate the conflict. + * * @author Ben Gunter * @since Stripes 1.5.1 */ public class UrlBindingConflictException extends StripesRuntimeException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - /** Generate the message to pass to the superclass constructor */ - protected static String getMessage(Class targetClass, String path, - Collection matches) { - return (targetClass == null ? "" : "Failure generating URL for " + targetClass + ". ") - + "The path " + path + " cannot be mapped to a single ActionBean because multiple " - + "URL bindings match it. The matching URL bindings are " + matches + ". If you " - + "generated the URL using the Stripes tag library (stripes:link, stripes:url, " - + "stripes:form, etc.) then you must embed enough stripes:param tags within the " - + "parent tag to produce a URL that maps to exactly one of the indicated matches. " - + "If you generated the URL by some other means, then you must embed enough " - + "information in the URL to achieve the same end."; - } + /** Generate the message to pass to the superclass constructor */ + protected static String getMessage( + Class targetClass, String path, Collection matches) { + return (targetClass == null ? "" : "Failure generating URL for " + targetClass + ". ") + + "The path " + + path + + " cannot be mapped to a single ActionBean because multiple " + + "URL bindings match it. The matching URL bindings are " + + matches + + ". If you " + + "generated the URL using the Stripes tag library (stripes:link, stripes:url, " + + "stripes:form, etc.) then you must embed enough stripes:param tags within the " + + "parent tag to produce a URL that maps to exactly one of the indicated matches. " + + "If you generated the URL by some other means, then you must embed enough " + + "information in the URL to achieve the same end."; + } - private String path; - private Collection matches; - private Class targetClass; + private String path; + private Collection matches; + private Class targetClass; - /** - * New exception indicating that the {@code path} does not map to a single ActionBean because it - * potentially matches all the URL bindings in the {@code matches} collection. - * - * @param message An informative message about what went wrong - * @param targetClass The class for which a URL could not be generated. - * @param path The offending path - * @param matches A collection of all the potentially matching URL bindings - */ - public UrlBindingConflictException(String message, Class targetClass, - String path, Collection matches) { - super(message); - this.targetClass = targetClass; - this.path = path; - this.matches = matches; - } + /** + * New exception indicating that the {@code path} does not map to a single ActionBean because it + * potentially matches all the URL bindings in the {@code matches} collection. + * + * @param message An informative message about what went wrong + * @param targetClass The class for which a URL could not be generated. + * @param path The offending path + * @param matches A collection of all the potentially matching URL bindings + */ + public UrlBindingConflictException( + String message, + Class targetClass, + String path, + Collection matches) { + super(message); + this.targetClass = targetClass; + this.path = path; + this.matches = matches; + } - /** - * New exception indicating that the {@code path} does not map to a single ActionBean because it - * potentially matches all the URL bindings in the {@code matches} collection. - * - * @param targetClass The class for which a URL could not be generated. - * @param path The offending path - * @param matches A collection of all the potentially matching URL bindings - */ - public UrlBindingConflictException(Class targetClass, String path, - Collection matches) { - this(getMessage(targetClass, path, matches), targetClass, path, matches); - } + /** + * New exception indicating that the {@code path} does not map to a single ActionBean because it + * potentially matches all the URL bindings in the {@code matches} collection. + * + * @param targetClass The class for which a URL could not be generated. + * @param path The offending path + * @param matches A collection of all the potentially matching URL bindings + */ + public UrlBindingConflictException( + Class targetClass, String path, Collection matches) { + this(getMessage(targetClass, path, matches), targetClass, path, matches); + } - /** - * New exception indicating that the {@code path} does not map to a single ActionBean because it - * potentially matches all the URL bindings in the {@code matches} collection. - * - * @param message An informative message about what went wrong - * @param path The offending path - * @param matches A collection of all the potentially matching URL bindings - */ - public UrlBindingConflictException(String message, String path, Collection matches) { - this(message, null, path, matches); - } + /** + * New exception indicating that the {@code path} does not map to a single ActionBean because it + * potentially matches all the URL bindings in the {@code matches} collection. + * + * @param message An informative message about what went wrong + * @param path The offending path + * @param matches A collection of all the potentially matching URL bindings + */ + public UrlBindingConflictException(String message, String path, Collection matches) { + this(message, null, path, matches); + } - /** - * New exception indicating that the {@code path} does not map to a single ActionBean because it - * potentially matches all the URL bindings in the {@code matches} collection. - * - * @param path The offending path - * @param matches A collection of all the potentially matching URL bindings - */ - public UrlBindingConflictException(String path, Collection matches) { - this(getMessage(null, path, matches), path, matches); - } + /** + * New exception indicating that the {@code path} does not map to a single ActionBean because it + * potentially matches all the URL bindings in the {@code matches} collection. + * + * @param path The offending path + * @param matches A collection of all the potentially matching URL bindings + */ + public UrlBindingConflictException(String path, Collection matches) { + this(getMessage(null, path, matches), path, matches); + } - /** Get the path that failed to map to a single ActionBean */ - public String getPath() { - return path; - } + /** Get the path that failed to map to a single ActionBean */ + public String getPath() { + return path; + } - /** Get all the URL bindings on existing ActionBeans that match the path */ - public Collection getMatches() { - return matches; - } + /** Get all the URL bindings on existing ActionBeans that match the path */ + public Collection getMatches() { + return matches; + } - /** - * Get the {@link ActionBean} class for which a URL was being generated when this exception was - * thrown. If the exception occurred while dispatching a request, then this property will be - * null since the path cannot be associated with an ActionBean class. However, if it is thrown - * while generating a URL that is intended to point to an ActionBean, then this property will - * indicate the class that was being targeted. - */ - public Class getTargetClass() { - return targetClass; - } + /** + * Get the {@link ActionBean} class for which a URL was being generated when this exception was + * thrown. If the exception occurred while dispatching a request, then this property will be null + * since the path cannot be associated with an ActionBean class. However, if it is thrown while + * generating a URL that is intended to point to an ActionBean, then this property will indicate + * the class that was being targeted. + */ + public Class getTargetClass() { + return targetClass; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/format/DateFormatter.java b/stripes/src/main/java/net/sourceforge/stripes/format/DateFormatter.java index 823be16ed..18d2c311b 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/format/DateFormatter.java +++ b/stripes/src/main/java/net/sourceforge/stripes/format/DateFormatter.java @@ -14,150 +14,149 @@ */ package net.sourceforge.stripes.format; -import net.sourceforge.stripes.exception.StripesRuntimeException; - +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.Date; -import java.util.Map; import java.util.HashMap; import java.util.Locale; -import java.text.DateFormat; -import java.text.SimpleDateFormat; +import java.util.Map; +import net.sourceforge.stripes.exception.StripesRuntimeException; /** - *

    Implements a basic formatter for Date objects. Accepts several known types and patterns, as - * well as arbitrary patterns. Under the covers uses DateFormat and SimpleDateFormat objects - * from the java.text package - it is advised that you become familiar with these classes before - * attempting to use custom patterns.

    + * Implements a basic formatter for Date objects. Accepts several known types and patterns, as well + * as arbitrary patterns. Under the covers uses DateFormat and SimpleDateFormat objects from the + * java.text package - it is advised that you become familiar with these classes before attempting + * to use custom patterns. + * + *

    Format types affect the kind of information that is output. The supported format types are + * (values are not case sensitive): * - *

    Format types affect the kind of information that is output. The supported format types - * are (values are not case sensitive):

    *
      - *
    • date
    • - *
    • time
    • - *
    • datetime
    • + *
    • date + *
    • time + *
    • datetime *
    * - *

    Format strings affect the format of the selected output. One of the following known values - * may be supplied as the format string (named values are not case sensitive). If the value is not - * one of the following, it is passed to SimpleDateFormat as a pattern string. + *

    Format strings affect the format of the selected output. One of the following known values may + * be supplied as the format string (named values are not case sensitive). If the value is not one + * of the following, it is passed to SimpleDateFormat as a pattern string. + * *

      - *
    • short
    • - *
    • medium
    • - *
    • long
    • - *
    • full
    • + *
    • short + *
    • medium + *
    • long + *
    • full *
    * * @author Tim Fennell */ public class DateFormatter implements Formatter { - /** Maintains a map of named formats that can be used instead of patterns. */ - protected static final Map namedPatterns = new HashMap(); - - static { - namedPatterns.put("short", DateFormat.SHORT ); - namedPatterns.put("medium", DateFormat.MEDIUM); - namedPatterns.put("long", DateFormat.LONG ); - namedPatterns.put("full", DateFormat.FULL ); - } - - private String formatType; - private String formatPattern; - private Locale locale; - private DateFormat format; - - /** Sets the format type to be used to render dates as Strings. */ - public void setFormatType(String formatType) { - this.formatType = formatType; - } - - /** Gets the format type to be used to render dates as Strings. */ - public String getFormatType() { - return formatType; - } - - /** Sets the named format string or date pattern to use to format the date. */ - public void setFormatPattern(String formatPattern) { - this.formatPattern = formatPattern; - } - - /** Gets the named format string or date pattern to use to format the date. */ - public String getFormatPattern() { - return formatPattern; - } - - /** Sets the locale that output String should be in. */ - public void setLocale(Locale locale) { - this.locale = locale; - } - - /** Gets the locale that output String should be in. */ - public Locale getLocale() { - return locale; - } - - /** - * Constructs the DateFormat used for formatting, based on the values passed to the - * various setter methods on the class. If the formatString is one of the named formats - * then a DateFormat instance is created of the specified type and format, otherwise - * a SimpleDateFormat is constructed using the pattern provided and the formatType is ignored. - * - * @throws StripesRuntimeException if the formatType is not one of 'date', 'time' or 'datetime'. - */ - public void init() { - // Default these values if they were not supplied - if (formatPattern == null) { - formatPattern = "short"; - } - - if (formatType == null) { - formatType = "date"; - } - - String lcFormatString = formatPattern.toLowerCase(); - String lcFormatType = formatType.toLowerCase(); - - // Now figure out how to construct our date format for our locale - if ( namedPatterns.containsKey(lcFormatString) ) { - - if (lcFormatType.equals("date")) { - format = DateFormat.getDateInstance(namedPatterns.get(lcFormatString), locale); - } - else if (lcFormatType.equals("datetime")) { - format = DateFormat.getDateTimeInstance(namedPatterns.get(lcFormatString), - namedPatterns.get(lcFormatString), - locale); - } - else if (lcFormatType.equals("time")) { - format = DateFormat.getTimeInstance(namedPatterns.get(lcFormatString), locale); - } - else { - throw new StripesRuntimeException("Invalid formatType for Date: " + formatType + - ". Allowed types are 'date', 'time' and 'datetime'."); - } - } - else { - format = new SimpleDateFormat(formatPattern, locale); - } - } - - /** - * Gets the date format that will format the date. Subclasses that wish to alter the date format - * should override init(), call super.init(), and then obtain the date format object. - */ - public DateFormat getDateFormat() { - return this.format; + /** Maintains a map of named formats that can be used instead of patterns. */ + protected static final Map namedPatterns = new HashMap(); + + static { + namedPatterns.put("short", DateFormat.SHORT); + namedPatterns.put("medium", DateFormat.MEDIUM); + namedPatterns.put("long", DateFormat.LONG); + namedPatterns.put("full", DateFormat.FULL); + } + + private String formatType; + private String formatPattern; + private Locale locale; + private DateFormat format; + + /** Sets the format type to be used to render dates as Strings. */ + public void setFormatType(String formatType) { + this.formatType = formatType; + } + + /** Gets the format type to be used to render dates as Strings. */ + public String getFormatType() { + return formatType; + } + + /** Sets the named format string or date pattern to use to format the date. */ + public void setFormatPattern(String formatPattern) { + this.formatPattern = formatPattern; + } + + /** Gets the named format string or date pattern to use to format the date. */ + public String getFormatPattern() { + return formatPattern; + } + + /** Sets the locale that output String should be in. */ + public void setLocale(Locale locale) { + this.locale = locale; + } + + /** Gets the locale that output String should be in. */ + public Locale getLocale() { + return locale; + } + + /** + * Constructs the DateFormat used for formatting, based on the values passed to the various setter + * methods on the class. If the formatString is one of the named formats then a DateFormat + * instance is created of the specified type and format, otherwise a SimpleDateFormat is + * constructed using the pattern provided and the formatType is ignored. + * + * @throws StripesRuntimeException if the formatType is not one of 'date', 'time' or 'datetime'. + */ + public void init() { + // Default these values if they were not supplied + if (formatPattern == null) { + formatPattern = "short"; } - /** - * Sets the date format that will format the date. Subclasses that wish to set the date format - * should override init() and then set the date format object. - */ - public void setDateFormat(DateFormat dateFormat) { - this.format = dateFormat; + if (formatType == null) { + formatType = "date"; } - /** Formats a Date as a String using the rules supplied when the formatter was built. */ - public String format(Date input) { - return this.format.format(input); + String lcFormatString = formatPattern.toLowerCase(); + String lcFormatType = formatType.toLowerCase(); + + // Now figure out how to construct our date format for our locale + if (namedPatterns.containsKey(lcFormatString)) { + + if (lcFormatType.equals("date")) { + format = DateFormat.getDateInstance(namedPatterns.get(lcFormatString), locale); + } else if (lcFormatType.equals("datetime")) { + format = + DateFormat.getDateTimeInstance( + namedPatterns.get(lcFormatString), namedPatterns.get(lcFormatString), locale); + } else if (lcFormatType.equals("time")) { + format = DateFormat.getTimeInstance(namedPatterns.get(lcFormatString), locale); + } else { + throw new StripesRuntimeException( + "Invalid formatType for Date: " + + formatType + + ". Allowed types are 'date', 'time' and 'datetime'."); + } + } else { + format = new SimpleDateFormat(formatPattern, locale); } + } + + /** + * Gets the date format that will format the date. Subclasses that wish to alter the date format + * should override init(), call super.init(), and then obtain the date format object. + */ + public DateFormat getDateFormat() { + return this.format; + } + + /** + * Sets the date format that will format the date. Subclasses that wish to set the date format + * should override init() and then set the date format object. + */ + public void setDateFormat(DateFormat dateFormat) { + this.format = dateFormat; + } + + /** Formats a Date as a String using the rules supplied when the formatter was built. */ + public String format(Date input) { + return this.format.format(input); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/format/DefaultFormatterFactory.java b/stripes/src/main/java/net/sourceforge/stripes/format/DefaultFormatterFactory.java index 926a8526a..df8fa7f37 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/format/DefaultFormatterFactory.java +++ b/stripes/src/main/java/net/sourceforge/stripes/format/DefaultFormatterFactory.java @@ -17,7 +17,6 @@ import java.util.Date; import java.util.Locale; import java.util.Map; - import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.util.Log; import net.sourceforge.stripes.util.TypeHandlerCache; @@ -28,98 +27,97 @@ * formatter for a specific class, then it attempts to find the best available formatter by * searching for a match against the target implemented interfaces, class's superclasses, and * interface superclasses. - * + * * @author Tim Fennell */ public class DefaultFormatterFactory implements FormatterFactory { - private static final Log log = Log.getInstance(DefaultFormatterFactory.class); + private static final Log log = Log.getInstance(DefaultFormatterFactory.class); - /** Cache target type to Formatter class mappings. */ - private TypeHandlerCache>> cache; + /** Cache target type to Formatter class mappings. */ + private TypeHandlerCache>> cache; - /** Stores a reference to the Configuration passed in at initialization time. */ - private Configuration configuration; + /** Stores a reference to the Configuration passed in at initialization time. */ + private Configuration configuration; - /** Stores a reference to the configuration and configures the default formatters. */ - public void init(Configuration configuration) throws Exception { - this.configuration = configuration; - this.cache = new TypeHandlerCache>>(); - this.cache.setDefaultHandler(ObjectFormatter.class); + /** Stores a reference to the configuration and configures the default formatters. */ + public void init(Configuration configuration) throws Exception { + this.configuration = configuration; + this.cache = new TypeHandlerCache>>(); + this.cache.setDefaultHandler(ObjectFormatter.class); - add(Date.class, DateFormatter.class); - add(Number.class, NumberFormatter.class); - add(Enum.class, EnumFormatter.class); - } + add(Date.class, DateFormatter.class); + add(Number.class, NumberFormatter.class); + add(Enum.class, EnumFormatter.class); + } - /** Allows subclasses to access the stored configuration if needed. */ - protected Configuration getConfiguration() { - return this.configuration; - } + /** Allows subclasses to access the stored configuration if needed. */ + protected Configuration getConfiguration() { + return this.configuration; + } - /** - * Gets the (rather confusing) Map of Formatter objects. The Map uses the target class - * as the key in the Map, and the Class object representing the Formatter as the value. - * - * @return the Map of Formatter classes - */ - protected Map,Class>> getFormatters() { - return cache.getHandlers(); - } + /** + * Gets the (rather confusing) Map of Formatter objects. The Map uses the target class as the key + * in the Map, and the Class object representing the Formatter as the value. + * + * @return the Map of Formatter classes + */ + protected Map, Class>> getFormatters() { + return cache.getHandlers(); + } - /** - * Adds a Formatter to the set of registered Formatters, overriding an existing - * formatter if one was registered for the type. - * - * @param targetType the type for which the formatter will handle formatting - * @param formatterClass the implementation class that will handle the formatting - */ - public void add(Class targetType, Class> formatterClass) { - cache.add(targetType, formatterClass); - } + /** + * Adds a Formatter to the set of registered Formatters, overriding an existing formatter if one + * was registered for the type. + * + * @param targetType the type for which the formatter will handle formatting + * @param formatterClass the implementation class that will handle the formatting + */ + public void add(Class targetType, Class> formatterClass) { + cache.add(targetType, formatterClass); + } - /** - * Check to see if the there is a Formatter for the specified clazz. If a Formatter is found an - * instance is created, configured and returned. Otherwise returns null. - * - * @param clazz the type of object being formatted - * @param locale the Locale into which the object should be formatted - * @param formatType the type of output to produce (e.g. date, time etc.) - * @param formatPattern a named format string, or a format pattern - * @return Formatter an instance of a Formatter, or null - */ - public Formatter getFormatter(Class clazz, Locale locale, String formatType, String formatPattern) { - Class> formatterClass = cache.getHandler(clazz); - if (formatterClass != null) { - try { - return getInstance(formatterClass, formatType, formatPattern, locale); - } - catch (Exception e) { - log.error(e, "Unable to instantiate Formatter ", formatterClass); - return null; - } - } - else { - log.trace("Couldn't find a formatter for ", clazz); - return null; - } + /** + * Check to see if the there is a Formatter for the specified clazz. If a Formatter is found an + * instance is created, configured and returned. Otherwise returns null. + * + * @param clazz the type of object being formatted + * @param locale the Locale into which the object should be formatted + * @param formatType the type of output to produce (e.g. date, time etc.) + * @param formatPattern a named format string, or a format pattern + * @return Formatter an instance of a Formatter, or null + */ + public Formatter getFormatter( + Class clazz, Locale locale, String formatType, String formatPattern) { + Class> formatterClass = cache.getHandler(clazz); + if (formatterClass != null) { + try { + return getInstance(formatterClass, formatType, formatPattern, locale); + } catch (Exception e) { + log.error(e, "Unable to instantiate Formatter ", formatterClass); + return null; + } + } else { + log.trace("Couldn't find a formatter for ", clazz); + return null; } + } - /** - * Gets an instance of the Formatter class specified. - * - * @param clazz the Formatter type that is desired - * @return an instance of the Formatter specified - * @throws Exception if there is a problem instantiating the Formatter - */ - public Formatter getInstance(Class> clazz, - String formatType, String formatPattern, Locale locale) - throws Exception { + /** + * Gets an instance of the Formatter class specified. + * + * @param clazz the Formatter type that is desired + * @return an instance of the Formatter specified + * @throws Exception if there is a problem instantiating the Formatter + */ + public Formatter getInstance( + Class> clazz, String formatType, String formatPattern, Locale locale) + throws Exception { - Formatter formatter = getConfiguration().getObjectFactory().newInstance(clazz); - formatter.setFormatType(formatType); - formatter.setFormatPattern(formatPattern); - formatter.setLocale(locale); - formatter.init(); - return formatter; - } + Formatter formatter = getConfiguration().getObjectFactory().newInstance(clazz); + formatter.setFormatType(formatType); + formatter.setFormatPattern(formatPattern); + formatter.setLocale(locale); + formatter.init(); + return formatter; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/format/EnumFormatter.java b/stripes/src/main/java/net/sourceforge/stripes/format/EnumFormatter.java index e8dcbe1e4..6762edfc8 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/format/EnumFormatter.java +++ b/stripes/src/main/java/net/sourceforge/stripes/format/EnumFormatter.java @@ -17,37 +17,37 @@ import java.util.Locale; /** - * A simple formatter for Enum classes that always returns the value of Enum.name(). Intended - * really only to enable the seamless usage of enums as values in hidden fields, radio - * buttons, checkboxes etc. it is not intended that this will be used to format Enum - * values into text fields where a localized value might be more appropriate. + * A simple formatter for Enum classes that always returns the value of Enum.name(). Intended really + * only to enable the seamless usage of enums as values in hidden fields, radio buttons, checkboxes + * etc. it is not intended that this will be used to format Enum values into text fields where a + * localized value might be more appropriate. * * @author Tim Fennell * @since Stripes 1.4.1 */ public class EnumFormatter implements Formatter> { - /** Does nothing. Format types are not supported for Enums. */ - public void setFormatType(String formatType) { } + /** Does nothing. Format types are not supported for Enums. */ + public void setFormatType(String formatType) {} - /** Does nothing. Format patterns are not supported for Enums. */ - public void setFormatPattern(String formatPattern) { } + /** Does nothing. Format patterns are not supported for Enums. */ + public void setFormatPattern(String formatPattern) {} - /** Does nothing. Enums values are always formatted using name() which is not localizable. */ - public void setLocale(Locale locale) { } + /** Does nothing. Enums values are always formatted using name() which is not localizable. */ + public void setLocale(Locale locale) {} - /** Does nothing since no initialization is needed. */ - public void init() { } + /** Does nothing since no initialization is needed. */ + public void init() {} - /** - * Formats the supplied value as a String. If the value cannot be formatted because it is - * an inappropriate type, or because faulty pattern information was supplied, should fail - * loudly by throwing a RuntimeException or subclass thereof. - * - * @param input an object of a type that the formatter knows how to format - * @return a String version of the input, formatted for the chosen locale - */ - public String format(Enum input) { - if (input != null) return input.name(); - else return null; - } + /** + * Formats the supplied value as a String. If the value cannot be formatted because it is an + * inappropriate type, or because faulty pattern information was supplied, should fail loudly by + * throwing a RuntimeException or subclass thereof. + * + * @param input an object of a type that the formatter knows how to format + * @return a String version of the input, formatted for the chosen locale + */ + public String format(Enum input) { + if (input != null) return input.name(); + else return null; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/format/Formatter.java b/stripes/src/main/java/net/sourceforge/stripes/format/Formatter.java index 4166d3e68..eca116b07 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/format/Formatter.java +++ b/stripes/src/main/java/net/sourceforge/stripes/format/Formatter.java @@ -17,43 +17,43 @@ import java.util.Locale; /** - *

    Interface that is used to provide a relatively simple formatting interface to the rest of the - * system. Designed to wrap the complexity of the java.text format classes in an interface that - * can take two (or three if you include Locale) parameters from tags and apply formats in an - * intelligent way.

    + * Interface that is used to provide a relatively simple formatting interface to the rest of the + * system. Designed to wrap the complexity of the java.text format classes in an interface that can + * take two (or three if you include Locale) parameters from tags and apply formats in an + * intelligent way. * *

    In terms of lifecycle, a formatter will be instantiated, have setFormatType(), - * setFormatString() and setLocale() called in rapid succession. If no values were supplied, + * setFormatString() and setLocale() called in rapid succession. If no values were supplied, * setFormatType() and setFormatString() may not be called - and implementations should select - * reasonable defaults. Locale will always be provided. After the setters have been called, - * init() will be called, and the Formatter should use this opportunity to construct any internal - * objects necessary to perform formatting. The format() method will then be called one or more - * times before the Formatter is eventually dereferenced.

    + * reasonable defaults. Locale will always be provided. After the setters have been called, init() + * will be called, and the Formatter should use this opportunity to construct any internal objects + * necessary to perform formatting. The format() method will then be called one or more times before + * the Formatter is eventually dereferenced. * * @author Tim Fennell */ public interface Formatter { - /** Sets the type of format that should be created. */ - void setFormatType(String formatType); + /** Sets the type of format that should be created. */ + void setFormatType(String formatType); - /** Sets a named format, or format pattern to use in formatting objects. */ - void setFormatPattern(String formatPattern); + /** Sets a named format, or format pattern to use in formatting objects. */ + void setFormatPattern(String formatPattern); - /** Sets the Locale into which the object should be formatted. */ - void setLocale(Locale locale); + /** Sets the Locale into which the object should be formatted. */ + void setLocale(Locale locale); - /** Called once all setters have been invoked, to allow Formatter to prepare itself. */ - void init(); + /** Called once all setters have been invoked, to allow Formatter to prepare itself. */ + void init(); - /** - * Formats the supplied value as a String. If the value cannot be formatted because it is - * an inappropriate type, or because faulty pattern information was supplied, should fail - * loudly by throwing a RuntimeException or subclass thereof. Therefore this method should - * never return {@code null}. - - * @param input an object of a type that the formatter knows how to format - * @return a non-null, String version of the input, formatted for the chosen locale - */ - String format(T input); + /** + * Formats the supplied value as a String. If the value cannot be formatted because it is an + * inappropriate type, or because faulty pattern information was supplied, should fail loudly by + * throwing a RuntimeException or subclass thereof. Therefore this method should never return + * {@code null}. + * + * @param input an object of a type that the formatter knows how to format + * @return a non-null, String version of the input, formatted for the chosen locale + */ + String format(T input); } diff --git a/stripes/src/main/java/net/sourceforge/stripes/format/FormatterFactory.java b/stripes/src/main/java/net/sourceforge/stripes/format/FormatterFactory.java index 8915ef486..6bc2c1869 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/format/FormatterFactory.java +++ b/stripes/src/main/java/net/sourceforge/stripes/format/FormatterFactory.java @@ -14,42 +14,41 @@ */ package net.sourceforge.stripes.format; -import net.sourceforge.stripes.config.ConfigurableComponent; - import java.util.Locale; +import net.sourceforge.stripes.config.ConfigurableComponent; /** - * Interface for creating instances of formatter classes that are capable of formatting - * the types specified into Strings. + * Interface for creating instances of formatter classes that are capable of formatting the types + * specified into Strings. * * @see Formatter * @author Tim Fennell */ public interface FormatterFactory extends ConfigurableComponent { - /** - * Returns a configured formatter that meets the criteria specified. The formatter is ready - * for use as soon as it is returned from this method. - * - * @param clazz the type of object being formatted - * @param locale the Locale into which the object should be formatted - * @param formatType the manner in which the object should be formatted (allows nulls) - * @param formatPattern the named format, or format pattern to be applied (allows nulls) - * @return Formatter an instance of a Formatter, or null if no Formatter is available for - * the type specified - */ - Formatter getFormatter(Class clazz, Locale locale, String formatType, String formatPattern); + /** + * Returns a configured formatter that meets the criteria specified. The formatter is ready for + * use as soon as it is returned from this method. + * + * @param clazz the type of object being formatted + * @param locale the Locale into which the object should be formatted + * @param formatType the manner in which the object should be formatted (allows nulls) + * @param formatPattern the named format, or format pattern to be applied (allows nulls) + * @return Formatter an instance of a Formatter, or null if no Formatter is available for the type + * specified + */ + Formatter getFormatter(Class clazz, Locale locale, String formatType, String formatPattern); - /** - * Adds a formatter to the set of registered formatters, overriding an existing formatter if one - * was already registered for the type. This is an optional operation. If an implementation does - * not support adding formatters at runtime, then it must throw - * {@link UnsupportedOperationException}. - * - * @param targetType the type for which the formatter will handle formatting - * @param formatterClass the implementation class that will handle the formatting - * @throws UnsupportedOperationException if the implementation does not support adding - * formatters at runtime - */ - public void add(Class targetType, Class> formatterClass); + /** + * Adds a formatter to the set of registered formatters, overriding an existing formatter if one + * was already registered for the type. This is an optional operation. If an implementation does + * not support adding formatters at runtime, then it must throw {@link + * UnsupportedOperationException}. + * + * @param targetType the type for which the formatter will handle formatting + * @param formatterClass the implementation class that will handle the formatting + * @throws UnsupportedOperationException if the implementation does not support adding formatters + * at runtime + */ + public void add(Class targetType, Class> formatterClass); } diff --git a/stripes/src/main/java/net/sourceforge/stripes/format/NumberFormatter.java b/stripes/src/main/java/net/sourceforge/stripes/format/NumberFormatter.java index a2fa2bbe3..86d8bdfef 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/format/NumberFormatter.java +++ b/stripes/src/main/java/net/sourceforge/stripes/format/NumberFormatter.java @@ -14,134 +14,134 @@ */ package net.sourceforge.stripes.format; -import net.sourceforge.stripes.exception.StripesRuntimeException; - -import java.text.NumberFormat; import java.text.DecimalFormat; +import java.text.NumberFormat; import java.util.HashSet; import java.util.Locale; import java.util.Set; +import net.sourceforge.stripes.exception.StripesRuntimeException; /** - *

    Formats numbers into localized Strings for display. This class relies heavily on the - * NumberFormat and DecimalFormat classes in the java.text package, and it is suggested that you - * become familiar with those classes before using custom formats.

    + * Formats numbers into localized Strings for display. This class relies heavily on the NumberFormat + * and DecimalFormat classes in the java.text package, and it is suggested that you become familiar + * with those classes before using custom formats. + * + *

    Accepts the following named formatTypes (not case sensitive): * - *

    Accepts the following named formatTypes (not case sensitive):

    *
      - *
    • number
    • - *
    • currency
    • - *
    • percentage
    • + *
    • number + *
    • currency + *
    • percentage *
    * - *

    If a format type is not supplied the default value of "number" will be used. Format String - * can be either a custom pattern as used by NumberFormat, or one of the following named formats - * (not case sensitive):

    + *

    If a format type is not supplied the default value of "number" will be used. Format String can + * be either a custom pattern as used by NumberFormat, or one of the following named formats (not + * case sensitive): + * *

      - *
    • plain - Outputs text in a manner similar to toString(), but appropriate to a locale.
    • - *
    • integer - Outputs text with grouping characters and no decimals.
    • - *
    • decimal - Outputs text with grouping characters and 2-6 decimal positions as needed.
    • + *
    • plain - Outputs text in a manner similar to toString(), but appropriate to a locale. + *
    • integer - Outputs text with grouping characters and no decimals. + *
    • decimal - Outputs text with grouping characters and 2-6 decimal positions as needed. *
    * * @author Tim Fennell */ public class NumberFormatter implements Formatter { - /** Maintains a set of named formats that can be used instead of patterns. */ - protected static final Set namedPatterns = new HashSet(); - - static { - namedPatterns.add("plain"); - namedPatterns.add("integer"); - namedPatterns.add("decimal"); - } - - private String formatType; - private String formatPattern; - private Locale locale; - private NumberFormat format; - - /** Sets the format type to be used to render numbers as Strings. */ - public void setFormatType(String formatType) { - this.formatType = formatType; + /** Maintains a set of named formats that can be used instead of patterns. */ + protected static final Set namedPatterns = new HashSet(); + + static { + namedPatterns.add("plain"); + namedPatterns.add("integer"); + namedPatterns.add("decimal"); + } + + private String formatType; + private String formatPattern; + private Locale locale; + private NumberFormat format; + + /** Sets the format type to be used to render numbers as Strings. */ + public void setFormatType(String formatType) { + this.formatType = formatType; + } + + /** Gets the format type to be used to render numbers as Strings. */ + public String getFormatType() { + return formatType; + } + + /** Sets the named format string or number format pattern to use to format the number. */ + public void setFormatPattern(String formatPattern) { + this.formatPattern = formatPattern; + } + + /** Gets the named format string or number format pattern to use to format the number. */ + public String getFormatPattern() { + return formatPattern; + } + + /** Sets the locale that output String should be in. */ + public void setLocale(Locale locale) { + this.locale = locale; + } + + /** Gets the locale that output String should be in. */ + public Locale getLocale() { + return locale; + } + + /** Instantiates the NumberFormat based on the information provided through setter methods. */ + public void init() { + // Set some sensible defaults if things are null + if (this.formatType == null) { + this.formatType = "number"; } - /** Gets the format type to be used to render numbers as Strings. */ - public String getFormatType() { - return formatType; + // Figure out which kind of number formatter to get + if (this.formatPattern == null) { + this.formatPattern = "plain"; } - /** Sets the named format string or number format pattern to use to format the number. */ - public void setFormatPattern(String formatPattern) { - this.formatPattern = formatPattern; + if (this.formatType.equalsIgnoreCase("number")) { + this.format = NumberFormat.getInstance(locale); + } else if (this.formatType.equalsIgnoreCase("currency")) { + this.format = NumberFormat.getCurrencyInstance(locale); + } else if (this.formatType.equalsIgnoreCase("percentage")) { + this.format = NumberFormat.getPercentInstance(locale); + } else { + throw new StripesRuntimeException( + "Invalid format type supplied for formatting a " + + "number: " + + this.formatType + + ". Valid values are 'number', 'currency' " + + "and 'percentage'."); } - /** Gets the named format string or number format pattern to use to format the number. */ - public String getFormatPattern() { - return formatPattern; + // Do any extra configuration + if (this.formatPattern.equalsIgnoreCase("plain")) { + this.format.setGroupingUsed(false); + } else if (this.formatPattern.equalsIgnoreCase("integer")) { + this.format.setMaximumFractionDigits(0); + } else if (this.formatPattern.equalsIgnoreCase(("decimal"))) { + this.format.setMinimumFractionDigits(2); + this.format.setMaximumFractionDigits(6); + } else { + try { + ((DecimalFormat) this.format).applyPattern(this.formatPattern); + } catch (Exception e) { + throw new StripesRuntimeException( + "Custom pattern could not be applied to " + + "NumberFormat instance. Pattern was: " + + this.formatPattern, + e); + } } + } - /** Sets the locale that output String should be in. */ - public void setLocale(Locale locale) { - this.locale = locale; - } - - /** Gets the locale that output String should be in. */ - public Locale getLocale() { - return locale; - } - - /** Instantiates the NumberFormat based on the information provided through setter methods. */ - public void init() { - // Set some sensible defaults if things are null - if (this.formatType == null) { - this.formatType = "number"; - } - - // Figure out which kind of number formatter to get - if (this.formatPattern == null) { - this.formatPattern = "plain"; - } - - if (this.formatType.equalsIgnoreCase("number")) { - this.format = NumberFormat.getInstance(locale); - } - else if (this.formatType.equalsIgnoreCase("currency")) { - this.format = NumberFormat.getCurrencyInstance(locale); - } - else if (this.formatType.equalsIgnoreCase("percentage")) { - this.format = NumberFormat.getPercentInstance(locale); - } - else { - throw new StripesRuntimeException("Invalid format type supplied for formatting a " + - "number: " + this.formatType + ". Valid values are 'number', 'currency' " + - "and 'percentage'."); - } - - // Do any extra configuration - if (this.formatPattern.equalsIgnoreCase("plain")) { - this.format.setGroupingUsed(false); - } - else if (this.formatPattern.equalsIgnoreCase("integer")) { - this.format.setMaximumFractionDigits(0); - } - else if (this.formatPattern.equalsIgnoreCase(("decimal"))) { - this.format.setMinimumFractionDigits(2); - this.format.setMaximumFractionDigits(6); - } - else { - try { - ((DecimalFormat) this.format).applyPattern(this.formatPattern); - } - catch (Exception e) { - throw new StripesRuntimeException("Custom pattern could not be applied to " + - "NumberFormat instance. Pattern was: " + this.formatPattern, e); - } - } - } - - /** Formats the number supplied as a String. */ - public String format(Number input) { - return this.format.format(input); - } + /** Formats the number supplied as a String. */ + public String format(Number input) { + return this.format.format(input); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/format/ObjectFormatter.java b/stripes/src/main/java/net/sourceforge/stripes/format/ObjectFormatter.java index e6e0063d8..856482e84 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/format/ObjectFormatter.java +++ b/stripes/src/main/java/net/sourceforge/stripes/format/ObjectFormatter.java @@ -17,30 +17,37 @@ import java.util.Locale; /** - * This is the default formatter. It simply calls String.valueOf() on the - * object being formatted. + * This is the default formatter. It simply calls String.valueOf() on the object being formatted. * * @author Aaron Porter * @since Stripes 1.5 */ public class ObjectFormatter implements Formatter { - /** - * Converts the supplied parameter to a string using String.valueOf(). - * - * @param input an object of a type that the formatter knows how to format - * @return String.valueOf(input) - */ - public String format(Object input) { - return String.valueOf(input); - } + /** + * Converts the supplied parameter to a string using String.valueOf(). + * + * @param input an object of a type that the formatter knows how to format + * @return String.valueOf(input) + */ + public String format(Object input) { + return String.valueOf(input); + } - /** Does nothing. */ - public void init() { /* unused */ } - /** Does nothing. */ - public void setFormatPattern(String formatPattern) { /* unused */ } - /** Does nothing. */ - public void setFormatType(String formatType) { /* unused */ } - /** Does nothing. */ - public void setLocale(Locale locale) { /* unused */ } + /** Does nothing. */ + public void init() { + /* unused */ + } + /** Does nothing. */ + public void setFormatPattern(String formatPattern) { + /* unused */ + } + /** Does nothing. */ + public void setFormatType(String formatType) { + /* unused */ + } + /** Does nothing. */ + public void setLocale(Locale locale) { + /* unused */ + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/integration/spring/SpringBean.java b/stripes/src/main/java/net/sourceforge/stripes/integration/spring/SpringBean.java index 9de03a906..4f035050c 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/integration/spring/SpringBean.java +++ b/stripes/src/main/java/net/sourceforge/stripes/integration/spring/SpringBean.java @@ -14,26 +14,26 @@ */ package net.sourceforge.stripes.integration.spring; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.lang.annotation.ElementType; -import java.lang.annotation.Documented; /** - *

    Annotation used for injecting Spring managed beans into objects within Stripes - * (usually ActionBeans). The value of the annotation represents the name of the bean - * in the Spring application context to inject. If the value is omitted then Stripes - * will attempt to auto-wire first by property/field name and then by type.

    + * Annotation used for injecting Spring managed beans into objects within Stripes (usually + * ActionBeans). The value of the annotation represents the name of the bean in the Spring + * application context to inject. If the value is omitted then Stripes will attempt to auto-wire + * first by property/field name and then by type. * - *

    Both methods and fields can be annotated. If a field is annotated Stripes will use - * field access to attempt to inject the bean into the field. If a method is annotated Stripes - * will attempt to invoke the method and supply it the value to inject. In both cases - * non-public fields/methods are supported (i.e. values can be injected into private fields - * and through private methods).

    + *

    Both methods and fields can be annotated. If a field is annotated Stripes will use field + * access to attempt to inject the bean into the field. If a method is annotated Stripes will + * attempt to invoke the method and supply it the value to inject. In both cases non-public + * fields/methods are supported (i.e. values can be injected into private fields and through private + * methods). * - *

    For a more details description of the injection process and how auto-wiring occurs - * when explicit bean names are omitted see the {@link SpringHelper} class.

    + *

    For a more details description of the injection process and how auto-wiring occurs when + * explicit bean names are omitted see the {@link SpringHelper} class. * * @author Dan Hayes */ @@ -41,5 +41,5 @@ @Target({ElementType.METHOD, ElementType.FIELD}) @Documented public @interface SpringBean { - String value() default ""; + String value() default ""; } diff --git a/stripes/src/main/java/net/sourceforge/stripes/integration/spring/SpringHelper.java b/stripes/src/main/java/net/sourceforge/stripes/integration/spring/SpringHelper.java index 10be475db..65b7f2256 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/integration/spring/SpringHelper.java +++ b/stripes/src/main/java/net/sourceforge/stripes/integration/spring/SpringHelper.java @@ -1,4 +1,5 @@ /* Copyright 2005-2006 Tim Fennell + * Copyright 2023 Rick Grashel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +15,13 @@ */ package net.sourceforge.stripes.integration.spring; +import jakarta.servlet.ServletContext; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.controller.StripesFilter; import net.sourceforge.stripes.exception.StripesRuntimeException; @@ -21,296 +29,300 @@ import net.sourceforge.stripes.util.ReflectUtil; import org.springframework.context.ApplicationContext; import org.springframework.core.NestedRuntimeException; -import org.springframework.web.context.support.WebApplicationContextUtils; - -import javax.servlet.ServletContext; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.Collection; -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; /** - *

    Static helper class that is used to lookup Spring beans and inject them into objects - * (often ActionBeans). Is capable of injecting beans through setter methods (property access) - * and also through direct field access if the security policy allows it. Methods and fields - * must be annotated using the {@code @SpringBean} annotation.

    + * Static helper class that is used to lookup Spring beans and inject them into objects (often + * ActionBeans). Is capable of injecting beans through setter methods (property access) and also + * through direct field access if the security policy allows it. Methods and fields must be + * annotated using the {@code @SpringBean} annotation. * - *

    Methods and fields may be public, protected, package-access or private. If they are not - * public an attempt is made to call {@link Method#setAccessible(boolean)} in order to make - * them accessible from this class. If the attempt fails, an exception will be thrown.

    + *

    Methods and fields may be public, protected, package-access or private. If they are not public + * an attempt is made to call {@link Method#setAccessible(boolean)} in order to make them accessible + * from this class. If the attempt fails, an exception will be thrown. * - *

    Method names can take any form. For example {@code setSomeBean(Bean b)} or - * {@code someBean(bean b)}. In both cases, if a specific SpringBean name is not supplied, - * the default name of {@code someBean} will be used.

    + *

    Method names can take any form. For example {@code setSomeBean(Bean b)} or {@code + * someBean(bean b)}. In both cases, if a specific SpringBean name is not supplied, the default name + * of {@code someBean} will be used. * *

    The value of the {@code @SpringBean} annotation should be the bean name in the Spring - * application context if it is different from the field/property name. If the value - * is left blank, an attempt is made to auto-wire the bean; first by field/property name and - * then by type. If the value is left blank and more than one bean of the same type is found, - * an exception will be raised.

    + * application context if it is different from the field/property name. If the value is left blank, + * an attempt is made to auto-wire the bean; first by field/property name and then by type. If the + * value is left blank and more than one bean of the same type is found, an exception will be + * raised. * *

    The first time that any of the injection methods in this class is called with a specific type - * of object, the object's class is examined for annotated fields and methods. The discovered - * fields and methods are then cached for future usage.

    + * of object, object's class will be examined for annotated fields and methods. The discovered + * fields and methods are then cached for future usage. * * @see SpringBean * @author Dan Hayes, Tim Fennell */ +@SuppressWarnings("deprecation") public class SpringHelper { - private static final Log log = Log.getInstance(SpringHelper.class); + private static final Log log = Log.getInstance(SpringHelper.class); - /** Lazily filled in map of Class to methods annotated with SpringBean. */ - private static Map, Collection> methodMap = - new ConcurrentHashMap, Collection>(); + /** Lazily filled in map of Class to methods annotated with SpringBean. */ + private static final Map, Collection> methodMap = new ConcurrentHashMap<>(); - /** Lazily filled in map of Class to fields annotated with SpringBean. */ - private static Map, Collection> fieldMap = - new ConcurrentHashMap, Collection>(); + /** Lazily filled in map of Class to fields annotated with SpringBean. */ + private static final Map, Collection> fieldMap = new ConcurrentHashMap<>(); - /** - * Injects Spring managed beans into using a Web Application Context that is - * derived from the ServletContext, which is in turn looked up using the - * ActionBeanContext. - * - * @param bean the object into which to inject spring managed bean - * @param context the ActionBeanContext represented by the current request - */ - public static void injectBeans(Object bean, ActionBeanContext context) { - injectBeans(bean, StripesFilter.getConfiguration().getServletContext()); - } + /** + * Injects Spring managed beans into using a Web Application Context that is derived from the + * ServletContext, which is in turn looked up using the ActionBeanContext. + * + * @param bean the object into which to inject spring managed bean + * @param context the ActionBeanContext represented by the current request + */ + public static void injectBeans(Object bean, ActionBeanContext context) { + injectBeans(bean, StripesFilter.getConfiguration().getServletContext()); + } - /** - * Injects Spring managed beans using a Web Application Context derived from - * the ServletContext. - * - * @param bean the object to have beans injected into - * @param ctx the ServletContext to use to find the Spring ApplicationContext - */ - public static void injectBeans(Object bean, ServletContext ctx) { - ApplicationContext ac = WebApplicationContextUtils.getWebApplicationContext(ctx); - - if (ac == null) { - final String name = ctx.getServletContextName(); - throw new IllegalStateException( - "No Spring application context was found in servlet context \"" + name + "\""); - } + /** + * Injects Spring managed beans using a Web Application Context derived from the ServletContext. + * + * @param bean the object to have beans injected into + * @param ctx the ServletContext to use to find the Spring ApplicationContext + */ + public static void injectBeans(Object bean, ServletContext ctx) { + ApplicationContext ac = + (ApplicationContext) + ctx.getAttribute("org.springframework.web.context.WebApplicationContext.ROOT"); - injectBeans(bean, ac); + if (ac == null) { + final String name = ctx.getServletContextName(); + throw new IllegalStateException( + "No Spring application context was found in servlet context \"" + name + "\""); } - /** - * Looks for all methods and fields annotated with {@code @SpringBean} and attempts - * to lookup and inject a managed bean into the field/property. If any annotated - * element cannot be injected an exception is thrown. - * - * @param bean the bean into which to inject spring beans - * @param ctx the Spring application context - */ - public static void injectBeans(Object bean, ApplicationContext ctx) { - // First inject any values using annotated methods - for (Method m : getMethods(bean.getClass())) { - try { - SpringBean springBean = m.getAnnotation(SpringBean.class); - boolean nameSupplied = !"".equals(springBean.value()); - String name = nameSupplied ? springBean.value() : methodToPropertyName(m); - Class beanType = m.getParameterTypes()[0]; - Object managedBean = findSpringBean(ctx, name, beanType, !nameSupplied); - m.invoke(bean, managedBean); - } - catch (Exception e) { - throw new StripesRuntimeException("Exception while trying to lookup and inject " + - "a Spring bean into a bean of type " + bean.getClass().getSimpleName() + - " using method " + m.toString(), e); - } - } + injectBeans(bean, ac); + } - // And then inject any properties that are annotated - for (Field f : getFields(bean.getClass())) { - try { - SpringBean springBean = f.getAnnotation(SpringBean.class); - boolean nameSupplied = !"".equals(springBean.value()); - String name = nameSupplied ? springBean.value() : f.getName(); - Object managedBean = findSpringBean(ctx, name, f.getType(), !nameSupplied); - f.set(bean, managedBean); - } - catch (Exception e) { - throw new StripesRuntimeException("Exception while trying to lookup and inject " + - "a Spring bean into a bean of type " + bean.getClass().getSimpleName() + - " using field access on field " + f.toString(), e); - } - } + /** + * Looks for all methods and fields annotated with {@code @SpringBean} and attempts to look up and + * inject a managed bean into the field/property. If any annotated element cannot be injected an + * exception is thrown. + * + * @param bean the bean into which to inject spring beans + * @param ctx the Spring application context + */ + public static void injectBeans(Object bean, ApplicationContext ctx) { + // First inject any values using annotated methods + for (Method m : getMethods(bean.getClass())) { + try { + SpringBean springBean = m.getAnnotation(SpringBean.class); + boolean nameSupplied = !"".equals(springBean.value()); + String name = nameSupplied ? springBean.value() : methodToPropertyName(m); + Class beanType = m.getParameterTypes()[0]; + Object managedBean = findSpringBean(ctx, name, beanType, !nameSupplied); + m.invoke(bean, managedBean); + } catch (Exception e) { + throw new StripesRuntimeException( + "Exception while trying to lookup and inject " + + "a Spring bean into a bean of type " + + bean.getClass().getSimpleName() + + " using method " + + m.toString(), + e); + } } - /** - * Fetches the methods on a class that are annotated with SpringBean. The first time it - * is called for a particular class it will introspect the class and cache the results. - * All non-overridden methods are examined, including protected and private methods. - * If a method is not public an attempt it made to make it accessible - if it fails - * it is removed from the collection and an error is logged. - * - * @param clazz the class on which to look for SpringBean annotated methods - * @return the collection of methods with the annotation - */ - protected static Collection getMethods(Class clazz) { - Collection methods = methodMap.get(clazz); - if (methods == null) { - methods = ReflectUtil.getMethods(clazz); - Iterator iterator = methods.iterator(); + // And then inject any properties that are annotated + for (Field f : getFields(bean.getClass())) { + try { + SpringBean springBean = f.getAnnotation(SpringBean.class); + boolean nameSupplied = !"".equals(springBean.value()); + String name = nameSupplied ? springBean.value() : f.getName(); + Object managedBean = findSpringBean(ctx, name, f.getType(), !nameSupplied); + f.set(bean, managedBean); + } catch (Exception e) { + throw new StripesRuntimeException( + "Exception while trying to lookup and inject " + + "a Spring bean into a bean of type " + + bean.getClass().getSimpleName() + + " using field access on field " + + f.toString(), + e); + } + } + } - while (iterator.hasNext()) { - Method method = iterator.next(); - if (!method.isAnnotationPresent(SpringBean.class)) { - iterator.remove(); - } - else { - // If the method isn't public, try to make it accessible - if (!method.isAccessible()) { - try { - method.setAccessible(true); - } - catch (SecurityException se) { - throw new StripesRuntimeException( - "Method " + clazz.getName() + "." + method.getName() + "is marked " + - "with @SpringBean and is not public. An attempt to call " + - "setAccessible(true) resulted in a SecurityException. Please " + - "either make the method public or modify your JVM security " + - "policy to allow Stripes to setAccessible(true).", se); - } - } + /** + * Fetches the methods on a class that are annotated with SpringBean. The first time it is called + * for a particular class it will introspect the class and cache the results. All non-overridden + * methods are examined, including protected and private methods. If a method is not public an + * attempt it made to make it accessible - if it fails it is removed from the collection and an + * error is logged. + * + * @param clazz the class on which to look for SpringBean annotated methods + * @return the collection of methods with the annotation + */ + protected static Collection getMethods(Class clazz) { + Collection methods = methodMap.get(clazz); + if (methods == null) { + methods = ReflectUtil.getMethods(clazz); + Iterator iterator = methods.iterator(); - // Ensure the method has only the one parameter - if (method.getParameterTypes().length != 1) { - throw new StripesRuntimeException( - "A method marked with @SpringBean must have exactly one parameter: " + - "the bean to be injected. Method [" + method.toGenericString() + "] has " + - method.getParameterTypes().length + " parameters." - ); - } - } + while (iterator.hasNext()) { + Method method = iterator.next(); + if (!method.isAnnotationPresent(SpringBean.class)) { + iterator.remove(); + } else { + // If the method isn't public, try to make it accessible + if (!method.isAccessible()) { + try { + method.setAccessible(true); + } catch (SecurityException se) { + throw new StripesRuntimeException( + "Method " + + clazz.getName() + + "." + + method.getName() + + "is marked " + + "with @SpringBean and is not public. An attempt to call " + + "setAccessible(true) resulted in a SecurityException. Please " + + "either make the method public or modify your JVM security " + + "policy to allow Stripes to setAccessible(true).", + se); } + } - methodMap.put(clazz, methods); + // Ensure the method has only the one parameter + if (method.getParameterTypes().length != 1) { + throw new StripesRuntimeException( + "A method marked with @SpringBean must have exactly one parameter: " + + "the bean to be injected. Method [" + + method.toGenericString() + + "] has " + + method.getParameterTypes().length + + " parameters."); + } } + } - return methods; + methodMap.put(clazz, methods); } - /** - * Fetches the fields on a class that are annotated with SpringBean. The first time it - * is called for a particular class it will introspect the class and cache the results. - * All non-overridden fields are examined, including protected and private fields. - * If a field is not public an attempt it made to make it accessible - if it fails - * it is removed from the collection and an error is logged. - * - * @param clazz the class on which to look for SpringBean annotated fields - * @return the collection of methods with the annotation - */ - protected static Collection getFields(Class clazz) { - Collection fields = fieldMap.get(clazz); - if (fields == null) { - fields = ReflectUtil.getFields(clazz); - Iterator iterator = fields.iterator(); + return methods; + } - while (iterator.hasNext()) { - Field field = iterator.next(); - if (!field.isAnnotationPresent(SpringBean.class)) { - iterator.remove(); - } - else if (!field.isAccessible()) { - // If the field isn't public, try to make it accessible - try { - field.setAccessible(true); - } - catch (SecurityException se) { - throw new StripesRuntimeException( - "Field " + clazz.getName() + "." + field.getName() + "is marked " + - "with @SpringBean and is not public. An attempt to call " + - "setAccessible(true) resulted in a SecurityException. Please " + - "either make the field public, annotate a public setter instead " + - "or modify your JVM security policy to allow Stripes to " + - "setAccessible(true).", se); - } - } - } + /** + * Fetches the fields on a class that are annotated with SpringBean. The first time it is called + * for a particular class it will introspect the class and cache the results. All non-overridden + * fields are examined, including protected and private fields. If a field is not public an + * attempt it made to make it accessible - if it fails it is removed from the collection and an + * error is logged. + * + * @param clazz the class on which to look for SpringBean annotated fields + * @return the collection of methods with the annotation + */ + protected static Collection getFields(Class clazz) { + Collection fields = fieldMap.get(clazz); + if (fields == null) { + fields = ReflectUtil.getFields(clazz); + Iterator iterator = fields.iterator(); - fieldMap.put(clazz, fields); + while (iterator.hasNext()) { + Field field = iterator.next(); + if (!field.isAnnotationPresent(SpringBean.class)) { + iterator.remove(); + } else if (!field.isAccessible()) { + // If the field isn't public, try to make it accessible + try { + field.setAccessible(true); + } catch (SecurityException se) { + throw new StripesRuntimeException( + "Field " + + clazz.getName() + + "." + + field.getName() + + "is marked " + + "with @SpringBean and is not public. An attempt to call " + + "setAccessible(true) resulted in a SecurityException. Please " + + "either make the field public, annotate a public setter instead " + + "or modify your JVM security policy to allow Stripes to " + + "setAccessible(true).", + se); + } } + } - return fields; + fieldMap.put(clazz, fields); } - /** - * Looks up a Spring managed bean from an Application Context. First looks for a bean - * with name specified. If no such bean exists, looks for a bean by type. If there is - * only one bean of the appropriate type, it is returned. If zero or more than one bean - * of the correct type exists, an exception is thrown. - * - * @param ctx the Spring Application Context - * @param name the name of the spring bean to look for - * @param type the type of bean to look for - * @param allowFindByType true to indicate that finding a bean by type is acceptable - * if find by name fails. - * @exception RuntimeException various subclasses of RuntimeException are thrown if it - * is not possible to find a unique matching bean in the spring context given - * the constraints supplied. - */ - public static Object findSpringBean(ApplicationContext ctx, - String name, - Class type, - boolean allowFindByType) { - // First try to lookup using the name provided - try { - Object bean = ctx.getBean(name, type); - if (bean != null) { - log.debug("Found spring bean with name [", name, "] and type [", bean.getClass() - .getName(), "]"); - } - return bean; - } - catch (NestedRuntimeException nre) { - if (!allowFindByType) throw nre; - } + return fields; + } - // If we got here then we didn't find a bean yet, try by type - String[] beanNames = ctx.getBeanNamesForType(type); - if (beanNames.length == 0) { - throw new StripesRuntimeException( - "Unable to find SpringBean with name [" + name + "] or type [" + - type.getName() + "] in the Spring application context."); - } - else if (beanNames.length > 1) { - throw new StripesRuntimeException( - "Unable to find SpringBean with name [" + name + "] or unique bean with type [" + - type.getName() + "] in the Spring application context. Found " + beanNames.length + - "beans of matching type."); - } - else { - log.debug("Found unique SpringBean with type [" + type.getName() + "]. Matching on ", - "type is a little risky so watch out!"); - return ctx.getBean(beanNames[0], type); - } + /** + * Looks up a Spring managed bean from an Application Context. First looks for a bean with name + * specified. If no such bean exists, looks for a bean by type. If there is only one bean of the + * appropriate type, it is returned. If zero or more than one bean of the correct type exists, an + * exception is thrown. + * + * @param ctx the Spring Application Context + * @param name the name of the spring bean to look for + * @param type the type of bean to look for + * @param allowFindByType true to indicate that finding a bean by type is acceptable if the find + * by name fails. + * @exception RuntimeException various subclasses of RuntimeException are thrown if it is not + * possible to find a unique matching bean in the spring context given the constraints + * supplied. + */ + public static Object findSpringBean( + ApplicationContext ctx, String name, Class type, boolean allowFindByType) { + // First try to lookup using the name provided + try { + Object bean = ctx.getBean(name, type); + log.debug( + "Found spring bean with name [", name, "] and type [", bean.getClass().getName(), "]"); + return bean; + } catch (NestedRuntimeException nre) { + if (!allowFindByType) throw nre; } - /** - * A slightly unusual, and somewhat "loose" conversion of a method name to a property - * name. Assumes that the name is in fact a mutator for a property and will do the - * usual {@code setFoo} to {@code foo} conversion if the method follows the normal - * syntax, otherwise will just return the method name. - * - * @param m the method to determine the property name of - * @return a String property name - */ - protected static String methodToPropertyName(Method m) { - String name = m.getName(); - if (name.startsWith("set") && name.length() > 3) { - String ret = name.substring(3,4).toLowerCase(); - if (name.length() > 4) ret += name.substring(4); - return ret; - } - else { - return name; - } + // If we got here then we didn't find a bean yet, try by type + String[] beanNames = ctx.getBeanNamesForType(type); + if (beanNames.length == 0) { + throw new StripesRuntimeException( + "Unable to find SpringBean with name [" + + name + + "] or type [" + + type.getName() + + "] in the Spring application context."); + } else if (beanNames.length > 1) { + throw new StripesRuntimeException( + "Unable to find SpringBean with name [" + + name + + "] or unique bean with type [" + + type.getName() + + "] in the Spring application context. Found " + + beanNames.length + + "beans of matching type."); + } else { + log.debug( + "Found unique SpringBean with type [" + type.getName() + "]. Matching on ", + "type is a little risky so watch out!"); + return ctx.getBean(beanNames[0], type); + } + } + + /** + * A slightly unusual, and somewhat "loose" conversion of a method name to a property name. + * Assumes that the name is in fact a mutator for a property and will do the usual {@code setFoo} + * to {@code foo} conversion if the method follows the normal syntax, otherwise will just return + * the method name. + * + * @param m the method to determine the property name of + * @return a String property name + */ + protected static String methodToPropertyName(Method m) { + String name = m.getName(); + if (name.startsWith("set") && name.length() > 3) { + String ret = name.substring(3, 4).toLowerCase(); + if (name.length() > 4) ret += name.substring(4); + return ret; + } else { + return name; } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/integration/spring/SpringInjectionPostProcessor.java b/stripes/src/main/java/net/sourceforge/stripes/integration/spring/SpringInjectionPostProcessor.java index ef07fe754..2720c58db 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/integration/spring/SpringInjectionPostProcessor.java +++ b/stripes/src/main/java/net/sourceforge/stripes/integration/spring/SpringInjectionPostProcessor.java @@ -14,57 +14,60 @@ */ package net.sourceforge.stripes.integration.spring; -import javax.servlet.ServletContext; - +import jakarta.servlet.ServletContext; import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.controller.DefaultObjectFactory; import net.sourceforge.stripes.controller.ObjectPostProcessor; import net.sourceforge.stripes.util.Log; /** - *

    - * An implementation of {@link ObjectPostProcessor} that calls {@link - * SpringHelper#injectBeans((Object, ServletContext))} to inject dependencies marked with - * {@link SpringBean} in every type of object created by Stripes (Action Beans, Interceptors, Type - * Converters, Formatters, etc.). - *

    - * + * An implementation of {@link ObjectPostProcessor} that calls SpringHelper#injectBeans((Object, + * ServletContext)) to inject dependencies marked with {@link SpringBean} in every type of object + * created by Stripes (Action Beans, Interceptors, Type Converters, Formatters, etc.). + * * @author Freddy Daoud, Ben Gunter * @since Stripes 1.6 */ public class SpringInjectionPostProcessor implements ObjectPostProcessor { - private static final Log log = Log.getInstance(SpringInjectionPostProcessor.class); - private ServletContext servletContext; - - /** Get the servlet context from the object factory's configuration. */ - public void setObjectFactory(DefaultObjectFactory factory) { - Configuration configuration = factory.getConfiguration(); - if (configuration == null) { - final String name = getClass().getSimpleName(); - throw new IllegalStateException("The object factory passed to " + name - + " has no configuration. The configuration is required by " + name - + " to get the servlet context."); - } + private static final Log log = Log.getInstance(SpringInjectionPostProcessor.class); + private ServletContext servletContext; - ServletContext servletContext = configuration.getServletContext(); - if (this.servletContext != null && this.servletContext != servletContext) { - final String name = getClass().getSimpleName(); - throw new IllegalStateException("An attempt was made to use a single instance of " - + name + " in two different servlet contexts. " + name + " instances " - + "cannot be shared across servlet contexts."); - } - - this.servletContext = servletContext; + /** Get the servlet context from the object factory's configuration. */ + public void setObjectFactory(DefaultObjectFactory factory) { + Configuration configuration = factory.getConfiguration(); + if (configuration == null) { + final String name = getClass().getSimpleName(); + throw new IllegalStateException( + "The object factory passed to " + + name + + " has no configuration. The configuration is required by " + + name + + " to get the servlet context."); } - /** - * Calls {@link SpringHelper#injectBeans((Object, ServletContext))} to inject dependencies - * marked with {@link SpringBean} into the object before returning it. - */ - public Object postProcess(Object object) { - log.debug("Running Spring dependency injection for instance of ", object.getClass() - .getSimpleName()); - SpringHelper.injectBeans(object, servletContext); - return object; + ServletContext servletContext = configuration.getServletContext(); + if (this.servletContext != null && this.servletContext != servletContext) { + final String name = getClass().getSimpleName(); + throw new IllegalStateException( + "An attempt was made to use a single instance of " + + name + + " in two different servlet contexts. " + + name + + " instances " + + "cannot be shared across servlet contexts."); } + + this.servletContext = servletContext; + } + + /** + * Calls {@link SpringHelper#injectBeans(Object, ServletContext)} to inject dependencies marked + * with {@link SpringBean} into the object before returning it. + */ + public Object postProcess(Object object) { + log.debug( + "Running Spring dependency injection for instance of ", object.getClass().getSimpleName()); + SpringHelper.injectBeans(object, servletContext); + return object; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/localization/DefaultLocalePicker.java b/stripes/src/main/java/net/sourceforge/stripes/localization/DefaultLocalePicker.java index 58804b546..adcbacc79 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/localization/DefaultLocalePicker.java +++ b/stripes/src/main/java/net/sourceforge/stripes/localization/DefaultLocalePicker.java @@ -14,187 +14,186 @@ */ package net.sourceforge.stripes.localization; -import net.sourceforge.stripes.config.Configuration; -import net.sourceforge.stripes.util.Log; -import net.sourceforge.stripes.util.StringUtil; - -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.Collections; import java.util.Map; -import java.util.HashMap; -import java.nio.charset.Charset; +import net.sourceforge.stripes.config.Configuration; +import net.sourceforge.stripes.util.Log; +import net.sourceforge.stripes.util.StringUtil; /** - *

    Default locale picker that uses a comma separated list of locales in the servlet init - * parameters to determine the set of locales that are supported by the application. Then at - * request time matches the user's preference order list as specified by the headers included - * in the request until it finds one of those locales in the system list. If a match cannot be - * found, the first locale in the system list will be picked. If there is no list of configured - * locales then the picker will default the list to a one entry list containing the system locale.

    + * Default locale picker that uses a comma separated list of locales in the servlet init parameters + * to determine the set of locales that are supported by the application. Then at request time + * matches the user's preference order list as specified by the headers included in the request + * until it finds one of those locales in the system list. If a match cannot be found, the first + * locale in the system list will be picked. If there is no list of configured locales then the + * picker will default the list to a one entry list containing the system locale. * - *

    Locales are hierarchical, with up to three levels designating language, country and - * variant. Only the first level (language) is required. To provide the best match possible the - * DefaultLocalePicker tracks the one-level matches, two-level matches and three-level matches. If - * a three level match is found, it will be returned. If not the first two-level match will be - * returned if one was found. If not, the first one-level match will be returned. If not even a - * one-level match is found, the first locale supported by the system is returned.

    + *

    Locales are hierarchical, with up to three levels designating language, country and variant. + * Only the first level (language) is required. To provide the best match possible the + * DefaultLocalePicker tracks the one-level matches, two-level matches and three-level matches. If a + * three level match is found, it will be returned. If not the first two-level match will be + * returned if one was found. If not, the first one-level match will be returned. If not even a + * one-level match is found, the first locale supported by the system is returned. * * @author Tim Fennell */ public class DefaultLocalePicker implements LocalePicker { - /** - * The configuration parameter that is used to lookup a comma separated list of locales that - * the system supports. - */ - public static final String LOCALE_LIST = "LocalePicker.Locales"; - - /** Log instance for use within the class. */ - private static final Log log = Log.getInstance(DefaultLocalePicker.class); - - /** Stores a reference to the configuration passed in at initialization. */ - protected Configuration configuration; - - /** Stores the configured set of Locales that the system supports, looked up at init time. */ - protected List locales = new ArrayList(); - - /** Contains a map of Locale to preferred character encoding. */ - protected Map encodings = new HashMap(); - - /** - * Attempts to read the - * @param configuration - * @throws Exception - */ - public void init(Configuration configuration) throws Exception { - this.configuration = configuration; - - String configuredLocales = - configuration.getBootstrapPropertyResolver().getProperty(LOCALE_LIST); - - if (configuredLocales == null || configuredLocales.equals("")) { - log.info("No locale list specified, defaulting to single locale: ", Locale.getDefault()); - this.locales.add(Locale.getDefault()); + /** + * The configuration parameter that is used to lookup a comma separated list of locales that the + * system supports. + */ + public static final String LOCALE_LIST = "LocalePicker.Locales"; + + /** Log instance for use within the class. */ + private static final Log log = Log.getInstance(DefaultLocalePicker.class); + + /** Stores a reference to the configuration passed in at initialization. */ + protected Configuration configuration; + + /** Stores the configured set of Locales that the system supports, looked up at init time. */ + protected List locales = new ArrayList(); + + /** Contains a map of Locale to preferred character encoding. */ + protected Map encodings = new HashMap(); + + /** + * Attempts to read the + * + * @param configuration + * @throws Exception + */ + public void init(Configuration configuration) throws Exception { + this.configuration = configuration; + + String configuredLocales = + configuration.getBootstrapPropertyResolver().getProperty(LOCALE_LIST); + + if (configuredLocales == null || configuredLocales.equals("")) { + log.info("No locale list specified, defaulting to single locale: ", Locale.getDefault()); + this.locales.add(Locale.getDefault()); + } else { + // Split apart the Locales on commas, and then parse the local strings into their bits + String[] localeStrings = StringUtil.standardSplit(configuredLocales); + for (String localeString : localeStrings) { + // Each locale string can be made up of two parts, locale:encoding + // and the locale can be made up of up to three segment, e.g. en_US_PC + + String[] halves = localeString.split(":"); + String[] parts = halves[0].split("[-_]"); + Locale locale = null; + + if (parts.length == 1) { + locale = new Locale(parts[0].trim().toLowerCase()); + } else if (parts.length == 2) { + locale = new Locale(parts[0].trim().toLowerCase(), parts[1].trim().toUpperCase()); + } else if (parts.length == 3) { + locale = + new Locale( + parts[0].trim().toLowerCase(), parts[1].trim().toUpperCase(), parts[2].trim()); + } else { + log.error( + "Configuration property ", + LOCALE_LIST, + " contained a locale value ", + "that split into more than three parts! The parts were: ", + parts); } - else { - // Split apart the Locales on commas, and then parse the local strings into their bits - String[] localeStrings = StringUtil.standardSplit(configuredLocales); - for (String localeString : localeStrings) { - // Each locale string can be made up of two parts, locale:encoding - // and the locale can be made up of up to three segment, e.g. en_US_PC - - String[] halves = localeString.split(":"); - String[] parts = halves[0].split("[-_]"); - Locale locale = null; - - if (parts.length == 1) { - locale = new Locale(parts[0].trim().toLowerCase()); - } - else if (parts.length == 2) { - locale = new Locale(parts[0].trim().toLowerCase(), - parts[1].trim().toUpperCase()); - } - else if (parts.length == 3) { - locale = new Locale(parts[0].trim().toLowerCase(), - parts[1].trim().toUpperCase(), - parts[2].trim()); - } - else { - log.error("Configuration property ", LOCALE_LIST, " contained a locale value ", - "that split into more than three parts! The parts were: ", parts); - } - - this.locales.add(locale); - - // Now check to see if a character encoding was specified, and if so is it valid - if (halves.length == 2) { - String encoding = halves[1]; - - if (Charset.isSupported(encoding)) { - this.encodings.put(locale, halves[1]); - } - else { - log.error("Configuration property ", LOCALE_LIST, " contained a locale value ", - "with an unsupported character encoding. The offending entry is: ", - localeString); - } - } - } - log.debug("Configured DefaultLocalePicker with locales: ", this.locales); - log.debug("Configured DefaultLocalePicker with encodings: ", this.encodings); + this.locales.add(locale); + + // Now check to see if a character encoding was specified, and if so is it valid + if (halves.length == 2) { + String encoding = halves[1]; + + if (Charset.isSupported(encoding)) { + this.encodings.put(locale, halves[1]); + } else { + log.error( + "Configuration property ", + LOCALE_LIST, + " contained a locale value ", + "with an unsupported character encoding. The offending entry is: ", + localeString); + } } - } + } - /** - * Uses a preference matching algorithm to pick a Locale for the user's request. Iterates - * through the user's acceptable list of Locales, matching them against the system list. On the - * way through the list records the first Locale to match on Language, and the first locale to - * match on both Language and Country. If a match is found for all three, Language, Country - * and Variant, it will be returned. If no three-way match is found the first two-way match - * found will be returned. If no two-way match way found the first one-way match found will - * be returned. If no one way match was found, the default system locale will be returned. - * - * @param request the request being processed - * @return a Locale to use in processing the request - */ - @SuppressWarnings("unchecked") - public Locale pickLocale(HttpServletRequest request) { - Locale oneWayMatch = null; - Locale twoWayMatch= null; - - List preferredLocales = Collections.list(request.getLocales()); - for (Locale preferredLocale : preferredLocales) { - for (Locale systemLocale : this.locales) { - - if ( systemLocale.getLanguage().equals(preferredLocale.getLanguage()) ) { - - // We have a language match, let's go for two! - oneWayMatch = (oneWayMatch == null ? systemLocale : oneWayMatch); - String systemCountry = systemLocale.getCountry(); - String preferredCountry = preferredLocale.getCountry(); - - if ( (systemCountry == null && preferredCountry == null) || - (systemCountry != null && systemCountry.equals(preferredCountry)) ) { - - // Ooh, we have a two way match, can we make three? - twoWayMatch = (twoWayMatch == null ? systemLocale : twoWayMatch); - String systemVariant = systemLocale.getVariant(); - String preferredVariant = preferredLocale.getVariant(); - - if ( (systemVariant == null && preferredVariant == null) || - (systemVariant != null && systemVariant.equals(preferredVariant)) ) { - // Bingo! You sunk my battleship! - return systemLocale; - } - } - } + log.debug("Configured DefaultLocalePicker with locales: ", this.locales); + log.debug("Configured DefaultLocalePicker with encodings: ", this.encodings); + } + } + + /** + * Uses a preference matching algorithm to pick a Locale for the user's request. Iterates through + * the user's acceptable list of Locales, matching them against the system list. On the way + * through the list records the first Locale to match on Language, and the first locale to match + * on both Language and Country. If a match is found for all three, Language, Country and Variant, + * it will be returned. If no three-way match is found the first two-way match found will be + * returned. If no two-way match way found the first one-way match found will be returned. If no + * one way match was found, the default system locale will be returned. + * + * @param request the request being processed + * @return a Locale to use in processing the request + */ + @SuppressWarnings("unchecked") + public Locale pickLocale(HttpServletRequest request) { + Locale oneWayMatch = null; + Locale twoWayMatch = null; + + List preferredLocales = Collections.list(request.getLocales()); + for (Locale preferredLocale : preferredLocales) { + for (Locale systemLocale : this.locales) { + + if (systemLocale.getLanguage().equals(preferredLocale.getLanguage())) { + + // We have a language match, let's go for two! + oneWayMatch = (oneWayMatch == null ? systemLocale : oneWayMatch); + String systemCountry = systemLocale.getCountry(); + String preferredCountry = preferredLocale.getCountry(); + + if ((systemCountry == null && preferredCountry == null) + || (systemCountry != null && systemCountry.equals(preferredCountry))) { + + // Ooh, we have a two way match, can we make three? + twoWayMatch = (twoWayMatch == null ? systemLocale : twoWayMatch); + String systemVariant = systemLocale.getVariant(); + String preferredVariant = preferredLocale.getVariant(); + + if ((systemVariant == null && preferredVariant == null) + || (systemVariant != null && systemVariant.equals(preferredVariant))) { + // Bingo! You sunk my battleship! + return systemLocale; } + } } - - // We didn't get a match complete match, maybe partial will do - if (twoWayMatch != null) { - return twoWayMatch; - } - else if (oneWayMatch != null) { - return oneWayMatch; - } - else { - return this.locales.get(0); - } + } } - /** - * Returns the character encoding to use for the request and locale if one has been - * specified in the configuration. If no value has been specified, returns null. - * - * @param request the current request - * @param locale the locale picked for the request - * @return a valid character encoding or null - */ - public String pickCharacterEncoding(HttpServletRequest request, Locale locale) { - return this.encodings.get(locale); + // We didn't get a match complete match, maybe partial will do + if (twoWayMatch != null) { + return twoWayMatch; + } else if (oneWayMatch != null) { + return oneWayMatch; + } else { + return this.locales.get(0); } + } + + /** + * Returns the character encoding to use for the request and locale if one has been specified in + * the configuration. If no value has been specified, returns null. + * + * @param request the current request + * @param locale the locale picked for the request + * @return a valid character encoding or null + */ + public String pickCharacterEncoding(HttpServletRequest request, Locale locale) { + return this.encodings.get(locale); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/localization/DefaultLocalizationBundleFactory.java b/stripes/src/main/java/net/sourceforge/stripes/localization/DefaultLocalizationBundleFactory.java index ee5e0c884..73f29fede 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/localization/DefaultLocalizationBundleFactory.java +++ b/stripes/src/main/java/net/sourceforge/stripes/localization/DefaultLocalizationBundleFactory.java @@ -14,125 +14,125 @@ */ package net.sourceforge.stripes.localization; -import net.sourceforge.stripes.config.Configuration; - -import java.util.ResourceBundle; import java.util.Locale; import java.util.MissingResourceException; +import java.util.ResourceBundle; +import net.sourceforge.stripes.config.Configuration; /** - * Very simple default implementation of a bundle factory. Looks for configuration parameters in - * the bootstrap properties called "LocalizationBundleFactory.ErrorMessageBundle" and - * "LocalizationBundleFactory.FieldNameBundle". If one or both of these values is not specified - * the default bundle name of "StripesResources" will be used in its place. + * Very simple default implementation of a bundle factory. Looks for configuration parameters in the + * bootstrap properties called "LocalizationBundleFactory.ErrorMessageBundle" and + * "LocalizationBundleFactory.FieldNameBundle". If one or both of these values is not specified the + * default bundle name of "StripesResources" will be used in its place. * * @see net.sourceforge.stripes.config.BootstrapPropertyResolver * @author Tim Fennell */ public class DefaultLocalizationBundleFactory implements LocalizationBundleFactory { - /** The name of the default resource bundle for Stripes. */ - public static final String BUNDLE_NAME = "StripesResources"; + /** The name of the default resource bundle for Stripes. */ + public static final String BUNDLE_NAME = "StripesResources"; + + /** The configuration parameter for changing the default error message resource bundle. */ + public static final String ERROR_MESSAGE_BUNDLE = "LocalizationBundleFactory.ErrorMessageBundle"; - /** The configuration parameter for changing the default error message resource bundle. */ - public static final String ERROR_MESSAGE_BUNDLE = "LocalizationBundleFactory.ErrorMessageBundle"; + /** The configuration parameter for changing the default field name resource bundle. */ + public static final String FIELD_NAME_BUNDLE = "LocalizationBundleFactory.FieldNameBundle"; - /** The configuration parameter for changing the default field name resource bundle. */ - public static final String FIELD_NAME_BUNDLE = "LocalizationBundleFactory.FieldNameBundle"; + /** Holds the configuration passed in at initialization time. */ + private Configuration configuration; - /** Holds the configuration passed in at initialization time. */ - private Configuration configuration; - private String errorBundleName; - private String fieldBundleName; + private String errorBundleName; + private String fieldBundleName; - /** - * Uses the BootstrapPropertyResolver attached to the Configuration in order to look for - * configured bundle names in the servlet init parameters etc. If those can't be found then - * the default bundle names are put in place. - */ - public void init(Configuration configuration) throws Exception { - setConfiguration(configuration); + /** + * Uses the BootstrapPropertyResolver attached to the Configuration in order to look for + * configured bundle names in the servlet init parameters etc. If those can't be found then the + * default bundle names are put in place. + */ + public void init(Configuration configuration) throws Exception { + setConfiguration(configuration); - this.errorBundleName = configuration.getBootstrapPropertyResolver(). - getProperty(ERROR_MESSAGE_BUNDLE); - if (this.errorBundleName == null) { - this.errorBundleName = BUNDLE_NAME; - } + this.errorBundleName = + configuration.getBootstrapPropertyResolver().getProperty(ERROR_MESSAGE_BUNDLE); + if (this.errorBundleName == null) { + this.errorBundleName = BUNDLE_NAME; + } - this.fieldBundleName = configuration.getBootstrapPropertyResolver(). - getProperty(FIELD_NAME_BUNDLE); - if (this.fieldBundleName == null) { - this.fieldBundleName = BUNDLE_NAME; - } + this.fieldBundleName = + configuration.getBootstrapPropertyResolver().getProperty(FIELD_NAME_BUNDLE); + if (this.fieldBundleName == null) { + this.fieldBundleName = BUNDLE_NAME; } + } - /** - * Looks for a bundle called StripesResources with the supplied locale if one is provided, - * or with the default locale if the locale provided is null. - * - * @param locale an optional locale, may be null. - * @return ResourceBundle a bundle in which to look for localized error messages - * @throws MissingResourceException if a suitable bundle cannot be found - */ - public ResourceBundle getErrorMessageBundle(Locale locale) throws MissingResourceException { - try { - if (locale == null) { - return ResourceBundle.getBundle(this.errorBundleName); - } - else { - return ResourceBundle.getBundle(this.errorBundleName, locale); - } - } - catch (MissingResourceException mre) { - MissingResourceException mre2 = new MissingResourceException( - "Could not find the error message resource bundle needed by Stripes. This " + - "almost certainly means that a properties file called '" + - this.errorBundleName + ".properties' could not be found in the classpath. " + - "This properties file is needed to lookup validation error messages. Please " + - "ensure the file exists in WEB-INF/classes or elsewhere in your classpath.", - this.errorBundleName, ""); - mre2.setStackTrace(mre.getStackTrace()); - throw mre2; - } + /** + * Looks for a bundle called StripesResources with the supplied locale if one is provided, or with + * the default locale if the locale provided is null. + * + * @param locale an optional locale, may be null. + * @return ResourceBundle a bundle in which to look for localized error messages + * @throws MissingResourceException if a suitable bundle cannot be found + */ + public ResourceBundle getErrorMessageBundle(Locale locale) throws MissingResourceException { + try { + if (locale == null) { + return ResourceBundle.getBundle(this.errorBundleName); + } else { + return ResourceBundle.getBundle(this.errorBundleName, locale); + } + } catch (MissingResourceException mre) { + MissingResourceException mre2 = + new MissingResourceException( + "Could not find the error message resource bundle needed by Stripes. This " + + "almost certainly means that a properties file called '" + + this.errorBundleName + + ".properties' could not be found in the classpath. " + + "This properties file is needed to lookup validation error messages. Please " + + "ensure the file exists in WEB-INF/classes or elsewhere in your classpath.", + this.errorBundleName, + ""); + mre2.setStackTrace(mre.getStackTrace()); + throw mre2; } + } - /** - * Looks for a bundle called StripesResources with the supplied locale if one is provided, - * or with the default locale if the locale provided is null. - * - * @param locale an optional locale, may be null. - * @return ResourceBundle a bundle in which to look for localized field names - * @throws MissingResourceException if a suitable bundle cannot be found - */ - public ResourceBundle getFormFieldBundle(Locale locale) throws MissingResourceException { - try { - if (locale == null) { - return ResourceBundle.getBundle(this.fieldBundleName); - } - else { - return ResourceBundle.getBundle(this.fieldBundleName, locale); - } - } - catch (MissingResourceException mre) { - MissingResourceException mre2 = new MissingResourceException( - "Could not find the form field resource bundle needed by Stripes. This " + - "almost certainly means that a properties file called '" + - this.fieldBundleName + ".properties' could not be found in the classpath. " + - "This properties file is needed to lookup form field names. Please " + - "ensure the file exists in WEB-INF/classes or elsewhere in your classpath.", - this.fieldBundleName, ""); - mre2.setStackTrace(mre.getStackTrace()); - throw mre2; - } + /** + * Looks for a bundle called StripesResources with the supplied locale if one is provided, or with + * the default locale if the locale provided is null. + * + * @param locale an optional locale, may be null. + * @return ResourceBundle a bundle in which to look for localized field names + * @throws MissingResourceException if a suitable bundle cannot be found + */ + public ResourceBundle getFormFieldBundle(Locale locale) throws MissingResourceException { + try { + if (locale == null) { + return ResourceBundle.getBundle(this.fieldBundleName); + } else { + return ResourceBundle.getBundle(this.fieldBundleName, locale); + } + } catch (MissingResourceException mre) { + MissingResourceException mre2 = + new MissingResourceException( + "Could not find the form field resource bundle needed by Stripes. This " + + "almost certainly means that a properties file called '" + + this.fieldBundleName + + ".properties' could not be found in the classpath. " + + "This properties file is needed to lookup form field names. Please " + + "ensure the file exists in WEB-INF/classes or elsewhere in your classpath.", + this.fieldBundleName, + ""); + mre2.setStackTrace(mre.getStackTrace()); + throw mre2; } + } - protected Configuration getConfiguration() - { - return configuration; - } + protected Configuration getConfiguration() { + return configuration; + } - protected void setConfiguration(Configuration configuration) - { - this.configuration = configuration; - } + protected void setConfiguration(Configuration configuration) { + this.configuration = configuration; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/localization/LocalePicker.java b/stripes/src/main/java/net/sourceforge/stripes/localization/LocalePicker.java index cccb43c29..6f5f2637f 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/localization/LocalePicker.java +++ b/stripes/src/main/java/net/sourceforge/stripes/localization/LocalePicker.java @@ -14,45 +14,43 @@ */ package net.sourceforge.stripes.localization; -import net.sourceforge.stripes.config.ConfigurableComponent; - -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import java.util.Locale; +import net.sourceforge.stripes.config.ConfigurableComponent; /** - *

    A LocalePicker is a class that determines what Locale a particular request will use. At first + * A LocalePicker is a class that determines what Locale a particular request will use. At first * this may seem odd given that the request already has a method called getLocale(), but ask - * yourself this: if your site only supports English, and the user's browser requests the - * Japanese locale, in what locale should you accept their input?

    + * yourself this: if your site only supports English, and the user's browser requests the Japanese + * locale, in what locale should you accept their input? * - *

    The LocalPicker is given access to the request and can use any mechanism it chooses to - * decide upon a Locale. However, it must return a valid locale. It is suggested that if a locale - * cannot be chosen that the picker return the system locale.

    + *

    The LocalPicker is given access to the request and can use any mechanism it chooses to decide + * upon a Locale. However, it must return a valid locale. It is suggested that if a locale cannot be + * chosen that the picker return the system locale. * * @author Tim Fennell */ public interface LocalePicker extends ConfigurableComponent { - /** - * Picks a locale for the HttpServletRequest supplied. Given that the request could be a - * regular request or a form upload request it is suggested that the LocalePicker only rely - * on the headers in the request, and perhaps the session, and not look for parameters. - * - * @param request the current HttpServletRequest - * @return Locale the locale to be used throughout the request for input parsing and - * localized output - */ - Locale pickLocale(HttpServletRequest request); + /** + * Picks a locale for the HttpServletRequest supplied. Given that the request could be a regular + * request or a form upload request it is suggested that the LocalePicker only rely on the headers + * in the request, and perhaps the session, and not look for parameters. + * + * @param request the current HttpServletRequest + * @return Locale the locale to be used throughout the request for input parsing and localized + * output + */ + Locale pickLocale(HttpServletRequest request); - /** - * Picks the character encoding to use for the current request using the specified - * Locale. The character encoding will be set on both the request and the response. If the - * LocalePicker does not wish to change or specify a character encoding then this - * method should return null. - * - * @param request the current HttpServletRequest - * @param locale the Locale picked by the LocalePicker for this request - * @return the name of the character encoding to use, or null to use the default - */ - String pickCharacterEncoding(HttpServletRequest request, Locale locale); + /** + * Picks the character encoding to use for the current request using the specified Locale. The + * character encoding will be set on both the request and the response. If the LocalePicker does + * not wish to change or specify a character encoding then this method should return null. + * + * @param request the current HttpServletRequest + * @param locale the Locale picked by the LocalePicker for this request + * @return the name of the character encoding to use, or null to use the default + */ + String pickCharacterEncoding(HttpServletRequest request, Locale locale); } diff --git a/stripes/src/main/java/net/sourceforge/stripes/localization/LocalizationBundleFactory.java b/stripes/src/main/java/net/sourceforge/stripes/localization/LocalizationBundleFactory.java index 9e2df06c2..44ae42149 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/localization/LocalizationBundleFactory.java +++ b/stripes/src/main/java/net/sourceforge/stripes/localization/LocalizationBundleFactory.java @@ -14,38 +14,37 @@ */ package net.sourceforge.stripes.localization; -import net.sourceforge.stripes.config.ConfigurableComponent; - -import java.util.ResourceBundle; import java.util.Locale; import java.util.MissingResourceException; +import java.util.ResourceBundle; +import net.sourceforge.stripes.config.ConfigurableComponent; /** - *

    Extremely simple interface that is implemented to resolve the ResourceBundles from which - * various Strings are pulled at runtime. Can be implemented using property resource bundles, class - * resource bundles or anything else a developer can dream up.

    + * Extremely simple interface that is implemented to resolve the ResourceBundles from which various + * Strings are pulled at runtime. Can be implemented using property resource bundles, class resource + * bundles or anything else a developer can dream up. * *

    The bundles returned from each method do not need to be discrete, and may all be the same * bundle. */ public interface LocalizationBundleFactory extends ConfigurableComponent { - /** - * Returns the ResourceBundle from which to draw error messages for the specified locale. - * - * @param locale the locale that is in use for the current request - * @throws MissingResourceException when a bundle that is expected to be present cannot - * be located. - */ - ResourceBundle getErrorMessageBundle(Locale locale) throws MissingResourceException; + /** + * Returns the ResourceBundle from which to draw error messages for the specified locale. + * + * @param locale the locale that is in use for the current request + * @throws MissingResourceException when a bundle that is expected to be present cannot be + * located. + */ + ResourceBundle getErrorMessageBundle(Locale locale) throws MissingResourceException; - /** - * Returns the ResourceBundle from which to draw the names of form fields for the - * specified locale. - * - * @param locale the locale that is in use for the current request - * @throws MissingResourceException when a bundle that is expected to be present cannot - * be located. - */ - ResourceBundle getFormFieldBundle(Locale locale) throws MissingResourceException; + /** + * Returns the ResourceBundle from which to draw the names of form fields for the specified + * locale. + * + * @param locale the locale that is in use for the current request + * @throws MissingResourceException when a bundle that is expected to be present cannot be + * located. + */ + ResourceBundle getFormFieldBundle(Locale locale) throws MissingResourceException; } diff --git a/stripes/src/main/java/net/sourceforge/stripes/localization/LocalizationUtility.java b/stripes/src/main/java/net/sourceforge/stripes/localization/LocalizationUtility.java index c06e954b4..1533589f5 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/localization/LocalizationUtility.java +++ b/stripes/src/main/java/net/sourceforge/stripes/localization/LocalizationUtility.java @@ -14,164 +14,159 @@ */ package net.sourceforge.stripes.localization; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import net.sourceforge.stripes.action.ActionBean; +import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.controller.ParameterName; import net.sourceforge.stripes.controller.StripesFilter; import net.sourceforge.stripes.util.Log; import net.sourceforge.stripes.validation.ValidationMetadata; -import net.sourceforge.stripes.config.Configuration; -import net.sourceforge.stripes.action.ActionBean; - -import java.util.Locale; -import java.util.MissingResourceException; -import java.util.ResourceBundle; /** - * Provides simple localization utility methods that are used in multiple places in the Stripes - * code base. + * Provides simple localization utility methods that are used in multiple places in the Stripes code + * base. * * @author Tim Fennell * @since Stripes 1.1 */ public class LocalizationUtility { - private static final Log log = Log.getInstance(LocalizationUtility.class); - - /** - *

    Fetches the localized name for a form field if one exists in the form field resource bundle. - * If for any reason a localized value cannot be found (e.g. the bundle cannot be found, or - * does not contain the required properties) then null will be returned.

    - * - *

    Looks first for a property called {@code beanClassFQN.fieldName} in the resource bundle. - * If that is undefined, it next looks for {@code actionPath.fieldName} and - * if not defined, looks for a property called {@code fieldName}. Will strip any indexing - * from the field name prior to using it to construct property names (e.g. foo[12] will become - * simply foo).

    - * - * @param fieldName The name of the field whose localized name to look up - * @param actionPath The action path of the form in which the field is nested. If for some - * reason this is not available, null may be passed without causing errors. - * @param locale The desired locale of the looked up name. - * @return a localized field name if one can be found, or null otherwise. - */ - public static String getLocalizedFieldName(String fieldName, - String actionPath, - Class beanclass, - Locale locale) { - - ParameterName parameterName = new ParameterName(fieldName); - String strippedName = parameterName.getStrippedName(); - String localizedValue = null; - ResourceBundle bundle = null; - - try { - bundle = StripesFilter.getConfiguration().getLocalizationBundleFactory() - .getFormFieldBundle(locale); - } - catch (MissingResourceException mre) { - log.error(mre); - return null; - } - - // First with the bean class - if (beanclass != null) { - try { localizedValue = bundle.getString(beanclass.getName() + "." + strippedName); } - catch (MissingResourceException mre) { /* do nothing */ } - } - - // Then all by itself - if (localizedValue == null) { - try { localizedValue = bundle.getString(strippedName); } - catch (MissingResourceException mre2) { /* do nothing */ } - } - - // Lastly, check @Validate on the ActionBean property - if (localizedValue == null && beanclass != null) { - ValidationMetadata validate = StripesFilter.getConfiguration() - .getValidationMetadataProvider() - .getValidationMetadata(beanclass, parameterName); - if (validate != null && validate.label() != null && !"".equals(validate.label())) { - localizedValue = validate.label(); - } - } - - return localizedValue; + private static final Log log = Log.getInstance(LocalizationUtility.class); + + /** + * Fetches the localized name for a form field if one exists in the form field resource bundle. If + * for any reason a localized value cannot be found (e.g. the bundle cannot be found, or does not + * contain the required properties) then null will be returned. + * + *

    Looks first for a property called {@code beanClassFQN.fieldName} in the resource bundle. If + * that is undefined, it next looks for {@code actionPath.fieldName} and if not defined, looks for + * a property called {@code fieldName}. Will strip any indexing from the field name prior to using + * it to construct property names (e.g. foo[12] will become simply foo). + * + * @param fieldName The name of the field whose localized name to look up + * @param actionPath The action path of the form in which the field is nested. If for some reason + * this is not available, null may be passed without causing errors. + * @param locale The desired locale of the looked up name. + * @return a localized field name if one can be found, or null otherwise. + */ + public static String getLocalizedFieldName( + String fieldName, String actionPath, Class beanclass, Locale locale) { + + ParameterName parameterName = new ParameterName(fieldName); + String strippedName = parameterName.getStrippedName(); + String localizedValue = null; + ResourceBundle bundle = null; + + try { + bundle = + StripesFilter.getConfiguration() + .getLocalizationBundleFactory() + .getFormFieldBundle(locale); + } catch (MissingResourceException mre) { + log.error(mre); + return null; } + // First with the bean class + if (beanclass != null) { + try { + localizedValue = bundle.getString(beanclass.getName() + "." + strippedName); + } catch (MissingResourceException mre) { + /* do nothing */ + } + } - /** - * Makes a half hearted attempt to convert the property name of a field into a human - * friendly name by breaking it on periods and upper case letters and capitalizing each word. - * This is only used when developers do not provide names for their fields. - * - * @param fieldNameKey the programmatic name of a form field - * @return String a more user friendly name for the field in the absence of anything better - */ - public static String makePseudoFriendlyName(String fieldNameKey) { - StringBuilder builder = new StringBuilder(fieldNameKey.length() + 10); - char[] characters = fieldNameKey.toCharArray(); - builder.append( Character.toUpperCase(characters[0]) ); - boolean upcaseNextChar = false; - - for (int i=1; i c) { - if (c.getEnclosingClass() == null) - return c.getSimpleName(); - else - return prefixSimpleName(new StringBuilder(), c).toString(); + return localizedValue; + } + + /** + * Makes a half hearted attempt to convert the property name of a field into a human friendly name + * by breaking it on periods and upper case letters and capitalizing each word. This is only used + * when developers do not provide names for their fields. + * + * @param fieldNameKey the programmatic name of a form field + * @return String a more user friendly name for the field in the absence of anything better + */ + public static String makePseudoFriendlyName(String fieldNameKey) { + StringBuilder builder = new StringBuilder(fieldNameKey.length() + 10); + char[] characters = fieldNameKey.toCharArray(); + builder.append(Character.toUpperCase(characters[0])); + boolean upcaseNextChar = false; + + for (int i = 1; i < characters.length; ++i) { + if (characters[i] == '.') { + builder.append(' '); + upcaseNextChar = true; + } else if (Character.isUpperCase(characters[i])) { + builder.append(' ').append(characters[i]); + upcaseNextChar = false; + } else if (upcaseNextChar) { + builder.append(Character.toUpperCase(characters[i])); + upcaseNextChar = false; + } else { + builder.append(characters[i]); + upcaseNextChar = false; + } } - /** A recursive method used by {@link #getSimpleName(Class)}. */ - private static StringBuilder prefixSimpleName(StringBuilder s, Class c) { - if (c.getEnclosingClass() != null) - prefixSimpleName(s, c.getEnclosingClass()).append('.'); - return s.append(c.getSimpleName()); + return builder.toString(); + } + + /** + * Looks up the specified key in the error message resource bundle. If the bundle is missing or if + * the resource cannot be found, will return null instead of throwing an exception. + * + * @param locale the locale in which to lookup the resource + * @param key the exact resource key to lookup + * @return the resource String or null + */ + public static String getErrorMessage(Locale locale, String key) { + try { + Configuration config = StripesFilter.getConfiguration(); + ResourceBundle bundle = config.getLocalizationBundleFactory().getErrorMessageBundle(locale); + return bundle.getString(key); + } catch (MissingResourceException mre) { + return null; } + } + + /** + * Gets the simple name of a class for use as a key to look up a resource. This is usually the + * same as {@link Class#getSimpleName()}, but static inner classes are handled such that the + * simple name is {@code OuterClass.InnerClass}. Multiple layers of nesting are supported. + * + * @param c The class whose simple name is requested. + * @return The simple name of the class. + */ + public static String getSimpleName(Class c) { + if (c.getEnclosingClass() == null) return c.getSimpleName(); + else return prefixSimpleName(new StringBuilder(), c).toString(); + } + + /** A recursive method used by {@link #getSimpleName(Class)}. */ + private static StringBuilder prefixSimpleName(StringBuilder s, Class c) { + if (c.getEnclosingClass() != null) prefixSimpleName(s, c.getEnclosingClass()).append('.'); + return s.append(c.getSimpleName()); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/mock/MockBaseConfig.java b/stripes/src/main/java/net/sourceforge/stripes/mock/MockBaseConfig.java index 28a010a31..67ea866b0 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/mock/MockBaseConfig.java +++ b/stripes/src/main/java/net/sourceforge/stripes/mock/MockBaseConfig.java @@ -14,11 +14,11 @@ */ package net.sourceforge.stripes.mock; -import javax.servlet.ServletContext; -import java.util.Map; -import java.util.Enumeration; +import jakarta.servlet.ServletContext; import java.util.Collections; +import java.util.Enumeration; import java.util.HashMap; +import java.util.Map; /** * Common parent class for both MockServletConfig and MockFilterConfig since they are both @@ -28,32 +28,36 @@ * @since Stripes 1.1.1 */ public class MockBaseConfig { - private ServletContext servletContext; - private Map initParameters = new HashMap(); - - /** Sets the ServletContext that will be returned by getServletContext(). */ - public void setServletContext(ServletContext ctx) { this.servletContext = ctx; } - - /** Gets the ServletContext in whiich the filter is running. */ - public ServletContext getServletContext() { return this.servletContext; } - - /** Adds a value to the set of init parameters. */ - public void addInitParameter(String name, String value) { - this.initParameters.put(name, value); - } - - /** Adds all the values in the provided Map to the set of init parameters. */ - public void addAllInitParameters(Map parameters) { - this.initParameters.putAll(parameters); - } - - /** Gets the named init parameter if it exists, or null if it doesn't. */ - public String getInitParameter(String name) { - return this.initParameters.get(name); - } - - /** Gets an enumeration of all the init parameter names present. */ - public Enumeration getInitParameterNames() { - return Collections.enumeration( this.initParameters.keySet() ); - } + private ServletContext servletContext; + private final Map initParameters = new HashMap<>(); + + /** Sets the ServletContext that will be returned by getServletContext(). */ + public void setServletContext(ServletContext ctx) { + this.servletContext = ctx; + } + + /** Gets the ServletContext in which the filter is running. */ + public ServletContext getServletContext() { + return this.servletContext; + } + + /** Adds a value to the set of init parameters. */ + public void addInitParameter(String name, String value) { + this.initParameters.put(name, value); + } + + /** Adds all the values in the provided Map to the set of init parameters. */ + public void addAllInitParameters(Map parameters) { + this.initParameters.putAll(parameters); + } + + /** Gets the named init parameter if it exists, or null if it doesn't. */ + public String getInitParameter(String name) { + return this.initParameters.get(name); + } + + /** Gets an enumeration of all the init parameter names present. */ + public Enumeration getInitParameterNames() { + return Collections.enumeration(this.initParameters.keySet()); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/mock/MockFilterChain.java b/stripes/src/main/java/net/sourceforge/stripes/mock/MockFilterChain.java index 845e3363c..0950bf5a3 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/mock/MockFilterChain.java +++ b/stripes/src/main/java/net/sourceforge/stripes/mock/MockFilterChain.java @@ -14,12 +14,12 @@ */ package net.sourceforge.stripes.mock; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.Servlet; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @@ -27,43 +27,43 @@ import java.util.List; /** - * Mock implementation of a filter chain that allows a number of filters to be called - * before finally invoking the servlet that is the target of the request. + * Mock implementation of a filter chain that allows a number of filters to be called before finally + * invoking the servlet that is the target of the request. * * @author Tim Fennell * @since Stripes 1.1.1 */ public class MockFilterChain implements FilterChain { - private List filters = new ArrayList(); - private Iterator iterator; - private Servlet servlet; + private final List filters = new ArrayList<>(); + private Iterator iterator; + private Servlet servlet; - /** Adds a filter to the set of filters to be run. */ - public void addFilter(Filter filter) { - this.filters.add(filter); - } + /** Adds a filter to the set of filters to be run. */ + public void addFilter(Filter filter) { + this.filters.add(filter); + } - /** Adds an ordered list of filters to the filter chain. */ - public void addFilters(Collection filters) { - this.filters.addAll(filters); - } + /** Adds an ordered list of filters to the filter chain. */ + public void addFilters(Collection filters) { + this.filters.addAll(filters); + } - /** Sets the servlet that will receive the request after all filters are processed. */ - public void setServlet(Servlet servlet) { - this.servlet = servlet; - } + /** Sets the servlet that will receive the request after all filters are processed. */ + public void setServlet(Servlet servlet) { + this.servlet = servlet; + } - /** Used to coordinate the execution of the filters. */ - public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { - if (this.iterator == null) { - this.iterator = this.filters.iterator(); - } + /** Used to coordinate the execution of the filters. */ + public void doFilter(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + if (this.iterator == null) { + this.iterator = this.filters.iterator(); + } - if (this.iterator.hasNext()) { - this.iterator.next().doFilter(request, response, this); - } - else { - this.servlet.service(request, response); - } + if (this.iterator.hasNext()) { + this.iterator.next().doFilter(request, response, this); + } else { + this.servlet.service(request, response); } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/mock/MockFilterConfig.java b/stripes/src/main/java/net/sourceforge/stripes/mock/MockFilterConfig.java index de9d4fcdc..25f7f38c2 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/mock/MockFilterConfig.java +++ b/stripes/src/main/java/net/sourceforge/stripes/mock/MockFilterConfig.java @@ -14,7 +14,7 @@ */ package net.sourceforge.stripes.mock; -import javax.servlet.FilterConfig; +import jakarta.servlet.FilterConfig; /** * Mock implementation of the FilterConfig interface from the Http Servlet spec. @@ -23,11 +23,15 @@ * @since Stripes 1.1.1 */ public class MockFilterConfig extends MockBaseConfig implements FilterConfig { - private String filterName; + private String filterName; - /** Sets the filter name that will be retrieved by getFilterName(). */ - public void setFilterName(String filterName) { this.filterName = filterName; } + /** Sets the filter name that will be retrieved by getFilterName(). */ + public void setFilterName(String filterName) { + this.filterName = filterName; + } - /** Returns the name of the filter for which this is the config. */ - public String getFilterName() { return this.filterName; } + /** Returns the name of the filter for which this is the config. */ + public String getFilterName() { + return this.filterName; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/mock/MockHttpServletRequest.java b/stripes/src/main/java/net/sourceforge/stripes/mock/MockHttpServletRequest.java index 1008eb281..3003ef863 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/mock/MockHttpServletRequest.java +++ b/stripes/src/main/java/net/sourceforge/stripes/mock/MockHttpServletRequest.java @@ -14,6 +14,19 @@ */ package net.sourceforge.stripes.mock; +import jakarta.servlet.AsyncContext; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.ServletConnection; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpUpgradeHandler; +import jakarta.servlet.http.Part; import java.io.BufferedReader; import java.io.IOException; import java.security.Principal; @@ -28,17 +41,12 @@ import java.util.Map; import java.util.Set; -import javax.servlet.ServletInputStream; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - /** - *

    Mock implementation of an HttpServletRequest object. Allows for setting most values that - * are likely to be of interest (and can always be subclassed to affect others). Of key interest - * and perhaps not completely obvious, the way to get request parameters into an instance of - * MockHttpServletRequest is to fetch the parameter map using getParameterMap() and use the - * put() and putAll() methods on it. Values must be String arrays. Examples follow:

    + * Mock implementation of an HttpServletRequest object. Allows for setting most values that are + * likely to be of interest (and can always be subclassed to affect others). Of key interest and + * perhaps not completely obvious, the way to get request parameters into an instance of + * MockHttpServletRequest is to fetch the parameter map using getParameterMap() and use the put() + * and putAll() methods on it. Values must be String arrays. Examples follow: * *
      * MockHttpServletRequest req = new MockHttpServletRequest("/foo", "/bar.action");
    @@ -46,337 +54,534 @@
      * req.getParameterMap().put("param2", new String[] {"value1", "value2"});
      * 
    * - *

    It should also be noted that unless you generate an instance of MockHttpSession (or - * another implementation of HttpSession) and set it on the request, then your request will - * never have a session associated with it.

    + *

    It should also be noted that unless you generate an instance of MockHttpSession (or another + * implementation of HttpSession) and set it on the request, then your request will never + * have a session associated with it. * * @author Tim Fennell * @since Stripes 1.1.1 */ public class MockHttpServletRequest implements HttpServletRequest { - private String authType; - private Cookie[] cookies; - private Map headers = new HashMap(); - private Map attributes = new HashMap(); - private Map parameters = new HashMap(); - private String method = "POST"; - private HttpSession session; - private String characterEncoding = "UTF-8"; - private List locales = new ArrayList(); - private Principal userPrincipal; - private Set roles = new HashSet(); - private String forwardUrl; - private List includedUrls = new ArrayList(); - - // All the bits of the URL - private String protocol = "https"; - private String serverName = "localhost"; - private int serverPort = 8080; - private String contextPath = ""; - private String servletPath = ""; - private String pathInfo = ""; - private String queryString = ""; - - /** - * Minimal constructor that makes sense. Requires a context path (should be the same as - * the name of the servlet context, prepended with a '/') and a servlet path. E.g. - * new MockHttpServletRequest("/myapp", "/actionType/foo.action"). - * - * @param contextPath - * @param servletPath - */ - public MockHttpServletRequest(String contextPath, String servletPath) { - this.contextPath = contextPath; - this.servletPath = servletPath; - } - - /** Sets the auth type that will be reported by this request. */ - public void setAuthType(String authType) { this.authType = authType; } - - /** Gets the auth type being used by this request. */ - public String getAuthType() { return this.authType; } - - /** Sets the array of cookies that will be available from the request. */ - public void setCookies(Cookie[] cookies) { this.cookies = cookies; } - - /** Returns any cookies that are set on the request. */ - public Cookie[] getCookies() { return this.cookies; } - - /** - * Allows headers to be set on the request. These will be returned by the various getXxHeader() - * methods. If the header is a date header it should be set with a Long. If the header is an - * Int header it should be set with an Integer. - */ - public void addHeader(String name, Object value) { - this.headers.put(name.toLowerCase(), value); - } - - /** Gets the named header as a long. Must have been set as a long with addHeader(). */ - public long getDateHeader(String name) { return (Long) this.headers.get(name); } - - /** Returns any header as a String if it exists. */ - public String getHeader(String name) { - final Object header = this.headers.get(name == null ? null : name.toLowerCase()); - return header == null ? null : header.toString(); - } - - /** Returns an enumeration with single value of the named header, or an empty enum if no value. */ - public Enumeration getHeaders(String name) { - String header = getHeader(name); - Collection values = new ArrayList(); - if (header != null) { - values.add(header); - } - return Collections.enumeration(values); - } - - /** Returns an enumeration containing all the names of headers supplied. */ - public Enumeration getHeaderNames() { return Collections.enumeration(headers.keySet()); } - - /** Gets the named header as an int. Must have been set as an Integer with addHeader(). */ - public int getIntHeader(String name) { - String headerValue = getHeader( name ); - if( headerValue == null ) return -1; - return Integer.parseInt( headerValue ); - } - - /** Sets the method used by the request. Defaults to POST. */ - public void setMethod(String method) { this.method = method; } - - /** Gets the method used by the request. Defaults to POST. */ - public String getMethod() { return this.method; } - - /** Sets the path info. Defaults to the empty string. */ - public void setPathInfo(String pathInfo) { this.pathInfo = pathInfo; } - - /** Returns the path info. Defaults to the empty string. */ - public String getPathInfo() { return this.pathInfo; } - - /** Always returns the same as getPathInfo(). */ - public String getPathTranslated() { return getPathInfo(); } - - /** Sets the context path. Defaults to the empty string. */ - public void setContextPath(String contextPath) { this.contextPath = contextPath; } - - /** Returns the context path. Defaults to the empty string. */ - public String getContextPath() { return this.contextPath; } - - /** Sets the query string set on the request; this value is not parsed for anything. */ - public void setQueryString(String queryString) { this.queryString = queryString; } - - /** Returns the query string set on the request. */ - public String getQueryString() { return this.queryString; } - - /** Returns the name from the user principal if one exists, otherwise null. */ - public String getRemoteUser() { - Principal p = getUserPrincipal(); - return p == null ? null : p.getName(); - } - - /** Sets the set of roles that the user is deemed to be in for the request. */ - public void setRoles(Set roles) { this.roles = roles; } - - /** Returns true if the set of roles contains the role specified, false otherwise. */ - public boolean isUserInRole(String role) { - return this.roles.contains(role); - } - - /** Sets the Principal for the current request. */ - public void setUserPrincipal(Principal userPrincipal) { this.userPrincipal = userPrincipal; } - - /** Returns the Principal if one is set on the request. */ - public Principal getUserPrincipal() { return this.userPrincipal; } - - /** Returns the ID of the session if one is attached to this request. Otherwise null. */ - public String getRequestedSessionId() { - if (this.session == null) { - return null; - } - return this.session.getId(); - } - - /** Returns the request URI as defined by the servlet spec. */ - public String getRequestURI() { return this.contextPath + this.servletPath + this.pathInfo; } - - /** Returns (an attempt at) a reconstructed URL based on it's constituent parts. */ - public StringBuffer getRequestURL() { - return new StringBuffer().append(this.protocol) - .append("://") - .append(this.serverName) - .append(":") - .append(this.serverPort) - .append(this.contextPath) - .append(this.servletPath) - .append(this.pathInfo); - } - - /** Gets the part of the path which matched the servlet. */ - public String getServletPath() { return this.servletPath; } - - /** Gets the session object attached to this request. */ - public HttpSession getSession(boolean b) { return this.session; } - - /** Gets the session object attached to this request. */ - public HttpSession getSession() { return this.session; } - - /** Allows a session to be associated with the request. */ - public void setSession(HttpSession session) { this.session = session; } - - /** Always returns true. */ - public boolean isRequestedSessionIdValid() { return true; } - - /** Always returns true. */ - public boolean isRequestedSessionIdFromCookie() { return true; } - - /** Always returns false. */ - public boolean isRequestedSessionIdFromURL() { return false; } - - /** Always returns false. */ - public boolean isRequestedSessionIdFromUrl() { return false; } - - /** Gets the named request attribute from an internal Map. */ - public Object getAttribute(String key) { return this.attributes.get(key); } - - /** Gets an enumeration of all request attribute names. */ - public Enumeration getAttributeNames() { - return Collections.enumeration(this.attributes.keySet()); - } - - /** Gets the character encoding, defaults to UTF-8. */ - public String getCharacterEncoding() { return this.characterEncoding; } - - /** Sets the character encoding that will be returned by getCharacterEncoding(). */ - public void setCharacterEncoding(String encoding) { this.characterEncoding = encoding; } - - /** Always returns -1 (unknown). */ - public int getContentLength() { return -1; } - - /** Always returns null. */ - public String getContentType() { return null; } - - /** Always returns null. */ - public ServletInputStream getInputStream() throws IOException { return null; } - - /** Gets the first value of the named parameter or null if a value does not exist. */ - public String getParameter(String name) { - String[] values = getParameterValues(name); - if (values != null && values.length > 0) { - return values[0]; - } - - return null; + private String authType; + private Cookie[] cookies; + private final Map headers = new HashMap<>(); + private final Map attributes = new HashMap<>(); + private final Map parameters = new HashMap<>(); + private String method = "POST"; + private HttpSession session; + private String characterEncoding = "UTF-8"; + private final List locales = new ArrayList<>(); + private Principal userPrincipal; + private Set roles = new HashSet<>(); + private String forwardUrl; + private final List includedUrls = new ArrayList<>(); + + // All the bits of the URL + private String protocol = "https"; + private String serverName = "localhost"; + private int serverPort = 8080; + private String contextPath = ""; + private String servletPath = ""; + private String pathInfo = ""; + private String queryString = ""; + + /** + * Minimal constructor that makes sense. Requires a context path (should be the same as the name + * of the servlet context, prepended with a '/') and a servlet path. E.g. new + * MockHttpServletRequest("/myapp", "/actionType/foo.action"). + * + * @param contextPath the context path + * @param servletPath the servlet path + */ + public MockHttpServletRequest(String contextPath, String servletPath) { + this.contextPath = contextPath; + this.servletPath = servletPath; + } + + /** Sets the auth type that will be reported by this request. */ + public void setAuthType(String authType) { + this.authType = authType; + } + + /** Gets the auth type being used by this request. */ + public String getAuthType() { + return this.authType; + } + + /** Sets the array of cookies that will be available from the request. */ + public void setCookies(Cookie[] cookies) { + this.cookies = cookies; + } + + /** Returns any cookies that are set on the request. */ + public Cookie[] getCookies() { + return this.cookies; + } + + /** + * Allows headers to be set on the request. These will be returned by the various getXxHeader() + * methods. If the header is a date header it should be set with a Long. If the header is an Int + * header it should be set with an Integer. + */ + public void addHeader(String name, Object value) { + this.headers.put(name.toLowerCase(), value); + } + + /** Gets the named header as a long. Must have been set as a long with addHeader(). */ + public long getDateHeader(String name) { + return (Long) this.headers.get(name); + } + + /** Returns any header as a String if it exists. */ + public String getHeader(String name) { + final Object header = this.headers.get(name == null ? null : name.toLowerCase()); + return header == null ? null : header.toString(); + } + + /** Returns an enumeration with single value of the named header, or an empty enum if no value. */ + public Enumeration getHeaders(String name) { + String header = getHeader(name); + Collection values = new ArrayList<>(); + if (header != null) { + values.add(header); } - - /** Gets an enumeration containing all the parameter names present. */ - public Enumeration getParameterNames() { - return Collections.enumeration(this.parameters.keySet()); + return Collections.enumeration(values); + } + + /** Returns an enumeration containing all the names of headers supplied. */ + public Enumeration getHeaderNames() { + return Collections.enumeration(headers.keySet()); + } + + /** Gets the named header as an int. Must have been set as an Integer with addHeader(). */ + public int getIntHeader(String name) { + String headerValue = getHeader(name); + if (headerValue == null) return -1; + return Integer.parseInt(headerValue); + } + + /** Sets the method used by the request. Defaults to POST. */ + public void setMethod(String method) { + this.method = method; + } + + /** Gets the method used by the request. Defaults to POST. */ + public String getMethod() { + return this.method; + } + + /** Sets the path info. Defaults to the empty string. */ + public void setPathInfo(String pathInfo) { + this.pathInfo = pathInfo; + } + + /** Returns the path info. Defaults to the empty string. */ + public String getPathInfo() { + return this.pathInfo; + } + + /** Always returns the same as getPathInfo(). */ + public String getPathTranslated() { + return getPathInfo(); + } + + /** Sets the context path. Defaults to the empty string. */ + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + /** Returns the context path. Defaults to the empty string. */ + public String getContextPath() { + return this.contextPath; + } + + /** Sets the query string set on the request; this value is not parsed for anything. */ + public void setQueryString(String queryString) { + this.queryString = queryString; + } + + /** Returns the query string set on the request. */ + public String getQueryString() { + return this.queryString; + } + + /** Returns the name from the user principal if one exists, otherwise null. */ + public String getRemoteUser() { + Principal p = getUserPrincipal(); + return p == null ? null : p.getName(); + } + + /** Sets the set of roles that the user is deemed to be in for the request. */ + public void setRoles(Set roles) { + this.roles = roles; + } + + /** Returns true if the set of roles contains the role specified, false otherwise. */ + public boolean isUserInRole(String role) { + return this.roles.contains(role); + } + + /** Sets the Principal for the current request. */ + public void setUserPrincipal(Principal userPrincipal) { + this.userPrincipal = userPrincipal; + } + + /** Returns the Principal if one is set on the request. */ + public Principal getUserPrincipal() { + return this.userPrincipal; + } + + /** Returns the ID of the session if one is attached to this request. Otherwise, null. */ + public String getRequestedSessionId() { + if (this.session == null) { + return null; } - - /** Returns an array of all values for a parameter, or null if the parameter does not exist. */ - public String[] getParameterValues(String name) { - return this.parameters.get(name); + return this.session.getId(); + } + + /** Returns the request URI as defined by the servlet spec. */ + public String getRequestURI() { + return this.contextPath + this.servletPath + this.pathInfo; + } + + /** Returns (an attempt at) a reconstructed URL based on its constituent parts. */ + public StringBuffer getRequestURL() { + return new StringBuffer() + .append(this.protocol) + .append("://") + .append(this.serverName) + .append(":") + .append(this.serverPort) + .append(this.contextPath) + .append(this.servletPath) + .append(this.pathInfo); + } + + /** Gets the part of the path which matched the servlet. */ + public String getServletPath() { + return this.servletPath; + } + + /** Gets the session object attached to this request. */ + public HttpSession getSession(boolean b) { + return this.session; + } + + /** Gets the session object attached to this request. */ + public HttpSession getSession() { + return this.session; + } + + @Override + public String changeSessionId() { + return null; + } + + /** Allows a session to be associated with the request. */ + public void setSession(HttpSession session) { + this.session = session; + } + + /** Always returns true. */ + public boolean isRequestedSessionIdValid() { + return true; + } + + /** Always returns true. */ + public boolean isRequestedSessionIdFromCookie() { + return true; + } + + /** Always returns false. */ + public boolean isRequestedSessionIdFromURL() { + return false; + } + + @Override + public boolean authenticate(HttpServletResponse response) { + return false; + } + + @Override + public void login(String username, String password) {} + + @Override + public void logout() {} + + @Override + public Collection getParts() { + return null; + } + + @Override + public Part getPart(String name) { + return null; + } + + @Override + public T upgrade(Class handlerClass) { + return null; + } + + /** Always returns false. */ + public boolean isRequestedSessionIdFromUrl() { + return false; + } + + /** Gets the named request attribute from an internal Map. */ + public Object getAttribute(String key) { + return this.attributes.get(key); + } + + /** Gets an enumeration of all request attribute names. */ + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + /** Gets the character encoding, defaults to UTF-8. */ + public String getCharacterEncoding() { + return this.characterEncoding; + } + + /** Sets the character encoding that will be returned by getCharacterEncoding(). */ + public void setCharacterEncoding(String encoding) { + this.characterEncoding = encoding; + } + + /** Always returns -1 (unknown). */ + public int getContentLength() { + return -1; + } + + @Override + public long getContentLengthLong() { + return 0; + } + + /** Always returns null. */ + public String getContentType() { + return null; + } + + /** Always returns null. */ + public ServletInputStream getInputStream() throws IOException { + return null; + } + + /** Gets the first value of the named parameter or null if a value does not exist. */ + public String getParameter(String name) { + String[] values = getParameterValues(name); + if (values != null && values.length > 0) { + return values[0]; } - /** - * Provides access to the parameter map. Note that this returns a reference to the live, - * modifiable parameter map. As a result it can be used to insert parameters when constructing - * the request. - */ - public Map getParameterMap() { - return this.parameters; + return null; + } + + /** Gets an enumeration containing all the parameter names present. */ + public Enumeration getParameterNames() { + return Collections.enumeration(this.parameters.keySet()); + } + + /** Returns an array of all values for a parameter, or null if the parameter does not exist. */ + public String[] getParameterValues(String name) { + return this.parameters.get(name); + } + + /** + * Provides access to the parameter map. Note that this returns a reference to the live, + * modifiable parameter map. As a result it can be used to insert parameters when constructing the + * request. + */ + public Map getParameterMap() { + return this.parameters; + } + + /** Sets the protocol for the request. Defaults to "https". */ + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + /** Gets the protocol for the request. Defaults to "https". */ + public String getProtocol() { + return this.protocol; + } + + /** Always returns the same as getProtocol. */ + public String getScheme() { + return getProtocol(); + } + + /** Sets the server name. Defaults to "localhost". */ + public void setServerName(String serverName) { + this.serverName = serverName; + } + + /** Gets the server name. Defaults to "localhost". */ + public String getServerName() { + return this.serverName; + } + + /** Sets the server port. Defaults to 8080. */ + public void setServerPort(int serverPort) { + this.serverPort = serverPort; + } + + /** Returns the server port. Defaults to 8080. */ + public int getServerPort() { + return this.serverPort; + } + + /** Always returns null. */ + public BufferedReader getReader() { + return null; + } + + /** Always returns "127.0.0.1". */ + public String getRemoteAddr() { + return "127.0.0.1"; + } + + /** Always returns "localhost". */ + public String getRemoteHost() { + return "localhost"; + } + + /** Sets the supplied value for the named request attribute. */ + public void setAttribute(String name, Object value) { + this.attributes.put(name, value); + } + + /** Removes any value for the named request attribute. */ + public void removeAttribute(String name) { + this.attributes.remove(name); + } + + /** Adds a Locale to the set of requested locales. */ + public void addLocale(Locale locale) { + this.locales.add(locale); + } + + /** Returns the preferred locale. Defaults to the system locale. */ + public Locale getLocale() { + return getLocales().nextElement(); + } + + /** Returns an enumeration of requested locales. Defaults to the system locale. */ + public Enumeration getLocales() { + if (this.locales.isEmpty()) { + this.locales.add(Locale.getDefault()); } - /** Sets the protocol for the request. Defaults to "https". */ - public void setProtocol(String protocol) { this.protocol = protocol; } - - /** Gets the protocol for the request. Defaults to "https". */ - public String getProtocol() { return this.protocol; } - - /** Always returns the same as getProtocol. */ - public String getScheme() { return getProtocol(); } - - /** Sets the server name. Defaults to "localhost". */ - public void setServerName(String serverName) { this.serverName = serverName; } - - /** Gets the server name. Defaults to "localhost". */ - public String getServerName() { return this.serverName; } - - /** Sets the server port. Defaults to 8080. */ - public void setServerPort(int serverPort) { this.serverPort = serverPort; } - - /** Returns the server port. Defaults to 8080. */ - public int getServerPort() { return this.serverPort; } - - /** Always returns null. */ - public BufferedReader getReader() throws IOException { return null; } - - /** Aways returns "127.0.0.1". */ - public String getRemoteAddr() { return "127.0.0.1"; } - - /** Always returns "localhost". */ - public String getRemoteHost() { return "localhost"; } - - /** Sets the supplied value for the named request attribute. */ - public void setAttribute(String name, Object value) { - this.attributes.put(name, value); - } - - /** Removes any value for the named request attribute. */ - public void removeAttribute(String name) { this.attributes.remove(name); } - - /** Adds a Locale to the set of requested locales. */ - public void addLocale(Locale locale) { this.locales.add(locale); } - - /** Returns the preferred locale. Defaults to the system locale. */ - public Locale getLocale() { return getLocales().nextElement(); } - - /** Returns an enumeration of requested locales. Defaults to the system locale. */ - public Enumeration getLocales() { - if (this.locales.size() == 0) { - this.locales.add( Locale.getDefault() ); - } - - return Collections.enumeration(this.locales); - } - - /** Returns true if the protocol is set to https (default), false otherwise. */ - public boolean isSecure() { - return this.protocol.equalsIgnoreCase("https"); - } - - /** - * Returns an instance of MockRequestDispatcher that just records what URLs are forwarded - * to or included. The results can be examined later by calling getForwardUrl() and - * getIncludedUrls(). - */ - public MockRequestDispatcher getRequestDispatcher(String url) { - return new MockRequestDispatcher(url); - } - - /** Always returns the path passed in without any alteration. */ - public String getRealPath(String path) { return path; } - - /** Always returns 1088 (and yes, that was picked arbitrarily). */ - public int getRemotePort() { return 1088; } - - /** Always returns the same value as getServerName(). */ - public String getLocalName() { return getServerName(); } - - /** Always returns 127.0.0.1). */ - public String getLocalAddr() { return "127.0.0.1"; } - - /** Always returns the same value as getServerPort(). */ - public int getLocalPort() { return getServerPort(); } - - /** Used by the request dispatcher to set the forward URL when a forward is invoked. */ - void setForwardUrl(String url) { this.forwardUrl = url; } - - /** Gets the URL that was forwarded to, if a forward was processed. Null otherwise. */ - public String getForwardUrl() { return this.forwardUrl; } - - /** Used by the request dispatcher to record that a URL was included. */ - void addIncludedUrl(String url) { this.includedUrls.add(url); } - - /** Gets the list (potentially empty) or URLs that were included during the request. */ - public List getIncludedUrls() { return this.includedUrls; } + return Collections.enumeration(this.locales); + } + + /** Returns true if the protocol is set to https (default), false otherwise. */ + public boolean isSecure() { + return this.protocol.equalsIgnoreCase("https"); + } + + /** + * Returns an instance of MockRequestDispatcher that just records what URLs are forwarded to or + * included. The results can be examined later by calling getForwardUrl() and getIncludedUrls(). + */ + public MockRequestDispatcher getRequestDispatcher(String url) { + return new MockRequestDispatcher(url); + } + + /** Always returns the path passed in without any alteration. */ + public String getRealPath(String path) { + return path; + } + + /** Always returns 1088 (and yes, that was picked arbitrarily). */ + public int getRemotePort() { + return 1088; + } + + /** Always returns the same value as getServerName(). */ + public String getLocalName() { + return getServerName(); + } + + /** Always returns 127.0.0.1). */ + public String getLocalAddr() { + return "127.0.0.1"; + } + + /** Always returns the same value as getServerPort(). */ + public int getLocalPort() { + return getServerPort(); + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + return null; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) + throws IllegalStateException { + return null; + } + + @Override + public boolean isAsyncStarted() { + return false; + } + + @Override + public boolean isAsyncSupported() { + return false; + } + + @Override + public AsyncContext getAsyncContext() { + return null; + } + + @Override + public DispatcherType getDispatcherType() { + return null; + } + + @Override + public String getRequestId() { + return null; + } + + @Override + public String getProtocolRequestId() { + return null; + } + + @Override + public ServletConnection getServletConnection() { + return null; + } + + /** Used by the request dispatcher to set the forward URL when a forward is invoked. */ + void setForwardUrl(String url) { + this.forwardUrl = url; + } + + /** Gets the URL that was forwarded to, if a forward was processed. Null otherwise. */ + public String getForwardUrl() { + return this.forwardUrl; + } + + /** Used by the request dispatcher to record that a URL was included. */ + void addIncludedUrl(String url) { + this.includedUrls.add(url); + } + + /** Gets the list (potentially empty) or URLs that were included during the request. */ + public List getIncludedUrls() { + return this.includedUrls; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/mock/MockHttpServletResponse.java b/stripes/src/main/java/net/sourceforge/stripes/mock/MockHttpServletResponse.java index 5e416e04e..f53361917 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/mock/MockHttpServletResponse.java +++ b/stripes/src/main/java/net/sourceforge/stripes/mock/MockHttpServletResponse.java @@ -14,241 +14,289 @@ */ package net.sourceforge.stripes.mock; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.Cookie; -import javax.servlet.ServletOutputStream; -import java.io.IOException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; -import java.util.ListIterator; -import java.util.Locale; -import java.util.Map; import java.util.HashMap; import java.util.List; -import java.util.ArrayList; +import java.util.Locale; +import java.util.Map; /** - *

    Mock implementation of an HttpServletResponse. Captures any output is written along with - * any headers, status information etc. and makes it available through various getter methods.

    + * Mock implementation of an HttpServletResponse. Captures any output is written along with any + * headers, status information etc. and makes it available through various getter methods. * *

    Of major note is the fact that none of the setStatus(), sendError() or sendRedirect() methods - * have any real effect on the request processing lifecycle. Information is recorded so it can be - * verified what was invoked, but that is all.

    + * have any real effect on the request processing lifecycle. Information is recorded, so it can be + * verified what was invoked, but that is all. * * @author Tim Fennell * @since Stripes 1.1.1 */ public class MockHttpServletResponse implements HttpServletResponse { - private MockServletOutputStream out = new MockServletOutputStream(); - private PrintWriter writer = new PrintWriter(out, true); - private Locale locale = Locale.getDefault(); - private Map> headers = new HashMap>(); - private List cookies = new ArrayList(); - private int status = 200; - private String errorMessage; - private String characterEncoding = "UTF-8"; - private int contentLength; - private String contentType; - private String redirectUrl; - - public MockHttpServletResponse() { - setContentType("text/html"); - } - - /** Adds a cookie to the set of cookies in the response. */ - public void addCookie(Cookie cookie) { - // Remove existing cookies with the same name as the new one - ListIterator iterator = cookies.listIterator(); - while (iterator.hasNext()) { - if (iterator.next().getName().equals(cookie.getName())) - iterator.remove(); - } - - this.cookies.add(cookie); - } - - /** Gets the set of cookies stored in the response. */ - public Cookie[] getCookies() { return this.cookies.toArray(new Cookie[this.cookies.size()]); } - - /** Returns true if the specified header was placed in the response. */ - public boolean containsHeader(String name) { return this.headers.containsKey(name); } - - /** Returns the URL unchanged. */ - public String encodeURL(String url) { return url; } - - /** Returns the URL unchanged. */ - public String encodeRedirectURL(String url) { return url; } - - /** Returns the URL unchanged. */ - public String encodeUrl(String url) { return url; } - - /** Returns the URL unchanged. */ - public String encodeRedirectUrl(String url) { return url; } - - /** Sets the status code and saves the message so it can be retrieved later. */ - public void sendError(int status, String errorMessage) throws IOException { - this.status = status; - this.errorMessage = errorMessage; - } - - /** Sets that status code to the error code provided. */ - public void sendError(int status) throws IOException { this.status = status; } - - /** - * Simply sets the status code and stores the URL that was supplied, so that it can be examined - * later with getRedirectUrl. - */ - public void sendRedirect(String url) throws IOException { - this.status = HttpServletResponse.SC_MOVED_TEMPORARILY; - this.redirectUrl = url; - } - - /** - * If a call was made to sendRedirect() this method will return the URL that was supplied. - * Otherwise it will return null. - */ - public String getRedirectUrl() { return this.redirectUrl; } - - /** Stores the value in a Long and saves it as a header. */ - public void setDateHeader(String name, long value) { - this.headers.remove(name); - addDateHeader(name, value); - } - - /** Adds the specified value for the named header (does not remove/replace existing values). */ - public void addDateHeader(String name, long value) { - List values = this.headers.get(name); - if (values == null) { - this.headers.put(name, values = new ArrayList()); - } - values.add(value); - } - - /** Sets the value of the specified header to the single value provided. */ - public void setHeader(String name, String value) { - this.headers.remove(name); - addHeader(name, value); - } - - /** Adds the specified value for the named header (does not remove/replace existing values). */ - public void addHeader(String name, String value) { - List values = this.headers.get(name); - if (values == null) { - this.headers.put(name, values = new ArrayList()); - } - values.add(value); - } - - /** Stores the value in an Integer and saves it as a header. */ - public void setIntHeader(String name, int value) { - this.headers.remove(name); - addIntHeader(name, value); - } - - /** Adds the specified value for the named header (does not remove/replace existing values). */ - public void addIntHeader(String name, int value) { - List values = this.headers.get(name); - if (values == null) { - this.headers.put(name, values = new ArrayList()); - } - values.add(value); - } - - /** - * Provides access to all headers that were set. The format is a Map which uses the header - * name as the key, and stores a List of Objects, one per header value. The Objects will - * be either Strings (if setHeader() was used), Integers (if setIntHeader() was used) or - * Longs (if setDateHeader() was used). - */ - public Map> getHeaderMap() { return this.headers; } - - /** Sets the HTTP Status code of the response. */ - public void setStatus(int statusCode) { this.status = statusCode; } - - /** Saves the HTTP status code and the message provided. */ - public void setStatus(int status, String errorMessage) { - this.status = status; - this.errorMessage = errorMessage; - } - - /** Gets the status (or error) code if one was set. Defaults to 200 (HTTP OK). */ - public int getStatus() { return this.status; } - - /** Gets the error message if one was set with setStatus() or sendError(). */ - public String getErrorMessage() { return this.errorMessage; } - - /** Sets the character encoding on the request. */ - public void setCharacterEncoding(String encoding) { this.characterEncoding = encoding; } - - /** Gets the character encoding (defaults to UTF-8). */ - public String getCharacterEncoding() { return this.characterEncoding; } - - /** Sets the content type for the response. */ - public void setContentType(String contentType) { - this.contentType = contentType; - getHeaderMap().put("Content-type", Collections. singletonList(contentType)); - } - - /** Gets the content type for the response. Defaults to text/html. */ - public String getContentType() { return this.contentType; } - - /** - * Returns a reference to a ServletOutputStream to be used for output. The output is captured - * and can be examined at the end of a test run by calling getOutputBytes() or - * getOutputString(). - */ - public ServletOutputStream getOutputStream() throws IOException { return this.out; } - - /** - * Returns a reference to a PrintWriter to be used for character output. The output is captured - * and can be examined at the end of a test run by calling getOutputBytes() or - * getOutputString(). - */ - public PrintWriter getWriter() throws IOException { return this.writer; } - - /** Gets the output that was written to the output stream, as a byte[]. */ - public byte[] getOutputBytes() { - this.writer.flush(); - return this.out.getBytes(); - } - - /** Gets the output that was written to the output stream, as a character String. */ - public String getOutputString() { - this.writer.flush(); - return this.out.getString(); - } - - /** Sets a custom content length on the response. */ - public void setContentLength(int contentLength) { this.contentLength = contentLength; } - - /** Returns the content length if one was set on the response by calling setContentLength(). */ - public int getContentLength() { return this.contentLength; } - - /** Has no effect. */ - public void setBufferSize(int i) { } - - /** Always returns 0. */ - public int getBufferSize() { return 0; } - - /** Has no effect. */ - public void flushBuffer() throws IOException { } - - /** Always throws IllegalStateException. */ - public void resetBuffer() { - throw new IllegalStateException("reset() is not supported"); - } - - /** Always returns true. */ - public boolean isCommitted() { return true; } - - /** Always throws an IllegalStateException. */ - public void reset() { - throw new IllegalStateException("reset() is not supported"); - } - - /** Sets the response locale to the one specified. */ - public void setLocale(Locale locale) { this.locale = locale; } - - /** Gets the response locale. Default to the system default locale. */ - public Locale getLocale() { return this.locale; } + private final MockServletOutputStream out = new MockServletOutputStream(); + private final PrintWriter writer = new PrintWriter(out, true); + private Locale locale = Locale.getDefault(); + private final Map> headers = new HashMap<>(); + private final List cookies = new ArrayList<>(); + private int status = 200; + private String errorMessage; + private String characterEncoding = "UTF-8"; + private int contentLength; + private String contentType; + private String redirectUrl; + + public MockHttpServletResponse() { + setContentType("text/html"); + } + + /** Adds a cookie to the set of cookies in the response. */ + public void addCookie(Cookie cookie) { + // Remove existing cookies with the same name as the new one + cookies.removeIf(cookie1 -> cookie1.getName().equals(cookie.getName())); + + this.cookies.add(cookie); + } + + /** Gets the set of cookies stored in the response. */ + public Cookie[] getCookies() { + return this.cookies.toArray(new Cookie[0]); + } + + /** Returns true if the specified header was placed in the response. */ + public boolean containsHeader(String name) { + return this.headers.containsKey(name); + } + + /** Returns the URL unchanged. */ + public String encodeURL(String url) { + return url; + } + + /** Returns the URL unchanged. */ + public String encodeRedirectURL(String url) { + return url; + } + + /** Returns the URL unchanged. */ + public String encodeUrl(String url) { + return url; + } + + /** Returns the URL unchanged. */ + public String encodeRedirectUrl(String url) { + return url; + } + + /** Sets the status code and saves the message, so it can be retrieved later. */ + public void sendError(int status, String errorMessage) { + this.status = status; + this.errorMessage = errorMessage; + } + + /** Sets that status code to the error code provided. */ + public void sendError(int status) { + this.status = status; + } + + /** + * Simply sets the status code and stores the URL that was supplied, so that it can be examined + * later with getRedirectUrl. + */ + public void sendRedirect(String url) { + this.status = HttpServletResponse.SC_MOVED_TEMPORARILY; + this.redirectUrl = url; + } + + /** + * If a call was made to sendRedirect() this method will return the URL that was supplied. + * Otherwise, it will return null. + */ + public String getRedirectUrl() { + return this.redirectUrl; + } + + /** Stores the value in a Long and saves it as a header. */ + public void setDateHeader(String name, long value) { + this.headers.remove(name); + addDateHeader(name, value); + } + + /** Adds the specified value for the named header (does not remove/replace existing values). */ + public void addDateHeader(String name, long value) { + List values = this.headers.computeIfAbsent(name, k -> new ArrayList<>()); + values.add(value); + } + + /** Sets the value of the specified header to the single value provided. */ + public void setHeader(String name, String value) { + this.headers.remove(name); + addHeader(name, value); + } + + /** Adds the specified value for the named header (does not remove/replace existing values). */ + public void addHeader(String name, String value) { + List values = this.headers.computeIfAbsent(name, k -> new ArrayList<>()); + values.add(value); + } + + /** Stores the value in an Integer and saves it as a header. */ + public void setIntHeader(String name, int value) { + this.headers.remove(name); + addIntHeader(name, value); + } + + /** Adds the specified value for the named header (does not remove/replace existing values). */ + public void addIntHeader(String name, int value) { + List values = this.headers.computeIfAbsent(name, k -> new ArrayList<>()); + values.add(value); + } + + /** + * Provides access to all headers that were set. The format is a Map which uses the header name as + * the key, and stores a List of Objects, one per header value. The Objects will be either Strings + * (if setHeader() was used), Integers (if setIntHeader() was used) or Longs (if setDateHeader() + * was used). + */ + public Map> getHeaderMap() { + return this.headers; + } + + /** Sets the HTTP Status code of the response. */ + public void setStatus(int statusCode) { + this.status = statusCode; + } + + /** Saves the HTTP status code and the message provided. */ + public void setStatus(int status, String errorMessage) { + this.status = status; + this.errorMessage = errorMessage; + } + + /** Gets the status (or error) code if one was set. Defaults to 200 (HTTP OK). */ + public int getStatus() { + return this.status; + } + + @Override + public String getHeader(String name) { + return null; + } + + @Override + public Collection getHeaders(String name) { + return null; + } + + @Override + public Collection getHeaderNames() { + return null; + } + + /** Gets the error message if one was set with setStatus() or sendError(). */ + public String getErrorMessage() { + return this.errorMessage; + } + + /** Sets the character encoding on the request. */ + public void setCharacterEncoding(String encoding) { + this.characterEncoding = encoding; + } + + /** Gets the character encoding (defaults to UTF-8). */ + public String getCharacterEncoding() { + return this.characterEncoding; + } + + /** Sets the content type for the response. */ + public void setContentType(String contentType) { + this.contentType = contentType; + getHeaderMap().put("Content-type", Collections.singletonList(contentType)); + } + + /** Gets the content type for the response. Defaults to text/html. */ + public String getContentType() { + return this.contentType; + } + + /** + * Returns a reference to a ServletOutputStream to be used for output. The output is captured and + * can be examined at the end of a test run by calling getOutputBytes() or getOutputString(). + */ + public ServletOutputStream getOutputStream() { + return this.out; + } + + /** + * Returns a reference to a PrintWriter to be used for character output. The output is captured + * and can be examined at the end of a test run by calling getOutputBytes() or getOutputString(). + */ + public PrintWriter getWriter() { + return this.writer; + } + + /** Gets the output that was written to the output stream, as a byte[]. */ + public byte[] getOutputBytes() { + this.writer.flush(); + return this.out.getBytes(); + } + + /** Gets the output that was written to the output stream, as a character String. */ + public String getOutputString() { + this.writer.flush(); + return this.out.getString(); + } + + /** Sets a custom content length on the response. */ + public void setContentLength(int contentLength) { + this.contentLength = contentLength; + } + + @Override + public void setContentLengthLong(long len) {} + + /** Returns the content length if one was set on the response by calling setContentLength(). */ + public int getContentLength() { + return this.contentLength; + } + + /** Has no effect. */ + public void setBufferSize(int i) {} + + /** Always returns 0. */ + public int getBufferSize() { + return 0; + } + + /** Has no effect. */ + public void flushBuffer() {} + + /** Always throws IllegalStateException. */ + public void resetBuffer() { + throw new IllegalStateException("reset() is not supported"); + } + + /** Always returns true. */ + public boolean isCommitted() { + return true; + } + + /** Always throws an IllegalStateException. */ + public void reset() { + throw new IllegalStateException("reset() is not supported"); + } + + /** Sets the response locale to the one specified. */ + public void setLocale(Locale locale) { + this.locale = locale; + } + + /** Gets the response locale. Default to the system default locale. */ + public Locale getLocale() { + return this.locale; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/mock/MockHttpSession.java b/stripes/src/main/java/net/sourceforge/stripes/mock/MockHttpSession.java index b2a243072..2ae9ece87 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/mock/MockHttpSession.java +++ b/stripes/src/main/java/net/sourceforge/stripes/mock/MockHttpSession.java @@ -14,8 +14,8 @@ */ package net.sourceforge.stripes.mock; -import javax.servlet.ServletContext; -import javax.servlet.http.HttpSession; +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpSession; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; @@ -28,81 +28,97 @@ * @author Tim Fennell * @since Stripes 1.1.1 */ -@SuppressWarnings("deprecation") public class MockHttpSession implements HttpSession { - private long creationTime = System.currentTimeMillis(); - private String sessionId = String.valueOf(new Random().nextLong()); - private ServletContext context; - private Map attributes = new HashMap(); - - /** Default constructor which provides the session with access to the context. */ - public MockHttpSession(ServletContext context) { - this.context = context; - } - - /** Returns the time in milliseconds when the session was created. */ - public long getCreationTime() { return this.creationTime; } - - /** Returns an ID that was randomly generated when the session was created. */ - public String getId() { return this.sessionId; } - - /** Always returns the current time. */ - public long getLastAccessedTime() { return System.currentTimeMillis(); } - - /** Provides access to the servlet context within which the session exists. */ - public ServletContext getServletContext() { return this.context; } - - /** Sets the servlet context within which the session exists. */ - public void setServletContext(ServletContext context) { this.context = context; } - - /** Has no effect. */ - public void setMaxInactiveInterval(int i) { } - - /** Always returns Integer.MAX_VALUE. */ - public int getMaxInactiveInterval() { return Integer.MAX_VALUE; } - - /** Deprecated method always returns null. */ - public javax.servlet.http.HttpSessionContext getSessionContext() { return null; } - - /** Returns the value of the named attribute from an internal Map. */ - public Object getAttribute(String key) { return this.attributes.get(key); } - - /** Deprecated method. Use getAttribute() instead. */ - public Object getValue(String key) { return getAttribute(key); } - - /** Returns an enumeration of all the attribute names in the session. */ - public Enumeration getAttributeNames() { - return Collections.enumeration(this.attributes.keySet()); - } - - /** Returns a String[] of all the attribute names in session. Deprecated. */ - public String[] getValueNames() { - return this.attributes.keySet().toArray(new String[this.attributes.size()]); - } - - /** Stores the value in session, replacing any existing value with the same key. */ - public void setAttribute(String key, Object value) { - this.attributes.put(key, value); - } - - /** Stores the value in session, replacing any existing value with the same key. */ - public void putValue(String key, Object value) { - setAttribute(key, value); - } - - /** Removes any value stored in session with the key supplied. */ - public void removeAttribute(String key) { - this.attributes.remove(key); - } - - /** Removes any value stored in session with the key supplied. */ - public void removeValue(String key) { - removeAttribute(key); - } - - /** Clears the set of attributes, but has no other effect. */ - public void invalidate() { this.attributes.clear(); } - - /** Always returns false. */ - public boolean isNew() { return false; } + private final long creationTime = System.currentTimeMillis(); + private final String sessionId = String.valueOf(new Random().nextLong()); + private ServletContext context; + private final Map attributes = new HashMap<>(); + + /** Default constructor which provides the session with access to the context. */ + public MockHttpSession(ServletContext context) { + this.context = context; + } + + /** Returns the time in milliseconds when the session was created. */ + public long getCreationTime() { + return this.creationTime; + } + + /** Returns an ID that was randomly generated when the session was created. */ + public String getId() { + return this.sessionId; + } + + /** Always returns the current time. */ + public long getLastAccessedTime() { + return System.currentTimeMillis(); + } + + /** Provides access to the servlet context within which the session exists. */ + public ServletContext getServletContext() { + return this.context; + } + + /** Sets the servlet context within which the session exists. */ + public void setServletContext(ServletContext context) { + this.context = context; + } + + /** Has no effect. */ + public void setMaxInactiveInterval(int i) {} + + /** Always returns Integer.MAX_VALUE. */ + public int getMaxInactiveInterval() { + return Integer.MAX_VALUE; + } + + /** Returns the value of the named attribute from an internal Map. */ + public Object getAttribute(String key) { + return this.attributes.get(key); + } + + /** Deprecated method. Use getAttribute() instead. */ + public Object getValue(String key) { + return getAttribute(key); + } + + /** Returns an enumeration of all the attribute names in the session. */ + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + /** Returns a String[] of all the attribute names in session. Deprecated. */ + public String[] getValueNames() { + return this.attributes.keySet().toArray(new String[0]); + } + + /** Stores the value in session, replacing any existing value with the same key. */ + public void setAttribute(String key, Object value) { + this.attributes.put(key, value); + } + + /** Stores the value in session, replacing any existing value with the same key. */ + public void putValue(String key, Object value) { + setAttribute(key, value); + } + + /** Removes any value stored in session with the key supplied. */ + public void removeAttribute(String key) { + this.attributes.remove(key); + } + + /** Removes any value stored in session with the key supplied. */ + public void removeValue(String key) { + removeAttribute(key); + } + + /** Clears the set of attributes, but has no other effect. */ + public void invalidate() { + this.attributes.clear(); + } + + /** Always returns false. */ + public boolean isNew() { + return false; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/mock/MockRequestDispatcher.java b/stripes/src/main/java/net/sourceforge/stripes/mock/MockRequestDispatcher.java index 430c1b252..f5702fd1f 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/mock/MockRequestDispatcher.java +++ b/stripes/src/main/java/net/sourceforge/stripes/mock/MockRequestDispatcher.java @@ -14,46 +14,47 @@ */ package net.sourceforge.stripes.mock; -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequestWrapper; import java.io.IOException; /** - * Mock implementation of a RequesetDispatcher used for testing purposes. Note that the mock + * Mock implementation of a RequestDispatcher used for testing purposes. Note that the mock * implementation does not support actually forwarding the request, or including other resources. - * The methods are implemented to record that a forward/include took place and then simply - * return. + * The methods are implemented to record that a forward/include took place and then simply return. * * @author Tim Fennell * @since Stripes 1.1.1 */ public class MockRequestDispatcher implements RequestDispatcher { - private String url; + private final String url; - /** Constructs a request dispatcher, giving it a handle to the creating request. */ - public MockRequestDispatcher(String url) { - this.url = url; - } + /** Constructs a request dispatcher, giving it a handle to the creating request. */ + public MockRequestDispatcher(String url) { + this.url = url; + } - /** Simply stores the URL that was requested for forward, and returns. */ - public void forward(ServletRequest req, ServletResponse res) throws ServletException, IOException { - getMockRequest(req).setForwardUrl(this.url); - } + /** Simply stores the URL that was requested for forward, and returns. */ + public void forward(ServletRequest req, ServletResponse res) + throws ServletException, IOException { + getMockRequest(req).setForwardUrl(this.url); + } - /** Simply stores that the URL was included an then returns. */ - public void include(ServletRequest req, ServletResponse res) throws ServletException, IOException { - getMockRequest(req).addIncludedUrl(this.url); - } + /** Simply stores that the URL was included and then returns. */ + public void include(ServletRequest req, ServletResponse res) + throws ServletException, IOException { + getMockRequest(req).addIncludedUrl(this.url); + } - /** Locates the MockHttpServletRequest in case it is wrapped. */ - public MockHttpServletRequest getMockRequest(ServletRequest request) { - while (request != null & !(request instanceof MockHttpServletRequest)) { - request = ((HttpServletRequestWrapper) request).getRequest(); - } - - return (MockHttpServletRequest) request; + /** Locates the MockHttpServletRequest in case it is wrapped. */ + public MockHttpServletRequest getMockRequest(ServletRequest request) { + while (request != null & !(request instanceof MockHttpServletRequest)) { + request = ((HttpServletRequestWrapper) request).getRequest(); } + + return (MockHttpServletRequest) request; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/mock/MockRoundtrip.java b/stripes/src/main/java/net/sourceforge/stripes/mock/MockRoundtrip.java index 173d56789..4019e34fc 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/mock/MockRoundtrip.java +++ b/stripes/src/main/java/net/sourceforge/stripes/mock/MockRoundtrip.java @@ -14,14 +14,13 @@ */ package net.sourceforge.stripes.mock; +import jakarta.servlet.Filter; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.SortedMap; import java.util.TreeMap; - -import javax.servlet.Filter; - import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.controller.ActionResolver; import net.sourceforge.stripes.controller.AnnotatedClassActionResolver; @@ -32,25 +31,25 @@ import net.sourceforge.stripes.validation.ValidationErrors; /** - *

    Mock object that attempts to make it easier to use the other Mock objects in this package - * to interact with Stripes and to interrogate the results. Everything that is done in this class - * is do-able without this class! It simply exists to make things a bit easier. As a result all - * the methods in this class simply manipulate one or more of the underlying Mock objects. If - * some needed capability is not exposed through the MockRoundtrip it is always possible to fetch - * the underlying request, response and context and interact with them directly.

    + * Mock object that attempts to make it easier to use the other Mock objects in this package to + * interact with Stripes and to interrogate the results. Everything that is done in this class is + * do-able without this class! It simply exists to make things a bit easier. As a result all the + * methods in this class simply manipulate one or more of the underlying Mock objects. If some + * needed capability is not exposed through the MockRound-trip it is always possible to fetch the + * underlying request, response and context and interact with them directly. * *

    It is worth noting that the Mock system does not process forwards, includes and - * redirects. When an ActionBean (or other object) invokes the servlet APIs for any of these - * actions it is recorded so that it can be reported and verified later. In the majority of cases - * it should be sufficient to test ActionBeans in isolation and verify that they produced the - * expected output data and/or forward/redirect. If your ActionBeans depend on being able to include - * other resources before continuing, sorry - you're on your own!

    + * redirects. When an ActionBean (or other object) invokes the servlet APIs for any of these + * actions it is recorded so that it can be reported and verified later. In the majority of cases it + * should be sufficient to test ActionBeans in isolation and verify that they produced the expected + * output data and/or forward/redirect. If your ActionBeans depend on being able to include other + * resources before continuing, sorry - you're on your own! * - *

    An example usage of this class might look like:

    + *

    An example usage of this class might look like: * *

      * MockServletContext context = ...;
    - * MockRoundtrip trip = new MockRoundtrip(context, CalculatorActionBean.class);
    + * MockRound-trip trip = new MockRound-trip(context, CalculatorActionBean.class);
      * trip.setParameter("numberOne", "2");
      * trip.setParameter("numberTwo", "2");
      * trip.execute();
    @@ -63,324 +62,312 @@
      * @since Stripes 1.1.1
      */
     public class MockRoundtrip {
    -    /** Default value for the source page that generated this round trip request. */
    -    public static final String DEFAULT_SOURCE_PAGE = "_default_source_page_";
    -
    -    private MockHttpServletRequest request;
    -    private MockHttpServletResponse response;
    -    private MockServletContext context;
    -
    -    /**
    -     * Preferred constructor that will manufacture a request. Uses the ServletContext to ensure
    -     * that the request's context path matches. Pulls the UrlBinding of the ActionBean and uses
    -     * that as the requst URL. Constructs a new session for the request.
    -     *
    -     * @param context the MockServletContext that will receive this request
    -     * @param beanType a Class object representing the ActionBean that should receive the request
    -     */
    -    public MockRoundtrip(MockServletContext context, Class beanType) {
    -        this(context, beanType, new MockHttpSession(context) );
    -    }
    -
    -    /**
    -     * Preferred constructor that will manufacture a request. Uses the ServletContext to ensure
    -     * that the request's context path matches. Pulls the UrlBinding of the ActionBean and uses
    -     * that as the requst URL. Constructs a new session for the request.
    -     *
    -     * @param context the MockServletContext that will receive this request
    -     * @param beanType a Class object representing the ActionBean that should receive the request
    -     */
    -    public MockRoundtrip(MockServletContext context,
    -                         Class beanType,
    -                         MockHttpSession session) {
    -        this(context, getUrlBindingStub(beanType, context), session);
    -    }
    -
    -    /**
    -     * Constructor that will create a requeset suitable for the provided servlet context and
    -     * URL. Note that in general the contructors that take an ActionBean Class object are preferred
    -     * over those that take a URL.  Constructs a new session for the request.
    -     *
    -     * @param context the MockServletContext that will receive this request
    -     * @param actionBeanUrl the url binding of the action bean
    -     */
    -    public MockRoundtrip(MockServletContext context, String actionBeanUrl) {
    -        this(context, actionBeanUrl, new MockHttpSession(context));
    -    }
    -
    -    /**
    -     * Constructor that will create a requeset suitable for the provided servlet context and
    -     * URL. Note that in general the contructors that take an ActionBean Class object are preferred
    -     * over those that take a URL.  The request will use the provided session instead of creating
    -     * a new one.
    -     *
    -     * @param context the MockServletContext that will receive this request
    -     * @param actionBeanUrl the url binding of the action bean
    -     * @param session an instance of MockHttpSession to use for the request
    -     */
    -    public MockRoundtrip(MockServletContext context, String actionBeanUrl, MockHttpSession session) {
    -        // Look for a query string and parse out the parameters if one is present
    -        String path = actionBeanUrl;
    -        SortedMap> parameters = null;
    -        int qmark = actionBeanUrl.indexOf("?");
    -        if (qmark > 0) {
    -            path = actionBeanUrl.substring(0, qmark);
    -            if (qmark < actionBeanUrl.length()) {
    -                String query = actionBeanUrl.substring(qmark + 1);
    -                if (query != null && query.length() > 0) {
    -                    parameters = new TreeMap>();
    -                    for (String kv : query.split("&")) {
    -                        String[] parts = kv.split("=");
    -                        String key, value;
    -                        if (parts.length == 1) {
    -                            key = parts[0];
    -                            value = null;
    -                        }
    -                        else if (parts.length == 2) {
    -                            key = parts[0];
    -                            value = parts[1];
    -                        }
    -                        else {
    -                            key = value = null;
    -                        }
    -
    -                        if (key != null) {
    -                            List values = parameters.get(key);
    -                            if (values == null)
    -                                values = new ArrayList();
    -                            values.add(value);
    -                            parameters.put(key, values);
    -                        }
    -                    }
    -                }
    -            }
    -        }
    -
    -        this.context = context;
    -        this.request = new MockHttpServletRequest("/" + context.getServletContextName(), path);
    -        this.request.setSession(session);
    -        this.response = new MockHttpServletResponse();
    -        setSourcePage(DEFAULT_SOURCE_PAGE);
    -
    -        // Add any parameters that were embedded in the given URL
    -        if (parameters != null) {
    -            for (Map.Entry> entry : parameters.entrySet()) {
    -                for (String value : entry.getValue()) {
    -                    addParameter(entry.getKey(), value);
    -                }
    -            }
    +  /** Default value for the source page that generated this round trip request. */
    +  public static final String DEFAULT_SOURCE_PAGE = "_default_source_page_";
    +
    +  private MockHttpServletRequest request;
    +  private MockHttpServletResponse response;
    +  private MockServletContext context;
    +
    +  /**
    +   * Preferred constructor that will manufacture a request. Uses the ServletContext to ensure that
    +   * the request's context path matches. Pulls the UrlBinding of the ActionBean and uses that as the
    +   * request URL. Constructs a new session for the request.
    +   *
    +   * @param context the MockServletContext that will receive this request
    +   * @param beanType a Class object representing the ActionBean that should receive the request
    +   */
    +  public MockRoundtrip(MockServletContext context, Class beanType) {
    +    this(context, beanType, new MockHttpSession(context));
    +  }
    +
    +  /**
    +   * Preferred constructor that will manufacture a request. Uses the ServletContext to ensure that
    +   * the request's context path matches. Pulls the UrlBinding of the ActionBean and uses that as the
    +   * request URL. Constructs a new session for the request.
    +   *
    +   * @param context the MockServletContext that will receive this request
    +   * @param beanType a Class object representing the ActionBean that should receive the request
    +   */
    +  public MockRoundtrip(
    +      MockServletContext context, Class beanType, MockHttpSession session) {
    +    this(context, getUrlBindingStub(beanType, context), session);
    +  }
    +
    +  /**
    +   * Constructor that will create a request suitable for the provided servlet context and URL. Note
    +   * that in general the constructors that take an ActionBean Class object are preferred over those
    +   * that take a URL. Constructs a new session for the request.
    +   *
    +   * @param context the MockServletContext that will receive this request
    +   * @param actionBeanUrl the url binding of the action bean
    +   */
    +  public MockRoundtrip(MockServletContext context, String actionBeanUrl) {
    +    this(context, actionBeanUrl, new MockHttpSession(context));
    +  }
    +
    +  /**
    +   * Constructor that will create a request suitable for the provided servlet context and URL. Note
    +   * that in general the constructors that take an ActionBean Class object are preferred over those
    +   * that take a URL. The request will use the provided session instead of creating a new one.
    +   *
    +   * @param context the MockServletContext that will receive this request
    +   * @param actionBeanUrl the url binding of the action bean
    +   * @param session an instance of MockHttpSession to use for the request
    +   */
    +  public MockRoundtrip(MockServletContext context, String actionBeanUrl, MockHttpSession session) {
    +    // Look for a query string and parse out the parameters if one is present
    +    String path = actionBeanUrl;
    +    SortedMap> parameters = null;
    +    int qmark = actionBeanUrl.indexOf("?");
    +    if (qmark > 0) {
    +      path = actionBeanUrl.substring(0, qmark);
    +      String query = actionBeanUrl.substring(qmark + 1);
    +      if (!query.isEmpty()) {
    +        parameters = new TreeMap<>();
    +        for (String kv : query.split("&")) {
    +          String[] parts = kv.split("=");
    +          String key, value;
    +          if (parts.length == 1) {
    +            key = parts[0];
    +            value = null;
    +          } else if (parts.length == 2) {
    +            key = parts[0];
    +            value = parts[1];
    +          } else {
    +            key = value = null;
    +          }
    +
    +          if (key != null) {
    +            List values = parameters.get(key);
    +            if (values == null) values = new ArrayList<>();
    +            values.add(value);
    +            parameters.put(key, values);
    +          }
             }
    +      }
         }
     
    -    /** Get the servlet request object to be used by this round trip */
    -    public MockHttpServletRequest getRequest() {
    -        return request;
    -    }
    -
    -    /** Set the servlet request object to be used by this round trip */
    -    protected void setRequest(MockHttpServletRequest request) {
    -        this.request = request;
    -    }
    -
    -    /** Get the servlet response object to be used by this round trip */
    -    public MockHttpServletResponse getResponse() {
    -        return response;
    -    }
    -
    -    /** Set the servlet response object to be used by this round trip */
    -    protected void setResponse(MockHttpServletResponse response) {
    -        this.response = response;
    -    }
    -
    -    /** Get the ActionBean context to be used by this round trip */
    -    public MockServletContext getContext() {
    -        return context;
    -    }
    -
    -    /** Set the ActionBean context to be used by this round trip */
    -    protected void setContext(MockServletContext context) {
    -        this.context = context;
    -    }
    -
    -    /**
    -     * Sets the named request parameter to the value or values provided. Any existing values are
    -     * wiped out and replaced with the value(s) provided.
    -     */
    -    public void setParameter(String name, String... value) {
    -        this.request.getParameterMap().put(name, value);
    -    }
    -
    -    /**
    -     * Adds the value provided to the set of values for the named request parameter. If one or
    -     * more values already exist they will be retained, and the new value will be appended to the
    -     * set of values.
    -     */
    -    public void addParameter(String name, String... value) {
    -        if (this.request.getParameterValues(name) == null) {
    -            setParameter(name, value);
    -        }
    -        else {
    -            String[] oldValues = this.request.getParameterMap().get(name);
    -            String[] combined = new String[oldValues.length + value.length];
    -            System.arraycopy(oldValues, 0, combined, 0, oldValues.length);
    -            System.arraycopy(value, 0, combined, oldValues.length, value.length);
    -            setParameter(name, combined);
    +    this.context = context;
    +    this.request = new MockHttpServletRequest("/" + context.getServletContextName(), path);
    +    this.request.setSession(session);
    +    this.response = new MockHttpServletResponse();
    +    setSourcePage(DEFAULT_SOURCE_PAGE);
    +
    +    // Add any parameters that were embedded in the given URL
    +    if (parameters != null) {
    +      for (Map.Entry> entry : parameters.entrySet()) {
    +        for (String value : entry.getValue()) {
    +          addParameter(entry.getKey(), value);
             }
    +      }
         }
    -
    -    /**
    -     * All requests to Stripes that can generate validation errors are required to supply a
    -     * request parameter telling Stripes where the request came from. If you do not supply a
    -     * value for this parameter then the value of MockRoundTrip.DEFAULT_SOURCE_PAGE will be used.
    -     */
    -    public void setSourcePage(String url) {
    -        if (url != null) {
    -            url = CryptoUtil.encrypt(url);
    -        }
    -        setParameter(StripesConstants.URL_KEY_SOURCE_PAGE, url);
    +  }
    +
    +  /** Get the servlet request object to be used by this round trip */
    +  public MockHttpServletRequest getRequest() {
    +    return request;
    +  }
    +
    +  /** Set the servlet request object to be used by this round trip */
    +  protected void setRequest(MockHttpServletRequest request) {
    +    this.request = request;
    +  }
    +
    +  /** Get the servlet response object to be used by this round trip */
    +  public MockHttpServletResponse getResponse() {
    +    return response;
    +  }
    +
    +  /** Set the servlet response object to be used by this round trip */
    +  protected void setResponse(MockHttpServletResponse response) {
    +    this.response = response;
    +  }
    +
    +  /** Get the ActionBean context to be used by this round trip */
    +  public MockServletContext getContext() {
    +    return context;
    +  }
    +
    +  /** Set the ActionBean context to be used by this round trip */
    +  protected void setContext(MockServletContext context) {
    +    this.context = context;
    +  }
    +
    +  /**
    +   * Sets the named request parameter to the value or values provided. Any existing values are wiped
    +   * out and replaced with the value(s) provided.
    +   */
    +  public void setParameter(String name, String... value) {
    +    this.request.getParameterMap().put(name, value);
    +  }
    +
    +  /**
    +   * Adds the value provided to the set of values for the named request parameter. If one or more
    +   * values already exist they will be retained, and the new value will be appended to the set of
    +   * values.
    +   */
    +  public void addParameter(String name, String... value) {
    +    if (this.request.getParameterValues(name) == null) {
    +      setParameter(name, value);
    +    } else {
    +      String[] oldValues = this.request.getParameterMap().get(name);
    +      String[] combined = new String[oldValues.length + value.length];
    +      System.arraycopy(oldValues, 0, combined, 0, oldValues.length);
    +      System.arraycopy(value, 0, combined, oldValues.length, value.length);
    +      setParameter(name, combined);
         }
    -
    -    /**
    -     * Executes the request in the servlet context that was provided in the constructor. If the
    -     * request throws an Exception then that will be thrown from this method. Otherwise, once the
    -     * execution has completed you can use the other methods on this class to examine the outcome.
    -     */
    -    public void execute() throws Exception {
    -        this.context.acceptRequest(this.request, this.response);
    -    }
    -
    -    /**
    -     * Executes the request in the servlet context that was provided in the constructor. Sets up
    -     * the request so that it mimics the submission of a specific event, named by the 'event'
    -     * parameter to this method. If the request throws an Exception then that will be thrown from
    -     * this method. Otherwise, once the execution has completed you can use the other methods on
    -     * this class to examine the outcome.
    -     */
    -    public void execute(String event) throws Exception {
    -        setParameter(event, "");
    -        execute();
    -    }
    -
    -    /**
    -     * Gets the instance of the ActionBean type provided that was instantiated by Stripes to
    -     * handle the request. If a bean of this type was not instantiated, this method will
    -     * return null.
    -     *
    -     * @param type the Class object representing the ActionBean type expected
    -     * @return the instance of the ActionBean that was created by Stripes
    -     */
    -    @SuppressWarnings("unchecked")
    -	public  A getActionBean(Class type) {
    -        A bean = (A) this.request.getAttribute(getUrlBinding(type, this.context));
    -        if (bean == null) {
    -            bean = (A) this.request.getSession().getAttribute(getUrlBinding(type, this.context));
    -        }
    -        return bean;
    -    }
    -
    -    /**
    -     * Gets the (potentially empty) set of Validation Errors that were produced by the request.
    -     */
    -    public ValidationErrors getValidationErrors() {
    -        ActionBean bean = (ActionBean) this.request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN);
    -        return bean.getContext().getValidationErrors();
    -    }
    -
    -    /**
    -     * Gets, as bytes, any data that was written to the output stream associated with the
    -     * request. Note that since the Mock system does not write standard HTTP response information
    -     * (headers etc.) to the output stream, this will be exactly what was written by the
    -     * ActionBean.
    -     */
    -    public byte[] getOutputBytes() {
    -        return this.response.getOutputBytes();
    -    }
    -
    -    /**
    -     * Gets, as a String, any data that was written to the output stream associated with the
    -     * request. Note that since the Mock system does not write standard HTTP response information
    -     * (headers etc.) to the output stream, this will be exactly what was written by the
    -     * ActionBean.
    -     */
    -    public String getOutputString() {
    -        return this.response.getOutputString();
    -    }
    -
    -    /**
    -     * Gets the URL to which Stripes was directed after invoking the ActionBean. Assumes that
    -     * the request was either forwarded or redirected exactly once. If the request was forwarded
    -     * then the forwarded URL will be returned verbatim.  If the response was redirected and the
    -     * redirect URL was within the same web application, then the URL returned will exclude the
    -     * context path.  I.e. the URL returned will be the same regardless of whether the page was
    -     * forwarded to or redirected to.
    -     */
    -    public String getDestination() {
    -        String forward = this.request.getForwardUrl();
    -        String redirect = this.response.getRedirectUrl();
    -
    -        if (forward != null) {
    -            return forward;
    -        }
    -        else if (redirect != null) {
    -            String contextPath = this.request.getContextPath();
    -            if (contextPath.length() > 1 && redirect.startsWith(contextPath + '/'))
    -                redirect = redirect.substring(contextPath.length());
    -        }
    -
    -        return redirect;
    +  }
    +
    +  /**
    +   * All requests to Stripes that can generate validation errors are required to supply a request
    +   * parameter telling Stripes where the request came from. If you do not supply a value for this
    +   * parameter then the value of MockRoundTrip.DEFAULT_SOURCE_PAGE will be used.
    +   */
    +  public void setSourcePage(String url) {
    +    if (url != null) {
    +      url = CryptoUtil.encrypt(url);
         }
    -
    -    /** If the request resulted in a forward, returns the URL that was forwarded to. */
    -    public String getForwardUrl() {
    -        return this.request.getForwardUrl();
    +    setParameter(StripesConstants.URL_KEY_SOURCE_PAGE, url);
    +  }
    +
    +  /**
    +   * Executes the request in the servlet context that was provided in the constructor. If the
    +   * request throws an Exception then that will be thrown from this method. Otherwise, once the
    +   * execution has completed you can use the other methods on this class to examine the outcome.
    +   */
    +  public void execute() throws Exception {
    +    this.context.acceptRequest(this.request, this.response);
    +  }
    +
    +  /**
    +   * Executes the request in the servlet context that was provided in the constructor. Sets up the
    +   * request so that it mimics the submission of a specific event, named by the 'event' parameter to
    +   * this method. If the request throws an Exception then that will be thrown from this method.
    +   * Otherwise, once the execution has completed you can use the other methods on this class to
    +   * examine the outcome.
    +   */
    +  public void execute(String event) throws Exception {
    +    setParameter(event, "");
    +    execute();
    +  }
    +
    +  /**
    +   * Gets the instance of the ActionBean type provided that was instantiated by Stripes to handle
    +   * the request. If a bean of this type was not instantiated, this method will return null.
    +   *
    +   * @param type the Class object representing the ActionBean type expected
    +   * @return the instance of the ActionBean that was created by Stripes
    +   */
    +  @SuppressWarnings("unchecked")
    +  public  A getActionBean(Class type) {
    +    A bean = (A) this.request.getAttribute(getUrlBinding(type, this.context));
    +    if (bean == null) {
    +      bean = (A) this.request.getSession().getAttribute(getUrlBinding(type, this.context));
         }
    -
    -    /**
    -     * If the request resulted in a redirect, returns the URL that was redirected to. Unlike
    -     * getDestination(), the URL in this case will be the exact URL that would have been sent to
    -     * the browser (i.e. including the servlet context).
    -     */
    -    public String getRedirectUrl() {
    -        return this.response.getRedirectUrl();
    +    return bean;
    +  }
    +
    +  /** Gets the (potentially empty) set of Validation Errors that were produced by the request. */
    +  public ValidationErrors getValidationErrors() {
    +    ActionBean bean = (ActionBean) this.request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN);
    +    return bean.getContext().getValidationErrors();
    +  }
    +
    +  /**
    +   * Gets, as bytes, any data that was written to the output stream associated with the request.
    +   * Note that since the Mock system does not write standard HTTP response information (headers
    +   * etc.) to the output stream, this will be exactly what was written by the ActionBean.
    +   */
    +  public byte[] getOutputBytes() {
    +    return this.response.getOutputBytes();
    +  }
    +
    +  /**
    +   * Gets, as a String, any data that was written to the output stream associated with the request.
    +   * Note that since the Mock system does not write standard HTTP response information (headers
    +   * etc.) to the output stream, this will be exactly what was written by the ActionBean.
    +   */
    +  public String getOutputString() {
    +    return this.response.getOutputString();
    +  }
    +
    +  /**
    +   * Gets the URL to which Stripes was directed after invoking the ActionBean. Assumes that the
    +   * request was either forwarded or redirected exactly once. If the request was forwarded then the
    +   * forwarded URL will be returned verbatim. If the response was redirected and the redirect URL
    +   * was within the same web application, then the URL returned will exclude the context path. I.e.
    +   * the URL returned will be the same regardless of whether the page was forwarded to or redirected
    +   * to.
    +   */
    +  public String getDestination() {
    +    String forward = this.request.getForwardUrl();
    +    String redirect = this.response.getRedirectUrl();
    +
    +    if (forward != null) {
    +      return forward;
    +    } else if (redirect != null) {
    +      String contextPath = this.request.getContextPath();
    +      if (contextPath.length() > 1 && redirect.startsWith(contextPath + '/'))
    +        redirect = redirect.substring(contextPath.length());
         }
     
    -    /** Find and return the {@link AnnotatedClassActionResolver} for the given context. */
    -    private static AnnotatedClassActionResolver getActionResolver(MockServletContext context) {
    -        for (Filter filter : context.getFilters()) {
    -            if (filter instanceof StripesFilter) {
    -                ActionResolver resolver = ((StripesFilter) filter).getInstanceConfiguration()
    -                        .getActionResolver();
    -                if (resolver instanceof AnnotatedClassActionResolver) {
    -                    return (AnnotatedClassActionResolver) resolver;
    -                }
    -            }
    -        }
    -
    -        return null;
    -    }
    -
    -    /** Find and return the {@link UrlBindingFactory} for the given context. */
    -    private static UrlBindingFactory getUrlBindingFactory(MockServletContext context) {
    -        ActionResolver resolver = getActionResolver(context);
    +    return redirect;
    +  }
    +
    +  /** If the request resulted in a forward, returns the URL that was forwarded to. */
    +  public String getForwardUrl() {
    +    return this.request.getForwardUrl();
    +  }
    +
    +  /**
    +   * If the request resulted in a redirect, returns the URL that was redirected to. Unlike
    +   * getDestination(), the URL in this case will be the exact URL that would have been sent to the
    +   * browser (i.e. including the servlet context).
    +   */
    +  public String getRedirectUrl() {
    +    return this.response.getRedirectUrl();
    +  }
    +
    +  /** Find and return the {@link AnnotatedClassActionResolver} for the given context. */
    +  private static AnnotatedClassActionResolver getActionResolver(MockServletContext context) {
    +    for (Filter filter : context.getFilters()) {
    +      if (filter instanceof StripesFilter) {
    +        ActionResolver resolver =
    +            ((StripesFilter) filter).getInstanceConfiguration().getActionResolver();
             if (resolver instanceof AnnotatedClassActionResolver) {
    -            return ((AnnotatedClassActionResolver) resolver).getUrlBindingFactory();
    +          return (AnnotatedClassActionResolver) resolver;
             }
    -
    -        return null;
    +      }
         }
     
    -    /**
    -     * A helper method that fetches the UrlBinding of a class in the manner it would be interpreted
    -     * by the current context configuration.
    -     */
    -    private static String getUrlBinding(Class clazz,
    -            MockServletContext context) {
    -        return getActionResolver(context).getUrlBinding(clazz);
    -    }
    +    return null;
    +  }
     
    -    /** Get the URL binding for an {@link ActionBean} class up to the first parameter. */
    -    private static String getUrlBindingStub(Class clazz,
    -            MockServletContext context) {
    -        return getUrlBindingFactory(context).getBindingPrototype(clazz).getPath();
    +  /** Find and return the {@link UrlBindingFactory} for the given context. */
    +  private static UrlBindingFactory getUrlBindingFactory(MockServletContext context) {
    +    AnnotatedClassActionResolver resolver = getActionResolver(context);
    +    if (resolver != null) {
    +      return resolver.getUrlBindingFactory();
         }
    +
    +    return null;
    +  }
    +
    +  /**
    +   * A helper method that fetches the UrlBinding of a class in the manner it would be interpreted by
    +   * the current context configuration.
    +   */
    +  private static String getUrlBinding(
    +      Class clazz, MockServletContext context) {
    +    return Objects.requireNonNull(getActionResolver(context)).getUrlBinding(clazz);
    +  }
    +
    +  /** Get the URL binding for an {@link ActionBean} class up to the first parameter. */
    +  private static String getUrlBindingStub(
    +      Class clazz, MockServletContext context) {
    +    return Objects.requireNonNull(getUrlBindingFactory(context))
    +        .getBindingPrototype(clazz)
    +        .getPath();
    +  }
     }
    diff --git a/stripes/src/main/java/net/sourceforge/stripes/mock/MockServletConfig.java b/stripes/src/main/java/net/sourceforge/stripes/mock/MockServletConfig.java
    index e55d41243..99c226938 100644
    --- a/stripes/src/main/java/net/sourceforge/stripes/mock/MockServletConfig.java
    +++ b/stripes/src/main/java/net/sourceforge/stripes/mock/MockServletConfig.java
    @@ -14,7 +14,7 @@
      */
     package net.sourceforge.stripes.mock;
     
    -import javax.servlet.ServletConfig;
    +import jakarta.servlet.ServletConfig;
     
     /**
      * Mock implementation of a Servlet Config.
    @@ -23,15 +23,15 @@
      * @since Stripes 1.1.1
      */
     public class MockServletConfig extends MockBaseConfig implements ServletConfig {
    -    private String servletName;
    +  private String servletName;
     
    -    /** Returns the name of the servlet for which this is the config. */
    -    public String getServletName() {
    -        return this.servletName;
    -    }
    +  /** Returns the name of the servlet for which this is the config. */
    +  public String getServletName() {
    +    return this.servletName;
    +  }
     
    -    /** Sets the name of the servlet for which this is the config. */
    -    public void setServletName(String name) {
    -        this.servletName = name;
    -    }
    +  /** Sets the name of the servlet for which this is the config. */
    +  public void setServletName(String name) {
    +    this.servletName = name;
    +  }
     }
    diff --git a/stripes/src/main/java/net/sourceforge/stripes/mock/MockServletContext.java b/stripes/src/main/java/net/sourceforge/stripes/mock/MockServletContext.java
    index fd5960c0e..8e407ea60 100644
    --- a/stripes/src/main/java/net/sourceforge/stripes/mock/MockServletContext.java
    +++ b/stripes/src/main/java/net/sourceforge/stripes/mock/MockServletContext.java
    @@ -14,315 +14,478 @@
      */
     package net.sourceforge.stripes.mock;
     
    -import javax.servlet.Filter;
    -import javax.servlet.RequestDispatcher;
    -import javax.servlet.Servlet;
    -import javax.servlet.ServletContext;
    -import javax.servlet.ServletContextEvent;
    -import javax.servlet.ServletContextListener;
    -import javax.servlet.ServletException;
    -import javax.servlet.http.Cookie;
    -import javax.servlet.http.HttpServlet;
    +import jakarta.servlet.Filter;
    +import jakarta.servlet.FilterRegistration;
    +import jakarta.servlet.RequestDispatcher;
    +import jakarta.servlet.Servlet;
    +import jakarta.servlet.ServletContext;
    +import jakarta.servlet.ServletContextEvent;
    +import jakarta.servlet.ServletContextListener;
    +import jakarta.servlet.ServletRegistration;
    +import jakarta.servlet.SessionCookieConfig;
    +import jakarta.servlet.SessionTrackingMode;
    +import jakarta.servlet.descriptor.JspConfigDescriptor;
    +import jakarta.servlet.http.Cookie;
    +import jakarta.servlet.http.HttpServlet;
     import java.io.InputStream;
    -import java.net.MalformedURLException;
     import java.net.URL;
     import java.util.ArrayList;
     import java.util.Collections;
     import java.util.Enumeration;
    +import java.util.EventListener;
     import java.util.HashMap;
     import java.util.List;
     import java.util.Map;
     import java.util.Set;
     
     /**
    - * 

    Mock implementation of a ServletContext. Provides implementation the most commonly used - * methods, namely those to manipulate init parameters and attributes. Additional methods are - * provided to allow the setting of initialization parameters etc.

    + * Mock implementation of a ServletContext. Provides implementation the most commonly used methods, + * namely those to manipulate init parameters and attributes. Additional methods are provided to + * allow the setting of initialization parameters etc. * *

    This mock implementation is meant only for testing purposes. As such there are certain - * limitations:

    + * limitations: * *
      - *
    • All configured Filters are applied to every request
    • - *
    • Only a single servlet is supported, and all requests are routed to it.
    • - *
    • Forwards, includes and redirects are recorded for posterity, but not processed.
    • - *
    • It may or may not be thread safe (not a priority since it is mainly for unit testing).
    • - *
    • You do your own session management (attach one to a request before executing).
    • + *
    • All configured Filters are applied to every request + *
    • Only a single servlet is supported, and all requests are routed to it. + *
    • Forwards, includes and redirects are recorded for posterity, but not processed. + *
    • It may or may not be thread safe (not a priority since it is mainly for unit testing). + *
    • You do your own session management (attach one to a request before executing). *
    * * @author Tim Fennell * @since Stripes 1.1.1 */ public class MockServletContext implements ServletContext { - private String contextName; - private Map initParameters = new HashMap(); - private Map attributes = new HashMap(); - private List filters = new ArrayList(); - private List listeners = new ArrayList(); - private HttpServlet servlet; - - /** Simple constructor that creates a new mock ServletContext with the supplied context name. */ - public MockServletContext(String contextName) { - this.contextName = contextName; - } - - /** If the url is within this servlet context, returns this. Otherwise returns null. */ - public ServletContext getContext(String url) { - if (url.startsWith("/" + this.contextName)) { - return this; - } - else { - return null; - } - } - - /** Servlet 2.3 method. Returns the context name with a leading slash. */ - public String getContextPath() { - return "/" + this.contextName; - } - - /** Always returns 2. */ - public int getMajorVersion() { return 2; } - - /** Always returns 4. */ - public int getMinorVersion() { return 4; } - - /** Always returns null (i.e. don't know). */ - public String getMimeType(String file) { return null; } - - /** Always returns null (i.e. there are no resources under this path). */ - public Set getResourcePaths(String path) { - return null; - } - - /** Uses the current classloader to fetch the resource if it can. */ - public URL getResource(String name) throws MalformedURLException { - while (name.startsWith("/")) - name = name.substring(1); - return Thread.currentThread().getContextClassLoader().getResource(name); - } - - /** Uses the current classloader to fetch the resource if it can. */ - public InputStream getResourceAsStream(String name) { - while (name.startsWith("/")) - name = name.substring(1); - return Thread.currentThread().getContextClassLoader().getResourceAsStream(name); - } - - /** Returns a MockRequestDispatcher for the url provided. */ - public RequestDispatcher getRequestDispatcher(String url) { - return new MockRequestDispatcher(url); - } - - /** Returns a MockRequestDispatcher for the named servlet provided. */ - public RequestDispatcher getNamedDispatcher(String name) { - return new MockRequestDispatcher(name); - } - - /** Deprecated method always returns null. */ - public Servlet getServlet(String string) throws ServletException { return null; } - - /** Deprecated method always returns an empty enumeration. */ - public Enumeration getServlets() { - return Collections.enumeration( Collections.emptySet() ); - } - - /** Deprecated method always returns an empty enumeration. */ - public Enumeration getServletNames() { - return Collections.enumeration( Collections.emptySet() ); - } - - /** Logs the message to System.out. */ - public void log(String message) { - System.out.println("MockServletContext: " + message); - } - - /** Logs the message and exception to System.out. */ - public void log(Exception exception, String message) { - log(message, exception); - } - - /** Logs the message and exception to System.out. */ - public void log(String message, Throwable throwable) { - log(message); - throwable.printStackTrace(System.out); - } - - /** Always returns null as this is standard behaviour for WAR resources. */ - public String getRealPath(String string) { return null; } - - /** Returns a version string identifying the Mock implementation. */ - public String getServerInfo() { - return "Stripes Mock Servlet Environment, version 1.0."; - } - - /** Adds an init parameter to the mock servlet context. */ - public void addInitParameter(String name, String value) { - this.initParameters.put(name, value); - } - - /** Adds all the values in the supplied Map to the set of init parameters. */ - public void addAllInitParameters(Map parameters) { - this.initParameters.putAll(parameters); - } - - /** Gets the value of an init parameter with the specified name, if one exists. */ - public String getInitParameter(String name) { - return this.initParameters.get(name); - } - - /** Returns an enumeration of all the initialization parameters in the context. */ - public Enumeration getInitParameterNames() { - return Collections.enumeration( this.initParameters.keySet() ); - } - - /** Gets an attribute that has been set on the context (i.e. application) scope. */ - public Object getAttribute(String name) { - return this.attributes.get(name); - } - - /** Returns an enumeration of all the names of attributes in the context. */ - public Enumeration getAttributeNames() { - return Collections.enumeration( this.attributes.keySet() ); - } - - /** Sets the supplied value for the attribute on the context. */ - public void setAttribute(String name, Object value) { - this.attributes.put(name, value); - } - - /** Removes the named attribute from the context. */ - public void removeAttribute(String name) { - this.attributes.remove(name); - } - - /** Returns the name of the mock context. */ - public String getServletContextName() { - return this.contextName; - } - - /** Adds a filter to the end of filter chain that will be used to filter requests.*/ - public MockServletContext addFilter(Class filterClass, - String filterName, - Map initParams) { - try { - MockFilterConfig config = new MockFilterConfig(); - config.setFilterName(filterName); - config.setServletContext(this); - if (initParams != null) config.addAllInitParameters(initParams); - - Filter filter = filterClass.newInstance(); - filter.init(config); - this.filters.add(filter); - return this; - } - catch (Exception e) { - throw new RuntimeException("Exception registering new filter with name " + filterName, e); - } - } - - /** Removes and destroys all registered filters. */ - public MockServletContext removeFilters() { - for (Filter each : filters) { - try { - each.destroy(); - } catch (Exception e) { - log("Error while destroying filter " + each, e); - } - } - filters.clear(); - return this; - } - - /** Provides access to the set of filters configured for this context. */ - public List getFilters() { - return this.filters; - } - - /** Adds a {@link ServletContextListener} to this context and initializes it. */ - public MockServletContext addListener(ServletContextListener listener) { - ServletContextEvent event = new ServletContextEvent(this); - listener.contextInitialized(event); - listeners.add(listener); - return this; - } - - /**Removes and destroys all registered {@link ServletContextListener}. */ - public MockServletContext removeListeners() { - ServletContextEvent e = new ServletContextEvent(this); - for (ServletContextListener l : listeners) { - l.contextDestroyed(e); - } - listeners.clear(); - return this; - } - - /** Sets the servlet that will receive all requests in this servlet context. */ - public MockServletContext setServlet(Class servletClass, - String servletName, - Map initParams) { + private final String contextName; + private final Map initParameters = new HashMap<>(); + private final Map attributes = new HashMap<>(); + private final List filters = new ArrayList<>(); + private final List listeners = new ArrayList<>(); + private HttpServlet servlet; + + /** Simple constructor that creates a new mock ServletContext with the supplied context name. */ + public MockServletContext(String contextName) { + this.contextName = contextName; + } + + /** If the url is within this servlet context, returns this. Otherwise, returns null. */ + public ServletContext getContext(String url) { + if (url.startsWith("/" + this.contextName)) { + return this; + } else { + return null; + } + } + + /** Servlet 2.3 method. Returns the context name with a leading slash. */ + public String getContextPath() { + return "/" + this.contextName; + } + + /** Always returns 2. */ + public int getMajorVersion() { + return 4; + } + + /** Always returns 4. */ + public int getMinorVersion() { + return 0; + } + + @Override + public int getEffectiveMajorVersion() { + return 0; + } + + @Override + public int getEffectiveMinorVersion() { + return 0; + } + + /** Always returns null (i.e. don't know). */ + public String getMimeType(String file) { + return null; + } + + /** Always returns null (i.e. there are no resources under this path). */ + public Set getResourcePaths(String path) { + return null; + } + + /** Uses the current classloader to fetch the resource if it can. */ + public URL getResource(String name) { + while (name.startsWith("/")) name = name.substring(1); + return Thread.currentThread().getContextClassLoader().getResource(name); + } + + /** Uses the current classloader to fetch the resource if it can. */ + public InputStream getResourceAsStream(String name) { + while (name.startsWith("/")) name = name.substring(1); + return Thread.currentThread().getContextClassLoader().getResourceAsStream(name); + } + + /** Returns a MockRequestDispatcher for the url provided. */ + public RequestDispatcher getRequestDispatcher(String url) { + return new MockRequestDispatcher(url); + } + + /** Returns a MockRequestDispatcher for the named servlet provided. */ + public RequestDispatcher getNamedDispatcher(String name) { + return new MockRequestDispatcher(name); + } + + /** Deprecated method always returns null. */ + public Servlet getServlet(String string) { + return null; + } + + /** Deprecated method always returns an empty enumeration. */ + public Enumeration getServlets() { + return Collections.enumeration(Collections.emptySet()); + } + + /** Deprecated method always returns an empty enumeration. */ + public Enumeration getServletNames() { + return Collections.enumeration(Collections.emptySet()); + } + + /** Logs the message to System.out. */ + public void log(String message) { + System.out.println("MockServletContext: " + message); + } + + /** Logs the message and exception to System.out. */ + public void log(Exception exception, String message) { + log(message, exception); + } + + /** Logs the message and exception to System.out. */ + public void log(String message, Throwable throwable) { + log(message); + throwable.printStackTrace(System.out); + } + + /** Always returns null as this is standard behaviour for WAR resources. */ + public String getRealPath(String string) { + return null; + } + + /** Returns a version string identifying the Mock implementation. */ + public String getServerInfo() { + return "Stripes Mock Servlet Environment, version 1.0."; + } + + /** Adds an init parameter to the mock servlet context. */ + public void addInitParameter(String name, String value) { + this.initParameters.put(name, value); + } + + /** Adds all the values in the supplied Map to the set of init parameters. */ + public void addAllInitParameters(Map parameters) { + this.initParameters.putAll(parameters); + } + + /** Gets the value of an init parameter with the specified name, if one exists. */ + public String getInitParameter(String name) { + return this.initParameters.get(name); + } + + /** Returns an enumeration of all the initialization parameters in the context. */ + public Enumeration getInitParameterNames() { + return Collections.enumeration(this.initParameters.keySet()); + } + + @Override + public boolean setInitParameter(String name, String value) { + return false; + } + + /** Gets an attribute that has been set on the context (i.e. application) scope. */ + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + /** Returns an enumeration of all the names of attributes in the context. */ + public Enumeration getAttributeNames() { + return Collections.enumeration(this.attributes.keySet()); + } + + /** Sets the supplied value for the attribute on the context. */ + public void setAttribute(String name, Object value) { + this.attributes.put(name, value); + } + + /** Removes the named attribute from the context. */ + public void removeAttribute(String name) { + this.attributes.remove(name); + } + + /** Returns the name of the mock context. */ + public String getServletContextName() { + return this.contextName; + } + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, String className) { + return null; + } + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) { + return null; + } + + @Override + public ServletRegistration.Dynamic addServlet( + String servletName, Class servletClass) { + return null; + } + + @Override + public ServletRegistration.Dynamic addJspFile(String servletName, String jspFile) { + return null; + } + + @Override + public T createServlet(Class clazz) { + return null; + } + + @Override + public ServletRegistration getServletRegistration(String servletName) { + return null; + } + + @Override + public Map getServletRegistrations() { + return null; + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, String className) { + return null; + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) { + return null; + } + + @Override + public FilterRegistration.Dynamic addFilter( + String filterName, Class filterClass) { + return null; + } + + @Override + public T createFilter(Class clazz) { + return null; + } + + @Override + public FilterRegistration getFilterRegistration(String filterName) { + return null; + } + + @Override + public Map getFilterRegistrations() { + return null; + } + + @Override + public SessionCookieConfig getSessionCookieConfig() { + return null; + } + + @Override + public void setSessionTrackingModes(Set sessionTrackingModes) {} + + @Override + public Set getDefaultSessionTrackingModes() { + return null; + } + + @Override + public Set getEffectiveSessionTrackingModes() { + return null; + } + + @Override + public void addListener(String className) {} + + @Override + public void addListener(T t) {} + + @Override + public void addListener(Class listenerClass) {} + + @Override + public T createListener(Class clazz) { + return null; + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() { + return null; + } + + @Override + public ClassLoader getClassLoader() { + return null; + } + + @Override + public void declareRoles(String... roleNames) {} + + @Override + public String getVirtualServerName() { + return null; + } + + @Override + public int getSessionTimeout() { + return 0; + } + + @Override + public void setSessionTimeout(int sessionTimeout) {} + + @Override + public String getRequestCharacterEncoding() { + return null; + } + + @Override + public void setRequestCharacterEncoding(String encoding) {} + + @Override + public String getResponseCharacterEncoding() { + return null; + } + + @Override + public void setResponseCharacterEncoding(String encoding) {} + + /** Adds a filter to the end of filter chain that will be used to filter requests. */ + public MockServletContext addFilter( + Class filterClass, String filterName, Map initParams) { + try { + MockFilterConfig config = new MockFilterConfig(); + config.setFilterName(filterName); + config.setServletContext(this); + if (initParams != null) config.addAllInitParameters(initParams); + + Filter filter = filterClass.getDeclaredConstructor().newInstance(); + filter.init(config); + this.filters.add(filter); + return this; + } catch (Exception e) { + throw new RuntimeException("Exception registering new filter with name " + filterName, e); + } + } + + /** Removes and destroys all registered filters. */ + public MockServletContext removeFilters() { + for (Filter each : filters) { + try { + each.destroy(); + } catch (Exception e) { + log("Error while destroying filter " + each, e); + } + } + filters.clear(); + return this; + } + + /** Provides access to the set of filters configured for this context. */ + public List getFilters() { + return this.filters; + } + + /** Adds a {@link ServletContextListener} to this context and initializes it. */ + public MockServletContext addListener(ServletContextListener listener) { + ServletContextEvent event = new ServletContextEvent(this); + listener.contextInitialized(event); + listeners.add(listener); + return this; + } + + /** Removes and destroys all registered {@link ServletContextListener}. */ + public MockServletContext removeListeners() { + ServletContextEvent e = new ServletContextEvent(this); + for (ServletContextListener l : listeners) { + l.contextDestroyed(e); + } + listeners.clear(); + return this; + } + + /** Sets the servlet that will receive all requests in this servlet context. */ + public MockServletContext setServlet( + Class servletClass, + String servletName, + Map initParams) { + try { + MockServletConfig config = new MockServletConfig(); + config.setServletName(servletName); + config.setServletContext(this); + if (initParams != null) config.addAllInitParameters(initParams); + + this.servlet = servletClass.getDeclaredConstructor().newInstance(); + this.servlet.init(config); + return this; + } catch (Exception e) { + throw new RuntimeException("Exception registering servlet with name " + servletName, e); + } + } + + /** + * Takes a request and response and runs them through the set of filters using a MockFilterChain, + * which, if everything goes well, will eventually execute the servlet that is registered with + * this context. + * + *

    Any exceptions that are raised during the processing of the request are simply passed + * through to the caller. I.e. they will be thrown from this method. + */ + public void acceptRequest(MockHttpServletRequest request, MockHttpServletResponse response) + throws Exception { + copyCookies(request, response); + MockFilterChain chain = new MockFilterChain(); + chain.setServlet(this.servlet); + chain.addFilters(this.filters); + chain.doFilter(request, response); + } + + /** + * Copies cookies from the request to the response. + * + * @param request The request. + * @param response The response. + */ + public void copyCookies(MockHttpServletRequest request, MockHttpServletResponse response) { + if (request.getCookies() != null) { + for (Cookie cookie : request.getCookies()) { + response.addCookie(cookie); + } + } + } + + /** Closes all filters and servlets for this context (application shutdown). */ + public void close() { + removeListeners(); + removeFilters(); + Enumeration servlets = getServlets(); + while (servlets.hasMoreElements()) { + Object servlet = servlets.nextElement(); + if (servlet instanceof Servlet) { try { - MockServletConfig config = new MockServletConfig(); - config.setServletName(servletName); - config.setServletContext(this); - if (initParams != null) config.addAllInitParameters(initParams); - - this.servlet = servletClass.newInstance(); - this.servlet.init(config); - return this; - } - catch (Exception e) { - throw new RuntimeException("Exception registering servlet with name " + servletName, e); - } - } - - /** - *

    Takes a request and response and runs them through the set of filters using a - * MockFilterChain, which if everything goes well, will eventually execute the servlet - * that is registered with this context.

    - * - *

    Any exceptions that are raised during the processing of the request are simply - * passed through to the caller. I.e. they will be thrown from this method.

    - */ - public void acceptRequest(MockHttpServletRequest request, MockHttpServletResponse response) - throws Exception { - copyCookies(request, response); - MockFilterChain chain = new MockFilterChain(); - chain.setServlet(this.servlet); - chain.addFilters(this.filters); - chain.doFilter(request, response); - } - - /** - * Copies cookies from the request to the response. - * - * @param request The request. - * @param response The response. - */ - public void copyCookies(MockHttpServletRequest request, MockHttpServletResponse response) { - if (request.getCookies() != null) { - for (Cookie cookie : request.getCookies()) { - response.addCookie(cookie); - } - } - } - - /** - * Closes all filters and servlets for this context (application shutdown). - */ - public void close() { - removeListeners(); - removeFilters(); - Enumeration servlets = getServlets(); - while (servlets.hasMoreElements()) { - Object servlet = servlets.nextElement(); - if (servlet instanceof Servlet) { - try { - ((Servlet)servlet).destroy(); - } catch (Exception e) { - log("Exception caught destroying servlet " + servlet + " contextName=" + contextName, e); - } - } + ((Servlet) servlet).destroy(); + } catch (Exception e) { + log("Exception caught destroying servlet " + servlet + " contextName=" + contextName, e); } + } } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/mock/MockServletOutputStream.java b/stripes/src/main/java/net/sourceforge/stripes/mock/MockServletOutputStream.java index 2e4f76e07..e97c87a8d 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/mock/MockServletOutputStream.java +++ b/stripes/src/main/java/net/sourceforge/stripes/mock/MockServletOutputStream.java @@ -14,31 +14,42 @@ */ package net.sourceforge.stripes.mock; -import javax.servlet.ServletOutputStream; -import java.io.IOException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; import java.io.ByteArrayOutputStream; +import java.io.IOException; /** - * Mock implementation of a ServletOutputStream that just uses a byte array output stream to - * capture any output and make it available after the test is done. + * Mock implementation of a ServletOutputStream that just uses a byte array output stream to capture + * any output and make it available after the test is done. * * @author Tim Fennell * @since Stripes 1.1 */ public class MockServletOutputStream extends ServletOutputStream { - private ByteArrayOutputStream out = new ByteArrayOutputStream(); + private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + /** Pass through method calls ByteArrayOutputStream.write(int b). */ + @Override + public void write(int b) throws IOException { + out.write(b); + } + + /** Returns the array of bytes that have been written to the output stream. */ + public byte[] getBytes() { + return out.toByteArray(); + } - /** Pass through method calls ByteArrayOutputStream.write(int b). */ - @Override - public void write(int b) throws IOException { out.write(b); } + /** Returns, as a character string, the output that was written to the output stream. */ + public String getString() { + return out.toString(); + } - /** Returns the array of bytes that have been written to the output stream. */ - public byte[] getBytes() { - return out.toByteArray(); - } + @Override + public boolean isReady() { + return false; + } - /** Returns, as a character string, the output that was written to the output stream. */ - public String getString() { - return out.toString(); - } + @Override + public void setWriteListener(WriteListener writeListener) {} } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/BeanFirstPopulationStrategy.java b/stripes/src/main/java/net/sourceforge/stripes/tag/BeanFirstPopulationStrategy.java index 0882403a9..e5fefd54a 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/BeanFirstPopulationStrategy.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/BeanFirstPopulationStrategy.java @@ -14,73 +14,70 @@ */ package net.sourceforge.stripes.tag; +import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.controller.StripesConstants; import net.sourceforge.stripes.exception.StripesJspException; -import net.sourceforge.stripes.action.ActionBean; +import net.sourceforge.stripes.util.Log; import net.sourceforge.stripes.util.bean.BeanUtil; import net.sourceforge.stripes.util.bean.ExpressionException; -import net.sourceforge.stripes.util.Log; /** - *

    An alternative tag population strategy that will normally prefer the value from the ActionBean - * over values from the request - even when the ActionBean returns null! Only if the ActionBean - * is not present, or does not define an attribute with the name supplied to the tag will other - * population sources be examined. When that happens, the strategy will check the value - * specified on the page next, and finally the value(s) in the request.

    + * An alternative tag population strategy that will normally prefer the value from the ActionBean + * over values from the request - even when the ActionBean returns null! Only if the ActionBean is + * not present, or does not define an attribute with the name supplied to the tag will other + * population sources be examined. When that happens, the strategy will check the value specified on + * the page next, and finally the value(s) in the request. * *

    If the field represented by the tag is determined to be in error (i.e. the ActionBean is - * present and has validation errors for the matching field) then the repopulation behaviour - * will revert to the default behaviour of preferring the request parameters.

    + * present and has validation errors for the matching field) then the repopulation behaviour will + * revert to the default behaviour of preferring the request parameters. * * @author Tim Fennell * @since Stripes 1.4 */ public class BeanFirstPopulationStrategy extends DefaultPopulationStrategy { - private static final Log log = Log.getInstance(BeanFirstPopulationStrategy.class); + private static final Log log = Log.getInstance(BeanFirstPopulationStrategy.class); - /** - * Implementation of the interface method that will follow the search described in the class - * level JavaDoc and attempt to find a value for this tag. - * - * @param tag the form input tag whose value to populate - * @return Object will be one of null, a single Object or an Array of Objects depending upon - * what was submitted in the prior request, and what is declared on the ActionBean - */ - @Override - public Object getValue(InputTagSupport tag) throws StripesJspException { - // If the specific tag is in error, grab the values from the request - if (tag.hasErrors()) { - return super.getValue(tag); + /** + * Implementation of the interface method that will follow the search described in the class level + * JavaDoc and attempt to find a value for this tag. + * + * @param tag the form input tag whose value to populate + * @return Object will be one of null, a single Object or an Array of Objects depending upon what + * was submitted in the prior request, and what is declared on the ActionBean + */ + @Override + public Object getValue(InputTagSupport tag) throws StripesJspException { + // If the specific tag is in error, grab the values from the request + if (tag.hasErrors()) { + return super.getValue(tag); + } else { + // Try getting from the ActionBean. If the bean is present and the property + // is defined, then the value from the bean takes precedence even if it's null + ActionBean bean = tag.getActionBean(); + Object value = null; + boolean kaboom = false; + if (bean != null) { + try { + value = BeanUtil.getPropertyValue(tag.getName(), bean); + } catch (ExpressionException ee) { + if (!StripesConstants.SPECIAL_URL_KEYS.contains(tag.getName())) { + log.info("Could not find property [", tag.getName(), "] on ActionBean.", ee); + } + kaboom = true; } - else { - // Try getting from the ActionBean. If the bean is present and the property - // is defined, then the value from the bean takes precedence even if it's null - ActionBean bean = tag.getActionBean(); - Object value = null; - boolean kaboom = false; - if (bean != null) { - try { - value = BeanUtil.getPropertyValue(tag.getName(), bean); - } - catch (ExpressionException ee) { - if (!StripesConstants.SPECIAL_URL_KEYS.contains(tag.getName())) { - log.info("Could not find property [", tag.getName(), "] on ActionBean.", ee); - } - kaboom = true; - } - } + } - // If there's no matching bean property, then look elsewhere - if (bean == null || kaboom) { - value = getValueFromTag(tag); + // If there's no matching bean property, then look elsewhere + if (bean == null || kaboom) { + value = getValueFromTag(tag); - if (value == null) { - value = getValuesFromRequest(tag); - } - } - - return value; + if (value == null) { + value = getValuesFromRequest(tag); } - } + } + return value; + } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/DefaultPopulationStrategy.java b/stripes/src/main/java/net/sourceforge/stripes/tag/DefaultPopulationStrategy.java index 8c8a28915..b3c5c018c 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/DefaultPopulationStrategy.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/DefaultPopulationStrategy.java @@ -27,146 +27,147 @@ import net.sourceforge.stripes.validation.ValidationMetadata; /** - *

    Default implementation of the form input tag population strategy. First looks to see if there - * is a parameter with the same name as the tag submitted in the current request. If there is, - * it will be returned as a String[] in order to support multiple-value parameters.

    + * Default implementation of the form input tag population strategy. First looks to see if there is + * a parameter with the same name as the tag submitted in the current request. If there is, it will + * be returned as a String[] in order to support multiple-value parameters. * *

    If there is no value in the request then an ActionBean bound to the current form will be - * looked for. If the ActionBean is found and the value is non-null it will be returned. - * If no value can be found in either place, null will returned. + * looked for. If the ActionBean is found and the value is non-null it will be returned. If no value + * can be found in either place, null will returned. * * @author Tim Fennell */ public class DefaultPopulationStrategy implements PopulationStrategy { - /** Configuration object handed to the class at init time. */ - private Configuration config; - - /** Log used to log any errors that occur. */ - private static final Log log = Log.getInstance(DefaultPopulationStrategy.class); - - /** Called by the Configuration to configure the component. */ - public void init(Configuration configuration) throws Exception { - this.config = configuration; + /** Configuration object handed to the class at init time. */ + private Configuration config; + + /** Log used to log any errors that occur. */ + private static final Log log = Log.getInstance(DefaultPopulationStrategy.class); + + /** Called by the Configuration to configure the component. */ + public void init(Configuration configuration) throws Exception { + this.config = configuration; + } + + /** Accessor for the configuration supplied when the population strategy is initialized. */ + protected Configuration getConfiguration() { + return this.config; + } + + /** + * Implementation of the interface method that will follow the search described in the class level + * JavaDoc and attempt to find a value for this tag. + * + * @param tag the form input tag whose value to populate + * @return Object will be one of null, a single Object or an Array of Objects depending upon what + * was submitted in the prior request, and what is declared on the ActionBean + */ + public Object getValue(InputTagSupport tag) throws StripesJspException { + // Look first for something that the user submitted in the current request + Object value = getValuesFromRequest(tag); + + // If that's not there, let's look on the ActionBean + if (value == null) { + value = getValueFromActionBean(tag); } - /** Accessor for the configuration supplied when the population strategy is initialized. */ - protected Configuration getConfiguration() { - return this.config; + // And if there's no value there, look at the tag's own value + if (value == null) { + value = getValueFromTag(tag); } - /** - * Implementation of the interface method that will follow the search described in the class - * level JavaDoc and attempt to find a value for this tag. - * - * @param tag the form input tag whose value to populate - * @return Object will be one of null, a single Object or an Array of Objects depending upon - * what was submitted in the prior request, and what is declared on the ActionBean + return value; + } + + /** + * Helper method that will check the current request for user submitted values for the tag + * supplied and return them as a String[] if there is one or more present. + * + * @param tag the tag whose values to look for + * @return a String[] if values are found, null otherwise + */ + protected String[] getValuesFromRequest(InputTagSupport tag) throws StripesJspException { + String[] value = tag.getPageContext().getRequest().getParameterValues(tag.getName()); + + /* + * If the value was pulled from a request parameter and the ActionBean property it would + * bind to is flagged as encrypted, then the value needs to be decrypted now. */ - public Object getValue(InputTagSupport tag) throws StripesJspException { - // Look first for something that the user submitted in the current request - Object value = getValuesFromRequest(tag); - - // If that's not there, let's look on the ActionBean - if (value == null) { - value = getValueFromActionBean(tag); - } - - // And if there's no value there, look at the tag's own value - if (value == null) { - value = getValueFromTag(tag); + if (value != null) { + // find the action bean class we're dealing with + Class beanClass = tag.getParentFormTag().getActionBeanClass(); + if (beanClass != null) { + ValidationMetadata validate = + config + .getValidationMetadataProvider() + .getValidationMetadata(beanClass, new ParameterName(tag.getName())); + if (validate != null && validate.encrypted()) { + String[] copy = new String[value.length]; + for (int i = 0; i < copy.length; i++) { + copy[i] = CryptoUtil.decrypt(value[i]); + } + value = copy; } - - return value; + } } - /** - * Helper method that will check the current request for user submitted values for the - * tag supplied and return them as a String[] if there is one or more present. - * - * @param tag the tag whose values to look for - * @return a String[] if values are found, null otherwise - */ - protected String[] getValuesFromRequest(InputTagSupport tag) throws StripesJspException { - String[] value = tag.getPageContext().getRequest().getParameterValues(tag.getName()); - - /* - * If the value was pulled from a request parameter and the ActionBean property it would - * bind to is flagged as encrypted, then the value needs to be decrypted now. - */ - if (value != null) { - // find the action bean class we're dealing with - Class beanClass = tag.getParentFormTag().getActionBeanClass(); - if (beanClass != null) { - ValidationMetadata validate = config.getValidationMetadataProvider() - .getValidationMetadata(beanClass, new ParameterName(tag.getName())); - if (validate != null && validate.encrypted()) { - String[] copy = new String[value.length]; - for (int i = 0; i < copy.length; i++) { - copy[i] = CryptoUtil.decrypt(value[i]); - } - value = copy; - } - } + return value; + } + + /** + * Helper method that will check to see if there is an ActionBean present in the request, and if + * so, retrieve the value for this tag from the ActionBean. + * + * @param tag the tag whose values to look for + * @return an Object, possibly null, representing the tag's value + */ + protected Object getValueFromActionBean(InputTagSupport tag) throws StripesJspException { + ActionBean actionBean = tag.getParentFormTag().getActionBean(); + Object value = null; + + if (actionBean != null) { + try { + value = BeanUtil.getPropertyValue(tag.getName(), actionBean); + } catch (ExpressionException ee) { + if (!StripesConstants.SPECIAL_URL_KEYS.contains(tag.getName())) { + log.info("Could not find property [", tag.getName(), "] on ActionBean.", ee); } - - return value; + } } - /** - * Helper method that will check to see if there is an ActionBean present in the request, - * and if so, retrieve the value for this tag from the ActionBean. - * - * @param tag the tag whose values to look for - * @return an Object, possibly null, representing the tag's value - */ - protected Object getValueFromActionBean(InputTagSupport tag) throws StripesJspException { - ActionBean actionBean = tag.getParentFormTag().getActionBean(); - Object value = null; - - if (actionBean != null) { - try { - value = BeanUtil.getPropertyValue(tag.getName(), actionBean); - } - catch (ExpressionException ee) { - if (!StripesConstants.SPECIAL_URL_KEYS.contains(tag.getName())) { - log.info("Could not find property [", tag.getName(), "] on ActionBean.", ee); - } - } - } - - return value; + return value; + } + + /** + * Helper method that will retrieve the preferred value set on the tag in the JSP. For most tags + * this is usually the body if it is present, or the value attribute. In some cases tags implement + * this differently, notably the radio and checkbox tags. + * + * @param tag the tag that is being repopulated + * @return a value for the tag if one is specified on the JSP + */ + protected Object getValueFromTag(InputTagSupport tag) { + return tag.getValueOnPage(); + } + + /** + * Helper method that will check to see if the form containing this tag is being rendered as a + * result of validation errors. This is not actually used by the default strategy, but is here to + * help subclasses provide different behaviour for when the form is rendering normally vs. in + * error. + * + * @param tag the tag that is being repopulated + * @return boolean true if the form is in error, false otherwise + */ + protected boolean isFormInError(InputTagSupport tag) throws StripesJspException { + boolean inError = false; + + ActionBean actionBean = tag.getParentFormTag().getActionBean(); + if (actionBean != null) { + ValidationErrors errors = actionBean.getContext().getValidationErrors(); + inError = (errors != null && errors.size() > 0); } - /** - * Helper method that will retrieve the preferred value set on the tag in the JSP. For - * most tags this is usually the body if it is present, or the value attribute. In some - * cases tags implement this differently, notably the radio and checkbox tags. - * - * @param tag the tag that is being repopulated - * @return a value for the tag if one is specified on the JSP - */ - protected Object getValueFromTag(InputTagSupport tag) { - return tag.getValueOnPage(); - } - - /** - * Helper method that will check to see if the form containing this tag is being rendered - * as a result of validation errors. This is not actually used by the default strategy, - * but is here to help subclasses provide different behaviour for when the form is rendering - * normally vs. in error. - * - * @param tag the tag that is being repopulated - * @return boolean true if the form is in error, false otherwise - */ - protected boolean isFormInError(InputTagSupport tag) throws StripesJspException { - boolean inError = false; - - ActionBean actionBean = tag.getParentFormTag().getActionBean(); - if (actionBean != null) { - ValidationErrors errors = actionBean.getContext().getValidationErrors(); - inError = (errors != null && errors.size() > 0); - } - - return inError; - } + return inError; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/DefaultTagErrorRenderer.java b/stripes/src/main/java/net/sourceforge/stripes/tag/DefaultTagErrorRenderer.java index d6d4f247d..17e5897f9 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/DefaultTagErrorRenderer.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/DefaultTagErrorRenderer.java @@ -15,59 +15,51 @@ package net.sourceforge.stripes.tag; /** - *

    This default implementation of the TagErrorRenderer interface sets the html class - * attribute to 'error'. More specifically, if the tag had no previous CSS class, it - * will have its class attribute set to error. If it previously had a CSS class attribute, - * e.g. class="foo", then it's class attribute will be re-written as class="foo error", - * which instructs the browser to apply both styles, with error taking precedence. The - * use of a single class name allows applications to define a single style for all input - * fields, and then override it for specific fields as they choose.

    + * This default implementation of the TagErrorRenderer interface sets the html class attribute to + * 'error'. More specifically, if the tag had no previous CSS class, it will have its class + * attribute set to error. If it previously had a CSS class attribute, e.g. class="foo", then it's + * class attribute will be re-written as class="foo error", which instructs the browser to apply + * both styles, with error taking precedence. The use of a single class name allows applications to + * define a single style for all input fields, and then override it for specific fields as they + * choose. + * + *

    An example of the css definition to set backgrounds to yellow by default, but to red for + * checkboxes and radio buttons follows: {@code input.error { background-color: yellow; } + * input[type="checkbox"].error, input[type="radio"].error {background-color: red; } } * - *

    An example of the css definition to set backgrounds to yellow by default, but - * to red for checkboxes and radio buttons follows:

    - - * {@code - * input.error { background-color: yellow; } - * input[type="checkbox"].error, input[type="radio"].error {background-color: red; } - * } * @author Greg Hinkle, Tim Fennell */ public class DefaultTagErrorRenderer implements TagErrorRenderer { - private InputTagSupport tag; - private String oldCssClass; + private InputTagSupport tag; + private String oldCssClass; - /** Simply stores the tag passed in. */ - public void init(InputTagSupport tag) { - this.tag = tag; - } + /** Simply stores the tag passed in. */ + public void init(InputTagSupport tag) { + this.tag = tag; + } - /** - * Returns the tag which is being rendered. Useful mostly when subclassing the default - * renderer to add further functionality. - * - * @return the input tag being rendered - */ - protected InputTagSupport getTag() { - return this.tag; - } + /** + * Returns the tag which is being rendered. Useful mostly when subclassing the default renderer to + * add further functionality. + * + * @return the input tag being rendered + */ + protected InputTagSupport getTag() { + return this.tag; + } - /** - * Ensures that the tag's list of CSS classes includes the "error" class. - */ - public void doBeforeStartTag() { - this.oldCssClass = tag.getCssClass(); - if (this.oldCssClass != null && this.oldCssClass.length() > 0) { - tag.setCssClass("error " + this.oldCssClass); - } - else { - tag.setCssClass("error"); - } + /** Ensures that the tag's list of CSS classes includes the "error" class. */ + public void doBeforeStartTag() { + this.oldCssClass = tag.getCssClass(); + if (this.oldCssClass != null && this.oldCssClass.length() > 0) { + tag.setCssClass("error " + this.oldCssClass); + } else { + tag.setCssClass("error"); } + } - /** - * Resets the tag's class attribute to it's original value in case the tag gets pooled. - */ - public void doAfterEndTag() { - tag.setCssClass(oldCssClass); - } + /** Resets the tag's class attribute to it's original value in case the tag gets pooled. */ + public void doAfterEndTag() { + tag.setCssClass(oldCssClass); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/DefaultTagErrorRendererFactory.java b/stripes/src/main/java/net/sourceforge/stripes/tag/DefaultTagErrorRendererFactory.java index 79c8d4448..e284781cb 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/DefaultTagErrorRendererFactory.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/DefaultTagErrorRendererFactory.java @@ -18,72 +18,73 @@ import net.sourceforge.stripes.exception.StripesRuntimeException; /** - *

    A straightforward implementation of the TagErrorRendererFactory interface that looks - * up the name of the renderer class in config, and if one is not supplied defaults to - * using the {@link DefaultTagErrorRenderer}. The same TagErrorRenderer is instantiated for - * all tags, and must be public and have a public no-arg constructor.

    + * A straightforward implementation of the TagErrorRendererFactory interface that looks up the name + * of the renderer class in config, and if one is not supplied defaults to using the {@link + * DefaultTagErrorRenderer}. The same TagErrorRenderer is instantiated for all tags, and must be + * public and have a public no-arg constructor. * - *

    To configure a different TagErrorRenderer use the configuration key - * {@code TagErrorRenderer.Class} and supply a fully qualified class name. For example, to - * do this in web.xml you would add the following parameter to the Stripes Filter:

    + *

    To configure a different TagErrorRenderer use the configuration key {@code + * TagErrorRenderer.Class} and supply a fully qualified class name. For example, to do this in + * web.xml you would add the following parameter to the Stripes Filter: * - *

    - *{@literal }
    + * 
    + * {@literal }
      *    {@literal TagErrorRenderer.Class}
      *    {@literal com.myco.web.util.CustomTagErrorRenderer}
    - *{@literal }
    - *
    + * {@literal
    } + *
    * * @author Greg Hinkle, Tim Fennell */ public class DefaultTagErrorRendererFactory implements TagErrorRendererFactory { - public static final String RENDERER_CLASS_KEY = "TagErrorRenderer.Class"; + public static final String RENDERER_CLASS_KEY = "TagErrorRenderer.Class"; - private Configuration configuration; - private Class rendererClass; + private Configuration configuration; + private Class rendererClass; - /** - * Looks up the name of the configured renderer class in the configuration and - * attempts to find the Class object for it. If one isn't provided then the default - * class is used. If the configured class cannot be found an exception will be - * thrown and the factory is deemed invalid. - */ - public void init(Configuration configuration) throws Exception { - setConfiguration(configuration); + /** + * Looks up the name of the configured renderer class in the configuration and attempts to find + * the Class object for it. If one isn't provided then the default class is used. If the + * configured class cannot be found an exception will be thrown and the factory is deemed invalid. + */ + public void init(Configuration configuration) throws Exception { + setConfiguration(configuration); - this.rendererClass = configuration.getBootstrapPropertyResolver(). - getClassProperty(RENDERER_CLASS_KEY, TagErrorRenderer.class); - - if (this.rendererClass == null) - this.rendererClass = DefaultTagErrorRenderer.class; - } + this.rendererClass = + configuration + .getBootstrapPropertyResolver() + .getClassProperty(RENDERER_CLASS_KEY, TagErrorRenderer.class); + + if (this.rendererClass == null) this.rendererClass = DefaultTagErrorRenderer.class; + } - /** - * Returns a new instance of the configured renderer that is ready for use. By default - * returns an instance of {@link DefaultTagErrorRenderer}. If a custom class is configured - * and cannot be instantiated, an exception will be thrown. - */ - public TagErrorRenderer getTagErrorRenderer(InputTagSupport tag) { - try { - TagErrorRenderer renderer = getConfiguration().getObjectFactory().newInstance( - this.rendererClass); - renderer.init(tag); - return renderer; - } - catch (Exception e) { - throw new StripesRuntimeException("Could not create an instance of the configured " + - "TagErrorRenderer class '" + this.rendererClass.getName() + "'. Please check " + - "that the class is public and has a no-arg public constructor.", e); - } + /** + * Returns a new instance of the configured renderer that is ready for use. By default returns an + * instance of {@link DefaultTagErrorRenderer}. If a custom class is configured and cannot be + * instantiated, an exception will be thrown. + */ + public TagErrorRenderer getTagErrorRenderer(InputTagSupport tag) { + try { + TagErrorRenderer renderer = + getConfiguration().getObjectFactory().newInstance(this.rendererClass); + renderer.init(tag); + return renderer; + } catch (Exception e) { + throw new StripesRuntimeException( + "Could not create an instance of the configured " + + "TagErrorRenderer class '" + + this.rendererClass.getName() + + "'. Please check " + + "that the class is public and has a no-arg public constructor.", + e); } + } - protected Configuration getConfiguration() - { - return configuration; - } + protected Configuration getConfiguration() { + return configuration; + } - protected void setConfiguration(Configuration configuration) - { - this.configuration = configuration; - } + protected void setConfiguration(Configuration configuration) { + this.configuration = configuration; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/ElFunctions.java b/stripes/src/main/java/net/sourceforge/stripes/tag/ElFunctions.java index ebf40c1c0..1309a7c33 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/ElFunctions.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/ElFunctions.java @@ -15,42 +15,37 @@ package net.sourceforge.stripes.tag; import java.util.List; - import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.validation.ValidationError; import net.sourceforge.stripes.validation.ValidationErrors; /** - * A collection of static functions that are included in the Stripes tag library. In most - * cases these are not functions that are specific to Stripes, but simply functions that - * make doing web development (especially with Java 5 constructs) easier. + * A collection of static functions that are included in the Stripes tag library. In most cases + * these are not functions that are specific to Stripes, but simply functions that make doing web + * development (especially with Java 5 constructs) easier. * * @author Tim Fennell * @since Stripes 1.1 */ public class ElFunctions { - /** Gets the name of the supplied enumerated value. */ - public static String name(Enum e) { - return e.name(); - } - - /** Indicates if validation errors exist for the given field of the given {@link ActionBean}. */ - public static boolean hasErrors(ActionBean actionBean, String field) { - if (actionBean == null || field == null) - return false; + /** Gets the name of the supplied enumerated value. */ + public static String name(Enum e) { + return e.name(); + } - ActionBeanContext context = actionBean.getContext(); - if (context == null) - return false; + /** Indicates if validation errors exist for the given field of the given {@link ActionBean}. */ + public static boolean hasErrors(ActionBean actionBean, String field) { + if (actionBean == null || field == null) return false; - ValidationErrors errors = context.getValidationErrors(); - if (errors == null || errors.isEmpty()) - return false; + ActionBeanContext context = actionBean.getContext(); + if (context == null) return false; - List fieldErrors = errors.get(field); - return fieldErrors != null && fieldErrors.size() > 0; - } + ValidationErrors errors = context.getValidationErrors(); + if (errors == null || errors.isEmpty()) return false; + List fieldErrors = errors.get(field); + return fieldErrors != null && fieldErrors.size() > 0; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsFooterTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsFooterTag.java index fe8477643..71758cd68 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsFooterTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsFooterTag.java @@ -14,30 +14,26 @@ */ package net.sourceforge.stripes.tag; -import javax.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspException; /** - * Can be used within a stripes:errors tag to show a footer on an error list. - * The contents of this tag will only be displayed on the last iteration of an - * errors list. + * Can be used within a stripes:errors tag to show a footer on an error list. The contents of this + * tag will only be displayed on the last iteration of an errors list. * * @author Greg Hinkle */ public class ErrorsFooterTag extends HtmlTagSupport { - @Override - public int doStartTag() throws JspException { - ErrorsTag errorsTag = getParentTag(ErrorsTag.class); + @Override + public int doStartTag() throws JspException { + ErrorsTag errorsTag = getParentTag(ErrorsTag.class); - if (errorsTag.isLast()) - return EVAL_BODY_INCLUDE; - else - return SKIP_BODY; - } - - @Override - public int doEndTag() throws JspException { - return EVAL_PAGE; - } + if (errorsTag.isLast()) return EVAL_BODY_INCLUDE; + else return SKIP_BODY; + } + @Override + public int doEndTag() throws JspException { + return EVAL_PAGE; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsHeaderTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsHeaderTag.java index 289b9bd4f..f302d40f7 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsHeaderTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsHeaderTag.java @@ -14,28 +14,25 @@ */ package net.sourceforge.stripes.tag; -import javax.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspException; /** - * Can be used within a stripes:errors tag to show a header on an error list. - * The contents of this tag will only be displayed on the first iteration of an - * errors list. + * Can be used within a stripes:errors tag to show a header on an error list. The contents of this + * tag will only be displayed on the first iteration of an errors list. * * @author Greg Hinkle */ public class ErrorsHeaderTag extends HtmlTagSupport { - @Override - public int doStartTag() throws JspException { - ErrorsTag errorsTag = getParentTag(ErrorsTag.class); - if (errorsTag.isFirst()) - return EVAL_BODY_INCLUDE; - else - return SKIP_BODY; - } + @Override + public int doStartTag() throws JspException { + ErrorsTag errorsTag = getParentTag(ErrorsTag.class); + if (errorsTag.isFirst()) return EVAL_BODY_INCLUDE; + else return SKIP_BODY; + } - @Override - public int doEndTag() throws JspException { - return EVAL_PAGE; - } + @Override + public int doEndTag() throws JspException { + return EVAL_PAGE; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsTag.java index e3428fc4e..a0c03302f 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsTag.java @@ -14,18 +14,10 @@ */ package net.sourceforge.stripes.tag; -import net.sourceforge.stripes.action.ActionBean; -import net.sourceforge.stripes.controller.StripesConstants; -import net.sourceforge.stripes.controller.StripesFilter; -import net.sourceforge.stripes.util.Log; -import net.sourceforge.stripes.validation.ValidationError; -import net.sourceforge.stripes.validation.ValidationErrors; -import net.sourceforge.stripes.exception.StripesJspException; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspWriter; -import javax.servlet.jsp.tagext.BodyTag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspWriter; +import jakarta.servlet.jsp.tagext.BodyTag; import java.io.IOException; import java.util.Comparator; import java.util.Iterator; @@ -35,411 +27,418 @@ import java.util.ResourceBundle; import java.util.SortedSet; import java.util.TreeSet; +import net.sourceforge.stripes.action.ActionBean; +import net.sourceforge.stripes.controller.StripesConstants; +import net.sourceforge.stripes.controller.StripesFilter; +import net.sourceforge.stripes.exception.StripesJspException; +import net.sourceforge.stripes.util.Log; +import net.sourceforge.stripes.validation.ValidationError; +import net.sourceforge.stripes.validation.ValidationErrors; /** - *

    The errors tag has two modes, one where it displays all validation errors in a list - * and a second mode when there is a single enclosed field-error tag that has no name attribute - * in which case this tag iterates over the body, displaying each error in turn in place - * of the field-error tag.

    + * The errors tag has two modes, one where it displays all validation errors in a list and a second + * mode when there is a single enclosed field-error tag that has no name attribute in which case + * this tag iterates over the body, displaying each error in turn in place of the field-error tag. * - *

    In the first mode, where the default output is used, it is possible to change the output - * for the entire application using a set of resources in the error messages bundle - * (StripesResources.properties unless you have configured another). If the properties are + *

    In the first mode, where the default output is used, it is possible to change the output for + * the entire application using a set of resources in the error messages bundle + * (StripesResources.properties unless you have configured another). If the properties are * undefined, the tag will output the text "Validation Errors" in a div with css class errorHeader, - * then output an unordered list of error messages. The following four resource strings - * (shown with their default values) can be modified to create different default output:

    + * then output an unordered list of error messages. The following four resource strings (shown with + * their default values) can be modified to create different default output: * *
      - *
    • stripes.errors.header={@literal
      Validation Errors
        } - *
      • stripes.errors.footer={@literal
      }
    • - *
    • stripes.errors.beforeError={@literal
    • }
    • - *
    • stripes.errors.afterError={@literal
    • } + *
    • stripes.errors.header={@literal
      Validation Errors
        } + *
      • stripes.errors.footer={@literal
      } + *
    • stripes.errors.beforeError={@literal
    • } + *
    • stripes.errors.afterError={@literal
    • } *
    * - *

    The errors tag can also be used to display errors for a single field by supplying it - * with a 'field' attribute which matches the name of a field on the page. In this case the tag - * will display only if errors exist for the named field. In this mode the tag will first look for - * resources named:

    + *

    The errors tag can also be used to display errors for a single field by supplying it with a + * 'field' attribute which matches the name of a field on the page. In this case the tag will + * display only if errors exist for the named field. In this mode the tag will first look for + * resources named: * *

      - *
    • stripes.fieldErrors.header
    • - *
    • stripes.fieldErrors.footer
    • - *
    • stripes.fieldErrors.beforeError
    • - *
    • stripes.fieldErrors.afterError
    • + *
    • stripes.fieldErrors.header + *
    • stripes.fieldErrors.footer + *
    • stripes.fieldErrors.beforeError + *
    • stripes.fieldErrors.afterError *
    * - *

    If the {@code fieldErrors} resources cannot be found, the tag will default to using the - * same resources and defaults as when displaying for all fields.

    + *

    If the {@code fieldErrors} resources cannot be found, the tag will default to using the same + * resources and defaults as when displaying for all fields. * - *

    Similar to the above, field specific, manner of display the errors tag can also be used - * to output only errors not associated with a field, i.e. global errors. This is done by setting - * the {@code globalErrorsOnly} attribute to true.

    + *

    Similar to the above, field specific, manner of display the errors tag can also be used to + * output only errors not associated with a field, i.e. global errors. This is done by setting the + * {@code globalErrorsOnly} attribute to true. * - *

    This tag has several ways of being attached to the errors of a specific action request. - * If the tag is inside a form tag, it will display only errors that are associated - * with that form. If supplied with an 'action' attribute, it will display errors only errors - * associated with a request to that URL. Finally, if neither is the case, it - * will always display as described in the paragraph above.

    + *

    This tag has several ways of being attached to the errors of a specific action request. If the + * tag is inside a form tag, it will display only errors that are associated with that form. If + * supplied with an 'action' attribute, it will display errors only errors associated with a request + * to that URL. Finally, if neither is the case, it will always display as described in the + * paragraph above. * * @author Greg Hinkle, Tim Fennell */ public class ErrorsTag extends HtmlTagSupport implements BodyTag { - private static final Log log = Log.getInstance(ErrorsTag.class); - - /** The header that will be emitted if no header is defined in the resource bundle. */ - public static final String DEFAULT_HEADER = - "

    Validation Errors
      "; - - /** The footer that will be emitted if no footer is defined in the resource bundle. */ - public static final String DEFAULT_FOOTER = "
    "; - - - /** - * True if this tag will display errors, otherwise false. This is determined by the logic - * laid out in the class level Javadoc around whether this errors tag is for the action - * that was submitted in the request. - */ - private boolean display = false; - - /** - * True if this tag contains a field-error child tag, which controls - * the place of output of each error - */ - private boolean nestedErrorTagPresent = false; - - /** Sets the form action for which errors should be displayed. */ - private String action; - - /** An optional attribute that declares a particular field to output errors for. */ - private String field; - - /** An optional attribute that specified to display only the global errors. */ - private boolean globalErrorsOnly; - - /** The collection of errors that match the filtering conditions */ - private SortedSet allErrors; - - /** An iterator of the list of matched errors */ - private Iterator errorIterator; - - /** The error displayed in the current iteration */ - private ValidationError currentError; - - /** An index of the error being displayed - zero based */ - private int index = 0; - - - /** - * Called by the IndividualErrorTag to fetch the current error from the set being iterated. - * - * @return The error displayed for this iteration of the errors tag - */ - public ValidationError getCurrentError() { - this.nestedErrorTagPresent = true; - return currentError; - } - - /** Returns true if the error displayed is the first matching error. */ - public boolean isFirst() { - return (this.allErrors.first() == this.currentError); - } - - /** Returns true if the error displayed is the last matching error. */ - public boolean isLast() { - return (this.allErrors.last() == currentError); - } - - /** Sets the (optional) action of the form to display errors for, if they exist. */ - public void setAction(String action) { - this.action = action; - } - - /** Returns the value set with setAction(). */ - public String getAction() { - return this.action; + private static final Log log = Log.getInstance(ErrorsTag.class); + + /** The header that will be emitted if no header is defined in the resource bundle. */ + public static final String DEFAULT_HEADER = + "
    Validation Errors
      "; + + /** The footer that will be emitted if no footer is defined in the resource bundle. */ + public static final String DEFAULT_FOOTER = "
    "; + + /** + * True if this tag will display errors, otherwise false. This is determined by the logic laid out + * in the class level Javadoc around whether this errors tag is for the action that was submitted + * in the request. + */ + private boolean display = false; + + /** + * True if this tag contains a field-error child tag, which controls the place of output of each + * error + */ + private boolean nestedErrorTagPresent = false; + + /** Sets the form action for which errors should be displayed. */ + private String action; + + /** An optional attribute that declares a particular field to output errors for. */ + private String field; + + /** An optional attribute that specified to display only the global errors. */ + private boolean globalErrorsOnly; + + /** The collection of errors that match the filtering conditions */ + private SortedSet allErrors; + + /** An iterator of the list of matched errors */ + private Iterator errorIterator; + + /** The error displayed in the current iteration */ + private ValidationError currentError; + + /** An index of the error being displayed - zero based */ + private int index = 0; + + /** + * Called by the IndividualErrorTag to fetch the current error from the set being iterated. + * + * @return The error displayed for this iteration of the errors tag + */ + public ValidationError getCurrentError() { + this.nestedErrorTagPresent = true; + return currentError; + } + + /** Returns true if the error displayed is the first matching error. */ + public boolean isFirst() { + return (this.allErrors.first() == this.currentError); + } + + /** Returns true if the error displayed is the last matching error. */ + public boolean isLast() { + return (this.allErrors.last() == currentError); + } + + /** Sets the (optional) action of the form to display errors for, if they exist. */ + public void setAction(String action) { + this.action = action; + } + + /** Returns the value set with setAction(). */ + public String getAction() { + return this.action; + } + + /** + * Sets the action attribute by figuring out what ActionBean class is identified and then in turn + * finding out the appropriate URL for the ActionBean. + * + * @param beanclass the FQN of an ActionBean class, or a Class object for one. + */ + public void setBeanclass(Object beanclass) throws StripesJspException { + String url = getActionBeanUrl(beanclass); + if (url == null) { + throw new StripesJspException( + "The 'beanclass' attribute provided could not be " + + "used to identify a valid and configured ActionBean. The value supplied was: " + + beanclass); + } else { + this.action = url; } - - /** - * Sets the action attribute by figuring out what ActionBean class is identified - * and then in turn finding out the appropriate URL for the ActionBean. - * - * @param beanclass the FQN of an ActionBean class, or a Class object for one. - */ - public void setBeanclass(Object beanclass) throws StripesJspException { - String url = getActionBeanUrl(beanclass); - if (url == null) { - throw new StripesJspException("The 'beanclass' attribute provided could not be " + - "used to identify a valid and configured ActionBean. The value supplied was: " + - beanclass); + } + + /** Sets the (optional) name of a field to display errors for, if errors exist. */ + public void setField(String field) { + this.field = field; + } + + /** Gets the value set with setField(). */ + public String getField() { + return field; + } + + /** Indicated whether the tag is displaying only global errors. */ + public boolean isGlobalErrorsOnly() { + return globalErrorsOnly; + } + + /** Tells the tag to display (or not) only global errors and no field level errors. */ + public void setGlobalErrorsOnly(boolean globalErrorsOnly) { + this.globalErrorsOnly = globalErrorsOnly; + } + + /** + * Determines if the tag should display errors based on the action that it is displaying for, and + * then fetches the appropriate list of errors and makes sure it is non-empty. + * + * @return SKIP_BODY if the errors are not to be output, or there aren't any
    + * EVAL_BODY_TAG if there are errors to display + */ + @Override + public int doStartTag() throws JspException { + HttpServletRequest request = (HttpServletRequest) getPageContext().getRequest(); + ActionBean mainBean = (ActionBean) request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN); + FormTag formTag = getParentTag(FormTag.class); + ValidationErrors errors = null; + + // If we are supplied with an 'action' attribute then display the errors + // only if that action matches the 'action' of the current action bean + if (getAction() != null) { + if (mainBean != null) { + String mainAction = + StripesFilter.getConfiguration().getActionResolver().getUrlBinding(mainBean.getClass()); + + if (getAction().equals(mainAction)) { + errors = mainBean.getContext().getValidationErrors(); } - else { - this.action = url; - } - } - - /** Sets the (optional) name of a field to display errors for, if errors exist. */ - public void setField(String field) { - this.field = field; + } } + // Else we don't have an 'action' attribute, so see if we are nested in + // a form tag + else if (formTag != null) { + ActionBean formBean = formTag.getActionBean(); + if (formBean != null) { + errors = formBean.getContext().getValidationErrors(); + } - /** Gets the value set with setField(). */ - public String getField() { - return field; } - - /** Indicated whether the tag is displaying only global errors. */ - public boolean isGlobalErrorsOnly() { return globalErrorsOnly; } - - /** Tells the tag to display (or not) only global errors and no field level errors. */ - public void setGlobalErrorsOnly(boolean globalErrorsOnly) { - this.globalErrorsOnly = globalErrorsOnly; + // Else if no name was set, and we're not in a action tag, we're global and ok to display + else if (mainBean != null) { + errors = mainBean.getContext().getValidationErrors(); } - /** - * Determines if the tag should display errors based on the action that it is displaying for, - * and then fetches the appropriate list of errors and makes sure it is non-empty. - * - * @return SKIP_BODY if the errors are not to be output, or there aren't any
    - * EVAL_BODY_TAG if there are errors to display - */ - @Override - public int doStartTag() throws JspException { - HttpServletRequest request = (HttpServletRequest) getPageContext().getRequest(); - ActionBean mainBean = (ActionBean) request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN); - FormTag formTag = getParentTag(FormTag.class); - ValidationErrors errors = null; - - // If we are supplied with an 'action' attribute then display the errors - // only if that action matches the 'action' of the current action bean - if (getAction() != null) { - if (mainBean != null) { - String mainAction = StripesFilter.getConfiguration() - .getActionResolver().getUrlBinding(mainBean.getClass()); - - if (getAction().equals(mainAction)) { - errors = mainBean.getContext().getValidationErrors(); - } - } - } - // Else we don't have an 'action' attribute, so see if we are nested in - // a form tag - else if (formTag != null) { - ActionBean formBean = formTag.getActionBean(); - if (formBean != null) { - errors = formBean.getContext().getValidationErrors(); - } - - } - // Else if no name was set, and we're not in a action tag, we're global and ok to display - else if (mainBean != null) { - errors = mainBean.getContext().getValidationErrors(); - } - - // If we found some errors that are applicable for display, figure out what to do - if (errors != null) { - // Using a set ensures that duplicate messages get filtered out, which can - // happen during multi-row validation - this.allErrors = new TreeSet(new ErrorComparator()); - - if (this.field != null) { - // we're filtering for a specific field - List fieldErrors = errors.get(this.field); - if (fieldErrors != null) { - this.allErrors.addAll(fieldErrors); - } - } - else if (this.globalErrorsOnly) { - List globalErrors = errors.get(ValidationErrors.GLOBAL_ERROR); - if (globalErrors != null) { - this.allErrors.addAll(globalErrors); - } - } - else { - for (List fieldErrors : errors.values()) { - if (fieldErrors != null) { - this.allErrors.addAll(fieldErrors); - } - } - } + // If we found some errors that are applicable for display, figure out what to do + if (errors != null) { + // Using a set ensures that duplicate messages get filtered out, which can + // happen during multi-row validation + this.allErrors = new TreeSet(new ErrorComparator()); + + if (this.field != null) { + // we're filtering for a specific field + List fieldErrors = errors.get(this.field); + if (fieldErrors != null) { + this.allErrors.addAll(fieldErrors); } - - // Make sure that after all this we really do have some errors - if (this.allErrors != null && this.allErrors.size() > 0) { - this.display = true; - this.errorIterator = this.allErrors.iterator(); - this.currentError = this.errorIterator.next(); // load up the first error - return EVAL_BODY_BUFFERED; + } else if (this.globalErrorsOnly) { + List globalErrors = errors.get(ValidationErrors.GLOBAL_ERROR); + if (globalErrors != null) { + this.allErrors.addAll(globalErrors); } - else { - this.display = false; - return SKIP_BODY; - + } else { + for (List fieldErrors : errors.values()) { + if (fieldErrors != null) { + this.allErrors.addAll(fieldErrors); + } } + } } - - /** Sets the context variables for the current error and index */ - public void doInitBody() throws JspException { - // Apply TEI attributes - getPageContext().setAttribute("index", this.index); - getPageContext().setAttribute("error", this.currentError); + // Make sure that after all this we really do have some errors + if (this.allErrors != null && this.allErrors.size() > 0) { + this.display = true; + this.errorIterator = this.allErrors.iterator(); + this.currentError = this.errorIterator.next(); // load up the first error + return EVAL_BODY_BUFFERED; + } else { + this.display = false; + return SKIP_BODY; } - - - /** - * Manages iteration, running again if there are more errors to display. If there is no - * nested FieldError tag, will ensure that the body is evaluated only once. - * - * @return EVAL_BODY_TAG if there are more errors to display, SKIP_BODY otherwise - */ - public int doAfterBody() throws JspException { - if (this.display && this.nestedErrorTagPresent && this.errorIterator.hasNext()) { - this.currentError = this.errorIterator.next(); - this.index++; - - // Reapply TEI attributes - getPageContext().setAttribute("index", this.index); - getPageContext().setAttribute("error", this.currentError); - return EVAL_BODY_BUFFERED; - } - else { - return SKIP_BODY; - } + } + + /** Sets the context variables for the current error and index */ + public void doInitBody() throws JspException { + // Apply TEI attributes + getPageContext().setAttribute("index", this.index); + getPageContext().setAttribute("error", this.currentError); + } + + /** + * Manages iteration, running again if there are more errors to display. If there is no nested + * FieldError tag, will ensure that the body is evaluated only once. + * + * @return EVAL_BODY_TAG if there are more errors to display, SKIP_BODY otherwise + */ + public int doAfterBody() throws JspException { + if (this.display && this.nestedErrorTagPresent && this.errorIterator.hasNext()) { + this.currentError = this.errorIterator.next(); + this.index++; + + // Reapply TEI attributes + getPageContext().setAttribute("index", this.index); + getPageContext().setAttribute("error", this.currentError); + return EVAL_BODY_BUFFERED; + } else { + return SKIP_BODY; } + } + + /** + * Output the error list if this was an empty body tag and we're fully controlling output* + * + * @return EVAL_PAGE always + * @throws JspException + */ + @Override + public int doEndTag() throws JspException { + try { + JspWriter writer = getPageContext().getOut(); + + if (this.display && !this.nestedErrorTagPresent) { + // Output all errors in a standard format + Locale locale = getPageContext().getRequest().getLocale(); + ResourceBundle bundle = null; - - /** - * Output the error list if this was an empty body tag and we're fully controlling output* - * - * @return EVAL_PAGE always - * @throws JspException - */ - @Override - public int doEndTag() throws JspException { try { - JspWriter writer = getPageContext().getOut(); - - if (this.display && !this.nestedErrorTagPresent) { - // Output all errors in a standard format - Locale locale = getPageContext().getRequest().getLocale(); - ResourceBundle bundle = null; - - try { - bundle = StripesFilter.getConfiguration() - .getLocalizationBundleFactory().getErrorMessageBundle(locale); - } - catch (MissingResourceException mre) { - log.warn("The errors tag could not find the error messages resource bundle. ", - "As a result default headers/footers etc. will be used. Check that ", - "you have a StripesResources.properties in your classpath (unless ", - "of course you have configured a different bundle)."); - } - - // Fetch the header and footer - String header = getResource(bundle, "header", DEFAULT_HEADER); - String footer = getResource(bundle, "footer", DEFAULT_FOOTER); - String openElement = getResource(bundle, "beforeError", "
  • "); - String closeElement = getResource(bundle, "afterError", "
  • "); - - // Write out the error messages - writer.write(header); - - for (ValidationError fieldError : this.allErrors) { - String message = fieldError.getMessage(locale); - if (message != null && message.length() > 0) { - writer.write(openElement); - writer.write(message); - writer.write(closeElement); - } - } - - writer.write(footer); - } - else if (this.display && this.nestedErrorTagPresent) { - // Output the collective body content - getBodyContent().writeOut(writer); - } - - // Reset the instance state in case the container decides to pool the tag - this.display = false; - this.nestedErrorTagPresent = false; - this.allErrors = null; - this.errorIterator = null; - this.currentError = null; - this.index = 0; - - return EVAL_PAGE; + bundle = + StripesFilter.getConfiguration() + .getLocalizationBundleFactory() + .getErrorMessageBundle(locale); + } catch (MissingResourceException mre) { + log.warn( + "The errors tag could not find the error messages resource bundle. ", + "As a result default headers/footers etc. will be used. Check that ", + "you have a StripesResources.properties in your classpath (unless ", + "of course you have configured a different bundle)."); } - catch (IOException e) { - JspException jspe = new JspException("IOException encountered while writing errors " + - "tag to the JspWriter.", e); - log.warn(jspe); - throw jspe; - } - } - /** - * Utility method that is used to lookup the resources used for the errors header, - * footer, and the strings that go before and after each error. - * - * @param bundle the bundle to look up the resource from - * @param name the name of the resource to lookup (prefixes will be added) - * @param fallback a value to return if no resource can be found - * @return the value to use for the named resource - */ - protected String getResource(ResourceBundle bundle, String name, String fallback) { - if (bundle == null) { - return fallback; + // Fetch the header and footer + String header = getResource(bundle, "header", DEFAULT_HEADER); + String footer = getResource(bundle, "footer", DEFAULT_FOOTER); + String openElement = getResource(bundle, "beforeError", "
  • "); + String closeElement = getResource(bundle, "afterError", "
  • "); + + // Write out the error messages + writer.write(header); + + for (ValidationError fieldError : this.allErrors) { + String message = fieldError.getMessage(locale); + if (message != null && message.length() > 0) { + writer.write(openElement); + writer.write(message); + writer.write(closeElement); + } } - String resource = null; - if (this.field != null) { - try { resource = bundle.getString("stripes.fieldErrors." + name); } - catch (MissingResourceException mre) { /* Do nothing */ } - } + writer.write(footer); + } else if (this.display && this.nestedErrorTagPresent) { + // Output the collective body content + getBodyContent().writeOut(writer); + } + + // Reset the instance state in case the container decides to pool the tag + this.display = false; + this.nestedErrorTagPresent = false; + this.allErrors = null; + this.errorIterator = null; + this.currentError = null; + this.index = 0; + + return EVAL_PAGE; + } catch (IOException e) { + JspException jspe = + new JspException( + "IOException encountered while writing errors " + "tag to the JspWriter.", e); + log.warn(jspe); + throw jspe; + } + } + + /** + * Utility method that is used to lookup the resources used for the errors header, footer, and the + * strings that go before and after each error. + * + * @param bundle the bundle to look up the resource from + * @param name the name of the resource to lookup (prefixes will be added) + * @param fallback a value to return if no resource can be found + * @return the value to use for the named resource + */ + protected String getResource(ResourceBundle bundle, String name, String fallback) { + if (bundle == null) { + return fallback; + } - if (resource == null) { - try { resource = bundle.getString("stripes.errors." + name); } - catch (MissingResourceException mre) { resource = fallback; } - } + String resource = null; + if (this.field != null) { + try { + resource = bundle.getString("stripes.fieldErrors." + name); + } catch (MissingResourceException mre) { + /* Do nothing */ + } + } - return resource; + if (resource == null) { + try { + resource = bundle.getString("stripes.errors." + name); + } catch (MissingResourceException mre) { + resource = fallback; + } } - /** - * Inner class Comparator used to provide a consistent ordering of validation errors. - * Sorting is done by field name (the programmatic one, not the user visible one). Errors - * without field names sort to the top since it is assumed that these are global errors - * as oppose to field specific ones. - */ - private static class ErrorComparator implements Comparator { - public int compare(ValidationError e1, ValidationError e2) { - // Identical errors should be suppressed - if (e1.equals(e2)) { - return 0; - } - - String fn1 = e1.getFieldName(); - String fn2 = e2.getFieldName(); - boolean e1Global = fn1 == null || fn1.equals(ValidationErrors.GLOBAL_ERROR); - boolean e2Global = fn2 == null || fn2.equals(ValidationErrors.GLOBAL_ERROR); - - // Sort globals above non-global errors - if (e1Global && !e2Global) { - return -1; - } - if (e2Global && !e1Global) { - return 1; - } - if (fn1 == null && fn2 == null) { - return 0; - } - - // Then sort by field name, and if field names match make the first one come first - int result = e1.getFieldName().compareTo(e2.getFieldName()); - if (result == 0) {result = 1;} - return result; - } + return resource; + } + + /** + * Inner class Comparator used to provide a consistent ordering of validation errors. Sorting is + * done by field name (the programmatic one, not the user visible one). Errors without field names + * sort to the top since it is assumed that these are global errors as oppose to field specific + * ones. + */ + private static class ErrorComparator implements Comparator { + public int compare(ValidationError e1, ValidationError e2) { + // Identical errors should be suppressed + if (e1.equals(e2)) { + return 0; + } + + String fn1 = e1.getFieldName(); + String fn2 = e2.getFieldName(); + boolean e1Global = fn1 == null || fn1.equals(ValidationErrors.GLOBAL_ERROR); + boolean e2Global = fn2 == null || fn2.equals(ValidationErrors.GLOBAL_ERROR); + + // Sort globals above non-global errors + if (e1Global && !e2Global) { + return -1; + } + if (e2Global && !e1Global) { + return 1; + } + if (fn1 == null && fn2 == null) { + return 0; + } + + // Then sort by field name, and if field names match make the first one come first + int result = e1.getFieldName().compareTo(e2.getFieldName()); + if (result == 0) { + result = 1; + } + return result; } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsTagExtraInfo.java b/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsTagExtraInfo.java index 8daae0664..272098633 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsTagExtraInfo.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/ErrorsTagExtraInfo.java @@ -14,36 +14,29 @@ */ package net.sourceforge.stripes.tag; +import jakarta.servlet.jsp.tagext.TagData; +import jakarta.servlet.jsp.tagext.TagExtraInfo; +import jakarta.servlet.jsp.tagext.VariableInfo; import net.sourceforge.stripes.validation.ValidationError; -import javax.servlet.jsp.tagext.TagExtraInfo; -import javax.servlet.jsp.tagext.VariableInfo; -import javax.servlet.jsp.tagext.TagData; - /** - * This tag extra info exposes index and error context variables for the body - * of the errors tag. + * This tag extra info exposes index and error context variables for the body of the errors tag. * * @author Greg Hinkle */ public class ErrorsTagExtraInfo extends TagExtraInfo { - /** Returns an array of length two, for the variables exposed. */ - @Override - public VariableInfo[] getVariableInfo(TagData data) { - VariableInfo[] scriptVars = new VariableInfo[2]; + /** Returns an array of length two, for the variables exposed. */ + @Override + public VariableInfo[] getVariableInfo(TagData data) { + VariableInfo[] scriptVars = new VariableInfo[2]; - scriptVars[0] = new VariableInfo("index", - "java.lang.Number", - true, - VariableInfo.NESTED); + scriptVars[0] = new VariableInfo("index", "java.lang.Number", true, VariableInfo.NESTED); - // TODO: ValidationError should expose properties like field name - scriptVars[1] = new VariableInfo("error", - ValidationError.class.getName(), - true, - VariableInfo.NESTED); + // TODO: ValidationError should expose properties like field name + scriptVars[1] = + new VariableInfo("error", ValidationError.class.getName(), true, VariableInfo.NESTED); - return scriptVars; - } + return scriptVars; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/FieldMetadataTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/FieldMetadataTag.java index a0f166b50..04003ae59 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/FieldMetadataTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/FieldMetadataTag.java @@ -1,5 +1,8 @@ package net.sourceforge.stripes.tag; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspWriter; +import jakarta.servlet.jsp.tagext.BodyTag; import java.io.IOException; import java.util.Arrays; import java.util.Date; @@ -9,11 +12,6 @@ import java.util.Map; import java.util.Random; import java.util.Set; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspWriter; -import javax.servlet.jsp.tagext.BodyTag; - import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.ajax.JavaScriptBuilder; import net.sourceforge.stripes.controller.ParameterName; @@ -27,409 +25,406 @@ import net.sourceforge.stripes.validation.ValidationMetadataProvider; /** - *

    Field metadata tag for use with the Stripes framework. Exposes field properties via JavaScript to + * Field metadata tag for use with the Stripes framework. Exposes field properties via JavaScript to * allow client side validation. If this tag has a body it will be wrapped with JavaScript tags for - * convenience.

    - * + * convenience. + * * @author Aaron Porter - * */ public class FieldMetadataTag extends HtmlTagSupport implements BodyTag { - /** Log used to log error and debugging information for this class. */ - private static final Log log = Log.getInstance(FormTag.class); - - /** Name of variable to hold metadata. */ - private String var; - /** Optional comma separated list of additional fields to expose. */ - private String fields; - /** Set to true to include type information for all fields. */ - private boolean includeType = false; - /** Set to true to include the fully qualified class name for all fields. */ - private boolean fqn = false; - /** Stores the value of the action attribute before the context gets appended. */ - private String actionWithoutContext; - - public FormTag getForm() { - return getParentTag(FormTag.class); + /** Log used to log error and debugging information for this class. */ + private static final Log log = Log.getInstance(FormTag.class); + + /** Name of variable to hold metadata. */ + private String var; + /** Optional comma separated list of additional fields to expose. */ + private String fields; + /** Set to true to include type information for all fields. */ + private boolean includeType = false; + /** Set to true to include the fully qualified class name for all fields. */ + private boolean fqn = false; + /** Stores the value of the action attribute before the context gets appended. */ + private String actionWithoutContext; + + public FormTag getForm() { + return getParentTag(FormTag.class); + } + + /** + * Builds a string that contains field metadata in a JavaScript object. + * + * @return JavaScript object containing field metadata + */ + private String getMetadata() { + ActionBean bean = null; + + String action = getAction(); + + FormTag form = getForm(); + + if (form != null) { + if (action != null) + log.warn( + "Parameters action and/or beanclass specified but field-metadata tag is inside of a Stripes form tag. The bean will be pulled from the form tag."); + + action = form.getAction(); } - /** - * Builds a string that contains field metadata in a JavaScript object. - * - * @return JavaScript object containing field metadata - */ - private String getMetadata() { - ActionBean bean = null; + if (form != null) bean = form.getActionBean(); - String action = getAction(); + Class beanClass = null; - FormTag form = getForm(); - - if (form != null) { - if (action != null) - log.warn("Parameters action and/or beanclass specified but field-metadata tag is inside of a Stripes form tag. The bean will be pulled from the form tag."); - - action = form.getAction(); + if (bean != null) beanClass = bean.getClass(); + else if (action != null) { + beanClass = StripesFilter.getConfiguration().getActionResolver().getActionBeanType(action); + if (beanClass != null) { + try { + bean = StripesFilter.getConfiguration().getObjectFactory().newInstance(beanClass); + } catch (Exception e) { + log.error(e); + return null; } + } + } - if (form != null) - bean = form.getActionBean(); - - Class beanClass = null; - - if (bean != null) - beanClass = bean.getClass(); - else if (action != null) { - beanClass = StripesFilter.getConfiguration().getActionResolver().getActionBeanType(action); - if (beanClass != null) { - try { - bean = StripesFilter.getConfiguration().getObjectFactory().newInstance(beanClass); - } - catch (Exception e) { - log.error(e); - return null; - } - } - } + if (beanClass == null) { + log.error( + "Couldn't determine ActionBean class from FormTag! One of the following conditions must be met:\r\n\t", + "1. Include this tag inside of a stripes:form tag\r\n\t", + "2. Use the action parameter\r\n\t", + "3. Use the beanclass parameter"); + return null; + } - if (beanClass == null) { - log.error("Couldn't determine ActionBean class from FormTag! One of the following conditions must be met:\r\n\t", - "1. Include this tag inside of a stripes:form tag\r\n\t", - "2. Use the action parameter\r\n\t", - "3. Use the beanclass parameter"); - return null; - } + ValidationMetadataProvider metadataProvider = + StripesFilter.getConfiguration().getValidationMetadataProvider(); - ValidationMetadataProvider metadataProvider = StripesFilter.getConfiguration() - .getValidationMetadataProvider(); + if (metadataProvider == null) { + log.error("Couldn't get ValidationMetadataProvider!"); + return null; + } - if (metadataProvider == null) { - log.error("Couldn't get ValidationMetadataProvider!"); - return null; - } + Map metadata = metadataProvider.getValidationMetadata(beanClass); - Map metadata = metadataProvider - .getValidationMetadata(beanClass); + StringBuilder sb = new StringBuilder("{\r\n\t\t"); - StringBuilder sb = new StringBuilder("{\r\n\t\t"); + Set fields = new HashSet(); - Set fields = new HashSet(); - - if (form != null) { - for (String field : form.getRegisteredFields()) { - fields.add(new ParameterName(field).getStrippedName()); - } - } + if (form != null) { + for (String field : form.getRegisteredFields()) { + fields.add(new ParameterName(field).getStrippedName()); + } + } - if ((this.fields != null) && (this.fields.trim().length() > 0)) - fields.addAll(Arrays.asList(this.fields.split(","))); - else if (form == null) { - log.error("Fields attribute is required when field-metadata tag isn't inside of a Stripes form tag."); - return null; - } + if ((this.fields != null) && (this.fields.trim().length() > 0)) + fields.addAll(Arrays.asList(this.fields.split(","))); + else if (form == null) { + log.error( + "Fields attribute is required when field-metadata tag isn't inside of a Stripes form tag."); + return null; + } - boolean first = true; - - Locale locale = getPageContext().getRequest().getLocale(); - - for (String field : fields) { - - PropertyExpressionEvaluation eval = null; - - try { - eval = new PropertyExpressionEvaluation(PropertyExpression.getExpression(field), bean); - } - catch (Exception e) { - continue; - } - - Class fieldType = eval.getType(); - - ValidationMetadata data = metadata.get(field); - - StringBuilder fieldInfo = new StringBuilder(); - - if (fieldType.isPrimitive() || Number.class.isAssignableFrom(fieldType) - || Date.class.isAssignableFrom(fieldType) || includeType) { - fieldInfo.append("type:").append( - JavaScriptBuilder.quote(fqn ? fieldType.getName() : fieldType - .getSimpleName())); - } - - Class typeConverterClass = null; - - if (data != null) { - if (fieldInfo.length() > 0) - fieldInfo.append(','); - - fieldInfo.append("required:").append(data.required()) - .append(",ignore:").append(data.ignore()) - .append(",encrypted:").append(data.encrypted()) - .append(",trim:").append(data.trim()); - - if (data.on() != null) { - fieldInfo.append(",on:["); - Iterator it = data.on().iterator(); - while (it.hasNext()) { - fieldInfo.append(JavaScriptBuilder.quote(it.next())); - if (it.hasNext()) - fieldInfo.append(","); - } - fieldInfo.append("]"); - } - if (data.mask() != null) - fieldInfo.append(",mask:").append("new RegExp(") - .append(JavaScriptBuilder.quote("^" + data.mask().toString() + "$")) - .append(")"); - if (data.minlength() != null) - fieldInfo.append(",minlength:").append(data.minlength()); - if (data.maxlength() != null) - fieldInfo.append(",maxlength:").append(data.maxlength()); - if (data.minvalue() != null) - fieldInfo.append(",minvalue:").append(data.minvalue()); - if (data.maxvalue() != null) - fieldInfo.append(",maxvalue:").append(data.maxvalue()); - - String label = data.label(); - if (data.label() == null) { - label = LocalizationUtility.getLocalizedFieldName(field, form == null ? null - : form.getAction(), form == null ? null : form.getActionBeanClass(), - locale); - } - if (label != null) - fieldInfo.append(",label:").append(JavaScriptBuilder.quote(label)); - - typeConverterClass = data.converter(); - } - - // If we couldn't get the converter from the validation annotation - // try to get it from the TypeConverterFactory - if (typeConverterClass == null) { - try { - typeConverterClass = StripesFilter.getConfiguration().getTypeConverterFactory() - .getTypeConverter(fieldType, pageContext.getRequest().getLocale()) - .getClass(); - } - catch (Exception e) { - // Just ignore it - } - } - - if (typeConverterClass != null) { - fieldInfo.append(fieldInfo.length() > 0 ? "," : "").append("typeConverter:") - .append( - JavaScriptBuilder.quote(fqn ? typeConverterClass.getName() - : typeConverterClass.getSimpleName())); - } - - - if (fieldInfo.length() > 0) { - if (first) - first = false; - else - sb.append(",\r\n\t\t"); - - sb.append(JavaScriptBuilder.quote(field)).append(":{"); - - sb.append(fieldInfo); - - sb.append("}"); - } - } + boolean first = true; - sb.append("\r\n\t}"); + Locale locale = getPageContext().getRequest().getLocale(); - return sb.toString(); - } + for (String field : fields) { - public FieldMetadataTag() { - getAttributes().put("type", "text/javascript"); - } + PropertyExpressionEvaluation eval = null; - public void doInitBody() throws JspException { - } + try { + eval = new PropertyExpressionEvaluation(PropertyExpression.getExpression(field), bean); + } catch (Exception e) { + continue; + } - public int doAfterBody() throws JspException { - return SKIP_BODY; - } + Class fieldType = eval.getType(); - @Override - public int doStartTag() throws JspException { - getPageContext().setAttribute(getVar(), new Var(getMetadata())); - return EVAL_BODY_BUFFERED; - } + ValidationMetadata data = metadata.get(field); - @Override - public int doEndTag() throws JspException { - JspWriter writer = getPageContext().getOut(); - - String body = getBodyContentAsString(); - - if (body != null) { - try { - String contentType = getPageContext().getResponse().getContentType(); - - // Catches application/x-javascript, text/javascript, and text/ecmascript - boolean pageIsScript = contentType != null && contentType.toLowerCase().contains("ascript"); - - // Don't write the script tags if this page is a script - if (!pageIsScript) { - writeOpenTag(writer, "script"); - writer.write("//"); - writeCloseTag(writer, "script"); - } - } - catch (IOException ioe) { - throw new StripesJspException("IOException while writing output in LinkTag.", ioe); - } + StringBuilder fieldInfo = new StringBuilder(); + + if (fieldType.isPrimitive() + || Number.class.isAssignableFrom(fieldType) + || Date.class.isAssignableFrom(fieldType) + || includeType) { + fieldInfo + .append("type:") + .append(JavaScriptBuilder.quote(fqn ? fieldType.getName() : fieldType.getSimpleName())); + } + + Class typeConverterClass = null; + + if (data != null) { + if (fieldInfo.length() > 0) fieldInfo.append(','); + + fieldInfo + .append("required:") + .append(data.required()) + .append(",ignore:") + .append(data.ignore()) + .append(",encrypted:") + .append(data.encrypted()) + .append(",trim:") + .append(data.trim()); + + if (data.on() != null) { + fieldInfo.append(",on:["); + Iterator it = data.on().iterator(); + while (it.hasNext()) { + fieldInfo.append(JavaScriptBuilder.quote(it.next())); + if (it.hasNext()) fieldInfo.append(","); + } + fieldInfo.append("]"); + } + if (data.mask() != null) + fieldInfo + .append(",mask:") + .append("new RegExp(") + .append(JavaScriptBuilder.quote("^" + data.mask().toString() + "$")) + .append(")"); + if (data.minlength() != null) fieldInfo.append(",minlength:").append(data.minlength()); + if (data.maxlength() != null) fieldInfo.append(",maxlength:").append(data.maxlength()); + if (data.minvalue() != null) fieldInfo.append(",minvalue:").append(data.minvalue()); + if (data.maxvalue() != null) fieldInfo.append(",maxvalue:").append(data.maxvalue()); + + String label = data.label(); + if (data.label() == null) { + label = + LocalizationUtility.getLocalizedFieldName( + field, + form == null ? null : form.getAction(), + form == null ? null : form.getActionBeanClass(), + locale); } - - // Only keep the type attribute between uses - String type = getAttributes().get("type"); - getAttributes().clear(); - getAttributes().put("type", type); + if (label != null) fieldInfo.append(",label:").append(JavaScriptBuilder.quote(label)); + + typeConverterClass = data.converter(); + } + + // If we couldn't get the converter from the validation annotation + // try to get it from the TypeConverterFactory + if (typeConverterClass == null) { + try { + typeConverterClass = + StripesFilter.getConfiguration() + .getTypeConverterFactory() + .getTypeConverter(fieldType, pageContext.getRequest().getLocale()) + .getClass(); + } catch (Exception e) { + // Just ignore it + } + } - return SKIP_BODY; - } + if (typeConverterClass != null) { + fieldInfo + .append(fieldInfo.length() > 0 ? "," : "") + .append("typeConverter:") + .append( + JavaScriptBuilder.quote( + fqn ? typeConverterClass.getName() : typeConverterClass.getSimpleName())); + } - public String getVar() { - return var; - } + if (fieldInfo.length() > 0) { + if (first) first = false; + else sb.append(",\r\n\t\t"); - /** - * Sets the name of the variable to hold metadata. - * - * @param var the name of the attribute that will contain field metadata - */ - public void setVar(String var) { - this.var = var; - } + sb.append(JavaScriptBuilder.quote(field)).append(":{"); - public String getFields() { - return fields; - } + sb.append(fieldInfo); - /** - * Optional comma separated list of additional fields to expose. Any fields that have - * already been added to the Stripes form tag will automatically be included. - * - * @param fields comma separated list of field names - */ - public void setFields(String fields) { - this.fields = fields; + sb.append("}"); + } } - public boolean isIncludeType() { - return includeType; - } + sb.append("\r\n\t}"); - /** - * Set to true to include type information for all fields. By default, type information is only - * included for primitives, numbers, and dates. - * - * @param includeType include type info for all fields - */ - public void setIncludeType(boolean includeType) { - this.includeType = includeType; - } + return sb.toString(); + } - public boolean isFqn() { - return fqn; - } + public FieldMetadataTag() { + getAttributes().put("type", "text/javascript"); + } - /** - * Set to true to include the fully qualified class name for all fields. - * - * @param fqn include fully qualified class name for all fields - */ - public void setFqn(boolean fqn) { - this.fqn = fqn; - } + public void doInitBody() throws JspException {} - /** - * Sets the action for the form. If the form action begins with a slash, and does not already - * contain the context path, then the context path of the web application will get prepended to - * the action before it is set. In general actions should be specified as "absolute" - * paths within the web application, therefore allowing them to function correctly regardless of - * the address currently shown in the browser's address bar. - * - * @param action the action path, relative to the root of the web application - */ - public void setAction(String action) { - // Use the action resolver to figure out what the appropriate URL binding if for - // this path and use that if there is one, otherwise just use the action passed in - String binding = StripesFilter.getConfiguration().getActionResolver() - .getUrlBindingFromPath(action); - if (binding != null) { - this.actionWithoutContext = binding; - } - else { - this.actionWithoutContext = action; - } - } + public int doAfterBody() throws JspException { + return SKIP_BODY; + } - public String getAction() { - return this.actionWithoutContext; - } + @Override + public int doStartTag() throws JspException { + getPageContext().setAttribute(getVar(), new Var(getMetadata())); + return EVAL_BODY_BUFFERED; + } - /** - * Sets the 'action' attribute by inspecting the bean class provided and asking the current - * ActionResolver what the appropriate URL is. - * - * @param beanclass the String FQN of the class, or a Class representing the class - * @throws StripesJspException if the URL cannot be determined for any reason, most likely - * because of a mis-spelled class name, or a class that's not an ActionBean - */ - public void setBeanclass(Object beanclass) throws StripesJspException { - String url = getActionBeanUrl(beanclass); - if (url == null) { - throw new StripesJspException( - "Could not determine action from 'beanclass' supplied. " - + "The value supplied was '" - + beanclass - + "'. Please ensure that this bean type " - + "exists and is in the classpath. If you are developing a page and the ActionBean " - + "does not yet exist, consider using the 'action' attribute instead for now."); + @Override + public int doEndTag() throws JspException { + JspWriter writer = getPageContext().getOut(); + + String body = getBodyContentAsString(); + + if (body != null) { + try { + String contentType = getPageContext().getResponse().getContentType(); + + // Catches application/x-javascript, text/javascript, and text/ecmascript + boolean pageIsScript = contentType != null && contentType.toLowerCase().contains("ascript"); + + // Don't write the script tags if this page is a script + if (!pageIsScript) { + writeOpenTag(writer, "script"); + writer.write("//"); + writeCloseTag(writer, "script"); } + } catch (IOException ioe) { + throw new StripesJspException("IOException while writing output in LinkTag.", ioe); + } } - /** Corresponding getter for 'beanclass', will always return null. */ - public Object getBeanclass() { - return null; + // Only keep the type attribute between uses + String type = getAttributes().get("type"); + getAttributes().clear(); + getAttributes().put("type", type); + + return SKIP_BODY; + } + + public String getVar() { + return var; + } + + /** + * Sets the name of the variable to hold metadata. + * + * @param var the name of the attribute that will contain field metadata + */ + public void setVar(String var) { + this.var = var; + } + + public String getFields() { + return fields; + } + + /** + * Optional comma separated list of additional fields to expose. Any fields that have already been + * added to the Stripes form tag will automatically be included. + * + * @param fields comma separated list of field names + */ + public void setFields(String fields) { + this.fields = fields; + } + + public boolean isIncludeType() { + return includeType; + } + + /** + * Set to true to include type information for all fields. By default, type information is only + * included for primitives, numbers, and dates. + * + * @param includeType include type info for all fields + */ + public void setIncludeType(boolean includeType) { + this.includeType = includeType; + } + + public boolean isFqn() { + return fqn; + } + + /** + * Set to true to include the fully qualified class name for all fields. + * + * @param fqn include fully qualified class name for all fields + */ + public void setFqn(boolean fqn) { + this.fqn = fqn; + } + + /** + * Sets the action for the form. If the form action begins with a slash, and does not already + * contain the context path, then the context path of the web application will get prepended to + * the action before it is set. In general actions should be specified as "absolute" + * paths within the web application, therefore allowing them to function correctly regardless of + * the address currently shown in the browser's address bar. + * + * @param action the action path, relative to the root of the web application + */ + public void setAction(String action) { + // Use the action resolver to figure out what the appropriate URL binding if for + // this path and use that if there is one, otherwise just use the action passed in + String binding = + StripesFilter.getConfiguration().getActionResolver().getUrlBindingFromPath(action); + if (binding != null) { + this.actionWithoutContext = binding; + } else { + this.actionWithoutContext = action; + } + } + + public String getAction() { + return this.actionWithoutContext; + } + + /** + * Sets the 'action' attribute by inspecting the bean class provided and asking the current + * ActionResolver what the appropriate URL is. + * + * @param beanclass the String FQN of the class, or a Class representing the class + * @throws StripesJspException if the URL cannot be determined for any reason, most likely because + * of a mis-spelled class name, or a class that's not an ActionBean + */ + public void setBeanclass(Object beanclass) throws StripesJspException { + String url = getActionBeanUrl(beanclass); + if (url == null) { + throw new StripesJspException( + "Could not determine action from 'beanclass' supplied. " + + "The value supplied was '" + + beanclass + + "'. Please ensure that this bean type " + + "exists and is in the classpath. If you are developing a page and the ActionBean " + + "does not yet exist, consider using the 'action' attribute instead for now."); + } else { + setAction(url); + } + } + + /** Corresponding getter for 'beanclass', will always return null. */ + public Object getBeanclass() { + return null; + } + /** + * This is what is placed into the request attribute. It allows us to get the field metadata as + * well as the form id. + */ + public class Var { + private String fieldMetadata, formId; + + private Var(String fieldMetadata) { + this.fieldMetadata = fieldMetadata; + FormTag form = getForm(); + if (form != null) { + if (form.getId() == null) form.setId("stripes-" + new Random().nextInt()); + this.formId = form.getId(); + } } - /** - * This is what is placed into the request attribute. It allows us to - * get the field metadata as well as the form id. - */ - public class Var { - private String fieldMetadata, formId; - - private Var(String fieldMetadata) { - this.fieldMetadata = fieldMetadata; - FormTag form = getForm(); - if (form != null) { - if (form.getId() == null) - form.setId("stripes-" + new Random().nextInt()); - this.formId = form.getId(); - } - } - @Override - public String toString() { - return fieldMetadata; - } + @Override + public String toString() { + return fieldMetadata; + } - public String getFormId() { - return formId; - } + public String getFormId() { + return formId; } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/FormTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/FormTag.java index 370349578..7197fd0d0 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/FormTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/FormTag.java @@ -14,513 +14,568 @@ */ package net.sourceforge.stripes.tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspWriter; +import jakarta.servlet.jsp.tagext.BodyTag; +import jakarta.servlet.jsp.tagext.TryCatchFinally; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.Wizard; +import net.sourceforge.stripes.controller.ActionResolver; import net.sourceforge.stripes.controller.StripesConstants; import net.sourceforge.stripes.controller.StripesFilter; -import net.sourceforge.stripes.controller.ActionResolver; import net.sourceforge.stripes.exception.StripesJspException; import net.sourceforge.stripes.util.CryptoUtil; import net.sourceforge.stripes.util.HtmlUtil; import net.sourceforge.stripes.util.Log; import net.sourceforge.stripes.util.StringUtil; import net.sourceforge.stripes.util.UrlBuilder; -import net.sourceforge.stripes.validation.ValidationErrors; import net.sourceforge.stripes.validation.ValidationError; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspWriter; -import javax.servlet.jsp.tagext.BodyTag; -import javax.servlet.jsp.tagext.TryCatchFinally; -import java.io.IOException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.List; +import net.sourceforge.stripes.validation.ValidationErrors; /** - *

    Form tag for use with the Stripes framework. Supports all of the HTML attributes applicable - * to the form tag, with one exception: due to JSP attribute naming restrictions accept-charset is - * specified as acceptcharset (but will be rendered correctly in the output HTML).

    + * Form tag for use with the Stripes framework. Supports all of the HTML attributes applicable to + * the form tag, with one exception: due to JSP attribute naming restrictions accept-charset is + * specified as acceptcharset (but will be rendered correctly in the output HTML). * * @author Tim Fennell */ -public class FormTag extends HtmlTagSupport implements BodyTag, TryCatchFinally, ParameterizableTag { - /** Log used to log error and debugging information for this class. */ - private static final Log log = Log.getInstance(FormTag.class); - - /** Stores the field name (or magic values ''/'first') to set focus on. */ - private String focus; - private boolean focusSet = false; - private boolean partial = false; - private String enctype = null; - private String method = null; - - /** Stores the value of the action attribute before the context gets appended. */ - private String actionWithoutContext; - - /** Stores the value of the beanclass attribute. */ - private Object beanclass; - - /** - * The {@link ActionBean} class to which the form will submit, as determined by the - * {@link ActionResolver}. This may be null if the action attribute is set but its value does - * not resolve to an {@link ActionBean}. - */ - private Class actionBeanClass; - - /** Builds the action attribute with parameters */ - private UrlBuilder urlBuilder; - - /** A map of field name to field type for all fields registered with the form. */ - private Map> fieldsPresent = new HashMap>(); - - /** - * Sets the action for the form. If the form action begins with a slash, and does not - * already contain the context path, then the context path of the web application will get - * prepended to the action before it is set. In general actions should be specified as - * "absolute" paths within the web application, therefore allowing them to function - * correctly regardless of the address currently shown in the browser's address bar. - * - * @param action the action path, relative to the root of the web application - */ - public void setAction(String action) { - this.actionWithoutContext = action; +public class FormTag extends HtmlTagSupport + implements BodyTag, TryCatchFinally, ParameterizableTag { + /** Log used to log error and debugging information for this class. */ + private static final Log log = Log.getInstance(FormTag.class); + + /** Stores the field name (or magic values ''/'first') to set focus on. */ + private String focus; + + private boolean focusSet = false; + private boolean partial = false; + private String enctype = null; + private String method = null; + + /** Stores the value of the action attribute before the context gets appended. */ + private String actionWithoutContext; + + /** Stores the value of the beanclass attribute. */ + private Object beanclass; + + /** + * The {@link ActionBean} class to which the form will submit, as determined by the {@link + * ActionResolver}. This may be null if the action attribute is set but its value does not resolve + * to an {@link ActionBean}. + */ + private Class actionBeanClass; + + /** Builds the action attribute with parameters */ + private UrlBuilder urlBuilder; + + /** A map of field name to field type for all fields registered with the form. */ + private Map> fieldsPresent = new HashMap>(); + + /** + * Sets the action for the form. If the form action begins with a slash, and does not already + * contain the context path, then the context path of the web application will get prepended to + * the action before it is set. In general actions should be specified as "absolute" + * paths within the web application, therefore allowing them to function correctly regardless of + * the address currently shown in the browser's address bar. + * + * @param action the action path, relative to the root of the web application + */ + public void setAction(String action) { + this.actionWithoutContext = action; + } + + public String getAction() { + return this.actionWithoutContext; + } + + /** Get the URL binding for the form's {@link ActionBean} from the {@link ActionResolver}. */ + protected String getActionBeanUrlBinding() { + ActionResolver resolver = StripesFilter.getConfiguration().getActionResolver(); + if (actionBeanClass == null) { + String path = StringUtil.trimFragment(this.actionWithoutContext); + String binding = resolver.getUrlBindingFromPath(path); + if (binding == null) binding = path; + return binding; + } else { + return resolver.getUrlBinding(actionBeanClass); } + } - public String getAction() { return this.actionWithoutContext; } - - /** Get the URL binding for the form's {@link ActionBean} from the {@link ActionResolver}. */ - protected String getActionBeanUrlBinding() { - ActionResolver resolver = StripesFilter.getConfiguration().getActionResolver(); - if (actionBeanClass == null) { - String path = StringUtil.trimFragment(this.actionWithoutContext); - String binding = resolver.getUrlBindingFromPath(path); - if (binding == null) - binding = path; - return binding; - } - else { - return resolver.getUrlBinding(actionBeanClass); - } + /** Lazily looks up and returns the type of action bean the form will submit to. */ + protected Class getActionBeanClass() { + if (this.actionBeanClass == null) { + ActionResolver resolver = StripesFilter.getConfiguration().getActionResolver(); + this.actionBeanClass = resolver.getActionBeanType(getActionBeanUrlBinding()); } - /** Lazily looks up and returns the type of action bean the form will submit to. */ - protected Class getActionBeanClass() { - if (this.actionBeanClass == null) { - ActionResolver resolver = StripesFilter.getConfiguration().getActionResolver(); - this.actionBeanClass = resolver.getActionBeanType(getActionBeanUrlBinding()); - } - - return this.actionBeanClass; + return this.actionBeanClass; + } + + /** + * Sets the 'action' attribute by inspecting the bean class provided and asking the current + * ActionResolver what the appropriate URL is. + * + * @param beanclass the String FQN of the class, or a Class representing the class + * @throws StripesJspException if the URL cannot be determined for any reason, most likely because + * of a mis-spelled class name, or a class that's not an ActionBean + */ + public void setBeanclass(Object beanclass) throws StripesJspException { + this.beanclass = beanclass; + + String url = getActionBeanUrl(beanclass); + if (url == null) { + throw new StripesJspException( + "Could not determine action from 'beanclass' supplied. " + + "The value supplied was '" + + beanclass + + "'. Please ensure that this bean type " + + "exists and is in the classpath. If you are developing a page and the ActionBean " + + "does not yet exist, consider using the 'action' attribute instead for now."); + } else { + setAction(url); } - - /** - * Sets the 'action' attribute by inspecting the bean class provided and asking the current - * ActionResolver what the appropriate URL is. - * - * @param beanclass the String FQN of the class, or a Class representing the class - * @throws StripesJspException if the URL cannot be determined for any reason, most likely - * because of a mis-spelled class name, or a class that's not an ActionBean - */ - public void setBeanclass(Object beanclass) throws StripesJspException { - this.beanclass = beanclass; - - String url = getActionBeanUrl(beanclass); - if (url == null) { - throw new StripesJspException("Could not determine action from 'beanclass' supplied. " + - "The value supplied was '" + beanclass + "'. Please ensure that this bean type " + - "exists and is in the classpath. If you are developing a page and the ActionBean " + - "does not yet exist, consider using the 'action' attribute instead for now."); - } - else { - setAction(url); - } + } + + /** Corresponding getter for 'beanclass', will always return null. */ + public Object getBeanclass() { + return null; + } + + /** Sets the name of the field that should receive focus when the form is rendered. */ + public void setFocus(String focus) { + this.focus = focus; + } + /** Gets the name of the field that should receive focus when the form is rendered. */ + public String getFocus() { + return focus; + } + + /** Gets the flag that indicates if this is a partial form. */ + public boolean isPartial() { + return partial; + } + /** Sets the flag that indicates if this is a partial form. */ + public void setPartial(boolean partial) { + this.partial = partial; + } + + /** Sets the form encoding. */ + public void setEnctype(String enctype) { + this.enctype = enctype; + } + /** Gets the form encoding. */ + public String getEnctype() { + return enctype; + } + + /** Sets the HTTP method to use when the form is submitted. */ + public void setMethod(String method) { + this.method = method; + } + /** Gets the HTTP method to use when the form is submitted. */ + public String getMethod() { + return method; + } + + //////////////////////////////////////////////////////////// + // Additional attributes specific to the form tag + //////////////////////////////////////////////////////////// + public void setAccept(String accept) { + set("accept", accept); + } + + public String getAccept() { + return get("accept"); + } + + public void setAcceptcharset(String acceptCharset) { + set("accept-charset", acceptCharset); + } + + public String getAcceptcharset() { + return get("accept-charset"); + } + + public void setName(String name) { + set("name", name); + } + + public String getName() { + return get("name"); + } + + public void setTarget(String target) { + set("target", target); + } + + public String getTarget() { + return get("target"); + } + + public void setOnreset(String onreset) { + set("onreset", onreset); + } + + public String getOnreset() { + return get("onreset"); + } + + public void setOnsubmit(String onsubmit) { + set("onsubmit", onsubmit); + } + + public String getOnsubmit() { + return get("onsubmit"); + } + + //////////////////////////////////////////////////////////// + // TAG methods + //////////////////////////////////////////////////////////// + + /** + * Does sanity checks and returns EVAL_BODY_BUFFERED. Everything else of interest happens in + * doEndTag. + */ + @Override + public int doStartTag() throws JspException { + if (this.actionWithoutContext == null) { + throw new StripesJspException( + "The form tag attributes 'beanClass' and 'action' " + + "are both null. One of the two must be supplied to determine which " + + "action bean should handle the form submission."); } - - /** Corresponding getter for 'beanclass', will always return null. */ - public Object getBeanclass() { return null; } - - /** Sets the name of the field that should receive focus when the form is rendered. */ - public void setFocus(String focus) { this.focus = focus; } - /** Gets the name of the field that should receive focus when the form is rendered. */ - public String getFocus() { return focus; } - - /** Gets the flag that indicates if this is a partial form. */ - public boolean isPartial() { return partial; } - /** Sets the flag that indicates if this is a partial form. */ - public void setPartial(boolean partial) { this.partial = partial; } - - /** Sets the form encoding. */ - public void setEnctype(String enctype) { this.enctype = enctype; } - /** Gets the form encoding. */ - public String getEnctype() { return enctype; } - - /** Sets the HTTP method to use when the form is submitted. */ - public void setMethod(String method) { this.method = method; } - /** Gets the HTTP method to use when the form is submitted. */ - public String getMethod() { return method; } - - //////////////////////////////////////////////////////////// - // Additional attributes specific to the form tag - //////////////////////////////////////////////////////////// - public void setAccept(String accept) { set("accept", accept); } - public String getAccept() { return get("accept"); } - - public void setAcceptcharset(String acceptCharset) { set("accept-charset", acceptCharset); } - public String getAcceptcharset() { return get("accept-charset"); } - - public void setName(String name) { set("name", name); } - public String getName() { return get("name"); } - - public void setTarget(String target) { set("target", target); } - public String getTarget() { return get("target"); } - - public void setOnreset(String onreset) { set("onreset", onreset); } - public String getOnreset() { return get("onreset"); } - - public void setOnsubmit(String onsubmit) { set("onsubmit", onsubmit); } - public String getOnsubmit() { return get("onsubmit"); } - - //////////////////////////////////////////////////////////// - // TAG methods - //////////////////////////////////////////////////////////// - - /** - * Does sanity checks and returns EVAL_BODY_BUFFERED. Everything else of interest happens in - * doEndTag. - */ - @Override - public int doStartTag() throws JspException { - if (this.actionWithoutContext == null) { - throw new StripesJspException("The form tag attributes 'beanClass' and 'action' " - + "are both null. One of the two must be supplied to determine which " - + "action bean should handle the form submission."); - } - getTagStack().push(this); - urlBuilder = new UrlBuilder(pageContext.getRequest().getLocale(), getAction(), false) - .setEvent(null); - return EVAL_BODY_BUFFERED; + getTagStack().push(this); + urlBuilder = + new UrlBuilder(pageContext.getRequest().getLocale(), getAction(), false).setEvent(null); + return EVAL_BODY_BUFFERED; + } + + /** No-op method. */ + public void doInitBody() throws JspException {} + + /** Just returns SKIP_BODY so that the body is included only once. */ + public int doAfterBody() throws JspException { + return SKIP_BODY; + } + + /** + * Writes things out in the following order: + * + *
      + *
    • The form open tag + *
    • Hidden fields for the form name and source page + *
    • The buffered body content + *
    • The form close tag + *
    + * + *

    All of this is done in doEndTag to allow form elements to modify the form tag itself if + * necessary. A prime example of this is the InputFileTag, which needs to ensure that the form + * method is POST and the enctype is correct. + */ + @Override + public int doEndTag() throws JspException { + try { + // Default the method to post + if (getMethod() == null) { + setMethod("post"); + } + + set("method", getMethod()); + set("enctype", getEnctype()); + set("action", buildAction()); + + JspWriter out = getPageContext().getOut(); + if (!isPartial()) { + writeOpenTag(out, "form"); + } + if (getBodyContent() != null) { + getBodyContent().writeOut(getPageContext().getOut()); + } + + if (!isPartial()) { + writeHiddenTags(out); + writeCloseTag(getPageContext().getOut(), "form"); + } + + // Write out a warning if focus didn't find a field + if (this.focus != null && !this.focusSet) { + log.error( + "Form with action [", + getAction(), + "] has 'focus' set to '", + this.focus, + "', but did not find a field with matching name to set focus on."); + } + + // Clean up any state that we've modified during tag processing, so that the container + // can use tag pooling + this.actionBeanClass = null; + this.fieldsPresent.clear(); + this.focusSet = false; + this.urlBuilder = null; + } catch (IOException ioe) { + throw new StripesJspException("IOException in FormTag.doEndTag().", ioe); } - /** No-op method. */ - public void doInitBody() throws JspException { } - - /** Just returns SKIP_BODY so that the body is included only once. */ - public int doAfterBody() throws JspException { - return SKIP_BODY; + return EVAL_PAGE; + } + + /** Rethrows the passed in throwable in all cases. */ + public void doCatch(Throwable throwable) throws Throwable { + throw throwable; + } + + /** + * Used to ensure that the form is always removed from the tag stack so that there is never any + * confusion about tag-parent hierarchies. + */ + public void doFinally() { + try { + getTagStack().pop(); + } catch (Throwable t) { + /* Suppress anything, because otherwise this might mask any causal exception. */ } + } - /** - * Writes things out in the following order: - *

      - *
    • The form open tag
    • - *
    • Hidden fields for the form name and source page
    • - *
    • The buffered body content
    • - *
    • The form close tag
    • - *
    - * - *

    All of this is done in doEndTag to allow form elements to modify the form tag itself if - * necessary. A prime example of this is the InputFileTag, which needs to ensure that the form - * method is POST and the enctype is correct.

    + /** Write out hidden tags that are internally required by Stripes to process request. */ + protected void writeHiddenTags(JspWriter out) throws IOException, JspException { + /* + * The div is necessary in order to be XHTML compliant, where a form can contain only block + * level elements (which seems stupid, but whatever). */ - @Override - public int doEndTag() throws JspException { - try { - // Default the method to post - if (getMethod() == null) { - setMethod("post"); - } - - set("method", getMethod()); - set("enctype", getEnctype()); - set("action", buildAction()); - - JspWriter out = getPageContext().getOut(); - if (!isPartial()) { - writeOpenTag(out, "form"); - } - if (getBodyContent() != null) { - getBodyContent().writeOut( getPageContext().getOut() ); - } - - if (!isPartial()) { - writeHiddenTags(out); - writeCloseTag(getPageContext().getOut(), "form"); - } - - // Write out a warning if focus didn't find a field - if (this.focus != null && !this.focusSet) { - log.error("Form with action [", getAction(), "] has 'focus' set to '", this.focus, - "', but did not find a field with matching name to set focus on."); - } - - // Clean up any state that we've modified during tag processing, so that the container - // can use tag pooling - this.actionBeanClass = null; - this.fieldsPresent.clear(); - this.focusSet = false; - this.urlBuilder = null; - } - catch (IOException ioe) { - throw new StripesJspException("IOException in FormTag.doEndTag().", ioe); - } - - return EVAL_PAGE; + out.write("
    "); + writeSourcePageHiddenField(out); + if (isWizard()) { + writeWizardFields(); } - - /** Rethrows the passed in throwable in all cases. */ - public void doCatch(Throwable throwable) throws Throwable { throw throwable; } - - /** - * Used to ensure that the form is always removed from the tag stack so that there is - * never any confusion about tag-parent hierarchies. - */ - public void doFinally() { - try { getTagStack().pop(); } - catch (Throwable t) { - /* Suppress anything, because otherwise this might mask any causal exception. */ + writeFieldsPresentHiddenField(out); + out.write("
    "); + } + + /** Write out a hidden field with the name of the page in it. */ + protected void writeSourcePageHiddenField(JspWriter out) throws IOException { + out.write("" : "\">"); + } + + /** Get the encrypted value for the hidden _sourcePage field. */ + protected String getSourcePageValue() { + HttpServletRequest request = (HttpServletRequest) getPageContext().getRequest(); + return CryptoUtil.encrypt(request.getServletPath()); + } + + /** + * In general writes out a hidden field notifying the server exactly what fields were present on + * the page. Exact behaviour depends upon whether or not the current form is a wizard or not. When + * the current form is not a wizard this method examines the form tag to determine what + * fields present in the form might not get submitted to the server (e.g. checkboxes, selects), + * writes out a hidden field that contains the names of all those fields so that we can detect + * non-submission when the request comes back. + * + *

    In the case of a wizard form the value output is the full list of all fields that were + * present on the page. This is done because the list is used to drive required field validation + * knowing that in a wizard required fields may be spread across several pages. + * + *

    In both cases the value is encrypted to stop the user maliciously spoofing the value. + * + * @param out the output writer into which the hidden tag should be written + * @throws IOException if the writer throws one + */ + protected void writeFieldsPresentHiddenField(JspWriter out) throws IOException { + out.write("" : "\">"); + } + + /** Get the encrypted value of the __fp hidden field. */ + protected String getFieldsPresentValue() { + // Figure out what set of names to include + Set namesToInclude = new HashSet(); + + if (isWizard()) { + namesToInclude.addAll(this.fieldsPresent.keySet()); + } else { + for (Map.Entry> entry : this.fieldsPresent.entrySet()) { + Class fieldClass = entry.getValue(); + if (InputSelectTag.class.isAssignableFrom(fieldClass) + || InputCheckBoxTag.class.isAssignableFrom(fieldClass)) { + namesToInclude.add(entry.getKey()); } + } } - /** Write out hidden tags that are internally required by Stripes to process request. */ - protected void writeHiddenTags(JspWriter out) throws IOException, JspException { - /* - * The div is necessary in order to be XHTML compliant, where a form can contain only block - * level elements (which seems stupid, but whatever). - */ - out.write("

    "); - writeSourcePageHiddenField(out); - if (isWizard()) { - writeWizardFields(); - } - writeFieldsPresentHiddenField(out); - out.write("
    "); + // Combine the names into a delimited String and encrypt it + String hiddenFieldValue = HtmlUtil.combineValues(namesToInclude); + return CryptoUtil.encrypt(hiddenFieldValue); + } + + /** + * Fetches the ActionBean associated with the form if one is present. An ActionBean will not be + * created (and hence not present) by default. An ActionBean will only be present if the current + * request got bound to the same ActionBean as the current form uses. E.g. if we are re-showing + * the page as the result of an error, or the same ActionBean is used for a "pre-Action" + * and the "post-action". + * + * @return ActionBean the ActionBean bound to the form if there is one + */ + protected ActionBean getActionBean() { + String binding = getActionBeanUrlBinding(); + HttpServletRequest request = (HttpServletRequest) getPageContext().getRequest(); + ActionBean bean = (ActionBean) request.getAttribute(binding); + if (bean == null) { + HttpSession session = request.getSession(false); + if (session != null) bean = (ActionBean) session.getAttribute(binding); } - - /** Write out a hidden field with the name of the page in it. */ - protected void writeSourcePageHiddenField(JspWriter out) throws IOException { - out.write("" : "\">"); + return bean; + } + + /** + * Returns true if the ActionBean this form posts to represents a Wizard action bean and false in + * all other situations. If the form cannot determine the ActionBean being posted to for any + * reason it will return false. + */ + protected boolean isWizard() { + ActionBean bean = getActionBean(); + Class clazz = null; + if (bean == null) { + clazz = getActionBeanClass(); + + if (clazz == null) { + log.error( + "Could not locate an ActionBean that was bound to the URL [", + this.actionWithoutContext, + "]. Without an ActionBean class Stripes ", + "cannot determine whether the ActionBean is a wizard or not. ", + "As a result wizard behaviour will be disabled."); + return false; + } + } else { + clazz = bean.getClass(); } - /** Get the encrypted value for the hidden _sourcePage field. */ - protected String getSourcePageValue() { - HttpServletRequest request = (HttpServletRequest) getPageContext().getRequest(); - return CryptoUtil.encrypt(request.getServletPath()); - } - - /** - *

    In general writes out a hidden field notifying the server exactly what fields were - * present on the page. Exact behaviour depends upon whether or not the current form - * is a wizard or not. When the current form is not a wizard this method examines - * the form tag to determine what fields present in the form might not get submitted to - * the server (e.g. checkboxes, selects), writes out a hidden field that contains the names - * of all those fields so that we can detect non-submission when the request comes back.

    - * - *

    In the case of a wizard form the value output is the full list of all fields that were - * present on the page. This is done because the list is used to drive required field - * validation knowing that in a wizard required fields may be spread across several pages.

    - * - *

    In both cases the value is encrypted to stop the user maliciously spoofing the value.

    - * - * @param out the output writer into which the hidden tag should be written - * @throws IOException if the writer throws one - */ - protected void writeFieldsPresentHiddenField(JspWriter out) throws IOException { - out.write("" : "\">"); - } - - /** Get the encrypted value of the __fp hidden field. */ - protected String getFieldsPresentValue() { - // Figure out what set of names to include - Set namesToInclude = new HashSet(); - - if (isWizard()) { - namesToInclude.addAll(this.fieldsPresent.keySet()); - } - else { - for (Map.Entry> entry : this.fieldsPresent.entrySet()) { - Class fieldClass = entry.getValue(); - if (InputSelectTag.class.isAssignableFrom(fieldClass) - || InputCheckBoxTag.class.isAssignableFrom(fieldClass)) { - namesToInclude.add(entry.getKey()); - } - } - } - - // Combine the names into a delimited String and encrypt it - String hiddenFieldValue = HtmlUtil.combineValues(namesToInclude); - return CryptoUtil.encrypt(hiddenFieldValue); - } - - /** - * Fetches the ActionBean associated with the form if one is present. An ActionBean will not - * be created (and hence not present) by default. An ActionBean will only be present if the - * current request got bound to the same ActionBean as the current form uses. E.g. if we are - * re-showing the page as the result of an error, or the same ActionBean is used for a - * "pre-Action" and the "post-action". - * - * @return ActionBean the ActionBean bound to the form if there is one - */ - protected ActionBean getActionBean() { - String binding = getActionBeanUrlBinding(); - HttpServletRequest request = (HttpServletRequest) getPageContext().getRequest(); - ActionBean bean = (ActionBean) request.getAttribute(binding); - if (bean == null) { - HttpSession session = request.getSession(false); - if (session != null) - bean = (ActionBean) session.getAttribute(binding); - } - return bean; - } - - /** - * Returns true if the ActionBean this form posts to represents a Wizard action bean and - * false in all other situations. If the form cannot determine the ActionBean being posted - * to for any reason it will return false. - */ - protected boolean isWizard() { - ActionBean bean = getActionBean(); - Class clazz = null; - if (bean == null) { - clazz = getActionBeanClass(); - - if (clazz == null) { - log.error("Could not locate an ActionBean that was bound to the URL [", - this.actionWithoutContext, "]. Without an ActionBean class Stripes ", - "cannot determine whether the ActionBean is a wizard or not. ", - "As a result wizard behaviour will be disabled."); - return false; - } - } - else { - clazz = bean.getClass(); - } - - return clazz.getAnnotation(Wizard.class) != null; + return clazz.getAnnotation(Wizard.class) != null; + } + + /** + * Writes out hidden fields for all fields that are present in the request but are not explicitly + * present in this form. Excludes any fields that have special meaning to Stripes and are not + * really application data. Uses the stripes:wizard-fields tag to do the grunt work. + */ + protected void writeWizardFields() throws JspException { + WizardFieldsTag tag = new WizardFieldsTag(); + tag.setPageContext(getPageContext()); + tag.setParent(this); + try { + tag.doStartTag(); + tag.doEndTag(); + } finally { + tag.doFinally(); + tag.release(); } - - /** - * Writes out hidden fields for all fields that are present in the request but are not - * explicitly present in this form. Excludes any fields that have special meaning to - * Stripes and are not really application data. Uses the stripes:wizard-fields tag to - * do the grunt work. - */ - protected void writeWizardFields() throws JspException { - WizardFieldsTag tag = new WizardFieldsTag(); - tag.setPageContext(getPageContext()); - tag.setParent(this); - try { - tag.doStartTag(); - tag.doEndTag(); + } + + /** + * Used by nested tags to notify the form that a field with the specified name has been written to + * the form. + * + * @param tag the input field tag being registered + */ + public void registerField(InputTagSupport tag) { + this.fieldsPresent.put(tag.getName(), tag.getClass()); + setFocusOnFieldIfRequired(tag); + } + + /** + * Checks to see if the field should receive focus either because it is the named field for + * receiving focus, because it is the first field in the form (and first field focus was + * specified), or because it is the first field in error. + * + * @param tag the input tag being registered with the form + */ + protected void setFocusOnFieldIfRequired(InputTagSupport tag) { + // Decide whether or not this field should be focused + if (this.focus != null && !this.focusSet) { + ActionBean bean = getActionBean(); + ValidationErrors errors = bean == null ? null : bean.getContext().getValidationErrors(); + + // If there are validation errors, select the first field in error + if (errors != null && errors.hasFieldErrors()) { + List fieldErrors = errors.get(tag.getName()); + if (fieldErrors != null && fieldErrors.size() > 0) { + tag.setFocus(true); + this.focusSet = true; } - finally { - tag.doFinally(); - tag.release(); + } + // Else set the named field, or the first field if that's desired + else if (this.focus.equals(tag.getName())) { + tag.setFocus(true); + this.focusSet = true; + } else if ("".equals(this.focus) || "first".equalsIgnoreCase(this.focus)) { + if (!(tag instanceof InputHiddenTag)) { + tag.setFocus(true); + this.focusSet = true; } + } } - - /** - * Used by nested tags to notify the form that a field with the specified name has been - * written to the form. - * - * @param tag the input field tag being registered - */ - public void registerField(InputTagSupport tag) { - this.fieldsPresent.put(tag.getName(), tag.getClass()); - setFocusOnFieldIfRequired(tag); - } - - /** - * Checks to see if the field should receive focus either because it is the named - * field for receiving focus, because it is the first field in the form (and first - * field focus was specified), or because it is the first field in error. - * - * @param tag the input tag being registered with the form - */ - protected void setFocusOnFieldIfRequired(InputTagSupport tag) { - // Decide whether or not this field should be focused - if (this.focus != null && !this.focusSet) { - ActionBean bean = getActionBean(); - ValidationErrors errors = bean == null ? null : bean.getContext().getValidationErrors(); - - // If there are validation errors, select the first field in error - if (errors != null && errors.hasFieldErrors()) { - List fieldErrors = errors.get(tag.getName()); - if (fieldErrors != null && fieldErrors.size() > 0) { - tag.setFocus(true); - this.focusSet = true; - } - } - // Else set the named field, or the first field if that's desired - else if (this.focus.equals(tag.getName())) { - tag.setFocus(true); - this.focusSet = true; - } - else if ("".equals(this.focus) || "first".equalsIgnoreCase(this.focus)) { - if ( !(tag instanceof InputHiddenTag) ) { - tag.setFocus(true); - this.focusSet = true; - } - } - } - } - - /** - * Gets the set of all field names for which fields have been referred within the form up - * until the point of calling this method. If this is called during doEndTag it will contain - * all field names, if it is called during the body of the tag it will only contain the - * input elements which have been processed up until that point. - * - * @return Set - the set of field names seen so far - */ - public Set getRegisteredFields() { - return this.fieldsPresent.keySet(); - } - - /** - * Appends a parameter to the "action" attribute of the form tag. For clean URLs the value will - * be embedded in the URL if possible. Otherwise, it will be added to the query string. - * - * @param name the parameter name - * @param valueOrValues the parameter value(s) - * @see ParameterizableTag#addParameter(String, Object) - */ - public void addParameter(String name, Object valueOrValues) { - urlBuilder.addParameter(name, valueOrValues); - } - - /** - * Builds the action attribute, including the context path and any parameters. - * - * @return the action attribute - */ - protected String buildAction() { - String action = urlBuilder.toString(); - if (action.startsWith("/")) { - HttpServletRequest request = (HttpServletRequest) getPageContext().getRequest(); - String contextPath = request.getContextPath(); - - // *Always* prepend the context path if "beanclass" was used - // Otherwise, *only* prepend it if it is not already present - if (contextPath.length() > 1 - && (beanclass != null || !action.startsWith(contextPath + '/'))) { - action = contextPath + action; - } - } - HttpServletResponse response = (HttpServletResponse) getPageContext().getResponse(); - return response.encodeURL(action); + } + + /** + * Gets the set of all field names for which fields have been referred within the form up until + * the point of calling this method. If this is called during doEndTag it will contain all field + * names, if it is called during the body of the tag it will only contain the input elements which + * have been processed up until that point. + * + * @return Set - the set of field names seen so far + */ + public Set getRegisteredFields() { + return this.fieldsPresent.keySet(); + } + + /** + * Appends a parameter to the "action" attribute of the form tag. For clean URLs the value will be + * embedded in the URL if possible. Otherwise, it will be added to the query string. + * + * @param name the parameter name + * @param valueOrValues the parameter value(s) + * @see ParameterizableTag#addParameter(String, Object) + */ + public void addParameter(String name, Object valueOrValues) { + urlBuilder.addParameter(name, valueOrValues); + } + + /** + * Builds the action attribute, including the context path and any parameters. + * + * @return the action attribute + */ + protected String buildAction() { + String action = urlBuilder.toString(); + if (action.startsWith("/")) { + HttpServletRequest request = (HttpServletRequest) getPageContext().getRequest(); + String contextPath = request.getContextPath(); + + // *Always* prepend the context path if "beanclass" was used + // Otherwise, *only* prepend it if it is not already present + if (contextPath.length() > 1 + && (beanclass != null || !action.startsWith(contextPath + '/'))) { + action = contextPath + action; + } } + HttpServletResponse response = (HttpServletResponse) getPageContext().getResponse(); + return response.encodeURL(action); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/FormatTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/FormatTag.java index 2aefa19f6..5cab8198c 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/FormatTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/FormatTag.java @@ -14,121 +14,114 @@ */ package net.sourceforge.stripes.tag; +import jakarta.servlet.jsp.JspException; import java.io.IOException; - -import javax.servlet.jsp.JspException; - import net.sourceforge.stripes.controller.StripesFilter; import net.sourceforge.stripes.format.Formatter; import net.sourceforge.stripes.format.FormatterFactory; import net.sourceforge.stripes.util.Log; /** - * This tag accepts an object and formats it using an appropriate - * {@link Formatter}. The resulting {@link String} can be assigned in the page, - * request, session or application scopes by using "var" and "scope" or it can - * be written directly to the JSP output. - * + * This tag accepts an object and formats it using an appropriate {@link Formatter}. The resulting + * {@link String} can be assigned in the page, request, session or application scopes by using "var" + * and "scope" or it can be written directly to the JSP output. + * * @author Ben Gunter * @since Stripes 1.5 */ public class FormatTag extends VarTagSupport { - private static final Log log = Log.getInstance(FormatTag.class); - private Object value; - private String formatType; - private String formatPattern; + private static final Log log = Log.getInstance(FormatTag.class); + private Object value; + private String formatType; + private String formatPattern; - /** Get the format pattern */ - public String getFormatPattern() { - return formatPattern; - } + /** Get the format pattern */ + public String getFormatPattern() { + return formatPattern; + } - /** Set the format pattern */ - public void setFormatPattern(String formatPattern) { - this.formatPattern = formatPattern; - } + /** Set the format pattern */ + public void setFormatPattern(String formatPattern) { + this.formatPattern = formatPattern; + } - /** Get the format type */ - public String getFormatType() { - return formatType; - } + /** Get the format type */ + public String getFormatType() { + return formatType; + } - /** Set the format type */ - public void setFormatType(String formatType) { - this.formatType = formatType; - } + /** Set the format type */ + public void setFormatType(String formatType) { + this.formatType = formatType; + } - /** Get the object to be formatted */ - public Object getValue() { - return value; - } + /** Get the object to be formatted */ + public Object getValue() { + return value; + } - /** Set the object to be formatted */ - public void setValue(Object value) { - this.value = value; - } + /** Set the object to be formatted */ + public void setValue(Object value) { + this.value = value; + } - /** - * Attempts to format an object using an appropriate {@link Formatter}. If - * no formatter is available for the object, then this method will call - * toString() on the object. A null value will - * be formatted as an empty string. - * - * @param value - * the object to be formatted - * @return the formatted value - */ - @SuppressWarnings("unchecked") - protected String format(Object value) { - if (value == null) - return ""; + /** + * Attempts to format an object using an appropriate {@link Formatter}. If no formatter is + * available for the object, then this method will call toString() on the object. A + * null value will be formatted as an empty string. + * + * @param value the object to be formatted + * @return the formatted value + */ + @SuppressWarnings("unchecked") + protected String format(Object value) { + if (value == null) return ""; - FormatterFactory factory = StripesFilter.getConfiguration().getFormatterFactory(); - Formatter formatter = factory.getFormatter(value.getClass(), - getPageContext().getRequest().getLocale(), - this.formatType, - this.formatPattern); - if (formatter == null) - return String.valueOf(value); - else - return formatter.format(value); - } + FormatterFactory factory = StripesFilter.getConfiguration().getFormatterFactory(); + Formatter formatter = + factory.getFormatter( + value.getClass(), + getPageContext().getRequest().getLocale(), + this.formatType, + this.formatPattern); + if (formatter == null) return String.valueOf(value); + else return formatter.format(value); + } - /** - * Calls {@link #format(Object)} and writes the resulting {@link String} to - * the JSP output. - * - * @param value - * the object to be formatted and written - * @throws JspException - */ - protected void writeOut(Object value) throws JspException { - String formatted = format(value); - try { - pageContext.getOut().print(formatted); - } - catch (IOException e) { - JspException jspe = new JspException( - "IOException encountered while writing formatted value '" - + formatted + " to the JspWriter.", e); - log.warn(jspe); - throw jspe; - } + /** + * Calls {@link #format(Object)} and writes the resulting {@link String} to the JSP output. + * + * @param value the object to be formatted and written + * @throws JspException + */ + protected void writeOut(Object value) throws JspException { + String formatted = format(value); + try { + pageContext.getOut().print(formatted); + } catch (IOException e) { + JspException jspe = + new JspException( + "IOException encountered while writing formatted value '" + + formatted + + " to the JspWriter.", + e); + log.warn(jspe); + throw jspe; } + } - @Override - public int doStartTag() throws JspException { - return SKIP_BODY; - } + @Override + public int doStartTag() throws JspException { + return SKIP_BODY; + } - @Override - public int doEndTag() throws JspException { - if (var == null) { - writeOut(value); - } - else { - export(format(value)); - } - return EVAL_PAGE; + @Override + public int doEndTag() throws JspException { + if (var == null) { + writeOut(value); + } else { + export(format(value)); } + return EVAL_PAGE; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/HtmlTagSupport.java b/stripes/src/main/java/net/sourceforge/stripes/tag/HtmlTagSupport.java index 1a5d4bea9..3ac89b78a 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/HtmlTagSupport.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/HtmlTagSupport.java @@ -14,310 +14,434 @@ */ package net.sourceforge.stripes.tag; -import net.sourceforge.stripes.exception.StripesJspException; -import net.sourceforge.stripes.util.Log; -import net.sourceforge.stripes.util.HtmlUtil; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspWriter; -import javax.servlet.jsp.tagext.BodyContent; -import javax.servlet.jsp.tagext.DynamicAttributes; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspWriter; +import jakarta.servlet.jsp.tagext.BodyContent; +import jakarta.servlet.jsp.tagext.DynamicAttributes; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import net.sourceforge.stripes.exception.StripesJspException; +import net.sourceforge.stripes.util.HtmlUtil; +import net.sourceforge.stripes.util.Log; /** * Provides basic facilities for any tag that wishes to mimic a standard HTML/XHTML tag. Includes - * getters and setters for all basic HTML attributes and JavaScript event attributes. Also includes + * getters and setters for all basic HTML attributes and JavaScript event attributes. Also includes * several of the support methods from the Tag interface, but does not directly or indirectly * implement either Tag or BodyTag. * * @author Tim Fennell */ public abstract class HtmlTagSupport extends StripesTagSupport implements DynamicAttributes { - /** Log implementation used to log errors during tag writing. */ - private static final Log log = Log.getInstance(HtmlTagSupport.class); - - /** Map containing all attributes of the tag. */ - private final Map attributes = new HashMap(); - - /** Storage for a BodyContent instance, should the eventual child class implement BodyTag. */ - private BodyContent bodyContent; - - /** Sets the named attribute to the supplied value. */ - protected final void set(String name, String value) { - if (value == null) { - this.attributes.remove(name); - } - else { - this.attributes.put(name, value); - } - } - - /** Gets the value of the named attribute, or null if it is not set. */ - protected final String get(String name) { - return this.attributes.get(name); - } - - /** Gets the map containing the attributes of the tag. */ - protected final Map getAttributes() { - return this.attributes; - } - - /** - * Accepts any dynamic attributes that are supplied to the tag and stored them - * in the map of attributes that get written back to the page. - * - * @param uri the URI of the namespace of the attribute if it has one. Totally ignored! - * @param name the name of the attribute - * @param value the value of the attribute - * @throws JspException not thrown from this class; included so that subclasses can - * override the method and throw the interface exception - */ - public void setDynamicAttribute(String uri, String name, Object value) throws JspException { - set(name, value == null ? "" : value.toString()); - } - - /** Returns the BodyContent of the tag if one has been provided by the JSP container. */ - public BodyContent getBodyContent() { - return bodyContent; - } - - /** Called by the JSP container to set the BodyContent on the tag. */ - public void setBodyContent(BodyContent bodyContent) { - this.bodyContent = bodyContent; - } - - /** Release method to clean up the state of the tag ready for re-use. */ - @Override - public void release() { - this.pageContext = null; - this.parentTag = null; - this.bodyContent = null; - this.attributes.clear(); - } - - /** - * Checks to see if there is a body content for this tag, and if its value is non-null - * and non-zero-length. If so, returns it as a String, otherwise returns null. - - * @return String the value of the body if one was set - */ - protected String getBodyContentAsString() { - String returnValue = null; + /** Log implementation used to log errors during tag writing. */ + private static final Log log = Log.getInstance(HtmlTagSupport.class); - if (this.bodyContent != null) { - String body = getBodyContent().getString(); + /** Map containing all attributes of the tag. */ + private final Map attributes = new HashMap(); - if (body != null && body.length() > 0) { - returnValue = body; - } - } + /** Storage for a BodyContent instance, should the eventual child class implement BodyTag. */ + private BodyContent bodyContent; - return returnValue; + /** Sets the named attribute to the supplied value. */ + protected final void set(String name, String value) { + if (value == null) { + this.attributes.remove(name); + } else { + this.attributes.put(name, value); } - - /** - * Returns true if HTML tags that have no body should be closed like XML tags, with "/>". - * False if such HTML tags should be closed in the style of HTML4, with just a ">". - * - * @see PageOptionsTag#setHtmlMode(String) - */ - protected boolean isXmlTags() { - return !"html".equalsIgnoreCase(PageOptionsTag.getHtmlMode(pageContext)); + } + + /** Gets the value of the named attribute, or null if it is not set. */ + protected final String get(String name) { + return this.attributes.get(name); + } + + /** Gets the map containing the attributes of the tag. */ + protected final Map getAttributes() { + return this.attributes; + } + + /** + * Accepts any dynamic attributes that are supplied to the tag and stored them in the map of + * attributes that get written back to the page. + * + * @param uri the URI of the namespace of the attribute if it has one. Totally ignored! + * @param name the name of the attribute + * @param value the value of the attribute + * @throws JspException not thrown from this class; included so that subclasses can override the + * method and throw the interface exception + */ + public void setDynamicAttribute(String uri, String name, Object value) throws JspException { + set(name, value == null ? "" : value.toString()); + } + + /** Returns the BodyContent of the tag if one has been provided by the JSP container. */ + public BodyContent getBodyContent() { + return bodyContent; + } + + /** Called by the JSP container to set the BodyContent on the tag. */ + public void setBodyContent(BodyContent bodyContent) { + this.bodyContent = bodyContent; + } + + /** Release method to clean up the state of the tag ready for re-use. */ + @Override + public void release() { + this.pageContext = null; + this.parentTag = null; + this.bodyContent = null; + this.attributes.clear(); + } + + /** + * Checks to see if there is a body content for this tag, and if its value is non-null and + * non-zero-length. If so, returns it as a String, otherwise returns null. + * + * @return String the value of the body if one was set + */ + protected String getBodyContentAsString() { + String returnValue = null; + + if (this.bodyContent != null) { + String body = getBodyContent().getString(); + + if (body != null && body.length() > 0) { + returnValue = body; + } } - /** - * Writes out an opening tag. Uses the parameter "tag" to determine the name of the open tag - * and then uses the map of attributes assembled through various setter calls to fill in the - * tag attributes. - * - * @param writer the JspWriter to write the open tag to - * @param tag the name of the tag to use - * @throws JspException if the JspWriter causes an exception - */ - protected void writeOpenTag(JspWriter writer, String tag) throws JspException { - try { - writer.print("<"); - writer.print(tag); - writeAttributes(writer); - writer.print(">"); - } - catch (IOException ioe) { - JspException jspe = new JspException("IOException encountered while writing open tag <" + - tag + "> to the JspWriter.", ioe); - log.warn(jspe); - throw jspe; - } + return returnValue; + } + + /** + * Returns true if HTML tags that have no body should be closed like XML tags, with "/>". False + * if such HTML tags should be closed in the style of HTML4, with just a ">". + * + * @see PageOptionsTag#setHtmlMode(String) + */ + protected boolean isXmlTags() { + return !"html".equalsIgnoreCase(PageOptionsTag.getHtmlMode(pageContext)); + } + + /** + * Writes out an opening tag. Uses the parameter "tag" to determine the name of the open tag and + * then uses the map of attributes assembled through various setter calls to fill in the tag + * attributes. + * + * @param writer the JspWriter to write the open tag to + * @param tag the name of the tag to use + * @throws JspException if the JspWriter causes an exception + */ + protected void writeOpenTag(JspWriter writer, String tag) throws JspException { + try { + writer.print("<"); + writer.print(tag); + writeAttributes(writer); + writer.print(">"); + } catch (IOException ioe) { + JspException jspe = + new JspException( + "IOException encountered while writing open tag <" + tag + "> to the JspWriter.", + ioe); + log.warn(jspe); + throw jspe; } - - /** - * Writes out a close tag using the tag name supplied. - * - * @param writer the JspWriter to write the open tag to - * @param tag the name of the tag to use - * @throws JspException if the JspWriter causes an exception - */ - protected void writeCloseTag(JspWriter writer, String tag) throws JspException { - try { - writer.print(""); - } - catch (IOException ioe) { - JspException jspe = new JspException("IOException encountered while writing close tag to the JspWriter.", ioe); - log.warn(jspe); - throw jspe; - } + } + + /** + * Writes out a close tag using the tag name supplied. + * + * @param writer the JspWriter to write the open tag to + * @param tag the name of the tag to use + * @throws JspException if the JspWriter causes an exception + */ + protected void writeCloseTag(JspWriter writer, String tag) throws JspException { + try { + writer.print(""); + } catch (IOException ioe) { + JspException jspe = + new JspException( + "IOException encountered while writing close tag to the JspWriter.", + ioe); + log.warn(jspe); + throw jspe; } - - /** - * Writes out a singleton tag (aka a bodiless tag or self-closing tag). Similar to - * writeOpenTag except that instead of leaving the tag open, it closes the tag. - * - * @param writer the JspWriter to write the open tag to - * @param tag the name of the tag to use - * @throws JspException if the JspWriter causes an exception - */ - protected void writeSingletonTag(JspWriter writer, String tag) throws JspException{ - try { - writer.print("<"); - writer.print(tag); - writeAttributes(writer); - writer.print(isXmlTags() ? " />" : ">"); - } - catch (IOException ioe) { - JspException jspe = new JspException("IOException encountered while writing singleton tag <" + - tag + "/> to the JspWriter.", ioe); - log.warn(jspe); - throw jspe; - } + } + + /** + * Writes out a singleton tag (aka a bodiless tag or self-closing tag). Similar to writeOpenTag + * except that instead of leaving the tag open, it closes the tag. + * + * @param writer the JspWriter to write the open tag to + * @param tag the name of the tag to use + * @throws JspException if the JspWriter causes an exception + */ + protected void writeSingletonTag(JspWriter writer, String tag) throws JspException { + try { + writer.print("<"); + writer.print(tag); + writeAttributes(writer); + writer.print(isXmlTags() ? " />" : ">"); + } catch (IOException ioe) { + JspException jspe = + new JspException( + "IOException encountered while writing singleton tag <" + + tag + + "/> to the JspWriter.", + ioe); + log.warn(jspe); + throw jspe; } - - /** - * For every attribute stored in the attributes map for this tag, writes out the tag - * attributes in the form x="y". All attributes are HTML encoded before being written - * to the page to ensure that HTML special characters are rendered properly. - * - * @param writer the JspWriter to write the open tag to - * @throws IOException if the JspWriter causes an exception - */ - protected void writeAttributes(JspWriter writer) throws IOException { - for (Map.Entry attr: getAttributes().entrySet() ) { - // Skip the output of blank attributes! - String value = attr.getValue(); - if (value == null) continue; - - writer.print(" "); - writer.print(attr.getKey()); - writer.print("=\""); - writer.print( HtmlUtil.encode(value) ); - writer.print("\""); - } + } + + /** + * For every attribute stored in the attributes map for this tag, writes out the tag attributes in + * the form x="y". All attributes are HTML encoded before being written to the page to ensure that + * HTML special characters are rendered properly. + * + * @param writer the JspWriter to write the open tag to + * @throws IOException if the JspWriter causes an exception + */ + protected void writeAttributes(JspWriter writer) throws IOException { + for (Map.Entry attr : getAttributes().entrySet()) { + // Skip the output of blank attributes! + String value = attr.getValue(); + if (value == null) continue; + + writer.print(" "); + writer.print(attr.getKey()); + writer.print("=\""); + writer.print(HtmlUtil.encode(value)); + writer.print("\""); } - - - /** - * Evaluates a single expression and returns the result. If the expression cannot be evaluated - * then an ELException is caught, wrapped in a JspException and re-thrown. - * - * @param expression the expression to be evaluated - * @param resultType the Class representing the desired return type from the expression - * @throws StripesJspException when an ELException occurs trying to evaluate the expression - */ - @SuppressWarnings({ "unchecked", "deprecation" }) - protected R evaluateExpression(String expression, Class resultType) throws StripesJspException { - try { - return (R) this.pageContext.getExpressionEvaluator(). - evaluate(expression, resultType, this.pageContext.getVariableResolver(), null); - } - catch (javax.servlet.jsp.el.ELException ele) { - throw new StripesJspException - ("Could not evaluate EL expression [" + expression + "] with result type [" + - resultType.getName() + "] in tag class of type: " + getClass().getName(), ele); - } - } - - - /** - * Returns a String representation of the class, including the map of attributes that - * are set on the tag, the toString of its parent tag, and the pageContext. - */ - @Override - public String toString() { - return getClass().getSimpleName()+ "{" + - "attributes=" + attributes + - ", parentTag=" + parentTag + - ", pageContext=" + pageContext + - "}"; + } + + /** + * Evaluates a single expression and returns the result. If the expression cannot be evaluated + * then an ELException is caught, wrapped in a JspException and re-thrown. + * + * @param expression the expression to be evaluated + * @param resultType the Class representing the desired return type from the expression + * @throws StripesJspException when an ELException occurs trying to evaluate the expression + */ + @SuppressWarnings({"unchecked", "deprecation"}) + protected R evaluateExpression(String expression, Class resultType) + throws StripesJspException { + try { + return (R) + this.pageContext + .getExpressionEvaluator() + .evaluate(expression, resultType, this.pageContext.getVariableResolver(), null); + } catch (jakarta.servlet.jsp.el.ELException ele) { + throw new StripesJspException( + "Could not evaluate EL expression [" + + expression + + "] with result type [" + + resultType.getName() + + "] in tag class of type: " + + getClass().getName(), + ele); } + } + + /** + * Returns a String representation of the class, including the map of attributes that are set on + * the tag, the toString of its parent tag, and the pageContext. + */ + @Override + public String toString() { + return getClass().getSimpleName() + + "{" + + "attributes=" + + attributes + + ", parentTag=" + + parentTag + + ", pageContext=" + + pageContext + + "}"; + } + + public void setId(String id) { + set("id", id); + } + + public String getId() { + return get("id"); + } + + public void setClass(String cssClass) { + set("class", cssClass); + } + + public void setCssClass(String cssClass) { + set("class", cssClass); + } + + public String getCssClass() { + return get("class"); + } + + public void setTitle(String title) { + set("title", title); + } + + public String getTitle() { + return get("title"); + } + + public void setStyle(String style) { + set("style", style); + } + + public String getStyle() { + return get("style"); + } + + public void setDir(String dir) { + set("dir", dir); + } + + public String getDir() { + return get("dir"); + } + + public void setLang(String lang) { + set("lang", lang); + } + + public String getLang() { + return get("lang"); + } + + public void setTabindex(String tabindex) { + set("tabindex", tabindex); + } + + public String getTabindex() { + return get("tabindex"); + } + + public void setAccesskey(String accesskey) { + set("accesskey", accesskey); + } + + public String getAccesskey() { + return get("accesskey"); + } + + public void setOnfocus(String onfocus) { + set("onfocus", onfocus); + } + + public String getOnfocus() { + return get("onfocus"); + } + + public void setOnblur(String onblur) { + set("onblur", onblur); + } + + public String getOnblur() { + return get("onblur"); + } + + public void setOnselect(String onselect) { + set("onselect", onselect); + } + + public String getOnselect() { + return get("onselect"); + } + + public void setOnchange(String onchange) { + set("onchange", onchange); + } + + public String getOnchange() { + return get("onchange"); + } + public void setOnclick(String onclick) { + set("onclick", onclick); + } - public void setId(String id) { set("id", id); } - public String getId() { return get("id"); } - - public void setClass(String cssClass) { set("class", cssClass); } - public void setCssClass(String cssClass) { set("class", cssClass); } - public String getCssClass() { return get("class"); } - - public void setTitle(String title) { set("title", title); } - public String getTitle() { return get("title"); } - - public void setStyle(String style) { set("style", style); } - public String getStyle() { return get("style"); } + public String getOnclick() { + return get("onclick"); + } - public void setDir(String dir) { set("dir", dir); } - public String getDir() { return get("dir"); } + public void setOndblclick(String ondblclick) { + set("ondblclick", ondblclick); + } - public void setLang(String lang) { set("lang", lang); } - public String getLang() { return get("lang"); } + public String getOndblclick() { + return get("ondblclick"); + } - public void setTabindex(String tabindex) { set("tabindex", tabindex); } - public String getTabindex() { return get("tabindex"); } + public void setOnmousedown(String onmousedown) { + set("onmousedown", onmousedown); + } - public void setAccesskey(String accesskey) { set("accesskey", accesskey); } - public String getAccesskey() { return get("accesskey"); } + public String getOnmousedown() { + return get("onmousedown"); + } - public void setOnfocus(String onfocus) { set("onfocus", onfocus); } - public String getOnfocus() { return get("onfocus"); } + public void setOnmouseup(String onmouseup) { + set("onmouseup", onmouseup); + } - public void setOnblur(String onblur) { set("onblur", onblur); } - public String getOnblur() { return get("onblur"); } + public String getOnmouseup() { + return get("onmouseup"); + } - public void setOnselect(String onselect) { set("onselect", onselect); } - public String getOnselect() { return get("onselect"); } + public void setOnmouseover(String onmouseover) { + set("onmouseover", onmouseover); + } - public void setOnchange(String onchange) { set("onchange", onchange); } - public String getOnchange() { return get("onchange"); } + public String getOnmouseover() { + return get("onmouseover"); + } - public void setOnclick(String onclick) { set("onclick", onclick); } - public String getOnclick() { return get("onclick"); } + public void setOnmousemove(String onmousemove) { + set("onmousemove", onmousemove); + } - public void setOndblclick(String ondblclick) { set("ondblclick", ondblclick); } - public String getOndblclick() { return get("ondblclick"); } + public String getOnmousemove() { + return get("onmousemove"); + } - public void setOnmousedown(String onmousedown) { set("onmousedown", onmousedown); } - public String getOnmousedown() { return get("onmousedown"); } + public void setOnmouseout(String onmouseout) { + set("onmouseout", onmouseout); + } - public void setOnmouseup(String onmouseup) { set("onmouseup", onmouseup); } - public String getOnmouseup() { return get("onmouseup"); } + public String getOnmouseout() { + return get("onmouseout"); + } - public void setOnmouseover(String onmouseover) { set("onmouseover", onmouseover); } - public String getOnmouseover() { return get("onmouseover"); } + public void setOnkeypress(String onkeypress) { + set("onkeypress", onkeypress); + } - public void setOnmousemove(String onmousemove) { set("onmousemove", onmousemove); } - public String getOnmousemove() { return get("onmousemove"); } + public String getOnkeypress() { + return get("onkeypress"); + } - public void setOnmouseout(String onmouseout) { set("onmouseout", onmouseout); } - public String getOnmouseout() { return get("onmouseout"); } + public void setOnkeydown(String onkeydown) { + set("onkeydown", onkeydown); + } - public void setOnkeypress(String onkeypress) { set("onkeypress", onkeypress); } - public String getOnkeypress() { return get("onkeypress"); } + public String getOnkeydown() { + return get("onkeydown"); + } - public void setOnkeydown(String onkeydown) { set("onkeydown", onkeydown); } - public String getOnkeydown() { return get("onkeydown"); } + public void setOnkeyup(String onkeyup) { + set("onkeyup", onkeyup); + } - public void setOnkeyup(String onkeyup) { set("onkeyup", onkeyup); } - public String getOnkeyup() { return get("onkeyup"); } + public String getOnkeyup() { + return get("onkeyup"); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/HtmlTagSupportBeanInfo.java b/stripes/src/main/java/net/sourceforge/stripes/tag/HtmlTagSupportBeanInfo.java index 62f89f95c..4c048f909 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/HtmlTagSupportBeanInfo.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/HtmlTagSupportBeanInfo.java @@ -14,71 +14,67 @@ */ package net.sourceforge.stripes.tag; -import net.sourceforge.stripes.util.Log; - import java.beans.PropertyDescriptor; import java.beans.SimpleBeanInfo; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import net.sourceforge.stripes.util.Log; /** - *

    Describes the properties supported by the HtmlTagSupport class which is the parent of all the - * HTML Form/Input tags in Stripes. Exists to provide some flexibility in the naming of methods - * and primarily to provide support for the "class" tag attribute in JSP containers that - * demand a javabean compliant getter and setter method. Since getClass() is rather special in Java + * Describes the properties supported by the HtmlTagSupport class which is the parent of all the + * HTML Form/Input tags in Stripes. Exists to provide some flexibility in the naming of methods and + * primarily to provide support for the "class" tag attribute in JSP containers that + * demand a javabean compliant getter and setter method. Since getClass() is rather special in Java * and cannot (and should not) be overridden, containers may not like calling setClass(String) - * without there being a corresponding getClass():String method. So the PropertyDescriptor for - * the "class" property specifies the methods getCssClass() and setCssClass.

    + * without there being a corresponding getClass():String method. So the PropertyDescriptor for the + * "class" property specifies the methods getCssClass() and setCssClass. * * @author Tim Fennell */ public class HtmlTagSupportBeanInfo extends SimpleBeanInfo { - private static final Log log = Log.getInstance(HtmlTagSupportBeanInfo.class); + private static final Log log = Log.getInstance(HtmlTagSupportBeanInfo.class); - /** - * Generates a simple set of PropertyDescriptors for the HtmlTagSupport class. - */ - @Override - public PropertyDescriptor[] getPropertyDescriptors() { - try { - List descriptors = new ArrayList(); + /** Generates a simple set of PropertyDescriptors for the HtmlTagSupport class. */ + @Override + public PropertyDescriptor[] getPropertyDescriptors() { + try { + List descriptors = new ArrayList(); - // Add the tricky one first - Method getter = HtmlTagSupport.class.getMethod("getCssClass"); - Method setter = HtmlTagSupport.class.getMethod("setCssClass", String.class); - descriptors.add( new PropertyDescriptor("class", getter, setter) ); + // Add the tricky one first + Method getter = HtmlTagSupport.class.getMethod("getCssClass"); + Method setter = HtmlTagSupport.class.getMethod("setCssClass", String.class); + descriptors.add(new PropertyDescriptor("class", getter, setter)); - // Now do all the vanilla properties - descriptors.add( new PropertyDescriptor("id", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("title", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("style", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("dir", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("lang", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("tabindex", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("accesskey", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("onfocus", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("onblur", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("onselect", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("onchange", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("onclick", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("ondblclick", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("onmousedown", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("onmouseup", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("onmouseover", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("onmousemove", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("onmouseout", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("onkeypress", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("onkeydown", HtmlTagSupport.class) ); - descriptors.add( new PropertyDescriptor("onkeyup", HtmlTagSupport.class) ); + // Now do all the vanilla properties + descriptors.add(new PropertyDescriptor("id", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("title", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("style", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("dir", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("lang", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("tabindex", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("accesskey", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("onfocus", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("onblur", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("onselect", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("onchange", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("onclick", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("ondblclick", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("onmousedown", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("onmouseup", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("onmouseover", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("onmousemove", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("onmouseout", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("onkeypress", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("onkeydown", HtmlTagSupport.class)); + descriptors.add(new PropertyDescriptor("onkeyup", HtmlTagSupport.class)); - PropertyDescriptor[] array = new PropertyDescriptor[descriptors.size()]; - return descriptors.toArray(array); - } - catch (Exception e) { - // This is crazy talk, we're only doing things that should always succeed - log.fatal(e, "Could not contruct bean info for HtmlTagSupport. This is very bad."); - return null; - } + PropertyDescriptor[] array = new PropertyDescriptor[descriptors.size()]; + return descriptors.toArray(array); + } catch (Exception e) { + // This is crazy talk, we're only doing things that should always succeed + log.fatal(e, "Could not contruct bean info for HtmlTagSupport. This is very bad."); + return null; } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/IndividualErrorTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/IndividualErrorTag.java index f8b8154c3..c0606c661 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/IndividualErrorTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/IndividualErrorTag.java @@ -14,62 +14,62 @@ */ package net.sourceforge.stripes.tag; -import net.sourceforge.stripes.util.Log; -import net.sourceforge.stripes.validation.ValidationError; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspWriter; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspWriter; import java.io.IOException; import java.util.Locale; +import net.sourceforge.stripes.util.Log; +import net.sourceforge.stripes.validation.ValidationError; /** - * The individual-error tag works in concert with a parent errors tag to control the - * output of each iteration of an error. Placed within a parent errors tag, - * the body output of the parent will be displayed for each matching error - * to output iterate the body of that parent displaying each error in turn. + * The individual-error tag works in concert with a parent errors tag to control the output of each + * iteration of an error. Placed within a parent errors tag, the body output of the parent will be + * displayed for each matching error to output iterate the body of that parent displaying each error + * in turn. * * @author Greg Hinkle */ public class IndividualErrorTag extends HtmlTagSupport { - private static final Log log = Log.getInstance(IndividualErrorTag.class); - - /** - * Does nothing - * @return SKIP_BODY always - * @throws JspException - */ - @Override - public int doStartTag() throws JspException { - return SKIP_BODY; - } + private static final Log log = Log.getInstance(IndividualErrorTag.class); - /** - * Outputs the error for the current iteration of the parent ErrorsTag. - * - * @return EVAL_PAGE in all circumstances - */ - @Override - public int doEndTag() throws JspException { + /** + * Does nothing + * + * @return SKIP_BODY always + * @throws JspException + */ + @Override + public int doStartTag() throws JspException { + return SKIP_BODY; + } - Locale locale = getPageContext().getRequest().getLocale(); - JspWriter writer = getPageContext().getOut(); + /** + * Outputs the error for the current iteration of the parent ErrorsTag. + * + * @return EVAL_PAGE in all circumstances + */ + @Override + public int doEndTag() throws JspException { - ErrorsTag parentErrorsTag = getParentTag(ErrorsTag.class); - if (parentErrorsTag != null) { - // Mode: sub-tag inside an errors tag - try { - ValidationError error = parentErrorsTag.getCurrentError(); - writer.write( error.getMessage(locale) ); - } - catch (IOException ioe) { - JspException jspe = new JspException("IOException encountered while writing " + - "error tag to the JspWriter.", ioe); - log.warn(jspe); - throw jspe; - } - } + Locale locale = getPageContext().getRequest().getLocale(); + JspWriter writer = getPageContext().getOut(); - return EVAL_PAGE; + ErrorsTag parentErrorsTag = getParentTag(ErrorsTag.class); + if (parentErrorsTag != null) { + // Mode: sub-tag inside an errors tag + try { + ValidationError error = parentErrorsTag.getCurrentError(); + writer.write(error.getMessage(locale)); + } catch (IOException ioe) { + JspException jspe = + new JspException( + "IOException encountered while writing " + "error tag to the JspWriter.", ioe); + log.warn(jspe); + throw jspe; + } } + + return EVAL_PAGE; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputButtonSupportTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputButtonSupportTag.java index 65f2bbd0e..220e1e10e 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputButtonSupportTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputButtonSupportTag.java @@ -14,82 +14,87 @@ */ package net.sourceforge.stripes.tag; -import javax.servlet.jsp.tagext.BodyTag; -import javax.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.BodyTag; /** - *

    Support tag class that can generate HTML form fields with localized value attributes. - * Primarily used to contain identical functionality between submit, reset and button input types. - * The only capability offered above and beyond a pure html tag is the ability to lookup the value - * of the button (i.e. the text on the button that the user sees) from a localized resource bundle. - * The tag will set it's value using the first non-null result from the following list:

    + * Support tag class that can generate HTML form fields with localized value attributes. Primarily + * used to contain identical functionality between submit, reset and button input types. The only + * capability offered above and beyond a pure html tag is the ability to lookup the value of the + * button (i.e. the text on the button that the user sees) from a localized resource bundle. The tag + * will set it's value using the first non-null result from the following list: * *
      - *
    • formName.buttonName from the localized resource bundle
    • - *
    • buttonName from the localized resource bundle
    • - *
    • the trimmed body of the tag
    • - *
    • the value attribute of the tag
    • + *
    • formName.buttonName from the localized resource bundle + *
    • buttonName from the localized resource bundle + *
    • the trimmed body of the tag + *
    • the value attribute of the tag *
    * * @author Tim Fennell */ public class InputButtonSupportTag extends InputTagSupport implements BodyTag { - private String value; + private String value; - /** Sets the value to use for the submit button if all other strategies fail. */ - public void setValue(String value) { this.value = value; } + /** Sets the value to use for the submit button if all other strategies fail. */ + public void setValue(String value) { + this.value = value; + } - /** Returns the value set with setValue(). */ - public String getValue() { return this.value; } + /** Returns the value set with setValue(). */ + public String getValue() { + return this.value; + } - /** - * Does nothing. - * @return EVAL_BODY_BUFFERED in all cases. - */ - @Override - public int doStartInputTag() throws JspException { - return EVAL_BODY_BUFFERED; - } + /** + * Does nothing. + * + * @return EVAL_BODY_BUFFERED in all cases. + */ + @Override + public int doStartInputTag() throws JspException { + return EVAL_BODY_BUFFERED; + } - /** Does nothing. */ - public void doInitBody() throws JspException { } + /** Does nothing. */ + public void doInitBody() throws JspException {} - /** - * Does nothing. - * @return SKIP_BODY in all cases. - */ - public int doAfterBody() throws JspException { - return SKIP_BODY; - } + /** + * Does nothing. + * + * @return SKIP_BODY in all cases. + */ + public int doAfterBody() throws JspException { + return SKIP_BODY; + } - /** - * Looks up the appropriate value to use for the submit button and then writes the tag - * out to the page. - * @return EVAL_PAGE in all cases. - * @throws javax.servlet.jsp.JspException if output cannot be written. - */ - @Override - public int doEndInputTag() throws JspException { - // Find out if we have a value from the PopulationStrategy - String body = getBodyContentAsString(); - String localizedValue = getLocalizedFieldName(); + /** + * Looks up the appropriate value to use for the submit button and then writes the tag out to the + * page. + * + * @return EVAL_PAGE in all cases. + * @throws jakarta.servlet.jsp.JspException if output cannot be written. + */ + @Override + public int doEndInputTag() throws JspException { + // Find out if we have a value from the PopulationStrategy + String body = getBodyContentAsString(); + String localizedValue = getLocalizedFieldName(); - // Figure out where to pull the value from - if (localizedValue != null) { - getAttributes().put("value", localizedValue); - } - else if (body != null) { - getAttributes().put("value", body.trim()); - } - else if (this.value != null) { - getAttributes().put("value", this.value); - } + // Figure out where to pull the value from + if (localizedValue != null) { + getAttributes().put("value", localizedValue); + } else if (body != null) { + getAttributes().put("value", body.trim()); + } else if (this.value != null) { + getAttributes().put("value", this.value); + } - writeSingletonTag(getPageContext().getOut(), "input"); + writeSingletonTag(getPageContext().getOut(), "input"); - // Restore the original state before we mucked with it - getAttributes().remove("value"); + // Restore the original state before we mucked with it + getAttributes().remove("value"); - return EVAL_PAGE; - } + return EVAL_PAGE; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputButtonTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputButtonTag.java index 3e8d08b83..b3de83255 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputButtonTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputButtonTag.java @@ -15,17 +15,16 @@ package net.sourceforge.stripes.tag; /** - *

    Tag that generates HTML form fields of type {@literal } which - * render buttons for submitting forms. The only capability offered above and beyond a pure - * html tag is the ability to lookup the value of the button (i.e. the text on the button that the - * user sees) from a localized resource bundle. For more details on operation see - * {@link InputButtonSupportTag}. + * Tag that generates HTML form fields of type {@literal } which render + * buttons for submitting forms. The only capability offered above and beyond a pure html tag is the + * ability to lookup the value of the button (i.e. the text on the button that the user sees) from a + * localized resource bundle. For more details on operation see {@link InputButtonSupportTag}. * * @author Tim Fennell */ public class InputButtonTag extends InputButtonSupportTag { - /** Sets the input tag type to be button. */ - public InputButtonTag() { - getAttributes().put("type", "button"); - } + /** Sets the input tag type to be button. */ + public InputButtonTag() { + getAttributes().put("type", "button"); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputCheckBoxTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputCheckBoxTag.java index e6d978a65..f796edc5b 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputCheckBoxTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputCheckBoxTag.java @@ -14,117 +14,118 @@ */ package net.sourceforge.stripes.tag; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.BodyTag; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.BodyTag; /** - *

    Implements an HTML tag that generates form fields of type {@literal }. + * Implements an HTML tag that generates form fields of type {@literal }. * Since a single checkbox widget on a HTML page can have only a single value, the value tag - * attribute must always resolve to a scalar value which will be converted to a String using - * the Stripes Formatting system, or by caling toString() if an appropriate Formatter is - * not found.

    + * attribute must always resolve to a scalar value which will be converted to a String using the + * Stripes Formatting system, or by caling toString() if an appropriate Formatter is not found. * - *

    Checkboxes perform automatic (re-)population of state. They prefer, in order, values in the + *

    Checkboxes perform automatic (re-)population of state. They prefer, in order, values in the * HttpServletRequest, values in the ActionBean and lastly values set using checked="" on the page. - * The "checked" attribute is a complex attribute and may be a Collection, an Array or a scalar - * Java Object. In the first two cases a check is performed to see if the value in the value="foo" - * attribute is one of the elements in the checked collection or array. In the last case, the - * value is matched directly against the String form of the checked attribute. If in any case a - * checkbox's value matches then a checked="checked" attribute will be added to the HTML written.

    + * The "checked" attribute is a complex attribute and may be a Collection, an Array or a scalar Java + * Object. In the first two cases a check is performed to see if the value in the value="foo" + * attribute is one of the elements in the checked collection or array. In the last case, the value + * is matched directly against the String form of the checked attribute. If in any case a checkbox's + * value matches then a checked="checked" attribute will be added to the HTML written. * *

    The tag may include a body and if present the body is converted to a String and overrides the - * checked tag attribute.

    + * checked tag attribute. * * @author Tim Fennell */ public class InputCheckBoxTag extends InputTagSupport implements BodyTag { - private Object checked; - private Object value = Boolean.TRUE; // default value to supply true/false checkbox behaviour - - /** Basic constructor that sets the input tag's type attribute to "checkbox". */ - public InputCheckBoxTag() { - getAttributes().put("type", "checkbox"); - } - - /** - * Sets the default checked values for checkboxes with this name. - * - * @param checked may be either a Collection or Array of checked values, or a single Checked - * value. Values do not have to be Strings, but will need to be convertible to String - * using the toString() method. - */ - public void setChecked(Object checked) { - this.checked = checked; - } - - /** Returns the value originally set using setChecked(). */ - public Object getChecked() { - return this.checked; - } - - /** Sets the value that this checkbox will submit if it is checked. */ - public void setValue(Object value) { this.value = value; } - - /** Returns the value that this checkbox will submit if it is checked. */ - public Object getValue() { return this.value; } - - - /** Does nothing. */ - @Override - public int doStartInputTag() throws JspException { - return EVAL_BODY_BUFFERED; + private Object checked; + private Object value = Boolean.TRUE; // default value to supply true/false checkbox behaviour + + /** Basic constructor that sets the input tag's type attribute to "checkbox". */ + public InputCheckBoxTag() { + getAttributes().put("type", "checkbox"); + } + + /** + * Sets the default checked values for checkboxes with this name. + * + * @param checked may be either a Collection or Array of checked values, or a single Checked + * value. Values do not have to be Strings, but will need to be convertible to String using + * the toString() method. + */ + public void setChecked(Object checked) { + this.checked = checked; + } + + /** Returns the value originally set using setChecked(). */ + public Object getChecked() { + return this.checked; + } + + /** Sets the value that this checkbox will submit if it is checked. */ + public void setValue(Object value) { + this.value = value; + } + + /** Returns the value that this checkbox will submit if it is checked. */ + public Object getValue() { + return this.value; + } + + /** Does nothing. */ + @Override + public int doStartInputTag() throws JspException { + return EVAL_BODY_BUFFERED; + } + + /** Does nothing. */ + public void doInitBody() throws JspException { + // To change body of implemented methods use File | Settings | File Templates. + } + + /** Ensure that the body is evaluated only once. */ + public int doAfterBody() throws JspException { + return SKIP_BODY; + } + + /** + * Returns the body of the tag if it is present and not empty, otherwise returns the value of the + * 'checked' attribute. + */ + @Override + public Object getValueOnPage() { + Object value = getBodyContentAsString(); + if (value != null) { + return value; + } else { + return this.checked; } - - /** Does nothing. */ - public void doInitBody() throws JspException { - //To change body of implemented methods use File | Settings | File Templates. + } + + /** + * Does the main work of the tag, including determining the tags state (checked or not) and + * writing out a singleton tag representing the checkbox. + * + * @return always returns EVAL_PAGE to continue page execution + * @throws JspException if the checkbox is not contained inside a stripes InputFormTag, or has + * problems writing to the output. + */ + @Override + public int doEndInputTag() throws JspException { + // Find out if we have a value from the PopulationStrategy + Object checked = getOverrideValueOrValues(); + + // If the value of this checkbox is contained in the value or override value, check it + getAttributes().put("value", format(this.value)); + if (isItemSelected(this.value, checked)) { + getAttributes().put("checked", "checked"); } - /** Ensure that the body is evaluated only once. */ - public int doAfterBody() throws JspException { - return SKIP_BODY; - } + writeSingletonTag(getPageContext().getOut(), "input"); - /** - * Returns the body of the tag if it is present and not empty, otherwise returns - * the value of the 'checked' attribute. - */ - @Override - public Object getValueOnPage() { - Object value = getBodyContentAsString(); - if (value != null) { - return value; - } - else { - return this.checked; - } - } + // Restore the tags state to before we mucked with it + getAttributes().remove("checked"); + getAttributes().remove("value"); - /** - * Does the main work of the tag, including determining the tags state (checked or not) and - * writing out a singleton tag representing the checkbox. - * - * @return always returns EVAL_PAGE to continue page execution - * @throws JspException if the checkbox is not contained inside a stripes InputFormTag, or has - * problems writing to the output. - */ - @Override - public int doEndInputTag() throws JspException { - // Find out if we have a value from the PopulationStrategy - Object checked = getOverrideValueOrValues(); - - // If the value of this checkbox is contained in the value or override value, check it - getAttributes().put("value", format(this.value)); - if (isItemSelected(this.value, checked)) { - getAttributes().put("checked", "checked"); - } - - writeSingletonTag(getPageContext().getOut(), "input"); - - // Restore the tags state to before we mucked with it - getAttributes().remove("checked"); - getAttributes().remove("value"); - - return EVAL_PAGE; - } + return EVAL_PAGE; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputFileTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputFileTag.java index 85a56986d..f540994a5 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputFileTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputFileTag.java @@ -14,61 +14,65 @@ */ package net.sourceforge.stripes.tag; -import javax.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspException; /** - *

    Tag that generates HTML form fields of type {@literal }. The only - * functionality provided above and beyond a straight HTML input tag is that the tag will find - * its enclosing form tag and ensure that the for is set to POST instead of GET, and that the - * encoding type of the form is properly set to multipart/form-data as both these settings are - * necessary to correctly perform file uploads.

    + * Tag that generates HTML form fields of type {@literal }. The only + * functionality provided above and beyond a straight HTML input tag is that the tag will find its + * enclosing form tag and ensure that the for is set to POST instead of GET, and that the encoding + * type of the form is properly set to multipart/form-data as both these settings are necessary to + * correctly perform file uploads. * *

    Does not perform repopulation because default values for {@literal Generates one or more {@literal } HTML tags based on the value - * supplied. The hidden tag assigns the value attribute by scanning in the following order: + * Generates one or more {@literal } HTML tags based on the value + * supplied. The hidden tag assigns the value attribute by scanning in the following order: + * *

      - *
    • for one or more values with the same name in the HttpServletRequest
    • - *
    • for a field on the ActionBean with the same name (if a bean instance is present)
    • - *
    • by collapsing the body content to a String, if a body is present
    • - *
    • referring to the result of the EL expression contained in the value attribute of the tag.
    • + *
    • for one or more values with the same name in the HttpServletRequest + *
    • for a field on the ActionBean with the same name (if a bean instance is present) + *
    • by collapsing the body content to a String, if a body is present + *
    • referring to the result of the EL expression contained in the value attribute of the tag. *
    - *

    * *

    The result of this scan can produce either a Collection, an Array or any other Object. In the * first two cases the tag will output an HTML hidden form field tag for each value in the - * Collection or Array. In all other cases the Object is toString()'d (unless it is null) and a - * single hidden field will be written.

    + * Collection or Array. In all other cases the Object is toString()'d (unless it is null) and a + * single hidden field will be written. * * @author Tim Fennell */ public class InputHiddenTag extends InputTagSupport implements BodyTag { - private Object value; + private Object value; - /** Basic constructor that sets the input tag's type attribute to "hidden". */ - public InputHiddenTag() { - getAttributes().put("type", "hidden"); - } - - /** - * Sets the value that will be used for the hidden field(s) if no body or repopulation - * value is found. - * - * @param value the result of an EL evaluation, can be a Collection, Array or other. - */ - public void setValue(Object value) { - this.value = value; - } + /** Basic constructor that sets the input tag's type attribute to "hidden". */ + public InputHiddenTag() { + getAttributes().put("type", "hidden"); + } - /** Returns the value set with setValue(). */ - public Object getValue() { - return this.value; - } + /** + * Sets the value that will be used for the hidden field(s) if no body or repopulation value is + * found. + * + * @param value the result of an EL evaluation, can be a Collection, Array or other. + */ + public void setValue(Object value) { + this.value = value; + } - /** - * Sets the tag up as a hidden tag. - * - * @return EVAL_BODY_BUFFERED to always buffer the body (so it can be used as the value) - */ - @Override - public int doStartInputTag() throws JspException { - return EVAL_BODY_BUFFERED; - } + /** Returns the value set with setValue(). */ + public Object getValue() { + return this.value; + } - /** Does nothing. */ - public void doInitBody() throws JspException { + /** + * Sets the tag up as a hidden tag. + * + * @return EVAL_BODY_BUFFERED to always buffer the body (so it can be used as the value) + */ + @Override + public int doStartInputTag() throws JspException { + return EVAL_BODY_BUFFERED; + } - } + /** Does nothing. */ + public void doInitBody() throws JspException {} - /** - * Does nothing. - * @return SKIP_BODY in all cases. - */ - public int doAfterBody() throws JspException { - return SKIP_BODY; - } + /** + * Does nothing. + * + * @return SKIP_BODY in all cases. + */ + public int doAfterBody() throws JspException { + return SKIP_BODY; + } - /** - * Determines the value(s) that will be used for the tag and then proceeds to generate - * one or more hidden fields to contain those values. - * - * @return EVAL_PAGE in all cases. - * @throws JspException if the enclosing form tag cannot be found, or output cannot be written. - */ - @Override - public int doEndInputTag() throws JspException { - // Find out if we have a value from the PopulationStrategy - Object valueOrValues = getOverrideValueOrValues(); + /** + * Determines the value(s) that will be used for the tag and then proceeds to generate one or more + * hidden fields to contain those values. + * + * @return EVAL_PAGE in all cases. + * @throws JspException if the enclosing form tag cannot be found, or output cannot be written. + */ + @Override + public int doEndInputTag() throws JspException { + // Find out if we have a value from the PopulationStrategy + Object valueOrValues = getOverrideValueOrValues(); - // Figure out how many times to write it out - if (valueOrValues == null) { - getAttributes().put("value", ""); - writeSingletonTag(getPageContext().getOut(), "input"); - } - else if (valueOrValues.getClass().isArray()) { - int len = Array.getLength(valueOrValues); - for (int i=0; i) { - for (Object value : (Collection) valueOrValues) { - getAttributes().put("value", format(value)); - writeSingletonTag(getPageContext().getOut(), "input"); - } - } - else { - getAttributes().put("value", format(valueOrValues)); - writeSingletonTag(getPageContext().getOut(), "input"); - } + // Figure out how many times to write it out + if (valueOrValues == null) { + getAttributes().put("value", ""); + writeSingletonTag(getPageContext().getOut(), "input"); + } else if (valueOrValues.getClass().isArray()) { + int len = Array.getLength(valueOrValues); + for (int i = 0; i < len; ++i) { + Object value = Array.get(valueOrValues, i); + getAttributes().put("value", format(value)); + writeSingletonTag(getPageContext().getOut(), "input"); + } + } else if (valueOrValues instanceof Collection) { + for (Object value : (Collection) valueOrValues) { + getAttributes().put("value", format(value)); + writeSingletonTag(getPageContext().getOut(), "input"); + } + } else { + getAttributes().put("value", format(valueOrValues)); + writeSingletonTag(getPageContext().getOut(), "input"); + } - // Clear out the value from the attributes - getAttributes().remove("value"); + // Clear out the value from the attributes + getAttributes().remove("value"); - return EVAL_PAGE; - } + return EVAL_PAGE; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputImageTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputImageTag.java index fbea8db99..491e230f1 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputImageTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputImageTag.java @@ -14,90 +14,114 @@ */ package net.sourceforge.stripes.tag; -import javax.servlet.jsp.JspException; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.jsp.JspException; /** - *

    Tag class that generates an image button for use in HTML forms, e.g:

    + * Tag class that generates an image button for use in HTML forms, e.g: * - *
    {@literal }
    + *
    {@literal }
    * - *

    Provides a couple of facilities above and beyond using plain HTML tags. The main - * advantage is a localization capability. The tag looks in the Stripes Field Name - * message bundle for resources to be used as the src URL for the image and the alt - * text of the image. In order it will look for and use:

    + *

    Provides a couple of facilities above and beyond using plain HTML tags. The main advantage is + * a localization capability. The tag looks in the Stripes Field Name message bundle for resources + * to be used as the src URL for the image and the alt text of the image. In order it will look for + * and use: * *

      - *
    • resource: actionPath.inputName.[src|alt]
    • - *
    • resource: inputName.[src|alt]
    • + *
    • resource: actionPath.inputName.[src|alt] + *
    • resource: inputName.[src|alt] *
    • tag attributes: src and alt *
    * - *

    If localized values exist these are preferred over the values specified directly - * on the tag.

    + *

    If localized values exist these are preferred over the values specified directly on the tag. * - *

    Additionally if the 'src' URL (whether acquired from the tag attribute or the - * resource bundle) starts with a slash, the tag will prepend the context path of the - * web application.

    + *

    Additionally if the 'src' URL (whether acquired from the tag attribute or the resource bundle) + * starts with a slash, the tag will prepend the context path of the web application. * * @author Tim Fennell * @since Stripes 1.3 */ public class InputImageTag extends InputTagSupport { - /** Sets the tag's type to be an image input. */ - public InputImageTag() { - set("type", "image"); + /** Sets the tag's type to be an image input. */ + public InputImageTag() { + set("type", "image"); + } + + /** + * Does nothing. + * + * @return SKIP_BODY in all cases + */ + @Override + public int doStartInputTag() throws JspException { + return SKIP_BODY; + } + + /** + * Does the major work of the tag as described in the class level javadoc. Checks for localized + * src and alt attributes and prepends the context path to any src URL that starts with a slash. + * + * @return EVAL_PAGE always + */ + @Override + public int doEndInputTag() throws JspException { + // See if we should use a URL to a localized image + String name = getAttributes().get("name"); + String src = getLocalizedFieldName(name + ".src"); + if (src != null) { + setSrc(src); } - /** - * Does nothing. - * @return SKIP_BODY in all cases - */ - @Override - public int doStartInputTag() throws JspException { return SKIP_BODY; } - - /** - * Does the major work of the tag as described in the class level javadoc. Checks for - * localized src and alt attributes and prepends the context path to any src URL that - * starts with a slash. - * - * @return EVAL_PAGE always - */ - @Override - public int doEndInputTag() throws JspException { - // See if we should use a URL to a localized image - String name = getAttributes().get("name"); - String src = getLocalizedFieldName(name + ".src"); - if (src != null) { setSrc(src); } - - // And see if we have localized alt text too - String alt = getLocalizedFieldName(name + ".alt"); - if (alt != null) { setAlt(alt); } - - // Prepend the context path to the src URL - src = getSrc(); - if (src != null && src.startsWith("/")) { - String ctx = ((HttpServletRequest) getPageContext().getRequest()).getContextPath(); - setSrc(ctx + src); - } - - writeSingletonTag(getPageContext().getOut(), "input"); - return EVAL_PAGE; + // And see if we have localized alt text too + String alt = getLocalizedFieldName(name + ".alt"); + if (alt != null) { + setAlt(alt); } - /////////////////////////////////////////////////////////////////////////// - // Getter/Setter methods for additional attributes - /////////////////////////////////////////////////////////////////////////// - public void setAlign(String align) { set("align", align); } - public String getAlign() { return get("align"); } + // Prepend the context path to the src URL + src = getSrc(); + if (src != null && src.startsWith("/")) { + String ctx = ((HttpServletRequest) getPageContext().getRequest()).getContextPath(); + setSrc(ctx + src); + } + + writeSingletonTag(getPageContext().getOut(), "input"); + return EVAL_PAGE; + } + + /////////////////////////////////////////////////////////////////////////// + // Getter/Setter methods for additional attributes + /////////////////////////////////////////////////////////////////////////// + public void setAlign(String align) { + set("align", align); + } + + public String getAlign() { + return get("align"); + } + + public void setAlt(String alt) { + set("alt", alt); + } + + public String getAlt() { + return get("alt"); + } + + public void setSrc(String src) { + set("src", src); + } - public void setAlt(String alt) { set("alt", alt); } - public String getAlt() { return get("alt"); } + public String getSrc() { + return get("src"); + } - public void setSrc(String src) { set("src", src); } - public String getSrc() { return get("src"); } + public void setValue(String value) { + set("value", value); + } - public void setValue(String value) { set("value", value); } - public String getValue() { return get("value"); } + public String getValue() { + return get("value"); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputLabelTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputLabelTag.java index 690e05658..b4b2fa5a8 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputLabelTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputLabelTag.java @@ -14,140 +14,144 @@ */ package net.sourceforge.stripes.tag; -import net.sourceforge.stripes.exception.StripesJspException; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.BodyTag; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.BodyTag; import java.io.IOException; +import net.sourceforge.stripes.exception.StripesJspException; import net.sourceforge.stripes.localization.LocalizationUtility; /** - *

    Tag handler for a tag that produces an HTML label tag which is capable of looking up - * localized field names and formatting the label when validation errors exist. The field being - * labeled is identified using either the {@code name} attribute (preferred) or the - * {@code for} attribute. If the {@code name} attribute is supplied this will always be used as - * the lookup key (optionally pre-pended with the form's action path). If the {@code name} field - * is not supplied, the tag will fall back to using the value supplied for the {@code for} - * attribute. This is done because the {@code for} attribute is used by HTML as a reference to the - * {@code id} of the input being labeled. In the case where the id is equal to the field name - * it is unnecessary to specify a {@code name} attribute for the label tag. In cases where the - * field name (or other localized resource name) does not match an HTML ID, the {@code name} - * attribute must be used.

    + * Tag handler for a tag that produces an HTML label tag which is capable of looking up localized + * field names and formatting the label when validation errors exist. The field being labeled is + * identified using either the {@code name} attribute (preferred) or the {@code for} attribute. If + * the {@code name} attribute is supplied this will always be used as the lookup key (optionally + * pre-pended with the form's action path). If the {@code name} field is not supplied, the tag will + * fall back to using the value supplied for the {@code for} attribute. This is done because the + * {@code for} attribute is used by HTML as a reference to the {@code id} of the input being + * labeled. In the case where the id is equal to the field name it is unnecessary to specify a + * {@code name} attribute for the label tag. In cases where the field name (or other localized + * resource name) does not match an HTML ID, the {@code name} attribute must be used. * - *

    The value used for the label is the localized field name if one exists. Localized field - * names are looked up in the field name resource bundle first using {@code formActionPath.fieldName}, - * and then (if no value is found) using just {@code fieldName}. If no localized String can be found + *

    The value used for the label is the localized field name if one exists. Localized field names + * are looked up in the field name resource bundle first using {@code formActionPath.fieldName}, and + * then (if no value is found) using just {@code fieldName}. If no localized String can be found * then the body of the label tag is used. If no body is supplied then a warning String will be used - * instead!

    + * instead! * * @author Tim Fennell * @since Stripes 1.1 */ public class InputLabelTag extends InputTagSupport implements BodyTag { - private boolean nameSet; - - /** Sets the HTML ID of the field for which this is a label. */ - public void setFor(String forId) { - set("for", forId); - - // If the name field isn't set yet, set it with the forId. - if (!nameSet) { - super.setName(forId); - } - } - - /** Gets the HTML ID of the field for which this is a label. */ - public String getFor() { return get("for"); } - - /** - * Sets the name of the form element/label to be rendered. Should only be invoked by - * the JSP container as it also tracks whether or not the container has set the name, in - * order to correctly handle pooling. - */ - @Override - public void setName(String name) { - super.setName(name); - this.nameSet = true; - } - - /** - * Does nothing. - * @return EVAL_BODY_BUFFERED in all cases. - */ - @Override - public int doStartInputTag() throws JspException { - return EVAL_BODY_BUFFERED; - } + private boolean nameSet; - /** Does nothing. */ - public void doInitBody() throws JspException { /** Do Nothing */ } + /** Sets the HTML ID of the field for which this is a label. */ + public void setFor(String forId) { + set("for", forId); - /** - * Does nothing. - * @return SKIP_BODY in all cases. - */ - public int doAfterBody() throws JspException { - return SKIP_BODY; + // If the name field isn't set yet, set it with the forId. + if (!nameSet) { + super.setName(forId); } - - /** - * Performs the main work of the tag as described in the class level javadoc. - * @return EVAL_PAGE in all cases. - * @throws JspException if an IOException is encountered writing to the output stream. - */ - @Override - public int doEndInputTag() throws JspException { - try { - String label = getLocalizedFieldName(); - String fieldName = getAttributes().remove("name"); - - if (label == null) { - label = getBodyContentAsString(); - } - - if (label == null) { - if (fieldName != null) { - label = LocalizationUtility.makePseudoFriendlyName(fieldName); - } - else { - label = "Label could not find localized field name and had no body nor name attribute."; - } - } - - // Write out the tag - writeOpenTag(getPageContext().getOut(), "label"); - getPageContext().getOut().write(label); - writeCloseTag(getPageContext().getOut(), "label"); - - // Reset the field name so as to not screw up tag pooling - if (this.nameSet) { - super.setName(fieldName); - } - - return EVAL_PAGE; - } - catch (IOException ioe) { - throw new StripesJspException("Encountered an exception while trying to write to " + - "the output from the stripes:label tag handler class, InputLabelTag.", ioe); + } + + /** Gets the HTML ID of the field for which this is a label. */ + public String getFor() { + return get("for"); + } + + /** + * Sets the name of the form element/label to be rendered. Should only be invoked by the JSP + * container as it also tracks whether or not the container has set the name, in order to + * correctly handle pooling. + */ + @Override + public void setName(String name) { + super.setName(name); + this.nameSet = true; + } + + /** + * Does nothing. + * + * @return EVAL_BODY_BUFFERED in all cases. + */ + @Override + public int doStartInputTag() throws JspException { + return EVAL_BODY_BUFFERED; + } + + /** Does nothing. */ + public void doInitBody() throws JspException { + /** Do Nothing */ + } + + /** + * Does nothing. + * + * @return SKIP_BODY in all cases. + */ + public int doAfterBody() throws JspException { + return SKIP_BODY; + } + + /** + * Performs the main work of the tag as described in the class level javadoc. + * + * @return EVAL_PAGE in all cases. + * @throws JspException if an IOException is encountered writing to the output stream. + */ + @Override + public int doEndInputTag() throws JspException { + try { + String label = getLocalizedFieldName(); + String fieldName = getAttributes().remove("name"); + + if (label == null) { + label = getBodyContentAsString(); + } + + if (label == null) { + if (fieldName != null) { + label = LocalizationUtility.makePseudoFriendlyName(fieldName); + } else { + label = "Label could not find localized field name and had no body nor name attribute."; } + } + + // Write out the tag + writeOpenTag(getPageContext().getOut(), "label"); + getPageContext().getOut().write(label); + writeCloseTag(getPageContext().getOut(), "label"); + + // Reset the field name so as to not screw up tag pooling + if (this.nameSet) { + super.setName(fieldName); + } + + return EVAL_PAGE; + } catch (IOException ioe) { + throw new StripesJspException( + "Encountered an exception while trying to write to " + + "the output from the stripes:label tag handler class, InputLabelTag.", + ioe); } - - /** Overridden to do nothing, since a label isn't really a form field. */ - @Override - protected void registerWithParentForm() throws StripesJspException { } - - /** - * Wraps the parent loadErrors() to suppress exceptions when the label is outside of a - * stripes form tag. - */ - @Override - protected void loadErrors() { - try { - super.loadErrors(); - } - catch (StripesJspException sje) { - // Do nothing, we're suppressing this error - } + } + + /** Overridden to do nothing, since a label isn't really a form field. */ + @Override + protected void registerWithParentForm() throws StripesJspException {} + + /** + * Wraps the parent loadErrors() to suppress exceptions when the label is outside of a stripes + * form tag. + */ + @Override + protected void loadErrors() { + try { + super.loadErrors(); + } catch (StripesJspException sje) { + // Do nothing, we're suppressing this error } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputOptionTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputOptionTag.java index e13fcdc4b..d45030743 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputOptionTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputOptionTag.java @@ -14,144 +14,153 @@ */ package net.sourceforge.stripes.tag; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.BodyTag; +import java.io.IOException; import net.sourceforge.stripes.exception.StripesJspException; import net.sourceforge.stripes.util.HtmlUtil; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.BodyTag; -import java.io.IOException; - /** - *

    Generates an {@literal } HTML tag. Coordinates with an - * enclosing select tag to determine it's state (i.e. whether or not it is selected.) As a result - * some of the logic regarding state repopulation is a bit complex.

    + * Generates an {@literal } HTML tag. Coordinates with an + * enclosing select tag to determine it's state (i.e. whether or not it is selected.) As a result + * some of the logic regarding state repopulation is a bit complex. * - *

    Since options can have only a single value per option the value attribute of the tag - * must be a scalar, which will be converted into a String using a Formatter if an - * appropriate one can be found, otherwise the toString() method will be invoked. The presence of - * a "selected" attribute is used as an indication that this option believes it should be selected - * by default - the value (as opposed to the presence) of the selected attribute is never used....

    + *

    Since options can have only a single value per option the value attribute of the tag must be a + * scalar, which will be converted into a String using a Formatter if an appropriate one can be + * found, otherwise the toString() method will be invoked. The presence of a "selected" attribute is + * used as an indication that this option believes it should be selected by default - the value (as + * opposed to the presence) of the selected attribute is never used.... * - *

    The option tag delegates to its enclosing select tag to determine whether or not it should - * be selected. See the {@link InputSelectTag "select tag"} for documentation on how it - * determines selection status. If the select tag has no opinion on selection state - * (note that this is not the same as select tag deeming the option should not be selected) then - * the presence of the selected attribute (or lack thereof) is used to turn selection on or off.

    + *

    The option tag delegates to its enclosing select tag to determine whether or not it should be + * selected. See the {@link InputSelectTag "select tag"} for documentation on how it determines + * selection status. If the select tag has no opinion on selection state (note that this is + * not the same as select tag deeming the option should not be selected) then the presence of the + * selected attribute (or lack thereof) is used to turn selection on or off. * - *

    If the option has a body then the String value of that body will be used to generate the - * body of the generated HTML option. If the body is empty or not present then the label attribute - * will be written into the body of the tag.

    + *

    If the option has a body then the String value of that body will be used to generate the body + * of the generated HTML option. If the body is empty or not present then the label attribute will + * be written into the body of the tag. * * @author Tim Fennell */ public class InputOptionTag extends InputTagSupport implements BodyTag { - private String selected; - private String label; - private Object value; - - /** Sets the value of this option. */ - public void setValue(Object value) { this.value = value; } - - /** Returns the value of the option as set with setValue(). */ - public Object getValue() { return this.value; } - - /** Sets the label that will be used as the option body if no body is supplied. */ - public void setLabel(String label) { this.label = label; } - - /** Returns the value set with setLabel(). */ - public String getLabel() { return this.label; } - - /** Sets whether or not this option believes it should be selected by default. */ - public void setSelected(String selected) { this.selected = selected; } - - /** Returns the value set with setSelected(). */ - public String getSelected() { return this.selected; } - - /** - * Does nothing. - * @return EVAL_BODY_BUFFERED in all cases. - */ - @Override - public int doStartInputTag() throws JspException { - return EVAL_BODY_BUFFERED; + private String selected; + private String label; + private Object value; + + /** Sets the value of this option. */ + public void setValue(Object value) { + this.value = value; + } + + /** Returns the value of the option as set with setValue(). */ + public Object getValue() { + return this.value; + } + + /** Sets the label that will be used as the option body if no body is supplied. */ + public void setLabel(String label) { + this.label = label; + } + + /** Returns the value set with setLabel(). */ + public String getLabel() { + return this.label; + } + + /** Sets whether or not this option believes it should be selected by default. */ + public void setSelected(String selected) { + this.selected = selected; + } + + /** Returns the value set with setSelected(). */ + public String getSelected() { + return this.selected; + } + + /** + * Does nothing. + * + * @return EVAL_BODY_BUFFERED in all cases. + */ + @Override + public int doStartInputTag() throws JspException { + return EVAL_BODY_BUFFERED; + } + + /** Does nothing. */ + public void doInitBody() throws JspException {} + + /** + * Does nothing. + * + * @return SKIP_BODY in all cases. + */ + public int doAfterBody() throws JspException { + return SKIP_BODY; + } + + /** + * Locates the option's parent select tag, determines selection state and then writes out an + * option tag with an appropriate body. + * + * @return EVAL_PAGE in all cases. + * @throws JspException if the option is not contained inside an InputSelectTag or output cannot + * be written. + */ + @Override + public int doEndInputTag() throws JspException { + // Find our mandatory enclosing select tag + InputSelectTag selectTag = getParentTag(InputSelectTag.class); + if (selectTag == null) { + throw new StripesJspException("Option tags must always be contained inside a select tag."); } - /** Does nothing. */ - public void doInitBody() throws JspException { + // Decide if the label will come from the body of the option, of the label attr + String actualLabel = getBodyContentAsString(); + if (actualLabel == null) { + actualLabel = HtmlUtil.encode(this.label); } - /** - * Does nothing. - * @return SKIP_BODY in all cases. - */ - public int doAfterBody() throws JspException { - return SKIP_BODY; + // If no explicit value attribute set, use the tag label as the value + Object actualValue; + if (this.value == null) { + actualValue = actualLabel; + } else { + actualValue = this.value; } + getAttributes().put("value", format(actualValue)); - /** - * Locates the option's parent select tag, determines selection state and then writes out - * an option tag with an appropriate body. - * - * @return EVAL_PAGE in all cases. - * @throws JspException if the option is not contained inside an InputSelectTag or output - * cannot be written. - */ - @Override - public int doEndInputTag() throws JspException { - // Find our mandatory enclosing select tag - InputSelectTag selectTag = getParentTag(InputSelectTag.class); - if (selectTag == null) { - throw new StripesJspException - ("Option tags must always be contained inside a select tag."); - } - - // Decide if the label will come from the body of the option, of the label attr - String actualLabel = getBodyContentAsString(); - if (actualLabel == null) { - actualLabel = HtmlUtil.encode(this.label); - } - - // If no explicit value attribute set, use the tag label as the value - Object actualValue; - if (this.value == null) { - actualValue = actualLabel; - } - else { - actualValue = this.value; - } - getAttributes().put("value", format(actualValue)); - - // Determine if the option should be selected - if (selectTag.isOptionSelected(actualValue, (this.selected != null))) { - getAttributes().put("selected", "selected"); - } - - // And finally write the tag out to the page - try { - writeOpenTag(getPageContext().getOut(), "option"); - if (actualLabel != null) { - getPageContext().getOut().write(actualLabel); - } - writeCloseTag(getPageContext().getOut(), "option"); - - // Clean out the attributes we modified - getAttributes().remove("selected"); - getAttributes().remove("value"); - } - catch (IOException ioe) { - throw new JspException("IOException in InputOptionTag.doEndTag().", ioe); - } - - return EVAL_PAGE; + // Determine if the option should be selected + if (selectTag.isOptionSelected(actualValue, (this.selected != null))) { + getAttributes().put("selected", "selected"); } - /** - * Overridden to make sure that options do not try and register themselves with - * the form tag. This is done because options are not standalone input tags, but - * always part of a select tag (which gets registered). - */ - @Override - protected void registerWithParentForm() throws StripesJspException { - // Do nothing, options are not standalone fields and should not register + // And finally write the tag out to the page + try { + writeOpenTag(getPageContext().getOut(), "option"); + if (actualLabel != null) { + getPageContext().getOut().write(actualLabel); + } + writeCloseTag(getPageContext().getOut(), "option"); + + // Clean out the attributes we modified + getAttributes().remove("selected"); + getAttributes().remove("value"); + } catch (IOException ioe) { + throw new JspException("IOException in InputOptionTag.doEndTag().", ioe); } + + return EVAL_PAGE; + } + + /** + * Overridden to make sure that options do not try and register themselves with the form tag. This + * is done because options are not standalone input tags, but always part of a select tag (which + * gets registered). + */ + @Override + protected void registerWithParentForm() throws StripesJspException { + // Do nothing, options are not standalone fields and should not register + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputOptionsCollectionTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputOptionsCollectionTag.java index b527fd9c7..f5921e423 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputOptionsCollectionTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputOptionsCollectionTag.java @@ -14,389 +14,399 @@ */ package net.sourceforge.stripes.tag; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspWriter; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; import net.sourceforge.stripes.exception.StripesJspException; import net.sourceforge.stripes.localization.LocalizationUtility; +import net.sourceforge.stripes.util.CollectionUtil; +import net.sourceforge.stripes.util.StringUtil; +import net.sourceforge.stripes.util.bean.BeanComparator; import net.sourceforge.stripes.util.bean.BeanUtil; import net.sourceforge.stripes.util.bean.ExpressionException; -import net.sourceforge.stripes.util.bean.BeanComparator; -import net.sourceforge.stripes.util.StringUtil; -import net.sourceforge.stripes.util.CollectionUtil; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspWriter; -import java.util.Collection; -import java.util.Locale; -import java.util.Collections; -import java.util.List; -import java.util.LinkedList; /** - *

    Writes a set of {@literal } tags to the page based on the - * contents of a Collection. Each element in the collection is represented by a single option - * tag on the page. Uses the label and value attributes on the tag to name the properties of the - * objects in the Collection that should be used to generate the body of the HTML option tag and - * the value attribute of the HTML option tag respectively.

    + * Writes a set of {@literal } tags to the page based on the + * contents of a Collection. Each element in the collection is represented by a single option tag on + * the page. Uses the label and value attributes on the tag to name the properties of the objects in + * the Collection that should be used to generate the body of the HTML option tag and the value + * attribute of the HTML option tag respectively. + * + *

    E.g. a tag declaration that looks like: * - *

    E.g. a tag declaration that looks like:

    - *
    {@literal }
    + *
    {@literal }
    + * 
    * - *

    would cause the container to look for a Collection called "cats" across the various JSP - * scopes and set it on the tag. The tag would then proceed to iterate through that collection - * calling getCatId() and getName() on each cat to produce HTML option tags.

    + *

    would cause the container to look for a Collection called "cats" across the various JSP scopes + * and set it on the tag. The tag would then proceed to iterate through that collection calling + * getCatId() and getName() on each cat to produce HTML option tags. * *

    By default, the tag will attempt to localize the labels attributes of the option tags that are * generated. To override this default and turn off this behavior, thus saving unnecessary resource - * bundle lookups, set the localizeLabels attribute to false.

    - * - *

    To do label localization, the tag will look up labels in the field resource bundle using:

    + * bundle lookups, set the localizeLabels attribute to false. + * + *

    To do label localization, the tag will look up labels in the field resource bundle using: * *

      - *
    • {className}.{labelPropertyValue}
    • - *
    • {packageName}.{className}.{labelPropertyValue}
    • - *
    • {className}.{valuePropertyValue}
    • - *
    • {packageName}.{className}.{valuePropertyValue}
    • + *
    • {className}.{labelPropertyValue} + *
    • {packageName}.{className}.{labelPropertyValue} + *
    • {className}.{valuePropertyValue} + *
    • {packageName}.{className}.{valuePropertyValue} *
    * *

    For example for a class com.myco.Gender supplied to the options-collection tag with - * label="description" and value="key", when rendering for an instance - * Gender[key="M", description="Male"] the following localized properties will be looked for: + * label="description" and value="key", when rendering for an instance Gender[key="M", + * description="Male"] the following localized properties will be looked for: * *

      - *
    • Gender.Male
    • - *
    • com.myco.Gender.Male
    • - *
    • Gender.M
    • - *
    • com.myco.Gender.M
    • + *
    • Gender.Male + *
    • com.myco.Gender.Male + *
    • Gender.M + *
    • com.myco.Gender.M *
    * - *

    If no localized label can be found, or if the localizeLabels attribute is set to false, - * then the value of the label property will be used.

    + *

    If no localized label can be found, or if the localizeLabels attribute is set to false, then + * the value of the label property will be used. * *

    Optionally, the group attribute may be used to generate <optgroup> tags. The value of - * this attribute is used to retrieve the corresponding property on each object of the collection. - * A new optgroup will be created each time the value changes. - *

    + * this attribute is used to retrieve the corresponding property on each object of the collection. A + * new optgroup will be created each time the value changes. * - *

    The rendered group may be localized by specifying one of the following properties:

    + *

    The rendered group may be localized by specifying one of the following properties: * *

      - *
    • {className}.{groupPropertyValue}
    • - *
    • {packageName}.{className}.{groupPropertyValue}
    • + *
    • {className}.{groupPropertyValue} + *
    • {packageName}.{className}.{groupPropertyValue} *
    * - *

    All other attributes on the tag (other than collection, value, label and group) are passed directly - * through to the InputOptionTag which is used to generate the individual HTML options tags. As a - * result the InputOptionsCollectionTag will exhibit the same re-population/selection behaviour - * as the regular options tag.

    + *

    All other attributes on the tag (other than collection, value, label and group) are passed + * directly through to the InputOptionTag which is used to generate the individual HTML options + * tags. As a result the InputOptionsCollectionTag will exhibit the same re-population/selection + * behaviour as the regular options tag. * - *

    Since the tag has no use for one it does not allow a body.

    + *

    Since the tag has no use for one it does not allow a body. * * @author Tim Fennell */ public class InputOptionsCollectionTag extends HtmlTagSupport { - /** A helper for writing HTML <optgroup> tags. */ - private final HtmlTagSupport optgroupSupport = new HtmlTagSupport() { + /** A helper for writing HTML <optgroup> tags. */ + private final HtmlTagSupport optgroupSupport = + new HtmlTagSupport() { @Override public int doStartTag() throws JspException { - return 0; + return 0; } @Override public int doEndTag() throws JspException { - return 0; + return 0; } - }; - - private Collection collection; - private String value; - private String label; - private String sort; - private String group; - private Boolean localizeLabels; - - /** - * A little container class that holds an entry in the collection of items being used - * to generate the options, along with the determined label and value (either from a - * property, or a localized value). - */ - public static class Entry { - public Object bean, label, value, group; - Entry(Object bean, Object label, Object value, Object group) { - this.bean = bean; - this.label = label; - this.value = value; - this.group = group; - } - } - - /** Internal list of entries that is assembled from the items in the collection. */ - private List entries = new LinkedList(); - - /** - *

    Sets the collection that will be used to generate options. In this case the term - * collection is used in the loosest possible sense - it means either a bonafide instance - * of {@link java.util.Collection}, or an implementation of {@link Iterable} other than a - * Collection, or an array of Objects or primitives.

    - * - *

    In the case of any input which is not an {@link java.util.Collection} it is converted - * to a Collection before storing it.

    - * - * @param in either a Collection, an Iterable or an Array - */ - @SuppressWarnings("unchecked") - public void setCollection(Object in) { - if (in == null) this.collection = null; - else if (in instanceof Collection) this.collection = (Collection) in; - else if (in instanceof Iterable) this.collection = CollectionUtil.asList((Iterable) in); - else if (in.getClass().isArray()) this.collection = CollectionUtil.asList(in); - else { - throw new IllegalArgumentException - ("A 'collection' was supplied that is not of a supported type: " + in.getClass()); - } - } - - /** - * Returns the value set by {@link #setCollection(Object)}. In the case that a - * {@link java.util.Collection} was supplied, the same collection will be returned. In all - * other cases a new collection created to hold the supplied elements will be returned. - */ - public Object getCollection() { - return this.collection; + }; + + private Collection collection; + private String value; + private String label; + private String sort; + private String group; + private Boolean localizeLabels; + + /** + * A little container class that holds an entry in the collection of items being used to generate + * the options, along with the determined label and value (either from a property, or a localized + * value). + */ + public static class Entry { + public Object bean, label, value, group; + + Entry(Object bean, Object label, Object value, Object group) { + this.bean = bean; + this.label = label; + this.value = value; + this.group = group; } - - /** - * Sets the name of the property that will be fetched on each bean in the collection in - * order to generate the value attribute of each option. - * - * @param value the name of the attribute - */ - public void setValue(String value) { - this.value = value; + } + + /** Internal list of entries that is assembled from the items in the collection. */ + private List entries = new LinkedList(); + + /** + * Sets the collection that will be used to generate options. In this case the term collection is + * used in the loosest possible sense - it means either a bonafide instance of {@link + * java.util.Collection}, or an implementation of {@link Iterable} other than a Collection, or an + * array of Objects or primitives. + * + *

    In the case of any input which is not an {@link java.util.Collection} it is converted to a + * Collection before storing it. + * + * @param in either a Collection, an Iterable or an Array + */ + @SuppressWarnings("unchecked") + public void setCollection(Object in) { + if (in == null) this.collection = null; + else if (in instanceof Collection) this.collection = (Collection) in; + else if (in instanceof Iterable) this.collection = CollectionUtil.asList((Iterable) in); + else if (in.getClass().isArray()) this.collection = CollectionUtil.asList(in); + else { + throw new IllegalArgumentException( + "A 'collection' was supplied that is not of a supported type: " + in.getClass()); } - - /** Returns the property name set with setValue(). */ - public String getValue() { - return value; + } + + /** + * Returns the value set by {@link #setCollection(Object)}. In the case that a {@link + * java.util.Collection} was supplied, the same collection will be returned. In all other cases a + * new collection created to hold the supplied elements will be returned. + */ + public Object getCollection() { + return this.collection; + } + + /** + * Sets the name of the property that will be fetched on each bean in the collection in order to + * generate the value attribute of each option. + * + * @param value the name of the attribute + */ + public void setValue(String value) { + this.value = value; + } + + /** Returns the property name set with setValue(). */ + public String getValue() { + return value; + } + + /** + * Sets the name of the property that will be fetched on each bean in the collection in order to + * generate the body of each option (i.e. what is seen by the user). + * + * @param label the name of the attribute + */ + public void setLabel(String label) { + this.label = label; + } + + /** Gets the property name set with setLabel(). */ + public String getLabel() { + return label; + } + + /** + * Sets a comma separated list of properties by which the beans in the collection will be sorted + * prior to rendering them as options. 'label' and 'value' are special case properties that are + * used to indicate the generated label and value of the option. + * + * @param sort the name of the attribute(s) used to sort the collection of options + */ + public void setSort(String sort) { + this.sort = sort; + } + + /** Gets the comma separated list of properties by which the collection is sorted. */ + public String getSort() { + return sort; + } + + /** Sets the flag that indicates whether or not attempts to localize labels should be made. */ + public void setLocalizeLabels(Boolean localizeLabels) { + this.localizeLabels = localizeLabels; + } + + /** Gets the flag that indicates whether or not attempts to localize labels should be made. */ + public Boolean getLocalizeLabels() { + return localizeLabels; + } + + protected boolean isAttemptToLocalizeLabels() { + return (localizeLabels == null) || (localizeLabels != null && localizeLabels.booleanValue()); + } + + /** + * Adds an entry to the internal list of items being used to generate options. + * + * @param item the object represented by the option + * @param label the actual label for the option + * @param value the actual value for the option + */ + protected void addEntry(Object item, Object label, Object value) { + this.entries.add(new Entry(item, label, value, null)); + } + + /** + * Adds an entry to the internal list of items being used to generate options. + * + * @param item the object represented by the option + * @param label the actual label for the option + * @param value the actual value for the option + * @param group the value to be used for optgroups + */ + protected void addEntry(Object item, Object label, Object value, Object group) { + this.entries.add(new Entry(item, label, value, group)); + } + + /** + * Iterates through the collection and generates the list of Entry objects that can then be sorted + * and rendered into options. It is assumed that each element in the collection has non-null + * values for the properties specified for generating the label and value. + * + * @return SKIP_BODY in all cases + * @throws JspException if either the label or value attributes specify properties that are not + * present on the beans in the collection + */ + @Override + public int doStartTag() throws JspException { + if (this.collection == null) return SKIP_BODY; + + String labelProperty = getLabel(); + String valueProperty = getValue(); + String groupProperty = getGroup(); + + try { + Locale locale = getPageContext().getRequest().getLocale(); + boolean attemptToLocalizeLabels = isAttemptToLocalizeLabels(); + + for (Object item : this.collection) { + Class clazz = item.getClass(); + + // Lookup the bean properties for the label, value and group + Object label = + (labelProperty == null) ? item : BeanUtil.getPropertyValue(labelProperty, item); + Object value = + (valueProperty == null) ? item : BeanUtil.getPropertyValue(valueProperty, item); + Object group = + (groupProperty == null) ? null : BeanUtil.getPropertyValue(groupProperty, item); + + if (attemptToLocalizeLabels) { + // Try to localize the label + String packageName = clazz.getPackage() == null ? "" : clazz.getPackage().getName(); + String simpleName = LocalizationUtility.getSimpleName(clazz); + String localizedLabel = null; + if (label != null) { + localizedLabel = + LocalizationUtility.getLocalizedFieldName( + simpleName + "." + label, packageName, null, locale); + } + if (localizedLabel == null && value != null) { + localizedLabel = + LocalizationUtility.getLocalizedFieldName( + simpleName + "." + value, packageName, null, locale); + } + if (localizedLabel != null) label = localizedLabel; + + // Try to localize the group + if (group != null) { + String localizedGroup = + LocalizationUtility.getLocalizedFieldName( + simpleName + "." + group, packageName, null, locale); + if (localizedGroup != null) group = localizedGroup; + } + } + addEntry(item, label, value, group); + } + } catch (ExpressionException ee) { + throw new StripesJspException( + "A problem occurred generating an options-collection. " + + "Most likely either [" + + labelProperty + + "] or [" + + valueProperty + + "] is not a " + + "valid property of the beans in the collection: " + + this.collection, + ee); } - /** - * Sets the name of the property that will be fetched on each bean in the collection in - * order to generate the body of each option (i.e. what is seen by the user). - * - * @param label the name of the attribute - */ - public void setLabel(String label) { - this.label = label; - } + return SKIP_BODY; + } + + /** + * Optionally sorts the assembled entries and then renders them into a series of option tags using + * an instance of InputOptionTag to do the rendering work. + * + * @return EVAL_PAGE in all cases. + */ + @Override + public int doEndTag() throws JspException { + // Determine if we're going to be sorting the collection + List sortedEntries = new LinkedList(this.entries); + if (this.sort != null) { + String[] props = StringUtil.standardSplit(this.sort); + for (int i = 0; i < props.length; ++i) { + if (!props[i].equals("label") && !props[i].equals("value")) { + props[i] = "bean." + props[i]; + } + } - /** Gets the property name set with setLabel(). */ - public String getLabel() { - return label; + Collections.sort( + sortedEntries, new BeanComparator(getPageContext().getRequest().getLocale(), props)); } - /** - * Sets a comma separated list of properties by which the beans in the collection will - * be sorted prior to rendering them as options. 'label' and 'value' are special case - * properties that are used to indicate the generated label and value of the option. - * - * @param sort the name of the attribute(s) used to sort the collection of options - */ - public void setSort(String sort) { - this.sort = sort; - } + InputOptionTag tag = new InputOptionTag(); + tag.setParent(this); + tag.setPageContext(getPageContext()); - /** Gets the comma separated list of properties by which the collection is sorted. */ - public String getSort() { - return sort; - } + Object lastGroup = null; - /** Sets the flag that indicates whether or not attempts to localize labels should be made. */ - public void setLocalizeLabels(Boolean localizeLabels) { - this.localizeLabels = localizeLabels; - } - - /** Gets the flag that indicates whether or not attempts to localize labels should be made. */ - public Boolean getLocalizeLabels() { - return localizeLabels; - } + JspWriter out = getPageContext().getOut(); + for (Entry entry : sortedEntries) { + // Set properties common to all options + tag.getAttributes().putAll(getAttributes()); - protected boolean isAttemptToLocalizeLabels() { - return (localizeLabels == null) || (localizeLabels != null && localizeLabels.booleanValue()); - } + // Set properties for this tag + tag.setLabel(entry.label == null ? null : entry.label.toString()); + tag.setValue(entry.value); + try { + if (entry.group != null && !entry.group.equals(lastGroup)) { + if (lastGroup != null) optgroupSupport.writeCloseTag(out, "optgroup"); - /** - * Adds an entry to the internal list of items being used to generate options. - * @param item the object represented by the option - * @param label the actual label for the option - * @param value the actual value for the option - */ - protected void addEntry(Object item, Object label, Object value) { - this.entries.add(new Entry(item, label, value, null)); - } - - /** - * Adds an entry to the internal list of items being used to generate options. - * @param item the object represented by the option - * @param label the actual label for the option - * @param value the actual value for the option - * @param group the value to be used for optgroups - */ - protected void addEntry(Object item, Object label, Object value, Object group) { - this.entries.add(new Entry(item, label, value, group)); - } - - /** - * Iterates through the collection and generates the list of Entry objects that can then - * be sorted and rendered into options. It is assumed that each element in the collection - * has non-null values for the properties specified for generating the label and value. - * - * @return SKIP_BODY in all cases - * @throws JspException if either the label or value attributes specify properties that are - * not present on the beans in the collection - */ - @Override - public int doStartTag() throws JspException { - if (this.collection == null) - return SKIP_BODY; - - String labelProperty = getLabel(); - String valueProperty = getValue(); - String groupProperty = getGroup(); + optgroupSupport.set("label", String.valueOf(entry.group)); + optgroupSupport.writeOpenTag(out, "optgroup"); + lastGroup = entry.group; + } + tag.doStartTag(); + tag.doInitBody(); + tag.doAfterBody(); + tag.doEndTag(); + } catch (Throwable t) { + /** Catch whatever comes back out of the doCatch() method and deal with it */ try { - Locale locale = getPageContext().getRequest().getLocale(); - boolean attemptToLocalizeLabels = isAttemptToLocalizeLabels(); - - for (Object item : this.collection) { - Class clazz = item.getClass(); - - // Lookup the bean properties for the label, value and group - Object label = (labelProperty == null) ? item : BeanUtil.getPropertyValue(labelProperty, item); - Object value = (valueProperty == null) ? item : BeanUtil.getPropertyValue(valueProperty, item); - Object group = (groupProperty == null) ? null : BeanUtil.getPropertyValue(groupProperty, item); - - if (attemptToLocalizeLabels) { - // Try to localize the label - String packageName = clazz.getPackage() == null ? "" : clazz.getPackage().getName(); - String simpleName = LocalizationUtility.getSimpleName(clazz); - String localizedLabel = null; - if (label != null) { - localizedLabel = LocalizationUtility.getLocalizedFieldName - (simpleName + "." + label, packageName, null, locale); - } - if (localizedLabel == null && value != null) { - localizedLabel = LocalizationUtility.getLocalizedFieldName - (simpleName + "." + value, packageName, null, locale); - } - if (localizedLabel != null) label = localizedLabel; - - // Try to localize the group - if (group != null) { - String localizedGroup = LocalizationUtility.getLocalizedFieldName( - simpleName + "." + group, packageName, null, locale); - if (localizedGroup != null) group = localizedGroup; - } - } - addEntry(item, label, value, group); - } + tag.doCatch(t); + } catch (Throwable t2) { + if (t2 instanceof JspException) throw (JspException) t2; + if (t2 instanceof RuntimeException) throw (RuntimeException) t2; + else throw new StripesJspException(t2); } - catch (ExpressionException ee) { - throw new StripesJspException("A problem occurred generating an options-collection. " + - "Most likely either [" + labelProperty + "] or ["+ valueProperty + "] is not a " + - "valid property of the beans in the collection: " + this.collection, ee); - } - - return SKIP_BODY; + } finally { + tag.doFinally(); + } } - /** - * Optionally sorts the assembled entries and then renders them into a series of - * option tags using an instance of InputOptionTag to do the rendering work. - * - * @return EVAL_PAGE in all cases. - */ - @Override - public int doEndTag() throws JspException { - // Determine if we're going to be sorting the collection - List sortedEntries = new LinkedList(this.entries); - if (this.sort != null) { - String[] props = StringUtil.standardSplit(this.sort); - for (int i=0;iWrites a set of {@literal } tags to the page based on the - * values of a enum. Each value in the enum is represented by a single option tag on the page. The - * options will be generated in ordinal value order (i.e. the order they are declared in the - * enum).

    + * Writes a set of {@literal } tags to the page based on the values + * of a enum. Each value in the enum is represented by a single option tag on the page. The options + * will be generated in ordinal value order (i.e. the order they are declared in the enum). * *

    Please note that as with the options-collection and options-map tags, resource bundle lookups - * are on by default but can be turned off by setting the localizeLabels attribute to false.

    + * are on by default but can be turned off by setting the localizeLabels attribute to false. * *

    The label (the value the user sees) is generated in one of three ways: by looking up a - * localized value (unless localizeLabels is set to false), by using the property named by the 'label' - * tag attribute if it is supplied and lastly by toString()'ing the enumeration value. - * For example the following tag:

    + * localized value (unless localizeLabels is set to false), by using the property named by the + * 'label' tag attribute if it is supplied and lastly by toString()'ing the enumeration value. For + * example the following tag: * - *
    {@literal }
    + *
    {@literal }
    + * 
    * * when generating the option for the value {@code EyeColor.BLUE} will look for a label in the - * following order:

    + * following order: * *
      - *
    • resource: EyeColor.BLUE (skipped if localizeLabels is set to false)
    • - *
    • resource: net.kitty.EyeColor.BLUE (skipped if localizeLabels is set to false)
    • - *
    • property: EyeColor.BLUE.getDescription() (because of the label="description" above)
    • - *
    • failsafe: EyeColor.BLUE.toString()
    • + *
    • resource: EyeColor.BLUE (skipped if localizeLabels is set to false) + *
    • resource: net.kitty.EyeColor.BLUE (skipped if localizeLabels is set to false) + *
    • property: EyeColor.BLUE.getDescription() (because of the label="description" above) + *
    • failsafe: EyeColor.BLUE.toString() *
    * - *

    If the class specified does not exist, or does not specify a Java 1.5 enum then a - * JspException will be raised.

    + *

    If the class specified does not exist, or does not specify a Java 1.5 enum then a JspException + * will be raised. * - *

    All attributes of the tag, other than enum and label, are passed directly through to - * the InputOptionTag which is used to generate the individual HTML options tags. As a - * result the InputOptionsEnumerationTag will exhibit the same re-population/selection behaviour - * as the regular options tag.

    + *

    All attributes of the tag, other than enum and label, are passed directly through to the + * InputOptionTag which is used to generate the individual HTML options tags. As a result the + * InputOptionsEnumerationTag will exhibit the same re-population/selection behaviour as the regular + * options tag. * - *

    Since the tag has no use for one it does not allow a body.

    + *

    Since the tag has no use for one it does not allow a body. * * @author Tim Fennell */ public class InputOptionsEnumerationTag extends InputOptionsCollectionTag { - private String className; + private String className; - /** Sets the fully qualified name of an enumeration class. */ - public void setEnum(String name) { - this.className = name; - } + /** Sets the fully qualified name of an enumeration class. */ + public void setEnum(String name) { + this.className = name; + } - /** Gets the enum class name set with setEnum(). */ - public String getEnum() { - return this.className; - } + /** Gets the enum class name set with setEnum(). */ + public String getEnum() { + return this.className; + } - /** - * Attempts to instantiate the Class object representing the enum and fetch the values of the - * enum. Then generates an option per value using an instance of an InputOptionTag. - * - * @return SKIP_BODY in all cases. - * @throws JspException if the class name supplied is not a valid class, or cannot be cast - * to Class. - */ - @Override - @SuppressWarnings("unchecked") - public int doStartTag() throws JspException { - Class clazz = null; - try { - clazz = ReflectUtil.findClass(this.className); - } - catch (Exception e) { - // Try replacing the last period with a $ just in case the enum in question - // is an inner class of another class - try { - int last = this.className.lastIndexOf('.'); - if (last > 0) { - String n2 = new StringBuilder(className).replace(last, last+1, "$").toString(); - clazz = ReflectUtil.findClass(n2); - } - } - // If our second attempt didn't work, wrap the *original* exception - catch (Exception e2) { - throw new StripesJspException - ("Could not process class [" + this.className + "]. Attribute 'enum' on " + - "tag options-enumeration must be the fully qualified name of a " + - "class which is a java 1.5 enum.", e); - } + /** + * Attempts to instantiate the Class object representing the enum and fetch the values of the + * enum. Then generates an option per value using an instance of an InputOptionTag. + * + * @return SKIP_BODY in all cases. + * @throws JspException if the class name supplied is not a valid class, or cannot be cast to + * Class. + */ + @Override + @SuppressWarnings("unchecked") + public int doStartTag() throws JspException { + Class clazz = null; + try { + clazz = ReflectUtil.findClass(this.className); + } catch (Exception e) { + // Try replacing the last period with a $ just in case the enum in question + // is an inner class of another class + try { + int last = this.className.lastIndexOf('.'); + if (last > 0) { + String n2 = new StringBuilder(className).replace(last, last + 1, "$").toString(); + clazz = ReflectUtil.findClass(n2); } + } + // If our second attempt didn't work, wrap the *original* exception + catch (Exception e2) { + throw new StripesJspException( + "Could not process class [" + + this.className + + "]. Attribute 'enum' on " + + "tag options-enumeration must be the fully qualified name of a " + + "class which is a java 1.5 enum.", + e); + } + } - if (!clazz.isEnum()) { - throw new StripesJspException - ("The class name supplied, [" + this.className + "], does not appear to be " + - "a JDK1.5 enum class."); - } - - Enum[] enums = clazz.getEnumConstants(); - - try { - Locale locale = getPageContext().getRequest().getLocale(); - boolean attemptToLocalizeLabels = isAttemptToLocalizeLabels(); + if (!clazz.isEnum()) { + throw new StripesJspException( + "The class name supplied, [" + + this.className + + "], does not appear to be " + + "a JDK1.5 enum class."); + } - for (Enum item : enums) { - Object label = null; - String simpleName = LocalizationUtility.getSimpleName(clazz); + Enum[] enums = clazz.getEnumConstants(); - if (attemptToLocalizeLabels) { - String packageName = clazz.getPackage() == null ? "" : clazz.getPackage().getName(); + try { + Locale locale = getPageContext().getRequest().getLocale(); + boolean attemptToLocalizeLabels = isAttemptToLocalizeLabels(); - // Check for a localized label using class.ENUM_VALUE and package.class.ENUM_VALUE - label = LocalizationUtility.getLocalizedFieldName(simpleName + "." + item.name(), - packageName, - null, - locale); - } - if (label == null) { - if (getLabel() != null) { - label = BeanUtil.getPropertyValue(getLabel(), item); - } - else { - label = item.toString(); - } - } + for (Enum item : enums) { + Object label = null; + String simpleName = LocalizationUtility.getSimpleName(clazz); - Object group = null; - if (getGroup() != null) - group = BeanUtil.getPropertyValue(getGroup(), item); + if (attemptToLocalizeLabels) { + String packageName = clazz.getPackage() == null ? "" : clazz.getPackage().getName(); - addEntry(item, label, item, group); - } + // Check for a localized label using class.ENUM_VALUE and package.class.ENUM_VALUE + label = + LocalizationUtility.getLocalizedFieldName( + simpleName + "." + item.name(), packageName, null, locale); } - catch (ExpressionException ee) { - throw new StripesJspException("A problem occurred generating an options-enumeration. " + - "Most likely either the class [" + getEnum() + "] is not an enum or, [" + - getLabel() + "] is not a valid property of the enum.", ee); + if (label == null) { + if (getLabel() != null) { + label = BeanUtil.getPropertyValue(getLabel(), item); + } else { + label = item.toString(); + } } - return SKIP_BODY; + Object group = null; + if (getGroup() != null) group = BeanUtil.getPropertyValue(getGroup(), item); + + addEntry(item, label, item, group); + } + } catch (ExpressionException ee) { + throw new StripesJspException( + "A problem occurred generating an options-enumeration. " + + "Most likely either the class [" + + getEnum() + + "] is not an enum or, [" + + getLabel() + + "] is not a valid property of the enum.", + ee); } + + return SKIP_BODY; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputOptionsMapTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputOptionsMapTag.java index 24276944f..f4d93e828 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputOptionsMapTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputOptionsMapTag.java @@ -14,63 +14,51 @@ */ package net.sourceforge.stripes.tag; +import jakarta.servlet.jsp.JspException; import java.util.Map; -import javax.servlet.jsp.JspException; - -import net.sourceforge.stripes.tag.InputOptionsCollectionTag; - /** - *

    Extracts the {@link java.util.Set} of {@link java.util.Map.Entry} from the - * specified {@link java.util.Map} and uses it as the {@link java.util.Collection} - * for the superclass {@link net.sourceforge.stripes.tag.InputOptionsCollectionTag}.

    - * - *

    The value and label parameters will be set to "key" and "value" respectively - * if they are null.

    - * - * @author Aaron Porter + * Extracts the {@link java.util.Set} of {@link java.util.Map.Entry} from the specified {@link + * java.util.Map} and uses it as the {@link java.util.Collection} for the superclass {@link + * net.sourceforge.stripes.tag.InputOptionsCollectionTag}. * + *

    The value and label parameters will be set to "key" and "value" respectively if they are null. + * + * @author Aaron Porter */ -public class InputOptionsMapTag extends InputOptionsCollectionTag -{ - private Map map; +public class InputOptionsMapTag extends InputOptionsCollectionTag { + private Map map; - /** - *

    Returns the {@link java.util.Map} that was passed in via setMap().

    - * - * @return the {@link java.util.Map} passed in via setMap(). - */ - public Map getMap() { - return map; - } + /** + * Returns the {@link java.util.Map} that was passed in via setMap(). + * + * @return the {@link java.util.Map} passed in via setMap(). + */ + public Map getMap() { + return map; + } - /** - *

    This function simply passes the result of Map.entrySet() - * as the collection to be used by the superclass and sets the value and label - * variables if they have not already been set.

    - * - * @param map a Map - */ - public void setMap(Map map) { - this.map = map; + /** + * This function simply passes the result of Map.entrySet() as the collection to be used by the + * superclass and sets the value and label variables if they have not already been set. + * + * @param map a Map + */ + public void setMap(Map map) { + this.map = map; - setCollection(map.entrySet()); + setCollection(map.entrySet()); - if (getValue() == null) - setValue("key"); + if (getValue() == null) setValue("key"); - if (getLabel() == null) - setLabel("value"); - } + if (getLabel() == null) setLabel("value"); + } - /** - * Calls super.doEndTag() and cleans up instance variables so this instance - * may be reused. - */ - @Override - public int doEndTag() throws JspException { - int result = super.doEndTag(); + /** Calls super.doEndTag() and cleans up instance variables so this instance may be reused. */ + @Override + public int doEndTag() throws JspException { + int result = super.doEndTag(); - return result; - } + return result; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputPasswordTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputPasswordTag.java index 242d0e6fd..cf498a85b 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputPasswordTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputPasswordTag.java @@ -14,60 +14,62 @@ */ package net.sourceforge.stripes.tag; -import javax.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspException; /** - * Tag class that implements an input tag of type password. Defines one attribute in addition - * to those provided by the HTML tag. If {@code repopulate} is set then the password tag will - * behave just like the text tag, and will repopulate values on error and during wizard flows. If - * {@code repopulate} is not set, or is set to false then values will not be re-populated, but + * Tag class that implements an input tag of type password. Defines one attribute in addition to + * those provided by the HTML tag. If {@code repopulate} is set then the password tag will behave + * just like the text tag, and will repopulate values on error and during wizard flows. If {@code + * repopulate} is not set, or is set to false then values will not be re-populated, but * initial/default values supplied to the tag on the JSP will be used. * * @author Tim Fennell * @since Stripes 1.1 */ public class InputPasswordTag extends InputTextTag { - private boolean repopulate = false; + private boolean repopulate = false; - /** Sets whether or not the tag will repopulate the value if one is present. */ - public void setRepopulate(boolean repopulate) { this.repopulate = repopulate; } + /** Sets whether or not the tag will repopulate the value if one is present. */ + public void setRepopulate(boolean repopulate) { + this.repopulate = repopulate; + } - /** Returns true if the tag will repopulate values, false otherwise. */ - public boolean getRepopulate() { return repopulate; } + /** Returns true if the tag will repopulate values, false otherwise. */ + public boolean getRepopulate() { + return repopulate; + } - /** - * Constructs a new tag for generating password fields. Delegates to InputTextTag which it - * extends, and then overrides the type of input field to "password". - */ - public InputPasswordTag() { - getAttributes().put("type", "password"); - } + /** + * Constructs a new tag for generating password fields. Delegates to InputTextTag which it + * extends, and then overrides the type of input field to "password". + */ + public InputPasswordTag() { + getAttributes().put("type", "password"); + } - @Override - public int doEndInputTag() throws JspException { - if (this.repopulate) { - // If repopulate is set, delegate to the parent input text tag - return super.doEndInputTag(); - } - else { - // Else just figure out if there is a default value and use it - String body = getBodyContentAsString(); + @Override + public int doEndInputTag() throws JspException { + if (this.repopulate) { + // If repopulate is set, delegate to the parent input text tag + return super.doEndInputTag(); + } else { + // Else just figure out if there is a default value and use it + String body = getBodyContentAsString(); - // Figure out where to pull the value from - if (body != null) { - getAttributes().put("value", body); - } - else if (getValue() != null) { - getAttributes().put("value", format(getValue())); - } + // Figure out where to pull the value from + if (body != null) { + getAttributes().put("value", body); + } else if (getValue() != null) { + getAttributes().put("value", format(getValue())); + } - set("maxlength", getEffectiveMaxlength()); - writeSingletonTag(getPageContext().getOut(), "input"); + set("maxlength", getEffectiveMaxlength()); + writeSingletonTag(getPageContext().getOut(), "input"); - // Restore the original state before we mucked with it - getAttributes().remove("value"); + // Restore the original state before we mucked with it + getAttributes().remove("value"); - return EVAL_PAGE; - } + return EVAL_PAGE; } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputRadioButtonTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputRadioButtonTag.java index fdf3cd03f..9ec416834 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputRadioButtonTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputRadioButtonTag.java @@ -14,113 +14,123 @@ */ package net.sourceforge.stripes.tag; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.BodyTag; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.BodyTag; /** - *

    Generates {@literal } HTML tags based on the attribute set - * on the tag and the state of the form. Since a single radio button widget on a HTML page can - * have only a single value, the value tag attribute must be a Scalar object. The value will be - * converted to a String using the Stripes formatting system (with appropriate defaults), or by - * calling toString if an appropriate Formatter does not exist. Similarly since radio button sets - * can have only a single selected value at a time the checked attribute of the tag must also be - * a scalar value.

    + * Generates {@literal } HTML tags based on the attribute set on + * the tag and the state of the form. Since a single radio button widget on a HTML page can have + * only a single value, the value tag attribute must be a Scalar object. The value will be converted + * to a String using the Stripes formatting system (with appropriate defaults), or by calling + * toString if an appropriate Formatter does not exist. Similarly since radio button sets can have + * only a single selected value at a time the checked attribute of the tag must also be a scalar + * value. * - *

    Radio buttons perform automatic (re-)population of state. They prefer, in order, the value - * in the HttpServletRequest, the value in the ActionBean and lastly the value set using - * checked="" on the page. If the value of the current radio button matches the checked value - * from the preferred source then the attribute checked="checked" will be written in the HTML - * tag.

    + *

    Radio buttons perform automatic (re-)population of state. They prefer, in order, the value in + * the HttpServletRequest, the value in the ActionBean and lastly the value set using checked="" on + * the page. If the value of the current radio button matches the checked value from the preferred + * source then the attribute checked="checked" will be written in the HTML tag. * *

    The tag may include a body and if present the body is converted to a String and overrides the - * checked tag attribute.

    + * checked tag attribute. * * @author Tim Fennell */ public class InputRadioButtonTag extends InputTagSupport implements BodyTag { - private String checked; - private Object value; - - /** Basic constructor that sets the input tag's type attribute to "radio". */ - public InputRadioButtonTag() { - getAttributes().put("type", "radio"); - } - - /** - * Sets the value amongst a set of radio buttons, that should be "checked" by default. - * @param checked the default value for a set of radio buttons - */ - public void setChecked(String checked) { this.checked = checked; } - - /** Returns the value set with setChecked(). */ - public String getChecked() { return this.checked; } - - /** Sets the Object value of this individual checkbox. */ - public void setValue(Object value) { this.value = value; } - - /** Returns the value set with setValue() */ - public Object getValue() { return this.value; } - - /** - * Sets the input tag type to "radio". - * @return EVAL_BODY_BUFFERED in all cases. - */ - @Override - public int doStartInputTag() throws JspException { - return EVAL_BODY_BUFFERED; + private String checked; + private Object value; + + /** Basic constructor that sets the input tag's type attribute to "radio". */ + public InputRadioButtonTag() { + getAttributes().put("type", "radio"); + } + + /** + * Sets the value amongst a set of radio buttons, that should be "checked" by default. + * + * @param checked the default value for a set of radio buttons + */ + public void setChecked(String checked) { + this.checked = checked; + } + + /** Returns the value set with setChecked(). */ + public String getChecked() { + return this.checked; + } + + /** Sets the Object value of this individual checkbox. */ + public void setValue(Object value) { + this.value = value; + } + + /** Returns the value set with setValue() */ + public Object getValue() { + return this.value; + } + + /** + * Sets the input tag type to "radio". + * + * @return EVAL_BODY_BUFFERED in all cases. + */ + @Override + public int doStartInputTag() throws JspException { + return EVAL_BODY_BUFFERED; + } + + /** Does nothing. */ + public void doInitBody() throws JspException {} + + /** + * Does nothing. + * + * @return SKIP_BODY in all cases. + */ + public int doAfterBody() throws JspException { + return SKIP_BODY; + } + + /** + * Returns the body of the tag if it is present and not empty, otherwise returns the value of the + * 'checked' attribute. + */ + @Override + public Object getValueOnPage() { + String body = getBodyContentAsString(); + if (body != null) { + return body; + } else { + return this.checked; } - - /** Does nothing. */ - public void doInitBody() throws JspException { } - - /** - * Does nothing. - * @return SKIP_BODY in all cases. - */ - public int doAfterBody() throws JspException { - return SKIP_BODY; - } - - /** - * Returns the body of the tag if it is present and not empty, otherwise returns - * the value of the 'checked' attribute. - */ - @Override - public Object getValueOnPage() { - String body = getBodyContentAsString(); - if (body != null) { - return body; - } - else { - return this.checked; - } + } + + /** + * Determines the state of the set of radio buttons and then writes the radio button to the output + * stream with checked="checked" or not as appropriate. + * + * @return EVAL_PAGE in all cases. + * @throws JspException if the parent form tag cannot be found, or output cannot be written. + */ + @Override + public int doEndInputTag() throws JspException { + Object actualChecked = getSingleOverrideValue(); + + // Now if the "checked" value matches this tags value, check it! + if (actualChecked != null + && this.value != null + && format(this.value, false).equals(format(actualChecked, false))) { + getAttributes().put("checked", "checked"); } - /** - * Determines the state of the set of radio buttons and then writes the radio button to the - * output stream with checked="checked" or not as appropriate. - * - * @return EVAL_PAGE in all cases. - * @throws JspException if the parent form tag cannot be found, or output cannot be written. - */ - @Override - public int doEndInputTag() throws JspException { - Object actualChecked = getSingleOverrideValue(); + getAttributes().put("value", format(this.value)); - // Now if the "checked" value matches this tags value, check it! - if (actualChecked != null && this.value != null - && format(this.value, false).equals(format(actualChecked, false))) { - getAttributes().put("checked", "checked"); - } + writeSingletonTag(getPageContext().getOut(), "input"); - getAttributes().put("value", format(this.value)); + // Restore the state of the tag to before we mucked with it + getAttributes().remove("checked"); + getAttributes().remove("value"); - writeSingletonTag(getPageContext().getOut(), "input"); - - // Restore the state of the tag to before we mucked with it - getAttributes().remove("checked"); - getAttributes().remove("value"); - - return EVAL_PAGE; - } + return EVAL_PAGE; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputResetTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputResetTag.java index b07a033be..96d1d23c8 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputResetTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputResetTag.java @@ -15,17 +15,17 @@ package net.sourceforge.stripes.tag; /** - *

    Tag that generates HTML form fields of type {@literal } which - * render buttons for resetting forms. The only capability offered above and beyond a pure - * html tag is the ability to lookup the value of the button (i.e. the text on the button that the - * user sees) from a localized resource bundle. For more details on operation see - * {@link net.sourceforge.stripes.tag.InputButtonSupportTag}. + * Tag that generates HTML form fields of type {@literal } which render + * buttons for resetting forms. The only capability offered above and beyond a pure html tag is the + * ability to lookup the value of the button (i.e. the text on the button that the user sees) from a + * localized resource bundle. For more details on operation see {@link + * net.sourceforge.stripes.tag.InputButtonSupportTag}. * * @author Tim Fennell */ public class InputResetTag extends InputButtonSupportTag { - /** Sets the input tag type to be reset. */ - public InputResetTag() { - getAttributes().put("type", "reset"); - } + /** Sets the input tag type to be reset. */ + public InputResetTag() { + getAttributes().put("type", "reset"); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputSelectTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputSelectTag.java index e920afa2d..87680c4e6 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputSelectTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputSelectTag.java @@ -14,146 +14,146 @@ */ package net.sourceforge.stripes.tag; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.BodyTag; import net.sourceforge.stripes.validation.BooleanTypeConverter; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.BodyTag; - /** - *

    Coordinates with one or more other tags to produce a well formed HTML select tag with state - * repopulation. The select tag itself really only writes out the basic - * {@literal } piece of the structure, and provides mechanisms - * for child options to determine whether or not they should render themselves as selected.

    + * Coordinates with one or more other tags to produce a well formed HTML select tag with state + * repopulation. The select tag itself really only writes out the basic {@literal } piece of the structure, and provides mechanisms for child options to + * determine whether or not they should render themselves as selected. * * @author Tim Fennell - * * @see InputOptionTag * @see InputOptionsCollectionTag * @see InputOptionsEnumerationTag */ public class InputSelectTag extends InputTagSupport implements BodyTag { - private Object value; - private Object selectedValueOrValues; + private Object value; + private Object selectedValueOrValues; - /** - * If the text value passed in matches the empty string or (ignoring case) "multiple", - * or if the value can be converted to true by the {@link BooleanTypeConverter} then the - * attribute will be set to "multiple", otherwise the attribute will not be output. - */ - public void setMultiple(String multiple) { - boolean isMultiple = "multiple".equalsIgnoreCase(multiple) || "".equals(multiple); - if (!isMultiple) { - BooleanTypeConverter converter = new BooleanTypeConverter(); - isMultiple = converter.convert(multiple, Boolean.class, null); - } + /** + * If the text value passed in matches the empty string or (ignoring case) "multiple", or if the + * value can be converted to true by the {@link BooleanTypeConverter} then the attribute will be + * set to "multiple", otherwise the attribute will not be output. + */ + public void setMultiple(String multiple) { + boolean isMultiple = "multiple".equalsIgnoreCase(multiple) || "".equals(multiple); + if (!isMultiple) { + BooleanTypeConverter converter = new BooleanTypeConverter(); + isMultiple = converter.convert(multiple, Boolean.class, null); + } - if (isMultiple) { - set("multiple", "multiple"); - } - else { - getAttributes().remove("multiple"); - } + if (isMultiple) { + set("multiple", "multiple"); + } else { + getAttributes().remove("multiple"); } + } - /** Gets the HTML attribute "multiple". **/ - public String getMultiple() { return get("multiple"); } + /** Gets the HTML attribute "multiple". * */ + public String getMultiple() { + return get("multiple"); + } - /** - * Stores the value attribute in an instance variable since it is used to determine which - * options are checked. - */ - public void setValue(Object value) { - this.value = value; - this.selectedValueOrValues = value; - } + /** + * Stores the value attribute in an instance variable since it is used to determine which options + * are checked. + */ + public void setValue(Object value) { + this.value = value; + this.selectedValueOrValues = value; + } - /** - * Returns the String value set by the tag attribute on the JSP, or the override value set in - * the HttpRequest, or the override value bound to the ActionBean object in scope. - * - * @return Object one of String if the value comes from the tag on the JSP, - * String[] if the value(s) comes from the HttpServletRequest, or the type - * of the field on the ActionBean which could be Collection, Object[] - * or Object. - */ - public Object getValue() { - return this.selectedValueOrValues; - } + /** + * Returns the String value set by the tag attribute on the JSP, or the override value set in the + * HttpRequest, or the override value bound to the ActionBean object in scope. + * + * @return Object one of String if the value comes from the tag on the JSP, + * String[] if the value(s) comes from the HttpServletRequest, or the type of the + * field on the ActionBean which could be Collection, Object[] or + * Object. + */ + public Object getValue() { + return this.selectedValueOrValues; + } - /** - * Returns the scalar value or Array or Collection of values that are to be selected in the - * select tag. This will either be the value returned by the PopulationStrategy, or the value - * supplied by the container to setValue(). - * - * @return an Object, Object[] or {@code Collection} of values that are selected - */ - public Object getSelectedValueOrValues() { - return this.selectedValueOrValues; - } + /** + * Returns the scalar value or Array or Collection of values that are to be selected in the select + * tag. This will either be the value returned by the PopulationStrategy, or the value supplied by + * the container to setValue(). + * + * @return an Object, Object[] or {@code Collection} of values that are selected + */ + public Object getSelectedValueOrValues() { + return this.selectedValueOrValues; + } - /** - * Checks to see if the option value should be rendered as selected or not. Consults with the - * override values on the form submission or backing form, if there is one. If there is no - * ActionBean object present, then return the values of selectedOnPage supplied by the - * option. - * - * @param optionValue the value of the option under consideration - * @param selectedOnPage true if the page contains selected=... and false otherwise - */ - public boolean isOptionSelected(Object optionValue, boolean selectedOnPage) { - if (this.selectedValueOrValues != null) { - return isItemSelected(optionValue, this.selectedValueOrValues); - } - else { - return selectedOnPage; - } + /** + * Checks to see if the option value should be rendered as selected or not. Consults with the + * override values on the form submission or backing form, if there is one. If there is no + * ActionBean object present, then return the values of selectedOnPage supplied by the + * option. + * + * @param optionValue the value of the option under consideration + * @param selectedOnPage true if the page contains selected=... and false otherwise + */ + public boolean isOptionSelected(Object optionValue, boolean selectedOnPage) { + if (this.selectedValueOrValues != null) { + return isItemSelected(optionValue, this.selectedValueOrValues); + } else { + return selectedOnPage; } + } - /** - * Writes out the opening {@literal } tag. Looks for values in the request and + * in the ActionBean if one is present, and caches those values so it can efficiently determine + * which child options should be selected or not. + * + * @return EVAL_BODY_INCLUDE in all cases + * @throws JspException if the enclosing form tag cannot be found or output cannot be written + */ + @Override + public int doStartInputTag() throws JspException { + writeOpenTag(getPageContext().getOut(), "select"); + Object override = getOverrideValueOrValues(); + if (override != null) { + this.selectedValueOrValues = override; } + return EVAL_BODY_INCLUDE; + } - /** Does nothing. */ - public void doInitBody() throws JspException { } + /** Does nothing. */ + public void doInitBody() throws JspException {} - /** - * Does nothing. - * @return SKIP_BODY in all cases. - */ - public int doAfterBody() throws JspException { - return SKIP_BODY; - } + /** + * Does nothing. + * + * @return SKIP_BODY in all cases. + */ + public int doAfterBody() throws JspException { + return SKIP_BODY; + } - /** - * Writes out the close select tag ({@literal }). - * @return EVAL_PAGE in all cases - * @throws JspException if output cannot be written. - */ - @Override - public int doEndInputTag() throws JspException { - writeCloseTag(getPageContext().getOut(), "select"); - this.selectedValueOrValues = this.value; // reset in case the tag is reused - return EVAL_PAGE; - } + /** + * Writes out the close select tag ({@literal }). + * + * @return EVAL_PAGE in all cases + * @throws JspException if output cannot be written. + */ + @Override + public int doEndInputTag() throws JspException { + writeCloseTag(getPageContext().getOut(), "select"); + this.selectedValueOrValues = this.value; // reset in case the tag is reused + return EVAL_PAGE; + } - /** Releases the discovered selected values and then calls super-release(). */ - @Override - public void release() { - this.selectedValueOrValues = null; - super.release(); - } + /** Releases the discovered selected values and then calls super-release(). */ + @Override + public void release() { + this.selectedValueOrValues = null; + super.release(); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputSubmitTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputSubmitTag.java index 1fb877e7e..ee5f5efbe 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputSubmitTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputSubmitTag.java @@ -15,17 +15,16 @@ package net.sourceforge.stripes.tag; /** - *

    Tag that generates HTML form fields of type {@literal } which - * render buttons for submitting forms. The only capability offered above and beyond a pure - * html tag is the ability to lookup the value of the button (i.e. the text on the button that the - * user sees) from a localized resource bundle. For more details on operation see - * {@link InputButtonSupportTag}. + * Tag that generates HTML form fields of type {@literal } which render + * buttons for submitting forms. The only capability offered above and beyond a pure html tag is the + * ability to lookup the value of the button (i.e. the text on the button that the user sees) from a + * localized resource bundle. For more details on operation see {@link InputButtonSupportTag}. * * @author Tim Fennell */ public class InputSubmitTag extends InputButtonSupportTag { - /** Sets the input tag type to be submit. */ - public InputSubmitTag() { - getAttributes().put("type", "submit"); - } + /** Sets the input tag type to be submit. */ + public InputSubmitTag() { + getAttributes().put("type", "submit"); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputTagSupport.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputTagSupport.java index b27158f81..367ba0690 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputTagSupport.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputTagSupport.java @@ -14,6 +14,18 @@ */ package net.sourceforge.stripes.tag; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspWriter; +import jakarta.servlet.jsp.tagext.TryCatchFinally; +import java.io.IOException; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.ListIterator; +import java.util.Locale; +import java.util.Random; +import java.util.Stack; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.controller.ParameterName; import net.sourceforge.stripes.controller.StripesConstants; @@ -29,545 +41,553 @@ import net.sourceforge.stripes.validation.ValidationErrors; import net.sourceforge.stripes.validation.ValidationMetadata; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspWriter; -import javax.servlet.jsp.tagext.TryCatchFinally; -import java.io.IOException; -import java.lang.reflect.Array; -import java.lang.reflect.Method; -import java.util.Collection; -import java.util.List; -import java.util.ListIterator; -import java.util.Locale; -import java.util.Random; -import java.util.Stack; - /** - * Parent class for all input tags in stripes. Provides support methods for retrieving all the - * attributes that are shared across form input tags. Also provides accessors for finding the + * Parent class for all input tags in stripes. Provides support methods for retrieving all the + * attributes that are shared across form input tags. Also provides accessors for finding the * specified "override" value and for finding the enclosing support tag. * * @author Tim Fennell */ public abstract class InputTagSupport extends HtmlTagSupport implements TryCatchFinally { - private String formatType; - private String formatPattern; - private boolean focus; - private boolean syntheticId; - - /** A list of the errors related to this input tag instance */ - protected List fieldErrors; - private boolean fieldErrorsLoaded = false; // used to track if fieldErrors is loaded yet - - /** The error renderer to be utilized for error output of this input tag */ - protected TagErrorRenderer errorRenderer; - - /** Sets the type of output to format, e.g. date or time. */ - public void setFormatType(String formatType) { this.formatType = formatType; } - - /** Returns the value set with setFormatAs() */ - public String getFormatType() { return this.formatType; } - - /** Sets the named format pattern, or a custom format pattern. */ - public void setFormatPattern(String formatPattern) { this.formatPattern = formatPattern; } - - /** Returns the value set with setFormatPattern() */ - public String getFormatPattern() { return this.formatPattern; } - - - /** - * Gets the value for this tag based on the current population strategy. The value returned - * could be a scalar value, or it could be an array or collection depending on what the - * population strategy finds. For example, if the user submitted multiple values for a - * checkbox, the default population strategy would return a String[] containing all submitted - * values. - * - * @return Object either a value/values for this tag or null - * @throws StripesJspException if the enclosing form tag (which is required at all times, and - * necessary to perform repopulation) cannot be located - */ - protected Object getOverrideValueOrValues() throws StripesJspException { - return StripesFilter.getConfiguration().getPopulationStrategy().getValue(this); + private String formatType; + private String formatPattern; + private boolean focus; + private boolean syntheticId; + + /** A list of the errors related to this input tag instance */ + protected List fieldErrors; + + private boolean fieldErrorsLoaded = false; // used to track if fieldErrors is loaded yet + + /** The error renderer to be utilized for error output of this input tag */ + protected TagErrorRenderer errorRenderer; + + /** Sets the type of output to format, e.g. date or time. */ + public void setFormatType(String formatType) { + this.formatType = formatType; + } + + /** Returns the value set with setFormatAs() */ + public String getFormatType() { + return this.formatType; + } + + /** Sets the named format pattern, or a custom format pattern. */ + public void setFormatPattern(String formatPattern) { + this.formatPattern = formatPattern; + } + + /** Returns the value set with setFormatPattern() */ + public String getFormatPattern() { + return this.formatPattern; + } + + /** + * Gets the value for this tag based on the current population strategy. The value returned could + * be a scalar value, or it could be an array or collection depending on what the population + * strategy finds. For example, if the user submitted multiple values for a checkbox, the default + * population strategy would return a String[] containing all submitted values. + * + * @return Object either a value/values for this tag or null + * @throws StripesJspException if the enclosing form tag (which is required at all times, and + * necessary to perform repopulation) cannot be located + */ + protected Object getOverrideValueOrValues() throws StripesJspException { + return StripesFilter.getConfiguration().getPopulationStrategy().getValue(this); + } + + /** + * Returns a single value for the the value of this field. This can be used to ensure that only a + * single value is returned by the population strategy, which is useful in the case of text inputs + * etc. which can have only a single value. + * + * @return Object either a single value or null + * @throws StripesJspException if the enclosing form tag (which is required at all times, and + * necessary to perform repopulation) cannot be located + */ + protected Object getSingleOverrideValue() throws StripesJspException { + Object unknown = getOverrideValueOrValues(); + Object returnValue = null; + + if (unknown != null && unknown.getClass().isArray()) { + if (Array.getLength(unknown) > 0) { + returnValue = Array.get(unknown, 0); + } + } else if (unknown != null && unknown instanceof Collection) { + Collection collection = (Collection) unknown; + if (collection.size() > 0) { + returnValue = collection.iterator().next(); + } + } else { + returnValue = unknown; } - /** - * Returns a single value for the the value of this field. This can be used to ensure that - * only a single value is returned by the population strategy, which is useful in the case - * of text inputs etc. which can have only a single value. - * - * @return Object either a single value or null - * @throws StripesJspException if the enclosing form tag (which is required at all times, and - * necessary to perform repopulation) cannot be located - */ - protected Object getSingleOverrideValue() throws StripesJspException { - Object unknown = getOverrideValueOrValues(); - Object returnValue = null; - - if (unknown != null && unknown.getClass().isArray()) { - if (Array.getLength(unknown) > 0) { - returnValue = Array.get(unknown, 0); - } - } - else if (unknown != null && unknown instanceof Collection) { - Collection collection = (Collection) unknown; - if (collection.size() > 0) { - returnValue = collection.iterator().next(); - } - } - else { - returnValue = unknown; - } - - return returnValue; + return returnValue; + } + + /** + * Used during repopulation to query the tag for a value of values provided to the tag on the JSP. + * This allows the PopulationStrategy to encapsulate all decisions about which source to use when + * repopulating tags. + * + * @return May return any of String[], Collection or Object + */ + public Object getValueOnPage() { + Object value = getBodyContentAsString(); + + if (value == null) { + try { + Method getValue = getClass().getMethod("getValue"); + value = getValue.invoke(this); + } catch (Exception e) { + // Not a lot we can do about this. It's either because the subclass in question + // doesn't have a getValue() method (which is ok), or it threw an exception. + } } - /** - * Used during repopulation to query the tag for a value of values provided to the tag - * on the JSP. This allows the PopulationStrategy to encapsulate all decisions about - * which source to use when repopulating tags. - * - * @return May return any of String[], Collection or Object - */ - public Object getValueOnPage() { - Object value = getBodyContentAsString(); - - if (value == null) { - try { - Method getValue = getClass().getMethod("getValue"); - value = getValue.invoke(this); - } - catch (Exception e) { - // Not a lot we can do about this. It's either because the subclass in question - // doesn't have a getValue() method (which is ok), or it threw an exception. - } + return value; + } + + /** + * Locates the enclosing stripes form tag. If no form tag can be found, because the tag was not + * enclosed in one on the JSP, an exception is thrown. + * + * @return FormTag the enclosing form tag on the JSP + * @throws StripesJspException if an enclosing form tag cannot be found + */ + public FormTag getParentFormTag() throws StripesJspException { + FormTag parent = getParentTag(FormTag.class); + + // find the first non-partial parent form tag + if (parent != null && parent.isPartial()) { + Stack stack = getTagStack(); + ListIterator iter = stack.listIterator(stack.size()); + while (iter.hasPrevious()) { + StripesTagSupport tag = iter.previous(); + if (tag instanceof FormTag && !((FormTag) tag).isPartial()) { + parent = (FormTag) tag; + break; } + } + } - return value; + if (parent == null) { + throw new StripesJspException( + "InputTag of type [" + + getClass().getName() + + "] must be enclosed inside a " + + "stripes form tag. If, for some reason, you do not wish to render a complete " + + "form you may surround stripes input tags with " + + "which will provide support to the input tags but not render the

    tag."); } - /** - *

    Locates the enclosing stripes form tag. If no form tag can be found, because the tag - * was not enclosed in one on the JSP, an exception is thrown.

    - * - * @return FormTag the enclosing form tag on the JSP - * @throws StripesJspException if an enclosing form tag cannot be found - */ - public FormTag getParentFormTag() throws StripesJspException { - FormTag parent = getParentTag(FormTag.class); - - // find the first non-partial parent form tag - if (parent != null && parent.isPartial()) { - Stack stack = getTagStack(); - ListIterator iter = stack.listIterator(stack.size()); - while (iter.hasPrevious()) { - StripesTagSupport tag = iter.previous(); - if (tag instanceof FormTag && !((FormTag) tag).isPartial()) { - parent = (FormTag) tag; - break; - } - } + return parent; + } + + /** + * Utility method for determining if a String value is contained within an Object, where the + * object may be either a String, String[], Object, Object[] or Collection. Used primarily by the + * InputCheckBoxTag and InputSelectTag to determine if specific check boxes or options should be + * selected based on the values contained in the JSP, HttpServletRequest and the ActionBean. + * + * @param value the value that we are searching for + * @param selected a String, String[], Object, Object[] or Collection (of scalars) denoting the + * selected items + * @return boolean true if the String can be found, false otherwise + */ + protected boolean isItemSelected(Object value, Object selected) { + // Since this is a checkbox, there could be more than one checked value, which means + // this could be a single value type, array or collection + if (selected != null) { + String stringValue = (value == null) ? "" : format(value, false); + + if (selected.getClass().isArray()) { + int length = Array.getLength(selected); + for (int i = 0; i < length; ++i) { + Object item = Array.get(selected, i); + if ((format(item, false).equals(stringValue))) { + return true; + } } - - if (parent == null) { - throw new StripesJspException - ("InputTag of type [" + getClass().getName() + "] must be enclosed inside a " + - "stripes form tag. If, for some reason, you do not wish to render a complete " + - "form you may surround stripes input tags with " + - "which will provide support to the input tags but not render the tag."); + } else if (selected instanceof Collection) { + Collection selectedIf = (Collection) selected; + for (Object item : selectedIf) { + if ((format(item, false).equals(stringValue))) { + return true; + } } - - return parent; - } - - /** - * Utility method for determining if a String value is contained within an Object, where the - * object may be either a String, String[], Object, Object[] or Collection. Used primarily - * by the InputCheckBoxTag and InputSelectTag to determine if specific check boxes or - * options should be selected based on the values contained in the JSP, HttpServletRequest and - * the ActionBean. - * - * @param value the value that we are searching for - * @param selected a String, String[], Object, Object[] or Collection (of scalars) denoting the - * selected items - * @return boolean true if the String can be found, false otherwise - */ - protected boolean isItemSelected(Object value, Object selected) { - // Since this is a checkbox, there could be more than one checked value, which means - // this could be a single value type, array or collection - if (selected != null) { - String stringValue = (value == null) ? "" : format(value, false); - - if (selected.getClass().isArray()) { - int length = Array.getLength(selected); - for (int i=0; i) { - Collection selectedIf = (Collection) selected; - for (Object item : selectedIf) { - if ( (format(item, false).equals(stringValue)) ) { - return true; - } - } - } - else { - if( format(selected, false).equals(stringValue) ) { - return true; - } - } + } else { + if (format(selected, false).equals(stringValue)) { + return true; } - - // If we got this far without returning, then this is not a selected item - return false; + } } - /** - * Fetches the localized name for this field if one exists in the resource bundle. Relies on - * there being a "name" attribute on the tag, and the pageContext being set on the tag. First - * checks for a value of {actionBean FQN}.{fieldName} in the specified bundle, then - * {actionPath}.{fieldName} then just "fieldName". - * - * @return a localized field name if one can be found, or null if one cannot be found. - */ - public String getLocalizedFieldName() throws StripesJspException { - String name = getAttributes().get("name"); - return getLocalizedFieldName(name); + // If we got this far without returning, then this is not a selected item + return false; + } + + /** + * Fetches the localized name for this field if one exists in the resource bundle. Relies on there + * being a "name" attribute on the tag, and the pageContext being set on the tag. First checks for + * a value of {actionBean FQN}.{fieldName} in the specified bundle, then {actionPath}.{fieldName} + * then just "fieldName". + * + * @return a localized field name if one can be found, or null if one cannot be found. + */ + public String getLocalizedFieldName() throws StripesJspException { + String name = getAttributes().get("name"); + return getLocalizedFieldName(name); + } + + /** + * Attempts to fetch a "field name" resource from the localization bundle. Delegates to {@link + * LocalizationUtility#getLocalizedFieldName(String, String, Class, java.util.Locale)} + * + * @param name the field name or resource to look up + * @return the localized String corresponding to the name provided + * @throws StripesJspException + */ + protected String getLocalizedFieldName(final String name) throws StripesJspException { + Locale locale = getPageContext().getRequest().getLocale(); + FormTag form = null; + + try { + form = getParentFormTag(); + } catch (StripesJspException sje) { + /* Do nothing. */ } - /** - * Attempts to fetch a "field name" resource from the localization bundle. Delegates - * to {@link LocalizationUtility#getLocalizedFieldName(String, String, Class, java.util.Locale)} - * - * @param name the field name or resource to look up - * @return the localized String corresponding to the name provided - * @throws StripesJspException - */ - protected String getLocalizedFieldName(final String name) throws StripesJspException { - Locale locale = getPageContext().getRequest().getLocale(); - FormTag form = null; - - try { form = getParentFormTag(); } - catch (StripesJspException sje) { /* Do nothing. */} - - String actionPath = null; - Class beanClass = null; - - if (form != null) { - actionPath = form.getAction(); - beanClass = form.getActionBeanClass(); - } - else { - ActionBean mainBean = (ActionBean) getPageContext().getRequest().getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN); - if (mainBean != null) { - beanClass = mainBean.getClass(); - } - } - return LocalizationUtility.getLocalizedFieldName(name, actionPath, beanClass, locale); + String actionPath = null; + Class beanClass = null; + + if (form != null) { + actionPath = form.getAction(); + beanClass = form.getActionBeanClass(); + } else { + ActionBean mainBean = + (ActionBean) + getPageContext().getRequest().getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN); + if (mainBean != null) { + beanClass = mainBean.getClass(); + } } - - protected ValidationMetadata getValidationMetadata() throws StripesJspException { - // find the action bean class we're dealing with - Class beanClass = getParentFormTag().getActionBeanClass(); - - if (beanClass != null) { - // ascend the tag stack until a tag name is found - String name = getName(); - if (name == null) { - InputTagSupport tag = getParentTag(InputTagSupport.class); - while (name == null && tag != null) { - name = tag.getName(); - tag = tag.getParentTag(InputTagSupport.class); - } - } - - // check validation for encryption flag - return StripesFilter.getConfiguration().getValidationMetadataProvider() - .getValidationMetadata(beanClass, new ParameterName(name)); - } - else { - return null; + return LocalizationUtility.getLocalizedFieldName(name, actionPath, beanClass, locale); + } + + protected ValidationMetadata getValidationMetadata() throws StripesJspException { + // find the action bean class we're dealing with + Class beanClass = getParentFormTag().getActionBeanClass(); + + if (beanClass != null) { + // ascend the tag stack until a tag name is found + String name = getName(); + if (name == null) { + InputTagSupport tag = getParentTag(InputTagSupport.class); + while (name == null && tag != null) { + name = tag.getName(); + tag = tag.getParentTag(InputTagSupport.class); } + } + + // check validation for encryption flag + return StripesFilter.getConfiguration() + .getValidationMetadataProvider() + .getValidationMetadata(beanClass, new ParameterName(name)); + } else { + return null; } - - /** - * Calls {@link #format(Object, boolean)} with {@code forOutput} set to true. - * - * @param input The object to be formatted - * @see #format(Object, boolean) - */ - protected String format(Object input) { - return format(input, true); + } + + /** + * Calls {@link #format(Object, boolean)} with {@code forOutput} set to true. + * + * @param input The object to be formatted + * @see #format(Object, boolean) + */ + protected String format(Object input) { + return format(input, true); + } + + /** + * Attempts to format an object using the Stripes formatting system. If no formatter can be found, + * then a simple String.valueOf(input) will be returned. If the value passed in is null, then the + * empty string will be returned. + * + * @param input The object to be formatted + * @param forOutput If true, then the object will be formatted for output to the JSP. Currently, + * that means that if encryption is enabled for the ActionBean property with the same name as + * this tag then the formatted value will be encrypted before it is returned. + */ + @SuppressWarnings("unchecked") + protected String format(Object input, boolean forOutput) { + if (input == null) { + return ""; } - /** - * Attempts to format an object using the Stripes formatting system. If no formatter can - * be found, then a simple String.valueOf(input) will be returned. If the value passed in - * is null, then the empty string will be returned. - * - * @param input The object to be formatted - * @param forOutput If true, then the object will be formatted for output to the JSP. Currently, - * that means that if encryption is enabled for the ActionBean property with the same - * name as this tag then the formatted value will be encrypted before it is returned. - */ - @SuppressWarnings("unchecked") - protected String format(Object input, boolean forOutput) { - if (input == null) { - return ""; - } - - // format the value - FormatterFactory factory = StripesFilter.getConfiguration().getFormatterFactory(); - Formatter formatter = factory.getFormatter(input.getClass(), - getPageContext().getRequest().getLocale(), - this.formatType, - this.formatPattern); - String formatted = (formatter == null) ? String.valueOf(input) : formatter.format(input); - - // encrypt the formatted value if required - if (forOutput && formatted != null) { - try { - ValidationMetadata validate = getValidationMetadata(); - if (validate != null && validate.encrypted()) - formatted = CryptoUtil.encrypt(formatted); - } - catch (JspException e) { - throw new StripesRuntimeException(e); - } - } - - return formatted; + // format the value + FormatterFactory factory = StripesFilter.getConfiguration().getFormatterFactory(); + Formatter formatter = + factory.getFormatter( + input.getClass(), + getPageContext().getRequest().getLocale(), + this.formatType, + this.formatPattern); + String formatted = (formatter == null) ? String.valueOf(input) : formatter.format(input); + + // encrypt the formatted value if required + if (forOutput && formatted != null) { + try { + ValidationMetadata validate = getValidationMetadata(); + if (validate != null && validate.encrypted()) formatted = CryptoUtil.encrypt(formatted); + } catch (JspException e) { + throw new StripesRuntimeException(e); + } } - /** - * Find errors that are related to the form field this input tag represents and place - * them in an instance variable to use during error rendering. - */ - protected void loadErrors() throws StripesJspException { - ActionBean actionBean = getActionBean(); - if (actionBean != null) { - ValidationErrors validationErrors = actionBean.getContext().getValidationErrors(); - - if (validationErrors != null) { - this.fieldErrors = validationErrors.get(getName()); - } - } + return formatted; + } + + /** + * Find errors that are related to the form field this input tag represents and place them in an + * instance variable to use during error rendering. + */ + protected void loadErrors() throws StripesJspException { + ActionBean actionBean = getActionBean(); + if (actionBean != null) { + ValidationErrors validationErrors = actionBean.getContext().getValidationErrors(); + + if (validationErrors != null) { + this.fieldErrors = validationErrors.get(getName()); + } } - - /** - * Access for the field errors that occurred on the form input this tag represents - * @return List the list of validation errors for this field - */ - public List getFieldErrors() throws StripesJspException { - if (!fieldErrorsLoaded) { - loadErrors(); - fieldErrorsLoaded = true; - } - - return fieldErrors; + } + + /** + * Access for the field errors that occurred on the form input this tag represents + * + * @return List the list of validation errors for this field + */ + public List getFieldErrors() throws StripesJspException { + if (!fieldErrorsLoaded) { + loadErrors(); + fieldErrorsLoaded = true; } - /** - * Returns true if one or more validation errors exist for the field represented by - * this input tag. - */ - public boolean hasErrors() throws StripesJspException { - List errors = getFieldErrors(); - return errors != null && errors.size() > 0; + return fieldErrors; + } + + /** + * Returns true if one or more validation errors exist for the field represented by this input + * tag. + */ + public boolean hasErrors() throws StripesJspException { + List errors = getFieldErrors(); + return errors != null && errors.size() > 0; + } + + /** + * Fetches the ActionBean associated with the form if one is present. An ActionBean will not be + * created (and hence not present) by default. An ActionBean will only be present if the current + * request got bound to the same ActionBean as the current form uses. E.g. if we are re-showing + * the page as the result of an error, or the same ActionBean is used for a "pre-Action" + * and the "post-action". + * + * @return ActionBean the ActionBean bound to the form if there is one + */ + public ActionBean getActionBean() throws StripesJspException { + return getParentFormTag().getActionBean(); + } + + /** + * Final implementation of the doStartTag() method that allows the base InputTagSupport class to + * insert functionality before and after the tag performs it's doStartTag equivalent method. Finds + * errors related to this field and intercepts with a {@link TagErrorRenderer} if appropriate. + * + * @return int the value returned by the child class from doStartInputTag() + */ + @Override + public final int doStartTag() throws JspException { + getTagStack().push(this); + registerWithParentForm(); + + // Deal with any error rendering + if (getFieldErrors() != null) { + this.errorRenderer = + StripesFilter.getConfiguration().getTagErrorRendererFactory().getTagErrorRenderer(this); + this.errorRenderer.doBeforeStartTag(); } - /** - * Fetches the ActionBean associated with the form if one is present. An ActionBean will not - * be created (and hence not present) by default. An ActionBean will only be present if the - * current request got bound to the same ActionBean as the current form uses. E.g. if we are - * re-showing the page as the result of an error, or the same ActionBean is used for a - * "pre-Action" and the "post-action". - * - * @return ActionBean the ActionBean bound to the form if there is one - */ - public ActionBean getActionBean() throws StripesJspException { - return getParentFormTag().getActionBean(); + return doStartInputTag(); + } + + /** + * Registers the field with the parent form within which it must be enclosed. + * + * @throws StripesJspException if the parent form tag is not found + */ + protected void registerWithParentForm() throws StripesJspException { + getParentFormTag().registerField(this); + } + + /** Abstract method implemented in child classes instead of doStartTag(). */ + public abstract int doStartInputTag() throws JspException; + + /** + * Final implementation of the doEndTag() method that allows the base InputTagSupport class to + * insert functionality before and after the tag performs it's doEndTag equivalent method. + * + * @return int the value returned by the child class from doStartInputTag() + */ + @Override + public final int doEndTag() throws JspException { + // Wrap in a try/finally because a custom error renderer could throw an + // exception, and some containers in their infinite wisdom continue to + // cache/pool the tag even after a JSPException is thrown! + try { + int result = doEndInputTag(); + + if (getFieldErrors() != null) { + this.errorRenderer.doAfterEndTag(); + } + + if (this.focus) { + makeFocused(); + } + + return result; + } finally { + this.errorRenderer = null; + this.fieldErrors = null; + this.fieldErrorsLoaded = false; + this.focus = false; } - - /** - * Final implementation of the doStartTag() method that allows the base InputTagSupport class - * to insert functionality before and after the tag performs it's doStartTag equivalent - * method. Finds errors related to this field and intercepts with a {@link TagErrorRenderer} - * if appropriate. - * - * @return int the value returned by the child class from doStartInputTag() - */ - @Override - public final int doStartTag() throws JspException { - getTagStack().push(this); - registerWithParentForm(); - - // Deal with any error rendering - if (getFieldErrors() != null) { - this.errorRenderer = StripesFilter.getConfiguration() - .getTagErrorRendererFactory().getTagErrorRenderer(this); - this.errorRenderer.doBeforeStartTag(); - } - - return doStartInputTag(); + } + + /** Rethrows the passed in throwable in all cases. */ + public void doCatch(Throwable throwable) throws Throwable { + throw throwable; + } + + /** + * Used to ensure that the input tag is always removed from the tag stack so that there is never + * any confusion about tag-parent hierarchies. + */ + public void doFinally() { + try { + getTagStack().pop(); + } catch (Throwable t) { + /* Suppress anything, because otherwise this might mask any causal exception. */ } - - /** - * Registers the field with the parent form within which it must be enclosed. - * @throws StripesJspException if the parent form tag is not found - */ - protected void registerWithParentForm() throws StripesJspException { - getParentFormTag().registerField(this); + } + + /** + * Informs the tag that it should render JavaScript to ensure that it is focused when the page is + * loaded. If the tag does not have an 'id' attribute a random one will be created and set so that + * the tag can be located easily. + * + * @param focus true if focus is desired, false otherwise + */ + public void setFocus(boolean focus) { + this.focus = focus; + + if (getId() == null) { + this.syntheticId = true; + setId("stripes-" + new Random().nextInt()); } - - /** Abstract method implemented in child classes instead of doStartTag(). */ - public abstract int doStartInputTag() throws JspException; - - /** - * Final implementation of the doEndTag() method that allows the base InputTagSupport class - * to insert functionality before and after the tag performs it's doEndTag equivalent - * method. - * - * @return int the value returned by the child class from doStartInputTag() - */ - @Override - public final int doEndTag() throws JspException { - // Wrap in a try/finally because a custom error renderer could throw an - // exception, and some containers in their infinite wisdom continue to - // cache/pool the tag even after a JSPException is thrown! - try { - int result = doEndInputTag(); - - if (getFieldErrors() != null) { - this.errorRenderer.doAfterEndTag(); - } - - if (this.focus) { - makeFocused(); - } - - return result; - } - finally { - this.errorRenderer = null; - this.fieldErrors = null; - this.fieldErrorsLoaded = false; - this.focus = false; - } + } + + /** Writes out a JavaScript string to set focus on the field as it is rendered. */ + protected void makeFocused() throws JspException { + try { + JspWriter out = getPageContext().getOut(); + out.write( + ""); + + // Clean up tag state involved with focus + this.focus = false; + if (this.syntheticId) getAttributes().remove("id"); + this.syntheticId = false; + } catch (IOException ioe) { + throw new StripesJspException("Could not write javascript focus code to jsp writer.", ioe); } - - /** Rethrows the passed in throwable in all cases. */ - public void doCatch(Throwable throwable) throws Throwable { throw throwable; } - - /** - * Used to ensure that the input tag is always removed from the tag stack so that there is - * never any confusion about tag-parent hierarchies. - */ - public void doFinally() { - try { getTagStack().pop(); } - catch (Throwable t) { - /* Suppress anything, because otherwise this might mask any causal exception. */ - } + } + + /** Abstract method implemented in child classes instead of doEndTag(). */ + public abstract int doEndInputTag() throws JspException; + + // Getters and setters only below this point. + + /** + * Checks to see if the value provided is either 'disabled' or a value that the {@link + * BooleanTypeConverter} believes it true. If so, adds a disabled attribute to the tag, otherwise + * does not. + */ + public void setDisabled(String disabled) { + boolean isDisabled = "disabled".equalsIgnoreCase(disabled); + if (!isDisabled) { + BooleanTypeConverter converter = new BooleanTypeConverter(); + isDisabled = converter.convert(disabled, Boolean.class, null); } - /** - * Informs the tag that it should render JavaScript to ensure that it is focused - * when the page is loaded. If the tag does not have an 'id' attribute a random - * one will be created and set so that the tag can be located easily. - * - * @param focus true if focus is desired, false otherwise - */ - public void setFocus(boolean focus) { - this.focus = focus; - - if ( getId() == null ) { - this.syntheticId = true; - setId("stripes-" + new Random().nextInt()); - } + if (isDisabled) { + set("disabled", "disabled"); + } else { + getAttributes().remove("disabled"); } - - /** Writes out a JavaScript string to set focus on the field as it is rendered. */ - protected void makeFocused() throws JspException { - try { - JspWriter out = getPageContext().getOut(); - out.write(""); - - // Clean up tag state involved with focus - this.focus = false; - if (this.syntheticId) getAttributes().remove("id"); - this.syntheticId = false; - } - catch (IOException ioe) { - throw new StripesJspException("Could not write javascript focus code to jsp writer.", ioe); - } + } + + public String getDisabled() { + return get("disabled"); + } + + /** + * Sets the value of the readonly attribute to "readonly" but only when the value passed in is + * either "readonly" itself, or is converted to true by the {@link + * net.sourceforge.stripes.validation.BooleanTypeConverter}. + * + *

    Although not all input tags support the readonly attribute, the method is located here + * because it is not a simple one-liner and is used by more than one tag. + */ + public void setReadonly(String readonly) { + boolean isReadOnly = "readonly".equalsIgnoreCase(readonly); + if (!isReadOnly) { + BooleanTypeConverter converter = new BooleanTypeConverter(); + isReadOnly = converter.convert(readonly, Boolean.class, null); } - /** Abstract method implemented in child classes instead of doEndTag(). */ - public abstract int doEndInputTag() throws JspException; - - // Getters and setters only below this point. - - /** - * Checks to see if the value provided is either 'disabled' or a value that the - * {@link BooleanTypeConverter} believes it true. If so, adds a disabled attribute - * to the tag, otherwise does not. - */ - public void setDisabled(String disabled) { - boolean isDisabled = "disabled".equalsIgnoreCase(disabled); - if (!isDisabled) { - BooleanTypeConverter converter = new BooleanTypeConverter(); - isDisabled = converter.convert(disabled, Boolean.class, null); - } - - if (isDisabled) { - set("disabled", "disabled"); - } - else { - getAttributes().remove("disabled"); - } - } - public String getDisabled() { return get("disabled"); } - - /** - *

    Sets the value of the readonly attribute to "readonly" but only when the value passed - * in is either "readonly" itself, or is converted to true by the - * {@link net.sourceforge.stripes.validation.BooleanTypeConverter}.

    - * - *

    Although not all input tags support the readonly attribute, the method is located here - * because it is not a simple one-liner and is used by more than one tag.

    - */ - public void setReadonly(String readonly) { - boolean isReadOnly = "readonly".equalsIgnoreCase(readonly); - if (!isReadOnly) { - BooleanTypeConverter converter = new BooleanTypeConverter(); - isReadOnly = converter.convert(readonly, Boolean.class, null); - } - - if (isReadOnly) { - set("readonly", "readonly"); - } - else { - getAttributes().remove("readonly"); - } + if (isReadOnly) { + set("readonly", "readonly"); + } else { + getAttributes().remove("readonly"); } + } - /** Gets the HTML attribute of the same name. */ - public String getReadonly() { return get("readonly"); } + /** Gets the HTML attribute of the same name. */ + public String getReadonly() { + return get("readonly"); + } - public void setName(String name) { set("name", name); } - public String getName() { return get("name"); } + public void setName(String name) { + set("name", name); + } - public void setSize(String size) { set("size", size); } - public String getSize() { return get("size"); } + public String getName() { + return get("name"); + } + public void setSize(String size) { + set("size", size); + } + public String getSize() { + return get("size"); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputTextAreaTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputTextAreaTag.java index d85960e85..79ea00795 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputTextAreaTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputTextAreaTag.java @@ -14,104 +14,113 @@ */ package net.sourceforge.stripes.tag; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.BodyTag; +import java.io.IOException; import net.sourceforge.stripes.exception.StripesJspException; import net.sourceforge.stripes.util.HtmlUtil; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.BodyTag; -import java.io.IOException; - /** - *

    Tag that generates HTML form fields of type - * {@literal }, which can dynamically re-populate their - * value. Textareas may have only a single value, whose default may be set using either the body - * of the textarea, or using the value="" attribute of the tag. At runtime the contents of the - * textarea are determined by looking for the first non-null value in the following list:

    + * Tag that generates HTML form fields of type {@literal }, + * which can dynamically re-populate their value. Textareas may have only a single value, whose + * default may be set using either the body of the textarea, or using the value="" attribute of the + * tag. At runtime the contents of the textarea are determined by looking for the first non-null + * value in the following list: * *
      - * A value with the same name in the HttpServletRequest - * A value on the ActionBean if an ActionBean instance is present - * The contents of the body of the textarea - * The value attribute of the tag + * A value with the same name in the HttpServletRequest A value on the ActionBean if an + * ActionBean instance is present The contents of the body of the textarea The value + * attribute of the tag *
    * * @author Tim Fennell */ public class InputTextAreaTag extends InputTagSupport implements BodyTag { - private Object value; - - /** Sets the default value of the textarea (if no body is present). */ - public void setValue(Object value) { this.value = value; } - - /** Returns the value set using setValue(). */ - public Object getValue() { return this.value; } - - - /** Sets the HTML attribute of the same name. */ - public void setCols(String cols) { set("cols", cols); } - /** Gets the HTML attribute of the same name. */ - public String getCols() { return get("cols"); } - - /** Sets the HTML attribute of the same name. */ - public void setRows(String rows) { set("rows", rows); } - /** Gets the HTML attribute of the same name. */ - public String getRows() { return get("rows"); } - - /** - * Does nothing. - * @return EVAL_BODY_BUFFERED in all cases. - */ - @Override - public int doStartInputTag() throws JspException { - return EVAL_BODY_BUFFERED; - } - - /** Does nothing. */ - public void doInitBody() throws JspException { } - - /** - * Does nothing. - * @return SKIP_BODY in all cases. - */ - public int doAfterBody() throws JspException { - return SKIP_BODY; - } - - - /** - * Determines which source is applicable for the contents of the textarea and then writes - * out the textarea tag including the body. - * - * @return EVAL_PAGE in all cases. - * @throws JspException if the enclosing form tag cannot be found, or output cannot be written. - */ - @Override - public int doEndInputTag() throws JspException { - try { - // Find out if we have a value from the PopulationStrategy - Object value = getSingleOverrideValue(); - - writeOpenTag(getPageContext().getOut(), "textarea"); - - // Write out the contents of the text area - if (value != null) { - // Most browsers have this annoying habit of eating the first newline - // in a textarea tag. Since this is probably not desired, sometimes - // we need to add an extra newline into the output before the value - String body = getBodyContentAsString(); - if (body == null || !body.equals(value)) { - getPageContext().getOut().write('\n'); - } + private Object value; + + /** Sets the default value of the textarea (if no body is present). */ + public void setValue(Object value) { + this.value = value; + } + + /** Returns the value set using setValue(). */ + public Object getValue() { + return this.value; + } + + /** Sets the HTML attribute of the same name. */ + public void setCols(String cols) { + set("cols", cols); + } + /** Gets the HTML attribute of the same name. */ + public String getCols() { + return get("cols"); + } + + /** Sets the HTML attribute of the same name. */ + public void setRows(String rows) { + set("rows", rows); + } + /** Gets the HTML attribute of the same name. */ + public String getRows() { + return get("rows"); + } + + /** + * Does nothing. + * + * @return EVAL_BODY_BUFFERED in all cases. + */ + @Override + public int doStartInputTag() throws JspException { + return EVAL_BODY_BUFFERED; + } + + /** Does nothing. */ + public void doInitBody() throws JspException {} + + /** + * Does nothing. + * + * @return SKIP_BODY in all cases. + */ + public int doAfterBody() throws JspException { + return SKIP_BODY; + } + + /** + * Determines which source is applicable for the contents of the textarea and then writes out the + * textarea tag including the body. + * + * @return EVAL_PAGE in all cases. + * @throws JspException if the enclosing form tag cannot be found, or output cannot be written. + */ + @Override + public int doEndInputTag() throws JspException { + try { + // Find out if we have a value from the PopulationStrategy + Object value = getSingleOverrideValue(); + + writeOpenTag(getPageContext().getOut(), "textarea"); + + // Write out the contents of the text area + if (value != null) { + // Most browsers have this annoying habit of eating the first newline + // in a textarea tag. Since this is probably not desired, sometimes + // we need to add an extra newline into the output before the value + String body = getBodyContentAsString(); + if (body == null || !body.equals(value)) { + getPageContext().getOut().write('\n'); + } - getPageContext().getOut().write( HtmlUtil.encode(format(value)) ); - } + getPageContext().getOut().write(HtmlUtil.encode(format(value))); + } - writeCloseTag(getPageContext().getOut(), "textarea"); + writeCloseTag(getPageContext().getOut(), "textarea"); - return EVAL_PAGE; - } - catch (IOException ioe) { - throw new StripesJspException("Could not write out textarea tag.", ioe); - } + return EVAL_PAGE; + } catch (IOException ioe) { + throw new StripesJspException("Could not write out textarea tag.", ioe); } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/InputTextTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/InputTextTag.java index 2f06d509c..c71332020 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/InputTextTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/InputTextTag.java @@ -14,115 +14,121 @@ */ package net.sourceforge.stripes.tag; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.BodyTag; - +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.BodyTag; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.exception.StripesJspException; import net.sourceforge.stripes.validation.Validate; import net.sourceforge.stripes.validation.ValidationMetadata; /** - *

    Tag that generates HTML form fields of type - * {@literal }, which can dynamically re-populate their - * value. Text tags may have only a single value, whose default may be set using either the body - * of the tag, or using the value="" attribute of the tag. At runtime the contents of the - * text field are determined by looking for the first non-null value in the following list:

    + * Tag that generates HTML form fields of type {@literal }, which can dynamically re-populate their value. Text tags may have only a single + * value, whose default may be set using either the body of the tag, or using the value="" attribute + * of the tag. At runtime the contents of the text field are determined by looking for the first + * non-null value in the following list: * *
      - *
    • A value with the same name in the HttpServletRequest
    • - *
    • A value on the ActionBean if an ActionBean instance is present
    • - *
    • The contents of the body of the tag
    • - *
    • The value attribute of the tag
    • + *
    • A value with the same name in the HttpServletRequest + *
    • A value on the ActionBean if an ActionBean instance is present + *
    • The contents of the body of the tag + *
    • The value attribute of the tag *
    * * @author Tim Fennell */ - public class InputTextTag extends InputTagSupport implements BodyTag { - private Object value; - private String maxlength; - - /** Basic constructor that sets the input tag's type attribute to "text". */ - public InputTextTag() { - getAttributes().put("type", "text"); +public class InputTextTag extends InputTagSupport implements BodyTag { + private Object value; + private String maxlength; + + /** Basic constructor that sets the input tag's type attribute to "text". */ + public InputTextTag() { + getAttributes().put("type", "text"); + } + + /** Sets the default value of the text field (if no body is present). */ + public void setValue(Object value) { + this.value = value; + } + + /** Returns the value set using setValue(). */ + public Object getValue() { + return this.value; + } + + /** Sets the HTML attribute of the same name. */ + public void setMaxlength(String maxlength) { + this.maxlength = maxlength; + } + + /** Gets the HTML attribute of the same name. */ + public String getMaxlength() { + return maxlength; + } + + /** + * Gets the maxlength value that is in effect for this tag, as determined by checking {@link + * #getMaxlength()} and then the {@code maxlength} element of the {@link Validate} annotation on + * the associated {@link ActionBean} property. + * + * @throws StripesJspException if thrown by {@link #getValidationMetadata()} + */ + protected String getEffectiveMaxlength() throws StripesJspException { + if (getMaxlength() == null) { + ValidationMetadata validation = getValidationMetadata(); + if (validation != null && validation.maxlength() != null) + return validation.maxlength().toString(); + else return null; + } else { + return getMaxlength(); } - - /** Sets the default value of the text field (if no body is present). */ - public void setValue(Object value) { this.value = value; } - - /** Returns the value set using setValue(). */ - public Object getValue() { return this.value; } - - - /** Sets the HTML attribute of the same name. */ - public void setMaxlength(String maxlength) { this.maxlength = maxlength; } - - /** Gets the HTML attribute of the same name. */ - public String getMaxlength() { return maxlength; } - - /** - * Gets the maxlength value that is in effect for this tag, as determined by checking - * {@link #getMaxlength()} and then the {@code maxlength} element of the {@link Validate} - * annotation on the associated {@link ActionBean} property. - * - * @throws StripesJspException if thrown by {@link #getValidationMetadata()} - */ - protected String getEffectiveMaxlength() throws StripesJspException { - if (getMaxlength() == null) { - ValidationMetadata validation = getValidationMetadata(); - if (validation != null && validation.maxlength() != null) - return validation.maxlength().toString(); - else - return null; - } - else { - return getMaxlength(); - } + } + + /** + * Sets type input tags type to "text". + * + * @return EVAL_BODY_BUFFERED in all cases. + */ + @Override + public int doStartInputTag() throws JspException { + return EVAL_BODY_BUFFERED; + } + + /** Does nothing. */ + public void doInitBody() throws JspException {} + + /** + * Does nothing. + * + * @return SKIP_BODY in all cases. + */ + public int doAfterBody() throws JspException { + return SKIP_BODY; + } + + /** + * Determines which source is applicable for the value of the text field and then writes out the + * tag. + * + * @return EVAL_PAGE in all cases. + * @throws JspException if the enclosing form tag cannot be found, or output cannot be written. + */ + @Override + public int doEndInputTag() throws JspException { + // Find out if we have a value from the PopulationStrategy + Object value = getSingleOverrideValue(); + + // Figure out where to pull the value from + if (value != null) { + getAttributes().put("value", format(value)); } - /** - * Sets type input tags type to "text". - * @return EVAL_BODY_BUFFERED in all cases. - */ - @Override - public int doStartInputTag() throws JspException { - return EVAL_BODY_BUFFERED; - } + set("maxlength", getEffectiveMaxlength()); + writeSingletonTag(getPageContext().getOut(), "input"); - /** Does nothing. */ - public void doInitBody() throws JspException { } + // Restore the original state before we mucked with it + getAttributes().remove("value"); - /** - * Does nothing. - * @return SKIP_BODY in all cases. - */ - public int doAfterBody() throws JspException { - return SKIP_BODY; - } - - /** - * Determines which source is applicable for the value of the text field and then writes - * out the tag. - * - * @return EVAL_PAGE in all cases. - * @throws JspException if the enclosing form tag cannot be found, or output cannot be written. - */ - @Override - public int doEndInputTag() throws JspException { - // Find out if we have a value from the PopulationStrategy - Object value = getSingleOverrideValue(); - - // Figure out where to pull the value from - if (value != null) { - getAttributes().put("value", format(value)); - } - - set("maxlength", getEffectiveMaxlength()); - writeSingletonTag(getPageContext().getOut(), "input"); - - // Restore the original state before we mucked with it - getAttributes().remove("value"); - - return EVAL_PAGE; - } + return EVAL_PAGE; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/LinkTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/LinkTag.java index d9862d6d4..5b58b0f88 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/LinkTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/LinkTag.java @@ -14,108 +14,158 @@ */ package net.sourceforge.stripes.tag; -import net.sourceforge.stripes.exception.StripesJspException; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.BodyTag; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.BodyTag; import java.io.IOException; +import net.sourceforge.stripes.exception.StripesJspException; /** - * Tag for generating links to pages or ActionBeans within a Stripes application. Provides - * basic services such as including the context path at the start of the href URL (only - * when the URL starts with a '/' and does not contain the context path already), and - * including a parameter to name the source page from which the link came. Also provides the - * ability to add complex parameters to the URL through the use of nested Param tags. + * Tag for generating links to pages or ActionBeans within a Stripes application. Provides basic + * services such as including the context path at the start of the href URL (only when the URL + * starts with a '/' and does not contain the context path already), and including a parameter to + * name the source page from which the link came. Also provides the ability to add complex + * parameters to the URL through the use of nested Param tags. * * @see ParamTag * @author Tim Fennell */ public class LinkTag extends LinkTagSupport implements BodyTag { - /** - * Does nothing. - * @return EVAL_BODY_BUFFERED in all cases - */ - @Override - public int doStartTag() throws JspException { - return EVAL_BODY_BUFFERED; + /** + * Does nothing. + * + * @return EVAL_BODY_BUFFERED in all cases + */ + @Override + public int doStartTag() throws JspException { + return EVAL_BODY_BUFFERED; + } + + /** Does nothing. */ + public void doInitBody() throws JspException { + /* Do Nothing. */ + } + + /** + * Does nothing. + * + * @return SKIP_BODY in all cases + */ + public int doAfterBody() throws JspException { + return SKIP_BODY; + } + + /** + * Prepends the context to the href attribute if necessary, and then folds all the registered + * parameters into the URL. + * + * @return EVAL_PAGE in all cases + * @throws JspException + */ + @Override + public int doEndTag() throws JspException { + try { + set("href", buildUrl()); + writeOpenTag(getPageContext().getOut(), "a"); + String body = getBodyContentAsString(); + if (body == null || body.trim().length() == 0) { + body = get("href"); + } + if (body != null) { + getPageContext().getOut().write(body.trim()); + } + writeCloseTag(getPageContext().getOut(), "a"); + } catch (IOException ioe) { + throw new StripesJspException("IOException while writing output in LinkTag.", ioe); } - /** Does nothing. */ - public void doInitBody() throws JspException { /* Do Nothing. */ } - - /** - * Does nothing. - * @return SKIP_BODY in all cases - */ - public int doAfterBody() throws JspException { - return SKIP_BODY; - } - - /** - * Prepends the context to the href attribute if necessary, and then folds all the - * registered parameters into the URL. - * - * @return EVAL_PAGE in all cases - * @throws JspException - */ - @Override - public int doEndTag() throws JspException { - try { - set("href", buildUrl()); - writeOpenTag(getPageContext().getOut(), "a"); - String body = getBodyContentAsString(); - if (body == null || body.trim().length() == 0) { - body = get("href"); - } - if (body != null) { - getPageContext().getOut().write(body.trim()); - } - writeCloseTag(getPageContext().getOut(), "a"); - } - catch (IOException ioe) { - throw new StripesJspException("IOException while writing output in LinkTag.", ioe); - } - - // Restore state and go on with the page - getAttributes().remove("href"); - clearParameters(); - return EVAL_PAGE; - } - - /** Pass through to {@link LinkTagSupport#setUrl(String)}. */ - public void setHref(String href) { setUrl(href); } - /** Pass through to {@link LinkTagSupport#getUrl()}. */ - public String getHref() { return getUrl(); } - - - /////////////////////////////////////////////////////////////////////////// - // Additional HTML Attributes supported by the tag - /////////////////////////////////////////////////////////////////////////// - public void setCharset(String charset) { set("charset", charset); } - public String getCharset() { return get("charset"); } - - public void setCoords(String coords) { set("coords", coords); } - public String getCoords() { return get("coords"); } - - public void setHreflang(String hreflang) { set("hreflang", hreflang); } - public String getHreflang() { return get("hreflang"); } - - public void setName(String name) { set("name", name); } - public String getName() { return get("name"); } - - public void setRel(String rel) { set("rel", rel); } - public String getRel() { return get("rel"); } - - public void setRev(String rev) { set("rev", rev); } - public String getRev() { return get("rev"); } - - public void setShape(String shape) { set("shape", shape); } - public String getShape() { return get("shape"); } - - public void setTarget(String target) { set("target", target); } - public String getTarget() { return get("target"); } - - public void setType(String type) { set("type", type); } - public String getType() { return get("type"); } + // Restore state and go on with the page + getAttributes().remove("href"); + clearParameters(); + return EVAL_PAGE; + } + + /** Pass through to {@link LinkTagSupport#setUrl(String)}. */ + public void setHref(String href) { + setUrl(href); + } + /** Pass through to {@link LinkTagSupport#getUrl()}. */ + public String getHref() { + return getUrl(); + } + + /////////////////////////////////////////////////////////////////////////// + // Additional HTML Attributes supported by the tag + /////////////////////////////////////////////////////////////////////////// + public void setCharset(String charset) { + set("charset", charset); + } + + public String getCharset() { + return get("charset"); + } + + public void setCoords(String coords) { + set("coords", coords); + } + + public String getCoords() { + return get("coords"); + } + + public void setHreflang(String hreflang) { + set("hreflang", hreflang); + } + + public String getHreflang() { + return get("hreflang"); + } + + public void setName(String name) { + set("name", name); + } + + public String getName() { + return get("name"); + } + + public void setRel(String rel) { + set("rel", rel); + } + + public String getRel() { + return get("rel"); + } + + public void setRev(String rev) { + set("rev", rev); + } + + public String getRev() { + return get("rev"); + } + + public void setShape(String shape) { + set("shape", shape); + } + + public String getShape() { + return get("shape"); + } + + public void setTarget(String target) { + set("target", target); + } + + public String getTarget() { + return get("target"); + } + + public void setType(String type) { + set("type", type); + } + + public String getType() { + return get("type"); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/LinkTagSupport.java b/stripes/src/main/java/net/sourceforge/stripes/tag/LinkTagSupport.java index ea213be38..3635c930d 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/LinkTagSupport.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/LinkTagSupport.java @@ -14,213 +14,227 @@ */ package net.sourceforge.stripes.tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; +import net.sourceforge.stripes.controller.StripesConstants; import net.sourceforge.stripes.exception.StripesJspException; import net.sourceforge.stripes.util.CryptoUtil; import net.sourceforge.stripes.util.Log; import net.sourceforge.stripes.util.UrlBuilder; -import net.sourceforge.stripes.controller.StripesConstants; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.util.Map; -import java.util.HashMap; /** - * Abstract support class for generating links. Used by both the LinkTag (which generates - * regular {@literal } style links) and the UrlTag which is a rough similie - * of the JSTL url tag. + * Abstract support class for generating links. Used by both the LinkTag (which generates regular + * {@literal } style links) and the UrlTag which is a rough similie of the JSTL url tag. * * @author Tim Fennell * @since Stripes 1.4 */ public abstract class LinkTagSupport extends HtmlTagSupport implements ParameterizableTag { - private static final Log log = Log.getInstance(LinkTagSupport.class); - - /** Initial value for fields to indicate they were not set by a tag attribute. */ - private static final String VALUE_NOT_SET = "VALUE_NOT_SET"; - - private Map parameters = new HashMap(); - private String event = VALUE_NOT_SET; - private Object beanclass; - private String url; - private String anchor; - private boolean addSourcePage = false; - private Boolean prependContext; - - /** - * Gets the URL that is supplied by the user/developer on the page. This is the basis - * for constructing the eventual URL that the tag generates. - * - * @return the URL that was supplied by the user - */ - public String getUrl() { return url; } - - /** - * Sets the URL that is supplied by the user/developer on the page. This is the basis - * for constructing the eventual URL that the tag generates. - * - * @param url the URL supplied on the page - */ - public void setUrl(String url) { this.url = url; } - - /** - * Used by stripes:param tags (and possibly other tags at some distant point in - * the future) to add a parameter to the parent link tag. - * - * @param name the name of the parameter(s) to add - * @param valueOrValues - */ - public void addParameter(String name, Object valueOrValues) { - this.parameters.put(name, valueOrValues); + private static final Log log = Log.getInstance(LinkTagSupport.class); + + /** Initial value for fields to indicate they were not set by a tag attribute. */ + private static final String VALUE_NOT_SET = "VALUE_NOT_SET"; + + private Map parameters = new HashMap(); + private String event = VALUE_NOT_SET; + private Object beanclass; + private String url; + private String anchor; + private boolean addSourcePage = false; + private Boolean prependContext; + + /** + * Gets the URL that is supplied by the user/developer on the page. This is the basis for + * constructing the eventual URL that the tag generates. + * + * @return the URL that was supplied by the user + */ + public String getUrl() { + return url; + } + + /** + * Sets the URL that is supplied by the user/developer on the page. This is the basis for + * constructing the eventual URL that the tag generates. + * + * @param url the URL supplied on the page + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * Used by stripes:param tags (and possibly other tags at some distant point in the future) to add + * a parameter to the parent link tag. + * + * @param name the name of the parameter(s) to add + * @param valueOrValues + */ + public void addParameter(String name, Object valueOrValues) { + this.parameters.put(name, valueOrValues); + } + + /** Retrieves the parameter values set on the tag. */ + public Map getParameters() { + return this.parameters; + } + + /** + * Clears all existing parameters. Subclasses should be careful to call this in doEndTag() to + * ensure that parameter state is cleared between pooled used of the tag. + */ + public void clearParameters() { + this.parameters.clear(); + } + + /** Sets the (optional) event name that the link will trigger. */ + public void setEvent(String event) { + this.event = event; + } + + /** Gets the (optional) event name that the link will trigger. */ + public String getEvent() { + return event; + } + + /** + * Sets the bean class (String FQN or Class) to generate a link for. Provides an alternative to + * using href for targeting ActionBeans. + * + * @param beanclass the name of an ActionBean class, or Class object + */ + public void setBeanclass(Object beanclass) { + this.beanclass = beanclass; + } + + /** + * Gets the bean class (String FQN or Class) to generate a link for. Provides an alternative to + * using href for targeting ActionBeans. + * + * @return the name of an ActionBean class, or Class object + */ + public Object getBeanclass() { + return beanclass; + } + + /** + * Gets the anchor element that is appended at the end of the URL. + * + * @return the anchor element + */ + public String getAnchor() { + return anchor; + } + + /** + * Sets the anchor element that is appended at the end of the URL. If the provided URL (set using + * setUrl method) already contains the anchor, then the anchor specified by this + * attribute takes precedence. + * + * @param anchor the name of the anchor to set + */ + public void setAnchor(String anchor) { + this.anchor = anchor; + } + + /** + * Get the flag that indicates if the _sourcePage parameter should be appended to the URL. + * + * @return true if _sourcePage is to be appended to the URL; false otherwise + */ + public boolean isAddSourcePage() { + return addSourcePage; + } + + /** Set the flag that indicates if the _sourcePage parameter should be appended to the URL. */ + public void setAddSourcePage(boolean addSourcePage) { + this.addSourcePage = addSourcePage; + } + + /** Get the flag that indicates if the application context should be included in the URL. */ + public Boolean isPrependContext() { + return prependContext; + } + + /** Set the flag that indicates if the application context should be included in the URL. */ + public void setPrependContext(Boolean prependContext) { + this.prependContext = prependContext; + } + + /** + * Returns the base URL that should be used for building the link. This is derived from the + * 'beanclass' attribute if it is set, else from the 'url' attribute. + * + * @return the preferred base URL for the link + * @throws StripesJspException if a beanclass attribute was specified, but does not identify an + * existing ActionBean + */ + protected String getPreferredBaseUrl() throws StripesJspException { + // If the beanclass attribute was supplied we'll prefer that to an href + if (this.beanclass != null) { + String beanHref = getActionBeanUrl(beanclass); + if (beanHref == null) { + throw new StripesJspException( + "The value supplied for the 'beanclass' attribute " + + "does not represent a valid ActionBean. The value supplied was '" + + this.beanclass + + "'. If you're prototyping, or your bean isn't ready yet " + + "and you want this exception to go away, just use 'href' for now instead."); + } else { + return beanHref; + } + } else { + return getUrl(); } - - /** Retrieves the parameter values set on the tag. */ - public Map getParameters() { - return this.parameters; + } + + /** + * Builds the URL based on the information currently stored in the tag. Ensures that all + * parameters are appended into the URL, along with event name if necessary and the source page + * information. + * + * @return the fully constructed URL + * @throws StripesJspException if the base URL cannot be determined + */ + protected String buildUrl() throws StripesJspException { + HttpServletRequest request = (HttpServletRequest) getPageContext().getRequest(); + HttpServletResponse response = (HttpServletResponse) getPageContext().getResponse(); + + // Add all the parameters and reset the href attribute; pass to false here because + // the HtmlTagSupport will HtmlEncode the ampersands for us + String base = getPreferredBaseUrl(); + UrlBuilder builder = new UrlBuilder(pageContext.getRequest().getLocale(), base, false); + if (this.event != VALUE_NOT_SET) { + builder.setEvent(this.event == null || this.event.length() < 1 ? null : this.event); } - - /** - * Clears all existing parameters. Subclasses should be careful to call this in - * doEndTag() to ensure that parameter state is cleared between pooled used of the tag. - */ - public void clearParameters() { - this.parameters.clear(); + if (addSourcePage) { + builder.addParameter( + StripesConstants.URL_KEY_SOURCE_PAGE, CryptoUtil.encrypt(request.getServletPath())); } - - /** Sets the (optional) event name that the link will trigger. */ - public void setEvent(String event) { this.event = event; } - - /** Gets the (optional) event name that the link will trigger. */ - public String getEvent() { return event; } - - /** - * Sets the bean class (String FQN or Class) to generate a link for. Provides an - * alternative to using href for targeting ActionBeans. - * - * @param beanclass the name of an ActionBean class, or Class object - */ - public void setBeanclass(Object beanclass) { this.beanclass = beanclass; } - - /** - * Gets the bean class (String FQN or Class) to generate a link for. Provides an - * alternative to using href for targeting ActionBeans. - * - * @return the name of an ActionBean class, or Class object - */ - public Object getBeanclass() { return beanclass; } - - /** - * Gets the anchor element that is appended at the end of the URL. - * - * @return the anchor element - */ - public String getAnchor() { - return anchor; + if (this.anchor != null) { + builder.setAnchor(anchor); } - - /** - * Sets the anchor element that is appended at the end of the URL. If the provided URL (set - * using setUrl method) already contains the anchor, then the anchor specified by - * this attribute takes precedence. - * - * @param anchor the name of the anchor to set - */ - public void setAnchor(String anchor) { - this.anchor = anchor; + builder.addParameters(this.parameters); + + // Prepend the context path, but only if the user didn't already + String url = builder.toString(); + String contextPath = request.getContextPath(); + if (contextPath.length() > 1) { + boolean prepend = + prependContext != null && prependContext + || prependContext == null && beanclass != null + || prependContext == null && url.startsWith("/") && !url.startsWith(contextPath); + + if (prepend) { + if (url.startsWith("/")) url = contextPath + url; + else + log.warn( + "Use of prependContext=\"true\" is only valid with a URL that starts with \"/\""); + } } - /** - * Get the flag that indicates if the _sourcePage parameter should be - * appended to the URL. - * - * @return true if _sourcePage is to be appended to the URL; false otherwise - */ - public boolean isAddSourcePage() { return addSourcePage; } - - /** - * Set the flag that indicates if the _sourcePage parameter should be - * appended to the URL. - */ - public void setAddSourcePage(boolean addSourcePage) { this.addSourcePage = addSourcePage; } - - /** Get the flag that indicates if the application context should be included in the URL. */ - public Boolean isPrependContext() { return prependContext; } - - /** Set the flag that indicates if the application context should be included in the URL. */ - public void setPrependContext(Boolean prependContext) { this.prependContext = prependContext; } - - /** - * Returns the base URL that should be used for building the link. This is derived from - * the 'beanclass' attribute if it is set, else from the 'url' attribute. - * - * @return the preferred base URL for the link - * @throws StripesJspException if a beanclass attribute was specified, but does not identify - * an existing ActionBean - */ - protected String getPreferredBaseUrl() throws StripesJspException { - // If the beanclass attribute was supplied we'll prefer that to an href - if (this.beanclass != null) { - String beanHref = getActionBeanUrl(beanclass); - if (beanHref == null) { - throw new StripesJspException("The value supplied for the 'beanclass' attribute " - + "does not represent a valid ActionBean. The value supplied was '" + - this.beanclass + "'. If you're prototyping, or your bean isn't ready yet " + - "and you want this exception to go away, just use 'href' for now instead."); - } - else { - return beanHref; - } - } - else { - return getUrl(); - } - } - - /** - * Builds the URL based on the information currently stored in the tag. Ensures that all - * parameters are appended into the URL, along with event name if necessary and the source - * page information. - * - * @return the fully constructed URL - * @throws StripesJspException if the base URL cannot be determined - */ - protected String buildUrl() throws StripesJspException { - HttpServletRequest request = (HttpServletRequest) getPageContext().getRequest(); - HttpServletResponse response = (HttpServletResponse) getPageContext().getResponse(); - - - // Add all the parameters and reset the href attribute; pass to false here because - // the HtmlTagSupport will HtmlEncode the ampersands for us - String base = getPreferredBaseUrl(); - UrlBuilder builder = new UrlBuilder(pageContext.getRequest().getLocale(), base, false); - if (this.event != VALUE_NOT_SET) { - builder.setEvent(this.event == null || this.event.length() < 1 ? null : this.event); - } - if (addSourcePage) { - builder.addParameter(StripesConstants.URL_KEY_SOURCE_PAGE, - CryptoUtil.encrypt(request.getServletPath())); - } - if (this.anchor != null) { - builder.setAnchor(anchor); - } - builder.addParameters(this.parameters); - - // Prepend the context path, but only if the user didn't already - String url = builder.toString(); - String contextPath = request.getContextPath(); - if (contextPath.length() > 1) { - boolean prepend = prependContext != null && prependContext - || prependContext == null && beanclass != null - || prependContext == null && url.startsWith("/") && !url.startsWith(contextPath); - - if (prepend) { - if (url.startsWith("/")) - url = contextPath + url; - else - log.warn("Use of prependContext=\"true\" is only valid with a URL that starts with \"/\""); - } - } - - return response.encodeURL(url); - } + return response.encodeURL(url); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/MessagesTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/MessagesTag.java index 5f2c6558d..9cf84111e 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/MessagesTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/MessagesTag.java @@ -14,149 +14,167 @@ */ package net.sourceforge.stripes.tag; -import net.sourceforge.stripes.action.Message; -import net.sourceforge.stripes.controller.StripesConstants; -import net.sourceforge.stripes.controller.StripesFilter; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspWriter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspWriter; import java.io.IOException; import java.util.List; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; +import net.sourceforge.stripes.action.Message; +import net.sourceforge.stripes.controller.StripesConstants; +import net.sourceforge.stripes.controller.StripesFilter; /** - *

    Displays a list of non-error messages to the user. The list of messages can come from - * either the request (preferred) or the session (checked 2nd). Lists of messages can be stored - * under any arbitrary key in request or session and the key can be specified to the messages - * tag. If no key is specified then the default key (and therefore default set of messages) is - * used. Note that by default the ActionBeanContext stores messages in a - * {@link net.sourceforge.stripes.controller.FlashScope} which causes them to be exposed as - * request attributes in both the current and subsequent request (assuming a redirect is used).

    + * Displays a list of non-error messages to the user. The list of messages can come from either the + * request (preferred) or the session (checked 2nd). Lists of messages can be stored under any + * arbitrary key in request or session and the key can be specified to the messages tag. If no key + * is specified then the default key (and therefore default set of messages) is used. Note that by + * default the ActionBeanContext stores messages in a {@link + * net.sourceforge.stripes.controller.FlashScope} which causes them to be exposed as request + * attributes in both the current and subsequent request (assuming a redirect is used). * *

    While similar in concept to the ErrorsTag, the MessagesTag is significantly simpler. It deals - * with a List of Message objects, and does not understand any association between messages and - * form fields, or even between messages and forms. It is designed to be used to show arbitrary - * messages to the user, the prime example being a confirmation message displayed on the subsequent - * page following an action.

    + * with a List of Message objects, and does not understand any association between messages and form + * fields, or even between messages and forms. It is designed to be used to show arbitrary messages + * to the user, the prime example being a confirmation message displayed on the subsequent page + * following an action. * *

    The messages tag outputs a header before the messages, the messages themselves, and a footer - * after the messages. Default values are set for each of these four items. Different values - * can be specified in the error messages resource bundle (StripesResources.properties unless you - * have configured another). The default configuration would look like this: + * after the messages. Default values are set for each of these four items. Different values can be + * specified in the error messages resource bundle (StripesResources.properties unless you have + * configured another). The default configuration would look like this: * *

      - *
    • stripes.messages.header={@literal
        } - *
      • stripes.messages.footer={@literal
      }
    • - *
    • stripes.messages.beforeMessage={@literal
    • }
    • - *
    • stripes.messages.afterMessage={@literal
    • } + *
    • stripes.messages.header={@literal
        } + *
      • stripes.messages.footer={@literal
      } + *
    • stripes.messages.beforeMessage={@literal
    • } + *
    • stripes.messages.afterMessage={@literal
    • } *
    * - *

    It should also be noted that while the errors tag supports custom headers and footers - * through the use of nested tags, the messages tag does not support this. In fact the - * messages tag does not support body content at all - it will simply be ignored.

    + *

    It should also be noted that while the errors tag supports custom headers and footers through + * the use of nested tags, the messages tag does not support this. In fact the messages tag does not + * support body content at all - it will simply be ignored. * * @author Tim Fennell * @since Stripes 1.1.2 */ public class MessagesTag extends HtmlTagSupport { - /** The header that will be emitted if no header is defined in the resource bundle. */ - public static final String DEFAULT_HEADER = "

      "; - - /** The footer that will be emitted if no footer is defined in the resource bundle. */ - public static final String DEFAULT_FOOTER = "
    "; - - /** The key that will be used to perform a scope search for messages. */ - private String key = StripesConstants.REQ_ATTR_MESSAGES; - - /** - * Does nothing, all processing is performed in doEndTag(). - * @return SKIP_BODY in all cases. - */ - @Override - public int doStartTag() throws JspException { - return SKIP_BODY; - } - - /** - * Outputs the set of messages appropriate for this tag. - * @return EVAL_PAGE always - */ - @Override - public int doEndTag() throws JspException { + /** The header that will be emitted if no header is defined in the resource bundle. */ + public static final String DEFAULT_HEADER = "
      "; + + /** The footer that will be emitted if no footer is defined in the resource bundle. */ + public static final String DEFAULT_FOOTER = "
    "; + + /** The key that will be used to perform a scope search for messages. */ + private String key = StripesConstants.REQ_ATTR_MESSAGES; + + /** + * Does nothing, all processing is performed in doEndTag(). + * + * @return SKIP_BODY in all cases. + */ + @Override + public int doStartTag() throws JspException { + return SKIP_BODY; + } + + /** + * Outputs the set of messages appropriate for this tag. + * + * @return EVAL_PAGE always + */ + @Override + public int doEndTag() throws JspException { + try { + List messages = getMessages(); + + if (messages != null && messages.size() > 0) { + JspWriter writer = getPageContext().getOut(); + + // Output all errors in a standard format + Locale locale = getPageContext().getRequest().getLocale(); + ResourceBundle bundle = + StripesFilter.getConfiguration() + .getLocalizationBundleFactory() + .getErrorMessageBundle(locale); + + // Fetch the header and footer + String header, footer, beforeMessage, afterMessage; try { - List messages = getMessages(); - - if (messages != null && messages.size() > 0) { - JspWriter writer = getPageContext().getOut(); - - // Output all errors in a standard format - Locale locale = getPageContext().getRequest().getLocale(); - ResourceBundle bundle = StripesFilter.getConfiguration() - .getLocalizationBundleFactory().getErrorMessageBundle(locale); - - // Fetch the header and footer - String header, footer, beforeMessage, afterMessage; - try { header = bundle.getString("stripes.messages.header"); } - catch (MissingResourceException mre) { header = DEFAULT_HEADER; } - - try { footer = bundle.getString("stripes.messages.footer"); } - catch (MissingResourceException mre) { footer = DEFAULT_FOOTER; } - - try { beforeMessage = bundle.getString("stripes.messages.beforeMessage"); } - catch (MissingResourceException mre) { beforeMessage = "
  • "; } - - try { afterMessage = bundle.getString("stripes.messages.afterMessage"); } - catch (MissingResourceException mre) { afterMessage = "
  • "; } - - // Write out the error messages - writer.write(header); + header = bundle.getString("stripes.messages.header"); + } catch (MissingResourceException mre) { + header = DEFAULT_HEADER; + } - for (Message message : messages) { - writer.write(beforeMessage); - writer.write(message.getMessage(locale)); - writer.write(afterMessage); - } + try { + footer = bundle.getString("stripes.messages.footer"); + } catch (MissingResourceException mre) { + footer = DEFAULT_FOOTER; + } - writer.write(footer); - } - return EVAL_PAGE; + try { + beforeMessage = bundle.getString("stripes.messages.beforeMessage"); + } catch (MissingResourceException mre) { + beforeMessage = "
  • "; } - catch (IOException e) { - throw new JspException("IOException encountered while writing messages " + - "tag to the JspWriter.", e); + + try { + afterMessage = bundle.getString("stripes.messages.afterMessage"); + } catch (MissingResourceException mre) { + afterMessage = "
  • "; } - } - /** Gets the key that will be used to scope search for messages to display. */ - public String getKey() { return key; } - - /** Sets the key that will be used to scope search for messages to display. */ - public void setKey(String key) { this.key = key; } - - /** - * Gets the list of messages that will be displayed by the tag. Looks first in the request - * under the specified key, and if none are found, then looks in session under the same key. - * - * @return List a possibly null list of messages to display - */ - @SuppressWarnings("unchecked") - protected List getMessages() { - HttpServletRequest request = (HttpServletRequest) getPageContext().getRequest(); - List messages = (List) request.getAttribute( getKey() ); - - if (messages == null) { - HttpSession session = request.getSession(false); - if (session != null) { - messages = (List) session.getAttribute(getKey()); - session.removeAttribute(getKey()); - } + // Write out the error messages + writer.write(header); + + for (Message message : messages) { + writer.write(beforeMessage); + writer.write(message.getMessage(locale)); + writer.write(afterMessage); } - return messages; + writer.write(footer); + } + return EVAL_PAGE; + } catch (IOException e) { + throw new JspException( + "IOException encountered while writing messages " + "tag to the JspWriter.", e); + } + } + + /** Gets the key that will be used to scope search for messages to display. */ + public String getKey() { + return key; + } + + /** Sets the key that will be used to scope search for messages to display. */ + public void setKey(String key) { + this.key = key; + } + + /** + * Gets the list of messages that will be displayed by the tag. Looks first in the request under + * the specified key, and if none are found, then looks in session under the same key. + * + * @return List a possibly null list of messages to display + */ + @SuppressWarnings("unchecked") + protected List getMessages() { + HttpServletRequest request = (HttpServletRequest) getPageContext().getRequest(); + List messages = (List) request.getAttribute(getKey()); + + if (messages == null) { + HttpSession session = request.getSession(false); + if (session != null) { + messages = (List) session.getAttribute(getKey()); + session.removeAttribute(getKey()); + } } + + return messages; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/PageOptionsTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/PageOptionsTag.java index e19f82742..e2b6455b8 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/PageOptionsTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/PageOptionsTag.java @@ -1,12 +1,12 @@ /* * Copyright 2010 Timothy Stone. - * + * * 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. @@ -16,93 +16,88 @@ */ package net.sourceforge.stripes.tag; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.PageContext; - +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.PageContext; import net.sourceforge.stripes.controller.StripesFilter; /** - *

    - * Provides a tag to override the {@link StripesFilter} configuration property - * Stripes.HtmlMode. - *

    - *

    - * htmlMode accepts any string value, however any value not equal to html, - * case-insensitive, puts Stripes into its default mode of XHTML-compatible output. - *

    - *

    - * Examples of the tag's use then might be: - *

    + * Provides a tag to override the {@link StripesFilter} configuration property + * Stripes.HtmlMode. + * + *

    htmlMode accepts any string value, however any value not equal to html + * , case-insensitive, puts Stripes into its default mode of XHTML-compatible output. + * + *

    Examples of the tag's use then might be: + * *

      - *
    • <s:options htmlMode="html" /> produces HTML4 and HTML5 form elements, e.g., <img src - * … >
    • - *
    • <s:options htmlMode="xhtml" /> produces XHTML-compatible form elements, e.g., <img - * src … />
    • - *
    • <s:options htmlMode="default" /> produces XHTML form elements
    • + *
    • <s:options htmlMode="html" /> produces HTML4 and HTML5 form elements, e.g., <img + * src … > + *
    • <s:options htmlMode="xhtml" /> produces XHTML-compatible form elements, e.g., <img + * src … /> + *
    • <s:options htmlMode="default" /> produces XHTML form elements *
    - *

    - * Typical use of the tag in context of a Stripes application follows: - *

    - *

    - * Deployer will set the application RuntimeConfiguration of Stripes.HtmlMode. A + * + *

    Typical use of the tag in context of a Stripes application follows: + * + *

    Deployer will set the application RuntimeConfiguration of Stripes.HtmlMode. A * deployer choosing not to set this option, defaults the Stripes application to its - * XHTML-compatible format. - *

    - * Stripes.HtmlMode will set the default X/HTML output for the entire - * application. Individual views of the application wishing to alter the application default will - * provide this tag, at or near the beginning of the view, or JSP.

    - * + * XHTML-compatible format. Stripes.HtmlMode will set the default X/HTML output for the + * entire application. Individual views of the application wishing to alter the + * application default will provide this tag, at or near the beginning of the view, or JSP. + * * @author Timothy Stone * @since 1.5.5 */ public class PageOptionsTag extends StripesTagSupport { - /** Configuration key that sets the default HTML mode for the application. */ - public static String CFG_KEY_HTML_MODE = "Stripes.HtmlMode"; - - /** Request attribute that affects how HTML is rendered by other tags. */ - public static String REQ_ATTR_HTML_MODE = "__stripes_html_mode"; + /** Configuration key that sets the default HTML mode for the application. */ + public static String CFG_KEY_HTML_MODE = "Stripes.HtmlMode"; - /** - * Get the HTML mode for the given page context. If the request attribute - * {@link #REQ_ATTR_HTML_MODE} is present then use that value. Otherwise, use the global - * configuration property {@link #CFG_KEY_HTML_MODE}. - */ - public static String getHtmlMode(PageContext pageContext) { - String htmlMode = (String) pageContext.getAttribute(REQ_ATTR_HTML_MODE, - PageContext.REQUEST_SCOPE); + /** Request attribute that affects how HTML is rendered by other tags. */ + public static String REQ_ATTR_HTML_MODE = "__stripes_html_mode"; - if (htmlMode == null) { - htmlMode = StripesFilter.getConfiguration().getBootstrapPropertyResolver() - .getProperty(CFG_KEY_HTML_MODE); - } + /** + * Get the HTML mode for the given page context. If the request attribute {@link + * #REQ_ATTR_HTML_MODE} is present then use that value. Otherwise, use the global configuration + * property {@link #CFG_KEY_HTML_MODE}. + */ + public static String getHtmlMode(PageContext pageContext) { + String htmlMode = + (String) pageContext.getAttribute(REQ_ATTR_HTML_MODE, PageContext.REQUEST_SCOPE); - return htmlMode; + if (htmlMode == null) { + htmlMode = + StripesFilter.getConfiguration() + .getBootstrapPropertyResolver() + .getProperty(CFG_KEY_HTML_MODE); } - /** - * This field is not initialized to null because null is a valid value that may be passed to - * {@link #setHtmlMode(String)}. Initializing to a constant differentiates between a field that - * was never changed after initialization and a field that was set to null. - */ - private String htmlMode = REQ_ATTR_HTML_MODE; + return htmlMode; + } - @Override - public int doStartTag() throws JspException { - return SKIP_BODY; - } + /** + * This field is not initialized to null because null is a valid value that may be passed to + * {@link #setHtmlMode(String)}. Initializing to a constant differentiates between a field that + * was never changed after initialization and a field that was set to null. + */ + private String htmlMode = REQ_ATTR_HTML_MODE; - @Override - public int doEndTag() throws JspException { - // This is an intentional use of identity instead of equality - if (this.htmlMode != REQ_ATTR_HTML_MODE) { - pageContext.getRequest().setAttribute(REQ_ATTR_HTML_MODE, this.htmlMode); - } + @Override + public int doStartTag() throws JspException { + return SKIP_BODY; + } - return EVAL_PAGE; + @Override + public int doEndTag() throws JspException { + // This is an intentional use of identity instead of equality + if (this.htmlMode != REQ_ATTR_HTML_MODE) { + pageContext.getRequest().setAttribute(REQ_ATTR_HTML_MODE, this.htmlMode); } - /** Set the HTML mode string. */ - public void setHtmlMode(String htmlMode) { - this.htmlMode = htmlMode; - } + return EVAL_PAGE; + } + + /** Set the HTML mode string. */ + public void setHtmlMode(String htmlMode) { + this.htmlMode = htmlMode; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/ParamTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/ParamTag.java index e79e1f882..4170085a6 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/ParamTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/ParamTag.java @@ -14,121 +14,129 @@ */ package net.sourceforge.stripes.tag; -import javax.servlet.jsp.tagext.BodyTag; -import javax.servlet.jsp.tagext.BodyContent; -import javax.servlet.jsp.tagext.Tag; -import javax.servlet.jsp.PageContext; -import javax.servlet.jsp.JspException; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.PageContext; +import jakarta.servlet.jsp.tagext.BodyContent; +import jakarta.servlet.jsp.tagext.BodyTag; +import jakarta.servlet.jsp.tagext.Tag; /** - *

    Used to supply parameters when nested inside tags that implement {@link ParameterizableTag}. - * The value is either obtained from the value attribute, or if that is not present, then the - * body of the tag.

    + * Used to supply parameters when nested inside tags that implement {@link ParameterizableTag}. The + * value is either obtained from the value attribute, or if that is not present, then the body of + * the tag. * *

    Once the value has been established the parent tag is looked for, and the parameter is handed - * over to it.

    + * over to it. + * + *

    Primarily used by the LinkTag and UrlTag. * - *

    Primarily used by the LinkTag and UrlTag.

    - * @author Tim Fennell * @since Stripes 1.4 * @see ParamTag */ public class ParamTag implements BodyTag { - private String name; - private Object value; - private BodyContent bodyContent; - private Tag parentTag; - private PageContext pageContext; - - /** Sets the value of the parameter(s) to be added to the URL. */ - public void setValue(Object value) { - this.value = value; - } - - /** Gets the value attribute, as set with setValue(). */ - public Object getValue() { - return value; - } - - /** Sets the name of the parameter(s) that will be added to the URL. */ - public void setName(String name) { - this.name = name; - } - - /** Gets the name of the parameter(s) that will be added to the URL. */ - public String getName() { - return name; + private String name; + private Object value; + private BodyContent bodyContent; + private Tag parentTag; + private PageContext pageContext; + + /** Sets the value of the parameter(s) to be added to the URL. */ + public void setValue(Object value) { + this.value = value; + } + + /** Gets the value attribute, as set with setValue(). */ + public Object getValue() { + return value; + } + + /** Sets the name of the parameter(s) that will be added to the URL. */ + public void setName(String name) { + this.name = name; + } + + /** Gets the name of the parameter(s) that will be added to the URL. */ + public String getName() { + return name; + } + + /** Used by the container to set the contents of the body of the tag. */ + public void setBodyContent(BodyContent bodyContent) { + this.bodyContent = bodyContent; + } + + /** Used by the container to set the page context for the tag. */ + public void setPageContext(PageContext pageContext) { + this.pageContext = pageContext; + } + + /** Used by the container to provide the tag with access to it's parent tag on the page. */ + public void setParent(Tag tag) { + this.parentTag = tag; + } + + /** Required spec method to allow others to access the parent of the tag. */ + public Tag getParent() { + return this.parentTag; + } + + /** Does nothing. */ + public void doInitBody() throws JspException { + /* Do Nothing */ + } + + /** + * Does nothing. + * + * @return SKIP_BODY in all cases. + */ + public int doAfterBody() throws JspException { + return SKIP_BODY; + } + + /** + * Does nothing. + * + * @return EVAL_BODY_BUFFERED in all cases. + */ + public int doStartTag() throws JspException { + return EVAL_BODY_BUFFERED; + } + + /** + * Figures out what to use as the value, and then finds the parent link and adds the parameter. + * + * @return EVAL_PAGE in all cases. + */ + public int doEndTag() throws JspException { + Object valueToSet = value; + + // First figure out what value to send to the parent link tag + if (value == null) { + if (this.bodyContent == null) { + valueToSet = ""; + } else { + valueToSet = this.bodyContent.getString(); + } } - /** Used by the container to set the contents of the body of the tag. */ - public void setBodyContent(BodyContent bodyContent) { - this.bodyContent = bodyContent; + // Find the parent link tag + Tag parameterizable = this.parentTag; + while (parameterizable != null && !(parameterizable instanceof ParameterizableTag)) { + parameterizable = parameterizable.getParent(); } - /** Used by the container to set the page context for the tag. */ - public void setPageContext(PageContext pageContext) { - this.pageContext = pageContext; - } - - /** Used by the container to provide the tag with access to it's parent tag on the page. */ - public void setParent(Tag tag) { - this.parentTag = tag; - } - - /** Required spec method to allow others to access the parent of the tag. */ - public Tag getParent() { - return this.parentTag; - } - - /** Does nothing. */ - public void doInitBody() throws JspException { /* Do Nothing */ } - - /** - * Does nothing. - * @return SKIP_BODY in all cases. - */ - public int doAfterBody() throws JspException { return SKIP_BODY; } - - /** - * Does nothing. - * @return EVAL_BODY_BUFFERED in all cases. - */ - public int doStartTag() throws JspException { return EVAL_BODY_BUFFERED; } - - /** - * Figures out what to use as the value, and then finds the parent link and adds - * the parameter. - * @return EVAL_PAGE in all cases. - */ - public int doEndTag() throws JspException { - Object valueToSet = value; - - // First figure out what value to send to the parent link tag - if (value == null) { - if (this.bodyContent == null) { - valueToSet = ""; - } - else { - valueToSet = this.bodyContent.getString(); - } - } - - // Find the parent link tag - Tag parameterizable = this.parentTag; - while (parameterizable != null && !(parameterizable instanceof ParameterizableTag)) { - parameterizable = parameterizable.getParent(); - } - - ((ParameterizableTag) parameterizable).addParameter(name, valueToSet); - return EVAL_PAGE; - } + ((ParameterizableTag) parameterizable).addParameter(name, valueToSet); + return EVAL_PAGE; + } - /** Does nothing. */ - public void release() { /* Do nothing. */ } + /** Does nothing. */ + public void release() { + /* Do nothing. */ + } - public PageContext getPageContext() - { - return pageContext; - } + public PageContext getPageContext() { + return pageContext; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/ParameterizableTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/ParameterizableTag.java index e9c5bdd6e..98445c8b9 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/ParameterizableTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/ParameterizableTag.java @@ -15,21 +15,21 @@ package net.sourceforge.stripes.tag; /** - * Interface to be implemented by tags which wish to be able to receive parameters from - * nested {@literal } tags. + * Interface to be implemented by tags which wish to be able to receive parameters from nested + * {@literal } tags. * * @author Tim Fennell * @since Stripes 1.4 * @see ParamTag */ public interface ParameterizableTag { - /** - * Adds a parameter to the tag. It is up to the tag to determine whether the new value(s) - * supplied supercede or add to previous values. The value can be of any type, and tags - * should handle Arrays and Collections gracefully. - * - * @param name the name of the parameter - * @param valueOrValues either a scalar value, an array or a collection - */ - void addParameter(String name, Object valueOrValues); + /** + * Adds a parameter to the tag. It is up to the tag to determine whether the new value(s) supplied + * supercede or add to previous values. The value can be of any type, and tags should handle + * Arrays and Collections gracefully. + * + * @param name the name of the parameter + * @param valueOrValues either a scalar value, an array or a collection + */ + void addParameter(String name, Object valueOrValues); } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/PopulationStrategy.java b/stripes/src/main/java/net/sourceforge/stripes/tag/PopulationStrategy.java index 2fffeef36..7c0bcb350 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/PopulationStrategy.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/PopulationStrategy.java @@ -14,17 +14,17 @@ */ package net.sourceforge.stripes.tag; -import net.sourceforge.stripes.exception.StripesJspException; import net.sourceforge.stripes.config.ConfigurableComponent; +import net.sourceforge.stripes.exception.StripesJspException; /** * Interface that implements the logic to determine how to populate/repopulate an input tag. - * Generally, population strategies will need to determine whether to pull the tag's value from - * the current request's parameters, from an ActionBean (if one is present), or from a value - * provided for the tag on the JSP. + * Generally, population strategies will need to determine whether to pull the tag's value from the + * current request's parameters, from an ActionBean (if one is present), or from a value provided + * for the tag on the JSP. * - * @author Tim Fennell + * @author Tim Fennell */ public interface PopulationStrategy extends ConfigurableComponent { - Object getValue(InputTagSupport tag) throws StripesJspException ; + Object getValue(InputTagSupport tag) throws StripesJspException; } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/StripesTagSupport.java b/stripes/src/main/java/net/sourceforge/stripes/tag/StripesTagSupport.java index e1c059ef0..9c8087782 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/StripesTagSupport.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/StripesTagSupport.java @@ -14,20 +14,19 @@ */ package net.sourceforge.stripes.tag; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.PageContext; +import jakarta.servlet.jsp.tagext.Tag; +import java.util.HashMap; +import java.util.ListIterator; +import java.util.Map; +import java.util.Stack; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.controller.StripesConstants; import net.sourceforge.stripes.controller.StripesFilter; import net.sourceforge.stripes.util.Log; import net.sourceforge.stripes.util.ReflectUtil; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.PageContext; -import javax.servlet.jsp.tagext.Tag; -import java.util.HashMap; -import java.util.ListIterator; -import java.util.Map; -import java.util.Stack; - /** * A very basic implementation of the Tag interface that is similar in manner to the standard * TagSupport class, but with less clutter. @@ -35,200 +34,192 @@ * @author Tim Fennell */ public abstract class StripesTagSupport implements Tag { - private static final Log log = Log.getInstance(StripesTagSupport.class); - - /** Storage for a PageContext during evaluation. */ - protected PageContext pageContext; - /** Storage for the parent tag of this tag. */ - protected Tag parentTag; - - /** - * A map that is used to store values of page context attributes before they were - * replaced with other values for the body of the tag. - */ - private Map previousAttributeValues; - - /** Called by the Servlet container to set the page context on the tag. */ - public void setPageContext(PageContext pageContext) { - this.pageContext = pageContext; + private static final Log log = Log.getInstance(StripesTagSupport.class); + + /** Storage for a PageContext during evaluation. */ + protected PageContext pageContext; + /** Storage for the parent tag of this tag. */ + protected Tag parentTag; + + /** + * A map that is used to store values of page context attributes before they were replaced with + * other values for the body of the tag. + */ + private Map previousAttributeValues; + + /** Called by the Servlet container to set the page context on the tag. */ + public void setPageContext(PageContext pageContext) { + this.pageContext = pageContext; + } + + /** Retrieves the pageContext handed to the tag by the container. */ + public PageContext getPageContext() { + return this.pageContext; + } + + /** From the Tag interface - allows the container to set the parent tag on the JSP. */ + public void setParent(Tag tag) { + this.parentTag = tag; + } + + /** From the Tag interface - allows fetching the parent tag on the JSP. */ + public Tag getParent() { + return this.parentTag; + } + + /** + * Abstract method from the Tag interface. Abstract because it seems to make the child tags more + * readable if they implement their own do() methods, even when they just return one of the + * constants and do nothing else. + */ + public abstract int doStartTag() throws JspException; + + /** + * Abstract method from the Tag interface. Abstract because it seems to make the child tags more + * readable if they implement their own do() methods, even when they just return one of the + * constants and do nothing else. + */ + public abstract int doEndTag() throws JspException; + + /** No-op implementation of release(). */ + public void release() {} + + /** + * Pushes new values for the attributes supplied into the page context, preserving the old values + * so that they can be put back into page context end of the tag's execution (usually in + * doEndTag). If this method is called, the tag must also call{@link + * #popPageContextAttributes()}. + */ + public void pushPageContextAttributes(Map attributes) { + this.previousAttributeValues = new HashMap(); + + for (Map.Entry entry : attributes.entrySet()) { + String name = entry.getKey(); + this.previousAttributeValues.put(name, pageContext.getAttribute(name)); + this.pageContext.setAttribute(name, entry.getValue()); } - - /** Retrieves the pageContext handed to the tag by the container. */ - public PageContext getPageContext() { - return this.pageContext; - } - - /** From the Tag interface - allows the container to set the parent tag on the JSP. */ - public void setParent(Tag tag) { - this.parentTag = tag; + } + + /** + * Attempts to restore page context attributes to their state prior to a call to + * pushPageContextAttributes(). Attributes that had values prior to the execution of this tag have + * their values restored. Attributes that did not have values are removed from the page context. + */ + public void popPageContextAttributes() { + for (Map.Entry entry : this.previousAttributeValues.entrySet()) { + if (entry.getValue() == null) { + this.pageContext.removeAttribute(entry.getKey()); + } else { + this.pageContext.setAttribute(entry.getKey(), entry.getValue()); + } } - /** From the Tag interface - allows fetching the parent tag on the JSP. */ - public Tag getParent() { - return this.parentTag; + // Null out the map so erroneous values don't get picked up on tag pooling! + this.previousAttributeValues = null; + } + + /** + * Locates the enclosing tag of the type supplied. If no enclosing tag of the type supplied can be + * found anywhere in the ancestry of this tag, null is returned. + * + * @return T Tag of the type supplied, or null if none can be found + */ + @SuppressWarnings("unchecked") + protected T getParentTag(Class tagType) { + Tag parent = getParent(); + while (parent != null) { + if (tagType.isAssignableFrom(parent.getClass())) { + return (T) parent; + } + parent = parent.getParent(); } - /** - * Abstract method from the Tag interface. Abstract because it seems to make the - * child tags more readable if they implement their own do() methods, even when - * they just return one of the constants and do nothing else. - */ - public abstract int doStartTag() throws JspException; - - /** - * Abstract method from the Tag interface. Abstract because it seems to make the - * child tags more readable if they implement their own do() methods, even when - * they just return one of the constants and do nothing else. - */ - public abstract int doEndTag() throws JspException; - - /** - * No-op implementation of release(). - */ - public void release() { } - - /** - * Pushes new values for the attributes supplied into the page context, preserving - * the old values so that they can be put back into page context end of the tag's - * execution (usually in doEndTag). If this method is called, the tag must - * also call{@link #popPageContextAttributes()}. - */ - public void pushPageContextAttributes(Map attributes) { - this.previousAttributeValues = new HashMap(); - - for (Map.Entry entry : attributes.entrySet()) { - String name = entry.getKey(); - this.previousAttributeValues.put(name, pageContext.getAttribute(name)); - this.pageContext.setAttribute(name, entry.getValue()); - } + // If we can't find it by the normal way, try our own tag stack! + Stack stack = getTagStack(); + ListIterator iterator = stack.listIterator(stack.size()); + while (iterator.hasPrevious() && iterator.previous() != this) continue; + while (iterator.hasPrevious()) { + StripesTagSupport tag = iterator.previous(); + if (tagType.isAssignableFrom(tag.getClass())) { + return (T) tag; + } } - /** - * Attempts to restore page context attributes to their state prior to a call to - * pushPageContextAttributes(). Attributes that had values prior to the execution of - * this tag have their values restored. Attributes that did not have values - * are removed from the page context. - */ - public void popPageContextAttributes() { - for (Map.Entry entry : this.previousAttributeValues.entrySet()) { - if (entry.getValue() == null) { - this.pageContext.removeAttribute(entry.getKey()); - } - else { - this.pageContext.setAttribute(entry.getKey(), entry.getValue()); - } - } - - // Null out the map so erroneous values don't get picked up on tag pooling! - this.previousAttributeValues = null; + return null; + } + + /** + * Fetches a tag stack that is stored in the request. This tag stack is used to help Stripes tags + * find one another when they are spread across multiple included JSPs and/or tag files - + * situations in which the usual parent tag relationship fails. + */ + @SuppressWarnings("unchecked") + protected Stack getTagStack() { + Stack stack = + (Stack) + getPageContext().getRequest().getAttribute(StripesConstants.REQ_ATTR_TAG_STACK); + + if (stack == null) { + stack = new Stack(); + getPageContext().getRequest().setAttribute(StripesConstants.REQ_ATTR_TAG_STACK, stack); } - - /** - *

    Locates the enclosing tag of the type supplied. If no enclosing tag of the type supplied - * can be found anywhere in the ancestry of this tag, null is returned.

    - * - * @return T Tag of the type supplied, or null if none can be found - */ - @SuppressWarnings("unchecked") - protected T getParentTag(Class tagType) { - Tag parent = getParent(); - while (parent != null) { - if (tagType.isAssignableFrom(parent.getClass())) { - return (T) parent; - } - parent = parent.getParent(); - } - - // If we can't find it by the normal way, try our own tag stack! - Stack stack = getTagStack(); - ListIterator iterator = stack.listIterator(stack.size()); - while (iterator.hasPrevious() && iterator.previous() != this) continue; - while (iterator.hasPrevious()) { - StripesTagSupport tag = iterator.previous(); - if (tagType.isAssignableFrom(tag.getClass())) { - return (T) tag; - } - } - + return stack; + } + + /** + * Helper method that takes an attribute which may be either a String class name or a Class object + * and returns the Class representing the appropriate ActionBean. If for any reason the Class + * cannot be determined, or it is not an ActionBean, null will be returned instead. + * + * @param nameOrClass either the String FQN of an ActionBean class, or a Class object + * @return the appropriate ActionBean class or null + */ + @SuppressWarnings("unchecked") + protected Class getActionBeanType(Object nameOrClass) { + Class result = null; + + // Figure out if it's a String of Class (or something else?) and act appropriately + if (nameOrClass instanceof String) { + try { + result = ReflectUtil.findClass((String) nameOrClass); + } catch (ClassNotFoundException cnfe) { + log.error(cnfe, "Could not find class of type: ", nameOrClass); return null; + } + } else if (nameOrClass instanceof Class) { + result = (Class) nameOrClass; + } else { + log.error( + "The value supplied to getActionBeanType() was neither a String nor a " + + "Class. Cannot infer ActionBean type from value: " + + nameOrClass); + return null; } - /** - * Fetches a tag stack that is stored in the request. This tag stack is used to help - * Stripes tags find one another when they are spread across multiple included JSPs - * and/or tag files - situations in which the usual parent tag relationship fails. - */ - @SuppressWarnings("unchecked") - protected Stack getTagStack() { - Stack stack = (Stack) - getPageContext().getRequest().getAttribute(StripesConstants.REQ_ATTR_TAG_STACK); - - if (stack == null) { - stack = new Stack(); - getPageContext().getRequest().setAttribute(StripesConstants.REQ_ATTR_TAG_STACK, stack); - } - - return stack; - } - - /** - * Helper method that takes an attribute which may be either a String class name - * or a Class object and returns the Class representing the appropriate ActionBean. - * If for any reason the Class cannot be determined, or it is not an ActionBean, null - * will be returned instead. - * - * @param nameOrClass either the String FQN of an ActionBean class, or a Class object - * @return the appropriate ActionBean class or null - */ - @SuppressWarnings("unchecked") - protected Class getActionBeanType(Object nameOrClass) { - Class result = null; - - // Figure out if it's a String of Class (or something else?) and act appropriately - if (nameOrClass instanceof String) { - try { - result = ReflectUtil.findClass((String) nameOrClass); - } - catch (ClassNotFoundException cnfe) { - log.error(cnfe, "Could not find class of type: ", nameOrClass); - return null; - } - } - else if (nameOrClass instanceof Class) { - result = (Class) nameOrClass; - } - else { - log.error("The value supplied to getActionBeanType() was neither a String nor a " + - "Class. Cannot infer ActionBean type from value: " + nameOrClass); - return null; - } - - // And for good measure, let's make sure it's an ActionBean implementation! - if (ActionBean.class.isAssignableFrom(result)) { - return result; - } - else { - log.error("Class '", result.getName(), "' specified in tag does not implement ", - "ActionBean."); - return null; - } + // And for good measure, let's make sure it's an ActionBean implementation! + if (ActionBean.class.isAssignableFrom(result)) { + return result; + } else { + log.error( + "Class '", result.getName(), "' specified in tag does not implement ", "ActionBean."); + return null; } - - /** - * Similar to the {@link #getActionBeanType(Object)} method except that instead of - * returning the Class of ActionBean it returns the URL Binding of the ActionBean. - * - * @param nameOrClass either the String FQN of an ActionBean class, or a Class object - * @return the URL of the appropriate ActionBean class or null - */ - protected String getActionBeanUrl(Object nameOrClass) { - Class beanType = getActionBeanType(nameOrClass); - if (beanType != null) { - return StripesFilter.getConfiguration().getActionResolver().getUrlBinding(beanType); - } - else { - return null; - } + } + + /** + * Similar to the {@link #getActionBeanType(Object)} method except that instead of returning the + * Class of ActionBean it returns the URL Binding of the ActionBean. + * + * @param nameOrClass either the String FQN of an ActionBean class, or a Class object + * @return the URL of the appropriate ActionBean class or null + */ + protected String getActionBeanUrl(Object nameOrClass) { + Class beanType = getActionBeanType(nameOrClass); + if (beanType != null) { + return StripesFilter.getConfiguration().getActionResolver().getUrlBinding(beanType); + } else { + return null; } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/TagErrorRenderer.java b/stripes/src/main/java/net/sourceforge/stripes/tag/TagErrorRenderer.java index 82a5a8984..5e66cb0cf 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/TagErrorRenderer.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/TagErrorRenderer.java @@ -15,35 +15,32 @@ package net.sourceforge.stripes.tag; /** - *

    Implementations of this interface are used to apply formatting to form input - * fields when there are associated errors. TagErrorRenderers can modify attributes - * of the tags output html before and/or after the tag renders itself.

    + * Implementations of this interface are used to apply formatting to form input fields when there + * are associated errors. TagErrorRenderers can modify attributes of the tags output html before + * and/or after the tag renders itself. * - *

    If the renderer modifies attributes of the form input tag, it is also responsible - * for re-setting those values to their prior values in the doAfterEndTag() method. If - * this is not done correctly and the tag is pooled by the container the results on the page - * may be pretty unexpected!

    + *

    If the renderer modifies attributes of the form input tag, it is also responsible for + * re-setting those values to their prior values in the doAfterEndTag() method. If this is not done + * correctly and the tag is pooled by the container the results on the page may be pretty + * unexpected! * * @author Greg Hinkle */ public interface TagErrorRenderer { - /** - * Initialize this renderer for a specific tag instance - * @param tag The InputTagSuppport subclass that will be modified - */ - void init(InputTagSupport tag); + /** + * Initialize this renderer for a specific tag instance + * + * @param tag The InputTagSuppport subclass that will be modified + */ + void init(InputTagSupport tag); - /** - * Executed before the start of rendering of the input tag. - * The input tag attributes can be modified here to be written - * out with other html attributes. - */ - void doBeforeStartTag(); + /** + * Executed before the start of rendering of the input tag. The input tag attributes can be + * modified here to be written out with other html attributes. + */ + void doBeforeStartTag(); - /** - * Executed after the end of rendering of the input tag, including - * its body and end tag. - */ - void doAfterEndTag(); + /** Executed after the end of rendering of the input tag, including its body and end tag. */ + void doAfterEndTag(); } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/TagErrorRendererFactory.java b/stripes/src/main/java/net/sourceforge/stripes/tag/TagErrorRendererFactory.java index a743b1e72..3ae30e596 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/TagErrorRendererFactory.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/TagErrorRendererFactory.java @@ -17,20 +17,18 @@ import net.sourceforge.stripes.config.ConfigurableComponent; /** - * Constructs and returns an instance of TagErrorRenderer to handle the - * error output of a specific form input tag. + * Constructs and returns an instance of TagErrorRenderer to handle the error output of a specific + * form input tag. * * @author Greg Hinkle */ public interface TagErrorRendererFactory extends ConfigurableComponent { - - /** - * Returns a new instance of a TagErrorRenderer that is utilized - * by the supplied tag. - * @param tag The tag that needs to be error rendered - * @return TagErrorRenderer the error renderer to render the error output - */ - public TagErrorRenderer getTagErrorRenderer(InputTagSupport tag); - + /** + * Returns a new instance of a TagErrorRenderer that is utilized by the supplied tag. + * + * @param tag The tag that needs to be error rendered + * @return TagErrorRenderer the error renderer to render the error output + */ + public TagErrorRenderer getTagErrorRenderer(InputTagSupport tag); } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/UrlTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/UrlTag.java index 30a3bf49d..a3b913094 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/UrlTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/UrlTag.java @@ -14,98 +14,114 @@ */ package net.sourceforge.stripes.tag; -import net.sourceforge.stripes.exception.StripesJspException; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.BodyTag; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.BodyTag; import java.io.IOException; +import net.sourceforge.stripes.exception.StripesJspException; /** - *

    A Stripes version of the {@literal } tag that adds some Stripes specific - * parameters to the URL. Designed to generate URLs and either write them into the page - * or set them into one of the JSP scopes.

    + * A Stripes version of the {@literal } tag that adds some Stripes specific parameters to + * the URL. Designed to generate URLs and either write them into the page or set them into one of + * the JSP scopes. * - *

    Cooperates with the Stripes ParamTag to accept any number of parameters that will be - * merged into the URL before rendering.

    + *

    Cooperates with the Stripes ParamTag to accept any number of parameters that will be merged + * into the URL before rendering. * * @author Tim Fennell * @since Stripes 1.4 * @see ParamTag */ public class UrlTag extends LinkTagSupport implements BodyTag { - String var; - String scope; + String var; + String scope; - /** - * Does nothing. - * @return {@link #EVAL_BODY_BUFFERED} in all cases. - */ - @Override - public int doStartTag() throws JspException { return EVAL_BODY_BUFFERED; } + /** + * Does nothing. + * + * @return {@link #EVAL_BODY_BUFFERED} in all cases. + */ + @Override + public int doStartTag() throws JspException { + return EVAL_BODY_BUFFERED; + } - /** Does nothing. */ - public void doInitBody() throws JspException { /* Do Nothing. */ } + /** Does nothing. */ + public void doInitBody() throws JspException { + /* Do Nothing. */ + } - /** - * Does nothing. - * @return {@link #SKIP_BODY} in all cases. - */ - public int doAfterBody() throws JspException { return SKIP_BODY; } + /** + * Does nothing. + * + * @return {@link #SKIP_BODY} in all cases. + */ + public int doAfterBody() throws JspException { + return SKIP_BODY; + } - /** - * Generates the URL and either writes it into the page or sets it in the appropraite - * JSP scope. - * - * @return {@link #EVAL_PAGE} in all cases. - * @throws JspException if the output stream cannot be written to. - */ - @Override - public int doEndTag() throws JspException { - String url = buildUrl(); + /** + * Generates the URL and either writes it into the page or sets it in the appropraite JSP scope. + * + * @return {@link #EVAL_PAGE} in all cases. + * @throws JspException if the output stream cannot be written to. + */ + @Override + public int doEndTag() throws JspException { + String url = buildUrl(); - // If the user specified a 'var', then set the url as a scoped variable - if (var != null) { - String s = (this.scope) == null ? "page" : this.scope; + // If the user specified a 'var', then set the url as a scoped variable + if (var != null) { + String s = (this.scope) == null ? "page" : this.scope; - if (s.equalsIgnoreCase("request")) { - getPageContext().getRequest().setAttribute(this.var, url); - } - else if (s.equalsIgnoreCase("session")) { - getPageContext().getSession().setAttribute(this.var, url); - } - else if (s.equalsIgnoreCase("application")) { - getPageContext().getServletContext().setAttribute(this.var, url); - } - else { - getPageContext().setAttribute(this.var, url); - } + if (s.equalsIgnoreCase("request")) { + getPageContext().getRequest().setAttribute(this.var, url); + } else if (s.equalsIgnoreCase("session")) { + getPageContext().getSession().setAttribute(this.var, url); + } else if (s.equalsIgnoreCase("application")) { + getPageContext().getServletContext().setAttribute(this.var, url); + } else { + getPageContext().setAttribute(this.var, url); + } - } - // Else just write it out to the page - else { - try { getPageContext().getOut().write(url); } - catch (IOException ioe) { - throw new StripesJspException("IOException while trying to write url to page.", ioe); - } - } + } + // Else just write it out to the page + else { + try { + getPageContext().getOut().write(url); + } catch (IOException ioe) { + throw new StripesJspException("IOException while trying to write url to page.", ioe); + } + } - clearParameters(); + clearParameters(); - return EVAL_PAGE; - } + return EVAL_PAGE; + } - /** Gets the name of the scoped variable to store the URL in. */ - public String getVar() { return var; } - /** Sets the name of the scoped variable to store the URL in. */ - public void setVar(String var) { this.var = var; } + /** Gets the name of the scoped variable to store the URL in. */ + public String getVar() { + return var; + } + /** Sets the name of the scoped variable to store the URL in. */ + public void setVar(String var) { + this.var = var; + } - /** Gets the name of scope to store the scoped variable specified by 'var' in. */ - public String getScope() { return scope; } - /** Sets the name of scope to store the scoped variable specified by 'var' in. */ - public void setScope(String scope) { this.scope = scope; } + /** Gets the name of scope to store the scoped variable specified by 'var' in. */ + public String getScope() { + return scope; + } + /** Sets the name of scope to store the scoped variable specified by 'var' in. */ + public void setScope(String scope) { + this.scope = scope; + } - /** Gets the URL as supplied on the page. */ - public String getValue() { return getUrl(); } - /** Sets the URL as supplied on the page. */ - public void setValue(String value) { setUrl(value); } + /** Gets the URL as supplied on the page. */ + public String getValue() { + return getUrl(); + } + /** Sets the URL as supplied on the page. */ + public void setValue(String value) { + setUrl(value); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/UseActionBeanTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/UseActionBeanTag.java index 91dfb16ed..810fe09fd 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/UseActionBeanTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/UseActionBeanTag.java @@ -14,235 +14,261 @@ */ package net.sourceforge.stripes.tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.jsp.JspException; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.action.Resolution; import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.controller.ActionResolver; import net.sourceforge.stripes.controller.DispatcherHelper; +import net.sourceforge.stripes.controller.DispatcherServlet; import net.sourceforge.stripes.controller.ExecutionContext; import net.sourceforge.stripes.controller.Interceptor; import net.sourceforge.stripes.controller.LifecycleStage; import net.sourceforge.stripes.controller.StripesFilter; -import net.sourceforge.stripes.controller.DispatcherServlet; import net.sourceforge.stripes.exception.StripesJspException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.jsp.JspException; - /** - *

    This tag supports the use of Stripes ActionBean classes as view helpers. - * It allows for the use of actions as the controller and then their reuse - * on the page, creating it if it does not exist. A typical usage pattern would - * be for a page that contains two types of information, the interaction with each being - * handled by separate ActionBean implementation. Some page events route to the first - * action and others to the second, but the page still requires data from both in - * order to render. This tag would define both ActionBeans in the page scope, creating - * the one that wasn't executing the event.

    + * This tag supports the use of Stripes ActionBean classes as view helpers. It allows for the use of + * actions as the controller and then their reuse on the page, creating it if it does not exist. A + * typical usage pattern would be for a page that contains two types of information, the interaction + * with each being handled by separate ActionBean implementation. Some page events route to the + * first action and others to the second, but the page still requires data from both in order to + * render. This tag would define both ActionBeans in the page scope, creating the one that wasn't + * executing the event. * - *

    This class will bind parameters to a created ActionBean just as the execution of - * an event on an ActionBean would. It does not rebind values to ActionBeans that - * were previously created for execution of the action. Validation is not done - * during this binding, except the type conversion required for binding, and no - * validation errors are produced.

    + *

    This class will bind parameters to a created ActionBean just as the execution of an event on + * an ActionBean would. It does not rebind values to ActionBeans that were previously created for + * execution of the action. Validation is not done during this binding, except the type conversion + * required for binding, and no validation errors are produced. * - *

    The binding of the ActionBean to the page scope happens whether the ActionBean - * is created or not, making for a consistent variable to always use when referencing - * the ActionBean.

    + *

    The binding of the ActionBean to the page scope happens whether the ActionBean is created or + * not, making for a consistent variable to always use when referencing the ActionBean. * * @author Greg Hinkle, Tim Fennell */ public class UseActionBeanTag extends StripesTagSupport { - /** The UrlBinding of the ActionBean to create */ - private String binding; - - /** The event, if any, to execute when creating */ - private String event; - - /** A page scope variable to which to bind the ActionBean */ - private String var; - - /** Indicates that validation should be executed. */ - private boolean validate = false; - - /** Indicates whether the event should be executed even if the bean was already present. */ - private boolean alwaysExecuteEvent = false; - - /** Indicates whether the resolution should be executed - false by default. */ - private boolean executeResolution = false; - - /** - * The main work method of the tag. Looks up the action bean, instantiates it, - * runs binding and then runs either the named event or the default. - * - * @return SKIP_BODY in all cases. - * @throws JspException if the ActionBean could not be instantiate and executed - */ - @Override - public int doStartTag() throws JspException { - // Check to see if the action bean already exists - ActionBean actionBean = (ActionBean) getPageContext().findAttribute(binding); - boolean beanNotPresent = actionBean == null; - - try { - final Configuration config = StripesFilter.getConfiguration(); - final ActionResolver resolver = StripesFilter.getConfiguration().getActionResolver(); - final HttpServletRequest request = (HttpServletRequest) getPageContext().getRequest(); - final HttpServletResponse response = (HttpServletResponse) getPageContext().getResponse(); - Resolution resolution = null; - ExecutionContext ctx = new ExecutionContext(); - - // Lookup the ActionBean if we don't already have it - if (beanNotPresent) { - ActionBeanContext tempContext = - config.getActionBeanContextFactory().getContextInstance(request, response); - tempContext.setServletContext(getPageContext().getServletContext()); - ctx.setLifecycleStage(LifecycleStage.ActionBeanResolution); - ctx.setActionBeanContext(tempContext); - - // Run action bean resolution - ctx.setInterceptors(config.getInterceptors(LifecycleStage.ActionBeanResolution)); - resolution = ctx.wrap( new Interceptor() { - public Resolution intercept(ExecutionContext ec) throws Exception { - ActionBean bean = resolver.getActionBean(ec.getActionBeanContext(), binding); - ec.setActionBean(bean); - return null; - } + /** The UrlBinding of the ActionBean to create */ + private String binding; + + /** The event, if any, to execute when creating */ + private String event; + + /** A page scope variable to which to bind the ActionBean */ + private String var; + + /** Indicates that validation should be executed. */ + private boolean validate = false; + + /** Indicates whether the event should be executed even if the bean was already present. */ + private boolean alwaysExecuteEvent = false; + + /** Indicates whether the resolution should be executed - false by default. */ + private boolean executeResolution = false; + + /** + * The main work method of the tag. Looks up the action bean, instantiates it, runs binding and + * then runs either the named event or the default. + * + * @return SKIP_BODY in all cases. + * @throws JspException if the ActionBean could not be instantiate and executed + */ + @Override + public int doStartTag() throws JspException { + // Check to see if the action bean already exists + ActionBean actionBean = (ActionBean) getPageContext().findAttribute(binding); + boolean beanNotPresent = actionBean == null; + + try { + final Configuration config = StripesFilter.getConfiguration(); + final ActionResolver resolver = StripesFilter.getConfiguration().getActionResolver(); + final HttpServletRequest request = (HttpServletRequest) getPageContext().getRequest(); + final HttpServletResponse response = (HttpServletResponse) getPageContext().getResponse(); + Resolution resolution = null; + ExecutionContext ctx = new ExecutionContext(); + + // Lookup the ActionBean if we don't already have it + if (beanNotPresent) { + ActionBeanContext tempContext = + config.getActionBeanContextFactory().getContextInstance(request, response); + tempContext.setServletContext(getPageContext().getServletContext()); + ctx.setLifecycleStage(LifecycleStage.ActionBeanResolution); + ctx.setActionBeanContext(tempContext); + + // Run action bean resolution + ctx.setInterceptors(config.getInterceptors(LifecycleStage.ActionBeanResolution)); + resolution = + ctx.wrap( + new Interceptor() { + public Resolution intercept(ExecutionContext ec) throws Exception { + ActionBean bean = resolver.getActionBean(ec.getActionBeanContext(), binding); + ec.setActionBean(bean); + return null; + } }); - } - else { - ctx.setActionBean(actionBean); - ctx.setActionBeanContext(actionBean.getContext()); - } - - // Then, if and only if an event was specified, run handler resolution - if (resolution == null && event != null && (beanNotPresent || this.alwaysExecuteEvent)) { - ctx.setLifecycleStage(LifecycleStage.HandlerResolution); - ctx.setInterceptors(config.getInterceptors(LifecycleStage.HandlerResolution)); - resolution = ctx.wrap( new Interceptor() { - public Resolution intercept(ExecutionContext ec) throws Exception { - ec.setHandler(resolver.getHandler(ec.getActionBean().getClass(), event)); - ec.getActionBeanContext().setEventName(event); - return null; - } + } else { + ctx.setActionBean(actionBean); + ctx.setActionBeanContext(actionBean.getContext()); + } + + // Then, if and only if an event was specified, run handler resolution + if (resolution == null && event != null && (beanNotPresent || this.alwaysExecuteEvent)) { + ctx.setLifecycleStage(LifecycleStage.HandlerResolution); + ctx.setInterceptors(config.getInterceptors(LifecycleStage.HandlerResolution)); + resolution = + ctx.wrap( + new Interceptor() { + public Resolution intercept(ExecutionContext ec) throws Exception { + ec.setHandler(resolver.getHandler(ec.getActionBean().getClass(), event)); + ec.getActionBeanContext().setEventName(event); + return null; + } }); - } - - // Make the PageContext available during the validation stage so that we - // can execute EL based expression validation - try { - DispatcherHelper.setPageContext(getPageContext()); - - // Bind applicable request parameters to the ActionBean - if (resolution == null && (beanNotPresent || this.validate == true)) { - resolution = DispatcherHelper.doBindingAndValidation(ctx, this.validate); - } - - // Run custom validations if we're validating - if (resolution == null && this.validate == true) { - String temp = config.getBootstrapPropertyResolver().getProperty( - DispatcherServlet.RUN_CUSTOM_VALIDATION_WHEN_ERRORS); - boolean validateWhenErrors = temp != null && Boolean.valueOf(temp); - - resolution = DispatcherHelper.doCustomValidation(ctx, validateWhenErrors); - } - } - finally { - DispatcherHelper.setPageContext(null); - } - - // Fill in any validation errors if they exist - if (resolution == null && this.validate == true) { - resolution = DispatcherHelper.handleValidationErrors(ctx); - } - - // And (again) if an event was supplied, then run the handler - if (resolution == null && event != null && (beanNotPresent || this.alwaysExecuteEvent)) { - resolution = DispatcherHelper.invokeEventHandler(ctx); - } - - DispatcherHelper.fillInValidationErrors(ctx); // just in case! - - if (resolution != null && this.executeResolution) { - DispatcherHelper.executeResolution(ctx, resolution); - } - - // If a name was specified, bind the ActionBean into page context - if (getVar() != null) { - pageContext.setAttribute(getVar(), ctx.getActionBean()); - } - - return SKIP_BODY; - } - catch(Exception e) { - throw new StripesJspException("Unabled to prepare ActionBean for JSP Usage",e); - } - } - - /** - * Does nothing. - * @return EVAL_PAGE in all cases. - */ - @Override - public int doEndTag() { return EVAL_PAGE; } - - /** - * Sets the binding attribute by figuring out what ActionBean class is identified - * and then in turn finding out the appropriate URL for the ActionBean. - * - * @param beanclass the FQN of an ActionBean class, or a Class object for one. - */ - public void setBeanclass(Object beanclass) throws StripesJspException { - String url = getActionBeanUrl(beanclass); - if (url == null) { - throw new StripesJspException("The 'beanclass' attribute provided could not be " + - "used to identify a valid and configured ActionBean. The value supplied was: " + - beanclass); - } - else { - this.binding = url; - } - } - - /** Get the UrlBinding of the requested ActionBean */ - public String getBinding() { return binding; } + } - /** Set the UrlBinding of the requested ActionBean */ - public void setBinding(String binding) { this.binding = binding; } + // Make the PageContext available during the validation stage so that we + // can execute EL based expression validation + try { + DispatcherHelper.setPageContext(getPageContext()); - /** The event name, if any to execute. */ - public String getEvent() { return event; } - - /** The event name, if any to execute. */ - public void setEvent(String event) { this.event = event; } - - /** Gets the name of the page scope variable to which the ActionBean will be bound. */ - public String getVar() { return var; } - - /** Sets the name of the page scope variable to which the ActionBean will be bound. */ - public void setVar(String var) { this.var = var; } - - /** Alias for getVar() so that the JSTL and jsp:useBean style are allowed. */ - public String getId() { return getVar(); } - - /** Alias for setVar() so that the JSTL and jsp:useBean style are allowed. */ - public void setId(String id) { setVar(id); } - - public boolean isValidate() { return validate; } - - public void setValidate(boolean validate) { this.validate = validate; } + // Bind applicable request parameters to the ActionBean + if (resolution == null && (beanNotPresent || this.validate == true)) { + resolution = DispatcherHelper.doBindingAndValidation(ctx, this.validate); + } - public boolean isAlwaysExecuteEvent() { return alwaysExecuteEvent; } + // Run custom validations if we're validating + if (resolution == null && this.validate == true) { + String temp = + config + .getBootstrapPropertyResolver() + .getProperty(DispatcherServlet.RUN_CUSTOM_VALIDATION_WHEN_ERRORS); + boolean validateWhenErrors = temp != null && Boolean.valueOf(temp); - public void setAlwaysExecuteEvent(boolean alwaysExecuteEvent) { - this.alwaysExecuteEvent = alwaysExecuteEvent; + resolution = DispatcherHelper.doCustomValidation(ctx, validateWhenErrors); + } + } finally { + DispatcherHelper.setPageContext(null); + } + + // Fill in any validation errors if they exist + if (resolution == null && this.validate == true) { + resolution = DispatcherHelper.handleValidationErrors(ctx); + } + + // And (again) if an event was supplied, then run the handler + if (resolution == null && event != null && (beanNotPresent || this.alwaysExecuteEvent)) { + resolution = DispatcherHelper.invokeEventHandler(ctx); + } + + DispatcherHelper.fillInValidationErrors(ctx); // just in case! + + if (resolution != null && this.executeResolution) { + DispatcherHelper.executeResolution(ctx, resolution); + } + + // If a name was specified, bind the ActionBean into page context + if (getVar() != null) { + pageContext.setAttribute(getVar(), ctx.getActionBean()); + } + + return SKIP_BODY; + } catch (Exception e) { + throw new StripesJspException("Unabled to prepare ActionBean for JSP Usage", e); } - - public boolean isExecuteResolution() { return executeResolution; } - - public void setExecuteResolution(boolean executeResolution) { - this.executeResolution = executeResolution; + } + + /** + * Does nothing. + * + * @return EVAL_PAGE in all cases. + */ + @Override + public int doEndTag() { + return EVAL_PAGE; + } + + /** + * Sets the binding attribute by figuring out what ActionBean class is identified and then in turn + * finding out the appropriate URL for the ActionBean. + * + * @param beanclass the FQN of an ActionBean class, or a Class object for one. + */ + public void setBeanclass(Object beanclass) throws StripesJspException { + String url = getActionBeanUrl(beanclass); + if (url == null) { + throw new StripesJspException( + "The 'beanclass' attribute provided could not be " + + "used to identify a valid and configured ActionBean. The value supplied was: " + + beanclass); + } else { + this.binding = url; } + } + + /** Get the UrlBinding of the requested ActionBean */ + public String getBinding() { + return binding; + } + + /** Set the UrlBinding of the requested ActionBean */ + public void setBinding(String binding) { + this.binding = binding; + } + + /** The event name, if any to execute. */ + public String getEvent() { + return event; + } + + /** The event name, if any to execute. */ + public void setEvent(String event) { + this.event = event; + } + + /** Gets the name of the page scope variable to which the ActionBean will be bound. */ + public String getVar() { + return var; + } + + /** Sets the name of the page scope variable to which the ActionBean will be bound. */ + public void setVar(String var) { + this.var = var; + } + + /** Alias for getVar() so that the JSTL and jsp:useBean style are allowed. */ + public String getId() { + return getVar(); + } + + /** Alias for setVar() so that the JSTL and jsp:useBean style are allowed. */ + public void setId(String id) { + setVar(id); + } + + public boolean isValidate() { + return validate; + } + + public void setValidate(boolean validate) { + this.validate = validate; + } + + public boolean isAlwaysExecuteEvent() { + return alwaysExecuteEvent; + } + + public void setAlwaysExecuteEvent(boolean alwaysExecuteEvent) { + this.alwaysExecuteEvent = alwaysExecuteEvent; + } + + public boolean isExecuteResolution() { + return executeResolution; + } + + public void setExecuteResolution(boolean executeResolution) { + this.executeResolution = executeResolution; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/UseActionBeanTagExtraInfo.java b/stripes/src/main/java/net/sourceforge/stripes/tag/UseActionBeanTagExtraInfo.java index aca4b2980..7d399e419 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/UseActionBeanTagExtraInfo.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/UseActionBeanTagExtraInfo.java @@ -14,73 +14,79 @@ */ package net.sourceforge.stripes.tag; -import javax.servlet.jsp.tagext.TagExtraInfo; -import javax.servlet.jsp.tagext.VariableInfo; -import javax.servlet.jsp.tagext.TagData; -import javax.servlet.jsp.tagext.ValidationMessage; -import java.util.Collection; +import jakarta.servlet.jsp.tagext.TagData; +import jakarta.servlet.jsp.tagext.TagExtraInfo; +import jakarta.servlet.jsp.tagext.ValidationMessage; +import jakarta.servlet.jsp.tagext.VariableInfo; import java.util.ArrayList; +import java.util.Collection; /** - * Validates that the mutually exclusive attribute pairs of the tag are provided correctly - * and attempts to provide type information to the container for the bean assigned - * to the variable named by the {@code var} or {@code id} attribute. The latter can only be done - * when the {@code beanclass} attribute is used instead of the {@code binding} attribute - * because runtime information is needed to translate {@code binding} into a class name. + * Validates that the mutually exclusive attribute pairs of the tag are provided correctly and + * attempts to provide type information to the container for the bean assigned to the variable named + * by the {@code var} or {@code id} attribute. The latter can only be done when the {@code + * beanclass} attribute is used instead of the {@code binding} attribute because runtime information + * is needed to translate {@code binding} into a class name. * * @author tfenne * @since Stripes 1.5 */ public class UseActionBeanTagExtraInfo extends TagExtraInfo { - private static final VariableInfo[] NO_INFO = new VariableInfo[0]; + private static final VariableInfo[] NO_INFO = new VariableInfo[0]; - /** - * Attempts to return type information so that the container can create a - * named variable for the action bean. - */ - @Override public VariableInfo[] getVariableInfo(final TagData tag) { - // We can only provide the type of 'var' if beanclass was used because - // if binding was used we need runtime information! - Object beanclass = tag.getAttribute("beanclass"); + /** + * Attempts to return type information so that the container can create a named variable for the + * action bean. + */ + @Override + public VariableInfo[] getVariableInfo(final TagData tag) { + // We can only provide the type of 'var' if beanclass was used because + // if binding was used we need runtime information! + Object beanclass = tag.getAttribute("beanclass"); - // Turns out beanclass="${...}" does NOT return TagData.REQUEST_TIME_VALUE; only beanclass="<%= ... %>". - if (beanclass != null && !beanclass.equals(TagData.REQUEST_TIME_VALUE)) { - String var = tag.getAttributeString("var"); - if (var == null) var = tag.getAttributeString("id"); + // Turns out beanclass="${...}" does NOT return TagData.REQUEST_TIME_VALUE; only beanclass="<%= + // ... %>". + if (beanclass != null && !beanclass.equals(TagData.REQUEST_TIME_VALUE)) { + String var = tag.getAttributeString("var"); + if (var == null) var = tag.getAttributeString("id"); - // Make sure we have the class name, not the class - if (beanclass instanceof Class) beanclass = ((Class) beanclass).getName(); + // Make sure we have the class name, not the class + if (beanclass instanceof Class) beanclass = ((Class) beanclass).getName(); - // Return the variable info - if (beanclass instanceof String) { - String string = (String) beanclass; - if (!string.startsWith("${")) { - return new VariableInfo[] { new VariableInfo(var, string, true, VariableInfo.AT_BEGIN) }; - } - } + // Return the variable info + if (beanclass instanceof String) { + String string = (String) beanclass; + if (!string.startsWith("${")) { + return new VariableInfo[] {new VariableInfo(var, string, true, VariableInfo.AT_BEGIN)}; } - return NO_INFO; + } } + return NO_INFO; + } - /** - * Checks to ensure that where the tag supports providing one of two attributes - * that one and only one is provided. - */ - @Override public ValidationMessage[] validate(final TagData tag) { - Collection errors = new ArrayList(); - - Object beanclass = tag.getAttribute("beanclass"); - Object binding = tag.getAttribute("binding"); - if (!(beanclass != null ^ binding != null)) { - errors.add(new ValidationMessage(tag.getId(), "Exactly one of 'beanclass' or 'binding' must be supplied.")); - } + /** + * Checks to ensure that where the tag supports providing one of two attributes that one and only + * one is provided. + */ + @Override + public ValidationMessage[] validate(final TagData tag) { + Collection errors = new ArrayList(); - String var = tag.getAttributeString("var"); - String id = tag.getAttributeString("id"); - if (!(var != null ^ id != null)) { - errors.add(new ValidationMessage(tag.getId(), "Exactly one of 'var' or 'id' must be supplied.")); - } + Object beanclass = tag.getAttribute("beanclass"); + Object binding = tag.getAttribute("binding"); + if (!(beanclass != null ^ binding != null)) { + errors.add( + new ValidationMessage( + tag.getId(), "Exactly one of 'beanclass' or 'binding' must be supplied.")); + } - return errors.toArray(new ValidationMessage[errors.size()]); + String var = tag.getAttributeString("var"); + String id = tag.getAttributeString("id"); + if (!(var != null ^ id != null)) { + errors.add( + new ValidationMessage(tag.getId(), "Exactly one of 'var' or 'id' must be supplied.")); } + + return errors.toArray(new ValidationMessage[errors.size()]); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/VarTagSupport.java b/stripes/src/main/java/net/sourceforge/stripes/tag/VarTagSupport.java index 10067d8f2..d55a562b7 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/VarTagSupport.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/VarTagSupport.java @@ -14,59 +14,54 @@ */ package net.sourceforge.stripes.tag; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; /** - * Provides support for tags that allow assigning a value in a named scope. The - * "var" and "scope" properties are provided, as is an {@link #export(Object)} - * method that assigns a value to the given name in the given scope. - * + * Provides support for tags that allow assigning a value in a named scope. The "var" and "scope" + * properties are provided, as is an {@link #export(Object)} method that assigns a value to the + * given name in the given scope. + * * @author Ben Gunter */ public abstract class VarTagSupport extends StripesTagSupport { - protected String var; - protected String scope; + protected String var; + protected String scope; - /** Get the scope in which the value will be stored */ - public String getScope() { - return scope; - } + /** Get the scope in which the value will be stored */ + public String getScope() { + return scope; + } - /** Set the scope in which the value will be stored */ - public void setScope(String scope) { - this.scope = scope; - } + /** Set the scope in which the value will be stored */ + public void setScope(String scope) { + this.scope = scope; + } - /** Get the name of the variable to which the value will be assigned */ - public String getVar() { - return var; - } + /** Get the name of the variable to which the value will be assigned */ + public String getVar() { + return var; + } - /** Set the name of the variable to which the value will be assigned */ - public void setVar(String var) { - this.var = var; - } + /** Set the name of the variable to which the value will be assigned */ + public void setVar(String var) { + this.var = var; + } - /** - * Assigns the value to an attribute named by - * var in the named scope. - * - * @param value - * the object to be exported - */ - protected void export(Object value) { - if ("request".equals(scope)) { - pageContext.getRequest().setAttribute(var, value); - } - else if ("session".equals(scope)) { - ((HttpServletRequest) pageContext.getRequest()).getSession() - .setAttribute(var, value); - } - else if ("application".equals(scope)) { - pageContext.getServletContext().setAttribute(var, value); - } - else { - pageContext.setAttribute(var, value); - } + /** + * Assigns the value to an attribute named by var in the named + * scope. + * + * @param value the object to be exported + */ + protected void export(Object value) { + if ("request".equals(scope)) { + pageContext.getRequest().setAttribute(var, value); + } else if ("session".equals(scope)) { + ((HttpServletRequest) pageContext.getRequest()).getSession().setAttribute(var, value); + } else if ("application".equals(scope)) { + pageContext.getServletContext().setAttribute(var, value); + } else { + pageContext.setAttribute(var, value); } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/WizardFieldsTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/WizardFieldsTag.java index d6debe878..fae046287 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/WizardFieldsTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/WizardFieldsTag.java @@ -16,13 +16,11 @@ import static net.sourceforge.stripes.controller.StripesConstants.URL_KEY_FIELDS_PRESENT; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.TryCatchFinally; import java.util.HashSet; import java.util.Set; - -import javax.servlet.ServletRequest; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.TryCatchFinally; - import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.controller.ActionResolver; import net.sourceforge.stripes.controller.StripesConstants; @@ -33,176 +31,176 @@ import net.sourceforge.stripes.util.HtmlUtil; /** - *

    Examines the request and include hidden fields for all parameters that have do - * not have form fields in the current form. Will include multiple values for - * parameters that have them. Excludes 'special' parameters like the source - * page parameter, and the parameter that conveyed the event name.

    + * Examines the request and include hidden fields for all parameters that have do not have form + * fields in the current form. Will include multiple values for parameters that have them. Excludes + * 'special' parameters like the source page parameter, and the parameter that conveyed the event + * name. * - *

    Very useful for implementing basic wizard flow without relying on session - * scoping of ActionBeans, and without having to name all the parameters that - * should be carried forward in the form.

    + *

    Very useful for implementing basic wizard flow without relying on session scoping of + * ActionBeans, and without having to name all the parameters that should be carried forward in the + * form. * * @author Tim Fennell */ public class WizardFieldsTag extends StripesTagSupport implements TryCatchFinally { - private boolean currentFormOnly = false; - - /** - * Sets whether or not the parameters should be output only if the form matches the current - * request. Defaults to false. - */ - public void setCurrentFormOnly(boolean currentFormOnly) { this.currentFormOnly = currentFormOnly; } - - /** Gets whether the tag will output fields for the current form only, or in all cases. */ - public boolean isCurrentFormOnly() { return currentFormOnly; } - - /** Skips over the body because there shouldn't be one. */ - @Override - public int doStartTag() throws JspException { - getTagStack().push(this); - return SKIP_BODY; + private boolean currentFormOnly = false; + + /** + * Sets whether or not the parameters should be output only if the form matches the current + * request. Defaults to false. + */ + public void setCurrentFormOnly(boolean currentFormOnly) { + this.currentFormOnly = currentFormOnly; + } + + /** Gets whether the tag will output fields for the current form only, or in all cases. */ + public boolean isCurrentFormOnly() { + return currentFormOnly; + } + + /** Skips over the body because there shouldn't be one. */ + @Override + public int doStartTag() throws JspException { + getTagStack().push(this); + return SKIP_BODY; + } + + /** + * Performs the main work of the tag, as described in the class level javadoc. + * + * @return EVAL_PAGE in all cases. + */ + @Override + public int doEndTag() throws JspException { + // Get the current form. + FormTag form = getParentTag(FormTag.class); + + // Get the action bean on this form + ActionBean actionBean = form.getActionBean(); + + // If current form only is not specified, go ahead, otherwise check that + // the current form had an ActionBean attached - which indicates that the + // last submit was to the same form/action as this form + if (!isCurrentFormOnly() || actionBean != null) { + writeWizardFields(form); } - /** - * Performs the main work of the tag, as described in the class level javadoc. - * @return EVAL_PAGE in all cases. - */ - @Override - public int doEndTag() throws JspException { - // Get the current form. - FormTag form = getParentTag(FormTag.class); - - // Get the action bean on this form - ActionBean actionBean = form.getActionBean(); - - // If current form only is not specified, go ahead, otherwise check that - // the current form had an ActionBean attached - which indicates that the - // last submit was to the same form/action as this form - if (!isCurrentFormOnly() || actionBean != null) { - writeWizardFields(form); - } - - return EVAL_PAGE; + return EVAL_PAGE; + } + + /** Rethrows the passed in throwable in all cases. */ + public void doCatch(Throwable throwable) throws Throwable { + throw throwable; + } + + /** + * Used to ensure that the input tag is always removed from the tag stack so that there is never + * any confusion about tag-parent hierarchies. + */ + public void doFinally() { + try { + getTagStack().pop(); + } catch (Throwable t) { + /* Suppress anything, because otherwise this might mask any causal exception. */ } - - /** Rethrows the passed in throwable in all cases. */ - public void doCatch(Throwable throwable) throws Throwable { throw throwable; } - - /** - * Used to ensure that the input tag is always removed from the tag stack so that there is - * never any confusion about tag-parent hierarchies. - */ - public void doFinally() { - try { getTagStack().pop(); } - catch (Throwable t) { - /* Suppress anything, because otherwise this might mask any causal exception. */ - } - } - - /** - * Write out a hidden field which contains parameters that should be sent along with the actual - * form fields. - */ - protected void writeWizardFields(FormTag form) throws JspException, StripesJspException { - // Set up a hidden tag to do the writing for us - InputHiddenTag hidden = new InputHiddenTag(); - hidden.setPageContext(getPageContext()); - hidden.setParent(getParent()); - - // Get the list of all parameters. - Set paramNames = getParamNames(); - // Figure out the list of parameters we should not include - Set excludes = getExcludes(form); - - // Loop through the request parameters and output the values - Class actionBeanType = form.getActionBeanClass(); - for (String name : paramNames) { - if (!excludes.contains(name) && !isEventName(actionBeanType, name)) { - hidden.setName(name); - try { - hidden.doStartTag(); - hidden.doAfterBody(); - hidden.doEndTag(); - } - catch (Throwable t) { - /** Catch whatever comes back out of the doCatch() method and deal with it */ - try { - hidden.doCatch(t); - } - catch (Throwable t2) { - if (t2 instanceof JspException) - throw (JspException) t2; - if (t2 instanceof RuntimeException) - throw (RuntimeException) t2; - else - throw new StripesJspException(t2); - } - } - finally { - hidden.doFinally(); - } - } + } + + /** + * Write out a hidden field which contains parameters that should be sent along with the actual + * form fields. + */ + protected void writeWizardFields(FormTag form) throws JspException, StripesJspException { + // Set up a hidden tag to do the writing for us + InputHiddenTag hidden = new InputHiddenTag(); + hidden.setPageContext(getPageContext()); + hidden.setParent(getParent()); + + // Get the list of all parameters. + Set paramNames = getParamNames(); + // Figure out the list of parameters we should not include + Set excludes = getExcludes(form); + + // Loop through the request parameters and output the values + Class actionBeanType = form.getActionBeanClass(); + for (String name : paramNames) { + if (!excludes.contains(name) && !isEventName(actionBeanType, name)) { + hidden.setName(name); + try { + hidden.doStartTag(); + hidden.doAfterBody(); + hidden.doEndTag(); + } catch (Throwable t) { + /** Catch whatever comes back out of the doCatch() method and deal with it */ + try { + hidden.doCatch(t); + } catch (Throwable t2) { + if (t2 instanceof JspException) throw (JspException) t2; + if (t2 instanceof RuntimeException) throw (RuntimeException) t2; + else throw new StripesJspException(t2); + } + } finally { + hidden.doFinally(); } + } } - - /** Returns all the submitted parameters in the current or the former requests. */ - @SuppressWarnings("unchecked") - protected Set getParamNames() { - // Combine actual parameter names with input names from the form, which might not be - // represented by a real request parameter - Set paramNames = new HashSet(); - ServletRequest request = getPageContext().getRequest(); - paramNames.addAll(request.getParameterMap().keySet()); - String fieldsPresent = request.getParameter(URL_KEY_FIELDS_PRESENT); - if (fieldsPresent != null) { - paramNames.addAll(HtmlUtil.splitValues(CryptoUtil.decrypt(fieldsPresent))); - } - return paramNames; + } + + /** Returns all the submitted parameters in the current or the former requests. */ + @SuppressWarnings("unchecked") + protected Set getParamNames() { + // Combine actual parameter names with input names from the form, which might not be + // represented by a real request parameter + Set paramNames = new HashSet(); + ServletRequest request = getPageContext().getRequest(); + paramNames.addAll(request.getParameterMap().keySet()); + String fieldsPresent = request.getParameter(URL_KEY_FIELDS_PRESENT); + if (fieldsPresent != null) { + paramNames.addAll(HtmlUtil.splitValues(CryptoUtil.decrypt(fieldsPresent))); } - - /** Returns the list of parameters that should be excluded from the hidden tag. */ - protected Set getExcludes(FormTag form) { - Set excludes = new HashSet(); - excludes.addAll(form.getRegisteredFields()); - excludes.add(StripesConstants.URL_KEY_SOURCE_PAGE); - excludes.add(StripesConstants.URL_KEY_FIELDS_PRESENT); - excludes.add(StripesConstants.URL_KEY_EVENT_NAME); - excludes.add(StripesConstants.URL_KEY_FLASH_SCOPE_ID); - - // Use the submitted action bean to eliminate any event related parameters - ServletRequest request = getPageContext().getRequest(); - ActionBean submittedActionBean = (ActionBean) request - .getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN); - - if (submittedActionBean != null) { - String eventName = submittedActionBean.getContext().getEventName(); - if (eventName != null) { - excludes.add(eventName); - excludes.add(eventName + ".x"); - excludes.add(eventName + ".y"); - } - } - return excludes; + return paramNames; + } + + /** Returns the list of parameters that should be excluded from the hidden tag. */ + protected Set getExcludes(FormTag form) { + Set excludes = new HashSet(); + excludes.addAll(form.getRegisteredFields()); + excludes.add(StripesConstants.URL_KEY_SOURCE_PAGE); + excludes.add(StripesConstants.URL_KEY_FIELDS_PRESENT); + excludes.add(StripesConstants.URL_KEY_EVENT_NAME); + excludes.add(StripesConstants.URL_KEY_FLASH_SCOPE_ID); + + // Use the submitted action bean to eliminate any event related parameters + ServletRequest request = getPageContext().getRequest(); + ActionBean submittedActionBean = + (ActionBean) request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN); + + if (submittedActionBean != null) { + String eventName = submittedActionBean.getContext().getEventName(); + if (eventName != null) { + excludes.add(eventName); + excludes.add(eventName + ".x"); + excludes.add(eventName + ".y"); + } } - - /** - * Returns true if {@code name} is the name of an event handled by {@link ActionBean}s of type - * {@code beanType}. - * - * @param beanType An {@link ActionBean} class - * @param name The name to look up - */ - protected boolean isEventName(Class beanType, String name) { - if (beanType == null || name == null) - return false; - - try { - ActionResolver actionResolver = StripesFilter.getConfiguration().getActionResolver(); - return actionResolver.getHandler(beanType, name) != null; - } - catch (StripesServletException e) { - // Ignore the exception and assume the name is not an event - return false; - } + return excludes; + } + + /** + * Returns true if {@code name} is the name of an event handled by {@link ActionBean}s of type + * {@code beanType}. + * + * @param beanType An {@link ActionBean} class + * @param name The name to look up + */ + protected boolean isEventName(Class beanType, String name) { + if (beanType == null || name == null) return false; + + try { + ActionResolver actionResolver = StripesFilter.getConfiguration().getActionResolver(); + return actionResolver.getHandler(beanType, name) != null; + } catch (StripesServletException e) { + // Ignore the exception and assume the name is not an event + return false; } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutComponentRenderer.java b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutComponentRenderer.java index b7a55ba4f..0dcdfdaec 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutComponentRenderer.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutComponentRenderer.java @@ -14,185 +14,217 @@ */ package net.sourceforge.stripes.tag.layout; +import jakarta.servlet.ServletException; +import jakarta.servlet.jsp.PageContext; import java.io.IOException; import java.util.LinkedList; - -import javax.servlet.ServletException; -import javax.servlet.jsp.PageContext; - import net.sourceforge.stripes.controller.StripesConstants; import net.sourceforge.stripes.util.Log; /** - *

    * An object that can be stuffed into a scope (page, request, application, etc.) and render a layout * component to a string. This allows for use of EL expressions to output a component (as described * in the book Stripes ... and web development is fun again) without requiring that all * components be evaluated and buffered just in case a string representation is needed. The * evaluation happens only when necessary, saving cycles and memory. - *

    - *

    - * When {@link #toString()} is called, the component renderer will evaluate the body of any + * + *

    When {@link #toString()} is called, the component renderer will evaluate the body of any * {@link LayoutComponentTag} found in the stack of {@link LayoutContext}s maintained in the JSP * {@link PageContext} having the same name as that passed to the constructor. The page context must * be provided with a call to {@link #pushPageContext(PageContext)} for the renderer to work * correctly. - *

    - * + * * @author Ben Gunter * @since Stripes 1.5.4 */ public class LayoutComponentRenderer { - private static final Log log = Log.getInstance(LayoutComponentRenderer.class); - - private LinkedList pageContext; - private String component; - private LayoutContext context; - - /** - * Create a new instance to render the named component to a string. - * - * @param component The name of the component to render. - */ - public LayoutComponentRenderer(String component) { - this.component = component; + private static final Log log = Log.getInstance(LayoutComponentRenderer.class); + + private LinkedList pageContext; + private String component; + private LayoutContext context; + + /** + * Create a new instance to render the named component to a string. + * + * @param component The name of the component to render. + */ + public LayoutComponentRenderer(String component) { + this.component = component; + } + + /** + * Push a new page context onto the page context stack. The last page context pushed onto the + * stack is the one that will be used to evaluate the component tag's body. + */ + public void pushPageContext(PageContext pageContext) { + if (this.pageContext == null) { + this.pageContext = new LinkedList(); } - /** - * Push a new page context onto the page context stack. The last page context pushed onto the - * stack is the one that will be used to evaluate the component tag's body. - */ - public void pushPageContext(PageContext pageContext) { - if (this.pageContext == null) { - this.pageContext = new LinkedList(); - } - - this.pageContext.addFirst(pageContext); - } - - /** Pop the last page context off the stack and return it. */ - public PageContext popPageContext() { - return pageContext == null ? null : pageContext.poll(); + this.pageContext.addFirst(pageContext); + } + + /** Pop the last page context off the stack and return it. */ + public PageContext popPageContext() { + return pageContext == null ? null : pageContext.poll(); + } + + /** Get the last page context that was pushed onto the stack. */ + public PageContext getPageContext() { + return pageContext == null ? null : pageContext.peek(); + } + + /** Get the path to the currently executing JSP. */ + public String getCurrentPage() { + return (String) + getPageContext().getRequest().getAttribute(StripesConstants.REQ_ATTR_INCLUDE_PATH); + } + + /** + * Write the component to the page context's writer, optionally buffering the output. + * + * @return True if the named component was found and it indicated that it successfully rendered; + * otherwise, false. + * @throws IOException If thrown by {@link LayoutContext#doInclude(PageContext, String)} + * @throws ServletException If thrown by {@link LayoutContext#doInclude(PageContext, String)} + */ + public boolean write() throws ServletException, IOException { + final PageContext pageContext = getPageContext(); + if (pageContext == null) { + log.error("Failed to render component \"", this.component, "\" without a page context!"); + return false; } - /** Get the last page context that was pushed onto the stack. */ - public PageContext getPageContext() { - return pageContext == null ? null : pageContext.peek(); + // Grab some values from the current context so they can be restored when we're done + final LayoutContext savedContext = this.context; + final LayoutContext currentContext = LayoutContext.lookup(pageContext); + log.debug("Render component \"", this.component, "\" in ", getCurrentPage()); + + // Descend the stack from here, trying each context where the component is registered + for (LayoutContext context = savedContext == null ? currentContext : savedContext.getPrevious(); + context != null; + context = context.getPrevious()) { + + // Skip contexts where the desired component is not registered. + if (!context.getComponents().containsKey(this.component)) { + log.trace( + "Not rendering \"", + this.component, + "\" in context ", + context.getRenderPage(), + " -> ", + context.getDefinitionPage()); + continue; + } + this.context = context; + + // Take a snapshot of the context state + final LayoutContext savedNext = context.getNext(); + final String savedComponent = context.getComponent(); + final boolean savedComponentRenderPhase = context.isComponentRenderPhase(); + final boolean savedSilent = context.getOut().isSilent(); + + try { + // Set up the context to render the component + context.setNext(null); + context.setComponentRenderPhase(true); + context.setComponent(this.component); + context.getOut().setSilent(true, pageContext); + + log.debug( + "Start execute \"", + this.component, + "\" in ", + currentContext.getRenderPage(), + " -> ", + currentContext.getDefinitionPage(), + " from ", + context.getRenderPage(), + " -> ", + context.getDefinitionPage()); + context.doInclude(pageContext, context.getRenderPage()); + log.debug( + "End execute \"", + this.component, + "\" in ", + currentContext.getRenderPage(), + " -> ", + currentContext.getDefinitionPage(), + " from ", + context.getRenderPage(), + " -> ", + context.getDefinitionPage()); + + // If the component name has been cleared then the component rendered + if (context.getComponent() == null) return true; + } finally { + // Restore the context state + context.setNext(savedNext); + context.setComponent(savedComponent); + context.setComponentRenderPhase(savedComponentRenderPhase); + context.getOut().setSilent(savedSilent, pageContext); + + // Restore the saved context + this.context = savedContext; + } } - /** Get the path to the currently executing JSP. */ - public String getCurrentPage() { - return (String) getPageContext().getRequest().getAttribute( - StripesConstants.REQ_ATTR_INCLUDE_PATH); + log.debug( + "Component \"", + this.component, + "\" evaluated to empty string in context ", + currentContext.getRenderPage(), + " -> ", + currentContext.getDefinitionPage()); + return false; + } + + /** + * Open a buffer in {@link LayoutWriter}, call {@link #write()} to render the component and then + * return the buffer contents. + */ + @Override + public String toString() { + final PageContext pageContext = getPageContext(); + if (pageContext == null) { + log.error("Failed to render component \"", this.component, "\" without a page context!"); + return "[Failed to render component \"" + this.component + "\" without a page context!]"; } - /** - * Write the component to the page context's writer, optionally buffering the output. - * - * @return True if the named component was found and it indicated that it successfully rendered; - * otherwise, false. - * @throws IOException If thrown by {@link LayoutContext#doInclude(PageContext, String)} - * @throws ServletException If thrown by {@link LayoutContext#doInclude(PageContext, String)} - */ - public boolean write() throws ServletException, IOException { - final PageContext pageContext = getPageContext(); - if (pageContext == null) { - log.error("Failed to render component \"", this.component, "\" without a page context!"); - return false; - } - - // Grab some values from the current context so they can be restored when we're done - final LayoutContext savedContext = this.context; - final LayoutContext currentContext = LayoutContext.lookup(pageContext); - log.debug("Render component \"", this.component, "\" in ", getCurrentPage()); - - // Descend the stack from here, trying each context where the component is registered - for (LayoutContext context = savedContext == null ? currentContext : savedContext - .getPrevious(); context != null; context = context.getPrevious()) { - - // Skip contexts where the desired component is not registered. - if (!context.getComponents().containsKey(this.component)) { - log.trace("Not rendering \"", this.component, "\" in context ", - context.getRenderPage(), " -> ", context.getDefinitionPage()); - continue; - } - this.context = context; - - // Take a snapshot of the context state - final LayoutContext savedNext = context.getNext(); - final String savedComponent = context.getComponent(); - final boolean savedComponentRenderPhase = context.isComponentRenderPhase(); - final boolean savedSilent = context.getOut().isSilent(); - - try { - // Set up the context to render the component - context.setNext(null); - context.setComponentRenderPhase(true); - context.setComponent(this.component); - context.getOut().setSilent(true, pageContext); - - log.debug("Start execute \"", this.component, "\" in ", - currentContext.getRenderPage(), " -> ", currentContext.getDefinitionPage(), - " from ", context.getRenderPage(), " -> ", context.getDefinitionPage()); - context.doInclude(pageContext, context.getRenderPage()); - log.debug("End execute \"", this.component, "\" in ", - currentContext.getRenderPage(), " -> ", currentContext.getDefinitionPage(), - " from ", context.getRenderPage(), " -> ", context.getDefinitionPage()); - - // If the component name has been cleared then the component rendered - if (context.getComponent() == null) - return true; - } - finally { - // Restore the context state - context.setNext(savedNext); - context.setComponent(savedComponent); - context.setComponentRenderPhase(savedComponentRenderPhase); - context.getOut().setSilent(savedSilent, pageContext); - - // Restore the saved context - this.context = savedContext; - } - } - - log.debug("Component \"", this.component, "\" evaluated to empty string in context ", - currentContext.getRenderPage(), " -> ", currentContext.getDefinitionPage()); - return false; + final LayoutContext context = LayoutContext.lookup(pageContext); + String contents; + context.getOut().openBuffer(pageContext); + try { + log.debug( + "Start stringify \"", + this.component, + "\" in ", + context.getRenderPage(), + " -> ", + context.getDefinitionPage()); + write(); + } catch (Exception e) { + log.error( + e, + "Unhandled exception trying to render component \"", + this.component, + "\" to a string in context ", + context.getRenderPage(), // + " -> ", + context.getDefinitionPage()); + return "[Failed to render \"" + this.component + "\". See log for details.]"; + } finally { + log.debug( + "End stringify \"", + this.component, + "\" in ", + context.getRenderPage(), + " -> ", + context.getDefinitionPage()); + contents = context.getOut().closeBuffer(pageContext); } - /** - * Open a buffer in {@link LayoutWriter}, call {@link #write()} to render the component and then - * return the buffer contents. - */ - @Override - public String toString() { - final PageContext pageContext = getPageContext(); - if (pageContext == null) { - log.error("Failed to render component \"", this.component, "\" without a page context!"); - return "[Failed to render component \"" + this.component - + "\" without a page context!]"; - } - - final LayoutContext context = LayoutContext.lookup(pageContext); - String contents; - context.getOut().openBuffer(pageContext); - try { - log.debug("Start stringify \"", this.component, "\" in ", context.getRenderPage(), - " -> ", context.getDefinitionPage()); - write(); - } - catch (Exception e) { - log.error(e, "Unhandled exception trying to render component \"", this.component, - "\" to a string in context ", context.getRenderPage(), // - " -> ", context.getDefinitionPage()); - return "[Failed to render \"" + this.component + "\". See log for details.]"; - } - finally { - log.debug("End stringify \"", this.component, "\" in ", context.getRenderPage(), - " -> ", context.getDefinitionPage()); - contents = context.getOut().closeBuffer(pageContext); - } - - return contents; - } + return contents; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutComponentTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutComponentTag.java index 950dd1565..3eb97fbc8 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutComponentTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutComponentTag.java @@ -14,242 +14,244 @@ */ package net.sourceforge.stripes.tag.layout; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.PageContext; import java.io.IOException; import java.util.regex.Pattern; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.PageContext; - import net.sourceforge.stripes.exception.StripesJspException; import net.sourceforge.stripes.exception.StripesRuntimeException; import net.sourceforge.stripes.util.Log; /** - * Defines a component in a layout. Used both to define the components in a layout definition - * and to provide overridden component definitions during a layout rendering request. + * Defines a component in a layout. Used both to define the components in a layout definition and to + * provide overridden component definitions during a layout rendering request. * * @author Tim Fennell, Ben Gunter * @since Stripes 1.1 */ public class LayoutComponentTag extends LayoutTag { - private static final Log log = Log.getInstance(LayoutComponentTag.class); + private static final Log log = Log.getInstance(LayoutComponentTag.class); - /** Regular expression that matches valid Java identifiers. */ - private static final Pattern javaIdentifierPattern = Pattern - .compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"); + /** Regular expression that matches valid Java identifiers. */ + private static final Pattern javaIdentifierPattern = + Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"); - private String name; - private LayoutContext context; - private boolean silent; - private Boolean componentRenderPhase; + private String name; + private LayoutContext context; + private boolean silent; + private Boolean componentRenderPhase; - /** Gets the name of the component. */ - public String getName() { return name; } + /** Gets the name of the component. */ + public String getName() { + return name; + } - /** Sets the name of the component. */ - public void setName(String name) { this.name = name; } + /** Sets the name of the component. */ + public void setName(String name) { + this.name = name; + } - @Override - public void setPageContext(PageContext pageContext) { - // Call super method - super.setPageContext(pageContext); + @Override + public void setPageContext(PageContext pageContext) { + // Call super method + super.setPageContext(pageContext); - // Initialize the layout context and related fields - context = LayoutContext.lookup(pageContext); + // Initialize the layout context and related fields + context = LayoutContext.lookup(pageContext); - if (context == null) { - throw new StripesRuntimeException("A component tag named \"" + getName() + "\" in " - + getCurrentPagePath() + " was unable to find a layout context."); - } + if (context == null) { + throw new StripesRuntimeException( + "A component tag named \"" + + getName() + + "\" in " + + getCurrentPagePath() + + " was unable to find a layout context."); + } - log.trace("Component ", getName() + " has context ", context.getRenderPage(), " -> ", - context.getDefinitionPage()); + log.trace( + "Component ", + getName() + " has context ", + context.getRenderPage(), + " -> ", + context.getDefinitionPage()); - silent = context.getOut().isSilent(); - } + silent = context.getOut().isSilent(); + } - /** - * True if this tag is the component to be rendered on this pass from - * {@link LayoutDefinitionTag}. - * - * @throws StripesJspException If a {@link LayoutContext} is not found. - */ - public boolean isCurrentComponent() throws StripesJspException { - String name = context.getComponent(); - if (name == null || !name.equals(getName())) - return false; - - final LayoutTag parent = getLayoutParent(); - if (!(parent instanceof LayoutRenderTag)) - return context.getComponentPath().getComponentPath() == null; - - final LayoutRenderTagPath got = ((LayoutRenderTag) parent).getPath(); - return got != null && got.equals(context.getComponentPath()); - } + /** + * True if this tag is the component to be rendered on this pass from {@link LayoutDefinitionTag}. + * + * @throws StripesJspException If a {@link LayoutContext} is not found. + */ + public boolean isCurrentComponent() throws StripesJspException { + String name = context.getComponent(); + if (name == null || !name.equals(getName())) return false; - /** - *

    - * If this tag is nested within a {@link LayoutDefinitionTag}, then evaluate the corresponding - * {@link LayoutComponentTag} nested within the {@link LayoutRenderTag} that invoked the parent - * {@link LayoutDefinitionTag}. If, after evaluating the corresponding tag, the component has - * not been rendered then evaluate this tag's body by returning {@code EVAL_BODY_INCLUDE}. - *

    - *

    - * If this tag is nested within a {@link LayoutRenderTag} and this tag is the current component, - * as indicated by {@link LayoutContext#getComponent()}, then evaluate this tag's body by - * returning {@code EVAL_BODY_INCLUDE}. - *

    - *

    - * In all other cases, skip this tag's body by returning SKIP_BODY. - *

    - * - * @return {@code EVAL_BODY_INCLUDE} or {@code SKIP_BODY}, as described above. - */ - @Override - public int doStartTag() throws JspException { - try { - if (context.isComponentRenderPhase()) { - if (isChildOfRender()) { - if (isCurrentComponent()) { - log.debug("Render ", getName(), " in ", context.getRenderPage()); - context.getOut().setSilent(false, pageContext); - return EVAL_BODY_INCLUDE; - } - else if (context.getComponentPath().isPathComponent(this)) { - log.debug("Silently execute '", getName(), "' in ", context.getRenderPage()); - context.getOut().setSilent(true, pageContext); - return EVAL_BODY_INCLUDE; - } - else { - log.debug("No-op for ", getName(), " in ", context.getRenderPage()); - } - } - else if (isChildOfDefinition()) { - log.debug("No-op for ", getName(), " in ", context.getDefinitionPage()); - } - else if (isChildOfComponent()) { - // Use a layout component renderer to do the heavy lifting - log.debug("Invoke component renderer for nested render of \"", getName(), "\""); - LayoutComponentRenderer renderer = (LayoutComponentRenderer) pageContext - .getAttribute(getName()); - if (renderer == null) - log.debug("No component renderer in page context for '" + getName() + "'"); - boolean rendered = renderer != null && renderer.write(); - - // If the component did not render then we need to output the default contents - // from the layout definition. - if (!rendered) { - log.debug("Component was not present in ", context.getRenderPage(), - " so using default content from ", context.getDefinitionPage()); - - context.getOut().setSilent(false, pageContext); - return EVAL_BODY_INCLUDE; - } - } - } - else { - if (isChildOfRender()) { - if (!javaIdentifierPattern.matcher(getName()).matches()) { - log.warn("The layout-component name '", getName(), - "' is not a valid Java identifier. While this may work, it can ", - "cause bugs that are difficult to track down. Please consider ", - "using valid Java identifiers for component names ", - "(no hyphens, no spaces, etc.)"); - } - - log.debug("Register component ", getName(), " with ", context.getRenderPage()); - - // Look for an existing renderer for a component with the same name - LayoutComponentRenderer renderer = null; - for (LayoutContext c = context; c != null && renderer == null; c = c.getPrevious()) { - renderer = c.getComponents().get(getName()); - } - - // If not found then create a new one - if (renderer == null) - renderer = new LayoutComponentRenderer(getName()); - - context.getComponents().put(getName(), renderer); - } - else if (isChildOfDefinition()) { - // Use a layout component renderer to do the heavy lifting - log.debug("Invoke component renderer for direct render of \"", getName(), "\""); - LayoutComponentRenderer renderer = (LayoutComponentRenderer) pageContext - .getAttribute(getName()); - if (renderer == null) - log.debug("No component renderer in page context for '" + getName() + "'"); - boolean rendered = renderer != null && renderer.write(); - - // If the component did not render then we need to output the default contents - // from the layout definition. - if (!rendered) { - log.debug("Component was not present in ", context.getRenderPage(), - " so using default content from ", context.getDefinitionPage()); - - componentRenderPhase = context.isComponentRenderPhase(); - context.setComponentRenderPhase(true); - context.setComponent(getName()); - context.getOut().setSilent(false, pageContext); - return EVAL_BODY_INCLUDE; - } - } - else if (isChildOfComponent()) { - /* - * This condition cannot be true since component tags do not execute except in - * component render phase, thus any component tags embedded with them will not - * execute either. I've left this block here just as a placeholder for this - * explanation. - */ - } - } + final LayoutTag parent = getLayoutParent(); + if (!(parent instanceof LayoutRenderTag)) + return context.getComponentPath().getComponentPath() == null; + final LayoutRenderTagPath got = ((LayoutRenderTag) parent).getPath(); + return got != null && got.equals(context.getComponentPath()); + } + + /** + * If this tag is nested within a {@link LayoutDefinitionTag}, then evaluate the corresponding + * {@link LayoutComponentTag} nested within the {@link LayoutRenderTag} that invoked the parent + * {@link LayoutDefinitionTag}. If, after evaluating the corresponding tag, the component has not + * been rendered then evaluate this tag's body by returning {@code EVAL_BODY_INCLUDE}. + * + *

    If this tag is nested within a {@link LayoutRenderTag} and this tag is the current + * component, as indicated by {@link LayoutContext#getComponent()}, then evaluate this tag's body + * by returning {@code EVAL_BODY_INCLUDE}. + * + *

    In all other cases, skip this tag's body by returning SKIP_BODY. + * + * @return {@code EVAL_BODY_INCLUDE} or {@code SKIP_BODY}, as described above. + */ + @Override + public int doStartTag() throws JspException { + try { + if (context.isComponentRenderPhase()) { + if (isChildOfRender()) { + if (isCurrentComponent()) { + log.debug("Render ", getName(), " in ", context.getRenderPage()); + context.getOut().setSilent(false, pageContext); + return EVAL_BODY_INCLUDE; + } else if (context.getComponentPath().isPathComponent(this)) { + log.debug("Silently execute '", getName(), "' in ", context.getRenderPage()); context.getOut().setSilent(true, pageContext); - return SKIP_BODY; + return EVAL_BODY_INCLUDE; + } else { + log.debug("No-op for ", getName(), " in ", context.getRenderPage()); + } + } else if (isChildOfDefinition()) { + log.debug("No-op for ", getName(), " in ", context.getDefinitionPage()); + } else if (isChildOfComponent()) { + // Use a layout component renderer to do the heavy lifting + log.debug("Invoke component renderer for nested render of \"", getName(), "\""); + LayoutComponentRenderer renderer = + (LayoutComponentRenderer) pageContext.getAttribute(getName()); + if (renderer == null) + log.debug("No component renderer in page context for '" + getName() + "'"); + boolean rendered = renderer != null && renderer.write(); + + // If the component did not render then we need to output the default contents + // from the layout definition. + if (!rendered) { + log.debug( + "Component was not present in ", + context.getRenderPage(), + " so using default content from ", + context.getDefinitionPage()); + + context.getOut().setSilent(false, pageContext); + return EVAL_BODY_INCLUDE; + } } - catch (Exception e) { - log.error(e, "Unhandled exception trying to render component \"", getName(), - "\" to a string in context ", context.getRenderPage(), " -> ", context - .getDefinitionPage()); - - if (e instanceof RuntimeException) - throw (RuntimeException) e; - else - throw new StripesJspException(e); + } else { + if (isChildOfRender()) { + if (!javaIdentifierPattern.matcher(getName()).matches()) { + log.warn( + "The layout-component name '", + getName(), + "' is not a valid Java identifier. While this may work, it can ", + "cause bugs that are difficult to track down. Please consider ", + "using valid Java identifiers for component names ", + "(no hyphens, no spaces, etc.)"); + } + + log.debug("Register component ", getName(), " with ", context.getRenderPage()); + + // Look for an existing renderer for a component with the same name + LayoutComponentRenderer renderer = null; + for (LayoutContext c = context; c != null && renderer == null; c = c.getPrevious()) { + renderer = c.getComponents().get(getName()); + } + + // If not found then create a new one + if (renderer == null) renderer = new LayoutComponentRenderer(getName()); + + context.getComponents().put(getName(), renderer); + } else if (isChildOfDefinition()) { + // Use a layout component renderer to do the heavy lifting + log.debug("Invoke component renderer for direct render of \"", getName(), "\""); + LayoutComponentRenderer renderer = + (LayoutComponentRenderer) pageContext.getAttribute(getName()); + if (renderer == null) + log.debug("No component renderer in page context for '" + getName() + "'"); + boolean rendered = renderer != null && renderer.write(); + + // If the component did not render then we need to output the default contents + // from the layout definition. + if (!rendered) { + log.debug( + "Component was not present in ", + context.getRenderPage(), + " so using default content from ", + context.getDefinitionPage()); + + componentRenderPhase = context.isComponentRenderPhase(); + context.setComponentRenderPhase(true); + context.setComponent(getName()); + context.getOut().setSilent(false, pageContext); + return EVAL_BODY_INCLUDE; + } + } else if (isChildOfComponent()) { + /* + * This condition cannot be true since component tags do not execute except in + * component render phase, thus any component tags embedded with them will not + * execute either. I've left this block here just as a placeholder for this + * explanation. + */ } + } + + context.getOut().setSilent(true, pageContext); + return SKIP_BODY; + } catch (Exception e) { + log.error( + e, + "Unhandled exception trying to render component \"", + getName(), + "\" to a string in context ", + context.getRenderPage(), + " -> ", + context.getDefinitionPage()); + + if (e instanceof RuntimeException) throw (RuntimeException) e; + else throw new StripesJspException(e); } + } - /** - * If this tag is the component that needs to be rendered, as indicated by - * {@link LayoutContext#getComponent()}, then set the current component name back to null to - * indicate that the component has rendered. - * - * @return SKIP_PAGE if this component is the current component, otherwise EVAL_PAGE. - */ - @Override - public int doEndTag() throws JspException { - try { - // Set current component name back to null as a signal to the component tag within the - // definition tag that the component did, indeed, render and it should not output the - // default contents. - if (isCurrentComponent()) - context.setComponent(null); - - // If the component render phase flag was changed, then restore it now - if (componentRenderPhase != null) - context.setComponentRenderPhase(componentRenderPhase); - - // Restore output's silent flag - context.getOut().setSilent(silent, pageContext); - - return EVAL_PAGE; - } - catch (IOException e) { - throw new JspException(e); - } - finally { - this.context = null; - this.silent = false; - this.componentRenderPhase = null; - } + /** + * If this tag is the component that needs to be rendered, as indicated by {@link + * LayoutContext#getComponent()}, then set the current component name back to null to indicate + * that the component has rendered. + * + * @return SKIP_PAGE if this component is the current component, otherwise EVAL_PAGE. + */ + @Override + public int doEndTag() throws JspException { + try { + // Set current component name back to null as a signal to the component tag within the + // definition tag that the component did, indeed, render and it should not output the + // default contents. + if (isCurrentComponent()) context.setComponent(null); + + // If the component render phase flag was changed, then restore it now + if (componentRenderPhase != null) context.setComponentRenderPhase(componentRenderPhase); + + // Restore output's silent flag + context.getOut().setSilent(silent, pageContext); + + return EVAL_PAGE; + } catch (IOException e) { + throw new JspException(e); + } finally { + this.context = null; + this.silent = false; + this.componentRenderPhase = null; } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutContext.java b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutContext.java index 6790da8dc..1ce41b28e 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutContext.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutContext.java @@ -14,6 +14,12 @@ */ package net.sourceforge.stripes.tag.layout; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; +import jakarta.servlet.jsp.PageContext; import java.io.IOException; import java.io.PrintWriter; import java.io.Writer; @@ -24,410 +30,438 @@ import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; - -import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; -import javax.servlet.jsp.PageContext; - import net.sourceforge.stripes.exception.StripesRuntimeException; import net.sourceforge.stripes.util.Log; /** - * Used to move contextual information about a layout rendering between a LayoutRenderTag and - * a LayoutDefinitionTag. Holds the set of overridden components and any parameters provided - * to the render tag. + * Used to move contextual information about a layout rendering between a LayoutRenderTag and a + * LayoutDefinitionTag. Holds the set of overridden components and any parameters provided to the + * render tag. * * @author Tim Fennell, Ben Gunter * @since Stripes 1.1 */ public class LayoutContext { - private static final Log log = Log.getInstance(LayoutContext.class); - - /** The attribute name by which the stack of layout contexts can be found in the request. */ - public static final String LAYOUT_CONTEXT_KEY = LayoutContext.class.getName() + "#Context"; - - /** - * The attribute name by which the indicator of broken include functionality in the application - * server can be found in the application scope. - */ - public static final String BROKEN_INCLUDE_KEY = LayoutContext.class.getName() - + "#BROKEN_INCLUDE"; + private static final Log log = Log.getInstance(LayoutContext.class); + + /** The attribute name by which the stack of layout contexts can be found in the request. */ + public static final String LAYOUT_CONTEXT_KEY = LayoutContext.class.getName() + "#Context"; + + /** + * The attribute name by which the indicator of broken include functionality in the application + * server can be found in the application scope. + */ + public static final String BROKEN_INCLUDE_KEY = LayoutContext.class.getName() + "#BROKEN_INCLUDE"; + + /** + * Create a new layout context for the given render tag and push it onto the stack of layout + * contexts in a JSP page context. + */ + public static LayoutContext push(LayoutRenderTag renderTag) { + LayoutContext context = new LayoutContext(renderTag); + log.debug("Push context ", context.getRenderPage(), " -> ", context.getDefinitionPage()); + + PageContext pageContext = renderTag.getPageContext(); + LayoutContext previous = lookup(pageContext); + if (previous == null) { + // Create a new layout writer and push a new body + context.out = new LayoutWriter(pageContext.getOut()); + pageContext.pushBody(context.out); + } else { + // Sanity check + if (previous.next != null) { + throw new StripesRuntimeException( + "Attempt to insert a new context into the middle of the stack"); + } + + // Link the two nodes + context.out = previous.out; + previous.next = context; + context.previous = previous; + } - /** - * Create a new layout context for the given render tag and push it onto the stack of layout - * contexts in a JSP page context. - */ - public static LayoutContext push(LayoutRenderTag renderTag) { - LayoutContext context = new LayoutContext(renderTag); - log.debug("Push context ", context.getRenderPage(), " -> ", context.getDefinitionPage()); - - PageContext pageContext = renderTag.getPageContext(); - LayoutContext previous = lookup(pageContext); - if (previous == null) { - // Create a new layout writer and push a new body - context.out = new LayoutWriter(pageContext.getOut()); - pageContext.pushBody(context.out); - } - else { - // Sanity check - if (previous.next != null) { - throw new StripesRuntimeException( - "Attempt to insert a new context into the middle of the stack"); - } - - // Link the two nodes - context.out = previous.out; - previous.next = context; - context.previous = previous; + pageContext.setAttribute(LAYOUT_CONTEXT_KEY, context); + return context; + } + + /** + * Look up the current layout context in a JSP page context. + * + * @param pageContext The JSP page context to search for the layout context stack. + */ + public static LayoutContext lookup(PageContext pageContext) { + LayoutContext context = (LayoutContext) pageContext.getAttribute(LAYOUT_CONTEXT_KEY); + if (context == null) { + context = (LayoutContext) pageContext.getRequest().getAttribute(LAYOUT_CONTEXT_KEY); + if (context != null) { + for (LayoutContext c = context.getFirst(); c != context; c = c.getNext()) { + for (Entry entry : c.getParameters().entrySet()) { + pageContext.setAttribute(entry.getKey(), entry.getValue()); + } } pageContext.setAttribute(LAYOUT_CONTEXT_KEY, context); - return context; - } - - /** - * Look up the current layout context in a JSP page context. - * - * @param pageContext The JSP page context to search for the layout context stack. - */ - public static LayoutContext lookup(PageContext pageContext) { - LayoutContext context = (LayoutContext) pageContext.getAttribute(LAYOUT_CONTEXT_KEY); - if (context == null) { - context = (LayoutContext) pageContext.getRequest().getAttribute(LAYOUT_CONTEXT_KEY); - if (context != null) { - for (LayoutContext c = context.getFirst(); c != context; c = c.getNext()) { - for (Entry entry : c.getParameters().entrySet()) { - pageContext.setAttribute(entry.getKey(), entry.getValue()); - } - } - - pageContext.setAttribute(LAYOUT_CONTEXT_KEY, context); - pageContext.getRequest().removeAttribute(LAYOUT_CONTEXT_KEY); - } - } - return context; + pageContext.getRequest().removeAttribute(LAYOUT_CONTEXT_KEY); + } } - - /** - * Remove the current layout context from the stack of layout contexts. - * - * @param pageContext The JSP page context to search for the layout context stack. - * @return The layout context that was popped off the stack, or null if the stack was not found - * or was empty. - */ - public static LayoutContext pop(PageContext pageContext) { - LayoutContext context = lookup(pageContext); - log.debug("Pop context ", context.getRenderPage(), " -> ", context.getDefinitionPage()); - - pageContext.setAttribute(LAYOUT_CONTEXT_KEY, context.previous); - - if (context.previous == null) { - pageContext.popBody(); - } - else { - context.previous.next = null; - context.previous = null; - } - - return context; + return context; + } + + /** + * Remove the current layout context from the stack of layout contexts. + * + * @param pageContext The JSP page context to search for the layout context stack. + * @return The layout context that was popped off the stack, or null if the stack was not found or + * was empty. + */ + public static LayoutContext pop(PageContext pageContext) { + LayoutContext context = lookup(pageContext); + log.debug("Pop context ", context.getRenderPage(), " -> ", context.getDefinitionPage()); + + pageContext.setAttribute(LAYOUT_CONTEXT_KEY, context.previous); + + if (context.previous == null) { + pageContext.popBody(); + } else { + context.previous.next = null; + context.previous = null; } - private LayoutContext previous, next; - private LayoutRenderTag renderTag; - private LayoutWriter out; - private Map components = new HashMap(); - private Map parameters = new HashMap(); - private String renderPage, component; - private LayoutRenderTagPath componentPath; - private boolean componentRenderPhase, rendered; - - /** - * A new context may be created only by a {@link LayoutRenderTag}. The tag provides all the - * information necessary to initialize the context. - * - * @param renderTag The tag that is beginning a new layout render process. - */ - public LayoutContext(LayoutRenderTag renderTag) { - this.renderTag = renderTag; - this.renderPage = renderTag.getCurrentPagePath(); - this.componentPath = new LayoutRenderTagPath(renderTag); - log.debug("Path is ", this.componentPath); + return context; + } + + private LayoutContext previous, next; + private LayoutRenderTag renderTag; + private LayoutWriter out; + private Map components = + new HashMap(); + private Map parameters = new HashMap(); + private String renderPage, component; + private LayoutRenderTagPath componentPath; + private boolean componentRenderPhase, rendered; + + /** + * A new context may be created only by a {@link LayoutRenderTag}. The tag provides all the + * information necessary to initialize the context. + * + * @param renderTag The tag that is beginning a new layout render process. + */ + public LayoutContext(LayoutRenderTag renderTag) { + this.renderTag = renderTag; + this.renderPage = renderTag.getCurrentPagePath(); + this.componentPath = new LayoutRenderTagPath(renderTag); + log.debug("Path is ", this.componentPath); + } + + /** Get the previous layout context from the stack. */ + public LayoutContext getPrevious() { + return previous; + } + + /** Get the next layout context from the stack. */ + public LayoutContext getNext() { + return next; + } + + /** Set the next layout context in the stack. */ + public void setNext(LayoutContext next) { + this.next = next; + } + + /** Get the first context in the list. */ + public LayoutContext getFirst() { + for (LayoutContext c = this; ; c = c.getPrevious()) { + if (c.getPrevious() == null) return c; } + } - /** Get the previous layout context from the stack. */ - public LayoutContext getPrevious() { return previous; } - - /** Get the next layout context from the stack. */ - public LayoutContext getNext() { return next; } - - /** Set the next layout context in the stack. */ - public void setNext(LayoutContext next) { this.next = next; } - - /** Get the first context in the list. */ - public LayoutContext getFirst() { - for (LayoutContext c = this;; c = c.getPrevious()) { - if (c.getPrevious() == null) - return c; - } + /** Get the last context in the list. */ + public LayoutContext getLast() { + for (LayoutContext c = this; ; c = c.getNext()) { + if (c.getNext() == null) return c; } - - /** Get the last context in the list. */ - public LayoutContext getLast() { - for (LayoutContext c = this;; c = c.getNext()) { - if (c.getNext() == null) - return c; - } + } + + /** + * Called when a layout tag needs to execute a page in order to execute another layout tag. + * Special handling is implemented to ensure the included page is aware of the current layout + * context while also ensuring that pages included by other means (e.g., {@code jsp:include} or + * {@code c:import}) are not aware of the current layout context. + * + *

    This method calls {@link PageContext#include(String, boolean)} with {@code false} as the + * second parameter so that the response is not flushed before the include request executes. + */ + public void doInclude(PageContext pageContext, String relativeUrlPath) + throws ServletException, IOException { + try { + pageContext.getRequest().setAttribute(LAYOUT_CONTEXT_KEY, this); + if (isIncludeBroken(pageContext)) doIncludeHack(pageContext, relativeUrlPath); + else pageContext.include(relativeUrlPath, false); + } finally { + pageContext.getRequest().removeAttribute(LAYOUT_CONTEXT_KEY); } - - /** - *

    - * Called when a layout tag needs to execute a page in order to execute another layout tag. - * Special handling is implemented to ensure the included page is aware of the current layout - * context while also ensuring that pages included by other means (e.g., {@code jsp:include} or - * {@code c:import}) are not aware of the current layout context. - *

    - *

    - * This method calls {@link PageContext#include(String, boolean)} with {@code false} as the - * second parameter so that the response is not flushed before the include request executes. - *

    - */ - public void doInclude(PageContext pageContext, String relativeUrlPath) throws ServletException, - IOException { - try { - pageContext.getRequest().setAttribute(LAYOUT_CONTEXT_KEY, this); - if (isIncludeBroken(pageContext)) - doIncludeHack(pageContext, relativeUrlPath); - else - pageContext.include(relativeUrlPath, false); - } - finally { - pageContext.getRequest().removeAttribute(LAYOUT_CONTEXT_KEY); - } + } + + /** + * Returns true if the current thread is executing in an application server that is known to have + * issues with doing normal includes using {@link PageContext#include(String, boolean)}. + */ + protected boolean isIncludeBroken(PageContext pageContext) { + Boolean b = (Boolean) pageContext.getServletContext().getAttribute(BROKEN_INCLUDE_KEY); + if (b == null) { + b = pageContext.getClass().getName().startsWith("weblogic."); + pageContext.getServletContext().setAttribute(BROKEN_INCLUDE_KEY, b); + if (b) { + log.info("This application server's include is broken so a workaround will be used."); + } } - /** - * Returns true if the current thread is executing in an application server that is known to - * have issues with doing normal includes using {@link PageContext#include(String, boolean)}. - */ - protected boolean isIncludeBroken(PageContext pageContext) { - Boolean b = (Boolean) pageContext.getServletContext().getAttribute(BROKEN_INCLUDE_KEY); - if (b == null) { - b = pageContext.getClass().getName().startsWith("weblogic."); - pageContext.getServletContext().setAttribute(BROKEN_INCLUDE_KEY, b); - if (b) { - log.info("This application server's include is broken so a workaround will be used."); - } - } + return b; + } - return b; - } + /** + * An alternative to {@link PageContext#include(String, boolean)} that works around broken include + * functionality in certain application servers, including several current and recent releases of + * WebLogic. + */ + protected void doIncludeHack(PageContext pageContext, String relativeUrlPath) + throws ServletException, IOException { /** - * An alternative to {@link PageContext#include(String, boolean)} that works around broken - * include functionality in certain application servers, including several current and recent - * releases of WebLogic. + * A servlet output stream implementation that decodes bytes to characters and writes the + * characters to an underlying writer. */ - protected void doIncludeHack(PageContext pageContext, String relativeUrlPath) - throws ServletException, IOException { - - /** - * A servlet output stream implementation that decodes bytes to characters and writes the - * characters to an underlying writer. - */ - class MyServletOutputStream extends ServletOutputStream { - static final String DEFAULT_CHARSET = "UTF-8"; - static final int BUFFER_SIZE = 1024; - - Writer out; - String charset = DEFAULT_CHARSET; - CharsetDecoder decoder; - ByteBuffer bbuf; - CharBuffer cbuf; - - /** Construct a new instance that sends output to the specified writer. */ - MyServletOutputStream(Writer out) { - this.out = out; - } - - /** Get the character set to which bytes will be decoded. */ - String getCharset() { - return charset; - } - - /** Set the character set to which bytes will be decoded. */ - void setCharset(String charset) { - if (charset == null) - charset = DEFAULT_CHARSET; - - // Create a new decoder only if the charset has changed - if (!charset.equals(this.charset)) - decoder = null; - - this.charset = charset; - } - - /** Initialize the character decoder, byte buffer and character buffer. */ - void initDecoder() { - if (decoder == null) { - decoder = Charset.forName(getCharset()).newDecoder(); - - if (bbuf == null) - bbuf = ByteBuffer.allocate(BUFFER_SIZE); - - int size = (int) Math.ceil(BUFFER_SIZE * decoder.maxCharsPerByte()); - if (cbuf == null || cbuf.capacity() != size) - cbuf = CharBuffer.allocate(size); - } - } - - /** - * Clear the byte buffer. If the byte buffer has any data remaining to be read, then - * those bytes are shifted to the front of the buffer and the buffer's position is - * updated accordingly. - */ - void resetBuffer() { - if (bbuf.hasRemaining()) { - ByteBuffer slice = bbuf.slice(); - bbuf.clear(); - bbuf.put(slice); - } - else { - bbuf.clear(); - } - } - - /** - * Decode the contents of the byte buffer to the character buffer and then write the - * contents of the character buffer to the underlying writer. - */ - void decodeBuffer() throws IOException { - bbuf.flip(); - cbuf.clear(); - decoder.decode(bbuf, cbuf, false); - cbuf.flip(); - out.write(cbuf.array(), cbuf.position(), cbuf.remaining()); - resetBuffer(); - } - - @Override - public void print(char c) throws IOException { - out.write(c); - } - - @Override - public void print(String s) throws IOException { - out.write(s); - } - - @Override - public void write(int b) throws IOException { - initDecoder(); - bbuf.put((byte) b); - decodeBuffer(); - } - - @Override - public void write(byte[] buf, int off, int len) throws IOException { - initDecoder(); - - for (int i = 0; i < len; i += bbuf.remaining()) { - int n = len - i; - if (n > bbuf.remaining()) - n = bbuf.remaining(); - - bbuf.put(buf, i, n); - decodeBuffer(); - } - } - - @Override - public void write(byte[] buf) throws IOException { - write(buf, 0, buf.length); - } - } + class MyServletOutputStream extends ServletOutputStream { + static final String DEFAULT_CHARSET = "UTF-8"; + static final int BUFFER_SIZE = 1024; - final MyServletOutputStream os = new MyServletOutputStream(pageContext.getOut()); - final PrintWriter writer = new PrintWriter(pageContext.getOut()); - final HttpServletResponse response = new HttpServletResponseWrapper( - (HttpServletResponse) pageContext.getResponse()) { - @Override - public String getCharacterEncoding() { - return os.getCharset(); - } - - @Override - public void setCharacterEncoding(String charset) { - os.setCharset(charset); - } - - @Override - public ServletOutputStream getOutputStream() throws IOException { - return os; - } - - @Override - public PrintWriter getWriter() throws IOException { - return writer; - } - }; + Writer out; + String charset = DEFAULT_CHARSET; + CharsetDecoder decoder; + ByteBuffer bbuf; + CharBuffer cbuf; - pageContext.getRequest().getRequestDispatcher(relativeUrlPath) - .include(pageContext.getRequest(), response); - } + /** Construct a new instance that sends output to the specified writer. */ + MyServletOutputStream(Writer out) { + this.out = out; + } - /** Get the render tag that created this context. */ - public LayoutRenderTag getRenderTag() { return renderTag; } + /** Get the character set to which bytes will be decoded. */ + String getCharset() { + return charset; + } - /** - * Gets the Map of overridden components. Will return an empty Map if no components were - * overridden. - */ - public Map getComponents() { return components; } + /** Set the character set to which bytes will be decoded. */ + void setCharset(String charset) { + if (charset == null) charset = DEFAULT_CHARSET; - /** Gets the Map of parameters. Will return an empty Map if none were provided. */ - public Map getParameters() { return parameters; } + // Create a new decoder only if the charset has changed + if (!charset.equals(this.charset)) decoder = null; - /** Returns true if the layout has been rendered, false otherwise. */ - public boolean isRendered() { return rendered; } + this.charset = charset; + } - /** False initially, should be set to true when the layout is actually rendered. */ - public void setRendered(final boolean rendered) { this.rendered = rendered; } + /** Initialize the character decoder, byte buffer and character buffer. */ + void initDecoder() { + if (decoder == null) { + decoder = Charset.forName(getCharset()).newDecoder(); - /** Get the path to the page that contains the {@link LayoutRenderTag} that created this context. */ - public String getRenderPage() { return renderPage; } + if (bbuf == null) bbuf = ByteBuffer.allocate(BUFFER_SIZE); - /** Get the path to the page that contains the {@link LayoutDefinitionTag} referenced by the render tag. */ - public String getDefinitionPage() { return getRenderTag().getName(); } - - /** True if the intention of the current page execution is solely to render a component. */ - public boolean isComponentRenderPhase() { return componentRenderPhase; } - - /** Set the flag that indicates that the coming execution phase is solely to render a component. */ - public void setComponentRenderPhase(boolean b) { this.componentRenderPhase = b; } + int size = (int) Math.ceil(BUFFER_SIZE * decoder.maxCharsPerByte()); + if (cbuf == null || cbuf.capacity() != size) cbuf = CharBuffer.allocate(size); + } + } + + /** + * Clear the byte buffer. If the byte buffer has any data remaining to be read, then those + * bytes are shifted to the front of the buffer and the buffer's position is updated + * accordingly. + */ + void resetBuffer() { + if (bbuf.hasRemaining()) { + ByteBuffer slice = bbuf.slice(); + bbuf.clear(); + bbuf.put(slice); + } else { + bbuf.clear(); + } + } + + /** + * Decode the contents of the byte buffer to the character buffer and then write the contents + * of the character buffer to the underlying writer. + */ + void decodeBuffer() throws IOException { + bbuf.flip(); + cbuf.clear(); + decoder.decode(bbuf, cbuf, false); + cbuf.flip(); + out.write(cbuf.array(), cbuf.position(), cbuf.remaining()); + resetBuffer(); + } + + @Override + public void print(char c) throws IOException { + out.write(c); + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setWriteListener(WriteListener writeListener) {} + + @Override + public void print(String s) throws IOException { + out.write(s); + } + + @Override + public void write(int b) throws IOException { + initDecoder(); + bbuf.put((byte) b); + decodeBuffer(); + } + + @Override + public void write(byte[] buf, int off, int len) throws IOException { + initDecoder(); + + for (int i = 0; i < len; i += bbuf.remaining()) { + int n = len - i; + if (n > bbuf.remaining()) n = bbuf.remaining(); + + bbuf.put(buf, i, n); + decodeBuffer(); + } + } - /** Get the name of the component to be rendered during the current phase of execution. */ - public String getComponent() { return component; } + @Override + public void write(byte[] buf) throws IOException { + write(buf, 0, buf.length); + } + } - /** Set the name of the component to be rendered during the current phase of execution. */ - public void setComponent(String component) { this.component = component; } + final MyServletOutputStream os = new MyServletOutputStream(pageContext.getOut()); + final PrintWriter writer = new PrintWriter(pageContext.getOut()); + final HttpServletResponse response = + new HttpServletResponseWrapper((HttpServletResponse) pageContext.getResponse()) { + @Override + public String getCharacterEncoding() { + return os.getCharset(); + } + + @Override + public void setCharacterEncoding(String charset) { + os.setCharset(charset); + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + return os; + } + + @Override + public PrintWriter getWriter() throws IOException { + return writer; + } + }; - /** - * Get the list of components in the render page that must execute so that the render tag that - * created this context can execute. - */ - public LayoutRenderTagPath getComponentPath() { return componentPath; } - - /** Get the layout writer to which the layout is rendered. */ - public LayoutWriter getOut() { return out; } - - /** To String implementation the parameters, and the component names. */ - @Override - public String toString() { - return "LayoutContext{" + - "component names=" + components.keySet() + - ", parameters=" + parameters + - '}'; - } + pageContext + .getRequest() + .getRequestDispatcher(relativeUrlPath) + .include(pageContext.getRequest(), response); + } + + /** Get the render tag that created this context. */ + public LayoutRenderTag getRenderTag() { + return renderTag; + } + + /** + * Gets the Map of overridden components. Will return an empty Map if no components were + * overridden. + */ + public Map getComponents() { + return components; + } + + /** Gets the Map of parameters. Will return an empty Map if none were provided. */ + public Map getParameters() { + return parameters; + } + + /** Returns true if the layout has been rendered, false otherwise. */ + public boolean isRendered() { + return rendered; + } + + /** False initially, should be set to true when the layout is actually rendered. */ + public void setRendered(final boolean rendered) { + this.rendered = rendered; + } + + /** + * Get the path to the page that contains the {@link LayoutRenderTag} that created this context. + */ + public String getRenderPage() { + return renderPage; + } + + /** + * Get the path to the page that contains the {@link LayoutDefinitionTag} referenced by the render + * tag. + */ + public String getDefinitionPage() { + return getRenderTag().getName(); + } + + /** True if the intention of the current page execution is solely to render a component. */ + public boolean isComponentRenderPhase() { + return componentRenderPhase; + } + + /** + * Set the flag that indicates that the coming execution phase is solely to render a component. + */ + public void setComponentRenderPhase(boolean b) { + this.componentRenderPhase = b; + } + + /** Get the name of the component to be rendered during the current phase of execution. */ + public String getComponent() { + return component; + } + + /** Set the name of the component to be rendered during the current phase of execution. */ + public void setComponent(String component) { + this.component = component; + } + + /** + * Get the list of components in the render page that must execute so that the render tag that + * created this context can execute. + */ + public LayoutRenderTagPath getComponentPath() { + return componentPath; + } + + /** Get the layout writer to which the layout is rendered. */ + public LayoutWriter getOut() { + return out; + } + + /** To String implementation the parameters, and the component names. */ + @Override + public String toString() { + return "LayoutContext{" + + "component names=" + + components.keySet() + + ", parameters=" + + parameters + + '}'; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutDefinitionTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutDefinitionTag.java index d64422c7c..9515c56d3 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutDefinitionTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutDefinitionTag.java @@ -14,96 +14,94 @@ */ package net.sourceforge.stripes.tag.layout; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.PageContext; import java.io.IOException; import java.util.Map; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.PageContext; - import net.sourceforge.stripes.exception.StripesRuntimeException; /** - * On the surface, allows a developer to define a layout using a custom tag - but is actually - * the tag responsible for generating the output of the layout. A layout can have zero or more - * nested components, as well as regular text and other custom tags nested within it. + * On the surface, allows a developer to define a layout using a custom tag - but is actually the + * tag responsible for generating the output of the layout. A layout can have zero or more nested + * components, as well as regular text and other custom tags nested within it. * * @author Tim Fennell, Ben Gunter * @since Stripes 1.1 */ public class LayoutDefinitionTag extends LayoutTag { - private LayoutContext context; - private boolean renderPhase, silent; + private LayoutContext context; + private boolean renderPhase, silent; - @Override - public void setPageContext(PageContext pageContext) { - // Call super method - super.setPageContext(pageContext); - - // Initialize layout context and related fields - context = LayoutContext.lookup(pageContext); + @Override + public void setPageContext(PageContext pageContext) { + // Call super method + super.setPageContext(pageContext); - if (context == null || getLayoutParent() != null) { - throw new StripesRuntimeException("The JSP page " + getCurrentPagePath() - + " contains a layout-definition tag and was invoked directly. " - + "A layout-definition can only be invoked by a page that contains " - + "a layout-render tag."); - } + // Initialize layout context and related fields + context = LayoutContext.lookup(pageContext); - renderPhase = context.isComponentRenderPhase(); - silent = context.getOut().isSilent(); + if (context == null || getLayoutParent() != null) { + throw new StripesRuntimeException( + "The JSP page " + + getCurrentPagePath() + + " contains a layout-definition tag and was invoked directly. " + + "A layout-definition can only be invoked by a page that contains " + + "a layout-render tag."); } - /** - * Looks up the layout context that has been setup by a {@link LayoutRenderTag}. Uses the - * context to push any dynamic attributes supplied to the render tag in to the page context - * available during the body of the {@link LayoutDefinitionTag}. - * - * @return {@code EVAL_BODY_INCLUDE} in all cases. - */ - @Override - public int doStartTag() throws JspException { - try { - // Flag this definition has rendered, even though it's not really done yet. - context.setRendered(true); + renderPhase = context.isComponentRenderPhase(); + silent = context.getOut().isSilent(); + } - // Put any additional parameters into page context for the definition to use - if (!renderPhase) { - for (Map.Entry entry : context.getParameters().entrySet()) { - pageContext.setAttribute(entry.getKey(), entry.getValue()); - } - } + /** + * Looks up the layout context that has been setup by a {@link LayoutRenderTag}. Uses the context + * to push any dynamic attributes supplied to the render tag in to the page context available + * during the body of the {@link LayoutDefinitionTag}. + * + * @return {@code EVAL_BODY_INCLUDE} in all cases. + */ + @Override + public int doStartTag() throws JspException { + try { + // Flag this definition has rendered, even though it's not really done yet. + context.setRendered(true); - // Put component renderers into the page context, even those from previous contexts - exportComponentRenderers(); + // Put any additional parameters into page context for the definition to use + if (!renderPhase) { + for (Map.Entry entry : context.getParameters().entrySet()) { + pageContext.setAttribute(entry.getKey(), entry.getValue()); + } + } - // Enable output only if this is the definition execution, not a component render - context.getOut().setSilent(renderPhase, pageContext); + // Put component renderers into the page context, even those from previous contexts + exportComponentRenderers(); - return EVAL_BODY_INCLUDE; - } - catch (IOException e) { - throw new JspException(e); - } + // Enable output only if this is the definition execution, not a component render + context.getOut().setSilent(renderPhase, pageContext); + + return EVAL_BODY_INCLUDE; + } catch (IOException e) { + throw new JspException(e); } + } - /** - * Causes page evaluation to end once the end of the layout definition is reached. - * @return SKIP_PAGE in all cases - */ - @Override - public int doEndTag() throws JspException { - try { - cleanUpComponentRenderers(); - context.getOut().setSilent(silent, pageContext); - return SKIP_PAGE; - } - catch (IOException e) { - throw new JspException(e); - } - finally { - this.context = null; - this.renderPhase = false; - this.silent = false; - } + /** + * Causes page evaluation to end once the end of the layout definition is reached. + * + * @return SKIP_PAGE in all cases + */ + @Override + public int doEndTag() throws JspException { + try { + cleanUpComponentRenderers(); + context.getOut().setSilent(silent, pageContext); + return SKIP_PAGE; + } catch (IOException e) { + throw new JspException(e); + } finally { + this.context = null; + this.renderPhase = false; + this.silent = false; } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutRenderTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutRenderTag.java index 3105c6a1e..1cdf7e37f 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutRenderTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutRenderTag.java @@ -14,213 +14,219 @@ */ package net.sourceforge.stripes.tag.layout; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.BodyContent; +import jakarta.servlet.jsp.tagext.BodyTag; +import jakarta.servlet.jsp.tagext.DynamicAttributes; +import jakarta.servlet.jsp.tagext.Tag; import java.io.IOException; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.BodyContent; -import javax.servlet.jsp.tagext.BodyTag; -import javax.servlet.jsp.tagext.DynamicAttributes; -import javax.servlet.jsp.tagext.Tag; - import net.sourceforge.stripes.exception.StripesJspException; import net.sourceforge.stripes.exception.StripesRuntimeException; import net.sourceforge.stripes.util.Log; /** * Renders a named layout, optionally overriding one or more components in the layout. Any - * attributes provided to the class other than 'name' will be placed into page context during - * the evaluation of the layout, making them available to other tags, and in EL. + * attributes provided to the class other than 'name' will be placed into page context during the + * evaluation of the layout, making them available to other tags, and in EL. * * @author Tim Fennell, Ben Gunter * @since Stripes 1.1 */ public class LayoutRenderTag extends LayoutTag implements BodyTag, DynamicAttributes { - private static final Log log = Log.getInstance(LayoutRenderTag.class); - - private String name; - private LayoutContext context; - private boolean contextIsNew, silent; - private LayoutRenderTagPath path; - private BodyContent bodyContent; - - /** Gets the name of the layout to be used. */ - public String getName() { return name; } - - /** Sets the name of the layout to be used and then calls {@link #initialize()}. */ - public void setName(String name) { - this.name = name; - initialize(); + private static final Log log = Log.getInstance(LayoutRenderTag.class); + + private String name; + private LayoutContext context; + private boolean contextIsNew, silent; + private LayoutRenderTagPath path; + private BodyContent bodyContent; + + /** Gets the name of the layout to be used. */ + public String getName() { + return name; + } + + /** Sets the name of the layout to be used and then calls {@link #initialize()}. */ + public void setName(String name) { + this.name = name; + initialize(); + } + + /** Get the {@link LayoutRenderTagPath} that identifies this tag within the current page. */ + public LayoutRenderTagPath getPath() { + return path; + } + + /** + * Initialize fields before execution begins. Typically, this would be done by overriding {@link + * #setPageContext(jakarta.servlet.jsp.PageContext)}, but that isn't possible in this case because + * some of the logic depends on {@link #setName(String)} having been called, which does not happen + * until after {@link #setPageContext(jakarta.servlet.jsp.PageContext)} has been called. + */ + protected void initialize() { + LayoutContext context = LayoutContext.lookup(pageContext); + + boolean create = + context == null || !context.isComponentRenderPhase() || isChildOfCurrentComponent(); + + LayoutRenderTagPath path; + if (create) { + context = LayoutContext.push(this); + path = context.getComponentPath(); + } else { + path = new LayoutRenderTagPath(this); } - /** Get the {@link LayoutRenderTagPath} that identifies this tag within the current page. */ - public LayoutRenderTagPath getPath( ) { return path; } - - /** - * Initialize fields before execution begins. Typically, this would be done by overriding - * {@link #setPageContext(javax.servlet.jsp.PageContext)}, but that isn't possible in this case - * because some of the logic depends on {@link #setName(String)} having been called, which does - * not happen until after {@link #setPageContext(javax.servlet.jsp.PageContext)} has been - * called. - */ - protected void initialize() { - LayoutContext context = LayoutContext.lookup(pageContext); - - boolean create = context == null || !context.isComponentRenderPhase() - || isChildOfCurrentComponent(); - - LayoutRenderTagPath path; - if (create) { - context = LayoutContext.push(this); - path = context.getComponentPath(); - } - else { - path = new LayoutRenderTagPath(this); - } - - this.context = context; - this.contextIsNew = create; - this.path = path; - this.silent = context.getOut().isSilent(); + this.context = context; + this.contextIsNew = create; + this.path = path; + this.silent = context.getOut().isSilent(); + } + + /** Returns true if this tag is a child of the current component tag. */ + public boolean isChildOfCurrentComponent() { + try { + LayoutTag parent = getLayoutParent(); + return parent instanceof LayoutComponentTag + && ((LayoutComponentTag) parent).isCurrentComponent(); + } catch (StripesJspException e) { + // This exception would have been thrown before this tag ever executed + throw new StripesRuntimeException("Something has happened that should never happen", e); } - - /** Returns true if this tag is a child of the current component tag. */ - public boolean isChildOfCurrentComponent() { - try { - LayoutTag parent = getLayoutParent(); - return parent instanceof LayoutComponentTag - && ((LayoutComponentTag) parent).isCurrentComponent(); - } - catch (StripesJspException e) { - // This exception would have been thrown before this tag ever executed - throw new StripesRuntimeException("Something has happened that should never happen", e); - } + } + + /** Used by the JSP container to provide the tag with dynamic attributes. */ + public void setDynamicAttribute(String uri, String localName, Object value) throws JspException { + context.getParameters().put(localName, value); + } + + /** + * On the first pass (see {@link LayoutContext#isComponentRenderPhase()}): + * + *
      + *
    • Push the values of any dynamic attributes into page context attributes for the duration + * of the tag. + *
    • Create a new context and places it in request scope. + *
    • Include the layout definition page named by the {@code name} attribute. + *
    + * + * @return EVAL_BODY_INCLUDE in all cases + */ + @Override + public int doStartTag() throws JspException { + try { + if (contextIsNew) { + log.debug("Start layout init in ", context.getRenderPage()); + pushPageContextAttributes(context.getParameters()); + } + + if (context.isComponentRenderPhase()) { + log.debug( + "Start component render phase for ", + context.getComponent(), + " in ", + context.getRenderPage()); + exportComponentRenderers(); + } + + // Render tags never output their contents directly + context.getOut().setSilent(true, pageContext); + + return contextIsNew ? EVAL_BODY_BUFFERED : EVAL_BODY_INCLUDE; + } catch (IOException e) { + throw new JspException(e); } + } + + /** + * Set the tag's body content. Called by the JSP engine during component registration phase, when + * {@link #doStartTag()} returns {@link BodyTag#EVAL_BODY_BUFFERED} + */ + public void setBodyContent(BodyContent bodyContent) { + this.bodyContent = bodyContent; + } + + /** Does nothing. */ + public void doInitBody() throws JspException {} + + /** Returns {@link Tag#SKIP_BODY}. */ + public int doAfterBody() throws JspException { + return SKIP_BODY; + } + + /** + * After the first pass (see {@link LayoutContext#isComponentRenderPhase()}): + * + *
      + *
    • Ensure the layout rendered successfully by checking {@link LayoutContext#isRendered()}. + *
    • Remove the current layout context from request scope. + *
    • Restore previous page context attribute values. + *
    + * + * @return EVAL_PAGE in all cases. + */ + @Override + public int doEndTag() throws JspException { + try { + if (contextIsNew) { + log.debug("End layout init in ", context.getRenderPage()); - /** Used by the JSP container to provide the tag with dynamic attributes. */ - public void setDynamicAttribute(String uri, String localName, Object value) throws JspException { - context.getParameters().put(localName, value); - } - - /** - * On the first pass (see {@link LayoutContext#isComponentRenderPhase()}): - *
      - *
    • Push the values of any dynamic attributes into page context attributes for the duration - * of the tag.
    • - *
    • Create a new context and places it in request scope.
    • - *
    • Include the layout definition page named by the {@code name} attribute.
    • - *
    - * - * @return EVAL_BODY_INCLUDE in all cases - */ - @Override - public int doStartTag() throws JspException { try { - if (contextIsNew) { - log.debug("Start layout init in ", context.getRenderPage()); - pushPageContextAttributes(context.getParameters()); - } - - if (context.isComponentRenderPhase()) { - log.debug("Start component render phase for ", context.getComponent(), " in ", - context.getRenderPage()); - exportComponentRenderers(); - } - - // Render tags never output their contents directly - context.getOut().setSilent(true, pageContext); - - return contextIsNew ? EVAL_BODY_BUFFERED : EVAL_BODY_INCLUDE; + log.debug("Start layout exec in ", context.getDefinitionPage()); + context.getOut().setSilent(true, pageContext); + context.doInclude(pageContext, getName()); + log.debug("End layout exec in ", context.getDefinitionPage()); + } catch (Exception e) { + throw new StripesJspException( + "An exception was raised while invoking a layout. The layout used was " + + "'" + + getName() + + "'. The following information was supplied to the render " + + "tag: " + + context.toString(), + e); } - catch (IOException e) { - throw new JspException(e); - } - } - - /** - * Set the tag's body content. Called by the JSP engine during component registration phase, - * when {@link #doStartTag()} returns {@link BodyTag#EVAL_BODY_BUFFERED} - */ - public void setBodyContent(BodyContent bodyContent) { - this.bodyContent = bodyContent; - } - - /** Does nothing. */ - public void doInitBody() throws JspException { - } - - /** Returns {@link Tag#SKIP_BODY}. */ - public int doAfterBody() throws JspException { - return SKIP_BODY; - } - /** - * After the first pass (see {@link LayoutContext#isComponentRenderPhase()}): - *
      - *
    • Ensure the layout rendered successfully by checking {@link LayoutContext#isRendered()}.
    • - *
    • Remove the current layout context from request scope.
    • - *
    • Restore previous page context attribute values.
    • - *
    - * - * @return EVAL_PAGE in all cases. - */ - @Override - public int doEndTag() throws JspException { - try { - if (contextIsNew) { - log.debug("End layout init in ", context.getRenderPage()); - - try { - log.debug("Start layout exec in ", context.getDefinitionPage()); - context.getOut().setSilent(true, pageContext); - context.doInclude(pageContext, getName()); - log.debug("End layout exec in ", context.getDefinitionPage()); - } - catch (Exception e) { - throw new StripesJspException( - "An exception was raised while invoking a layout. The layout used was " + - "'" + getName() + "'. The following information was supplied to the render " + - "tag: " + context.toString(), e); - } - - // Check that the layout actually got rendered as some containers will - // just quietly ignore includes of non-existent pages! - if (!context.isRendered()) { - throw new StripesJspException( - "Attempt made to render a layout that does not exist. The layout name " + - "provided was '" + getName() + "'. Please check that a JSP/view exists at " + - "that location within your web application." - ); - } - - context.getOut().setSilent(silent, pageContext); - LayoutContext.pop(pageContext); - popPageContextAttributes(); // remove any dynattrs from page scope - } - else { - context.getOut().setSilent(silent, pageContext); - } - - if (context.isComponentRenderPhase()) { - log.debug("End component render phase for ", context.getComponent(), " in ", - context.getRenderPage()); - cleanUpComponentRenderers(); - } - - return EVAL_PAGE; - } - catch (IOException e) { - throw new JspException(e); - } - finally { - this.context = null; - this.contextIsNew = false; - this.path = null; - this.silent = false; - - if (this.bodyContent != null) { - this.bodyContent.clearBody(); - this.bodyContent = null; - } + // Check that the layout actually got rendered as some containers will + // just quietly ignore includes of non-existent pages! + if (!context.isRendered()) { + throw new StripesJspException( + "Attempt made to render a layout that does not exist. The layout name " + + "provided was '" + + getName() + + "'. Please check that a JSP/view exists at " + + "that location within your web application."); } + + context.getOut().setSilent(silent, pageContext); + LayoutContext.pop(pageContext); + popPageContextAttributes(); // remove any dynattrs from page scope + } else { + context.getOut().setSilent(silent, pageContext); + } + + if (context.isComponentRenderPhase()) { + log.debug( + "End component render phase for ", + context.getComponent(), + " in ", + context.getRenderPage()); + cleanUpComponentRenderers(); + } + + return EVAL_PAGE; + } catch (IOException e) { + throw new JspException(e); + } finally { + this.context = null; + this.contextIsNew = false; + this.path = null; + this.silent = false; + + if (this.bodyContent != null) { + this.bodyContent.clearBody(); + this.bodyContent = null; + } } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutRenderTagPath.java b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutRenderTagPath.java index bfacb96a5..447c7d42d 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutRenderTagPath.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutRenderTagPath.java @@ -14,12 +14,10 @@ */ package net.sourceforge.stripes.tag.layout; +import jakarta.servlet.jsp.PageContext; import java.util.Iterator; import java.util.LinkedList; import java.util.List; - -import javax.servlet.jsp.PageContext; - import net.sourceforge.stripes.exception.StripesJspException; /** @@ -27,133 +25,127 @@ * render tags can be accessible via the same "path," where a path consists of zero or more * component tags that are parents of the render tag. This class helps to distinguish between * multiple render tags with the same component path by assigning sequential indexes to them. - * + * * @author Ben Gunter * @since Stripes 1.5.7 */ public class LayoutRenderTagPath { - private List componentPath; - private int index; - - /** Construct a new instance to identify the specified tag. */ - public LayoutRenderTagPath(LayoutRenderTag tag) { - this.componentPath = calculateComponentPath(tag); - this.index = incrementIndex(tag.getPageContext()); - } - - /** - * Calculate the path to a render tag. The path is a list of names of components that must - * execute, in order, so that the specified render tag can execute. - * - * @param tag The render tag. - * @return A list of component names or null if the render tag is not a child of a component. - */ - protected List calculateComponentPath(LayoutRenderTag tag) { - LinkedList path = null; - - for (LayoutTag parent = tag.getLayoutParent(); parent instanceof LayoutComponentTag;) { - if (path == null) - path = new LinkedList(); - - path.addFirst(((LayoutComponentTag) parent).getName()); - - parent = parent.getLayoutParent(); - parent = parent instanceof LayoutRenderTag ? parent.getLayoutParent() : null; - } - - return path; - } - - /** Get the next index for this path from the specified page context. */ - protected int incrementIndex(PageContext pageContext) { - String key = getClass().getName() + "#" + toStringWithoutIndex(); - Integer index = (Integer) pageContext.getAttribute(key); - if (index == null) - index = 0; - else - ++index; - pageContext.setAttribute(key, index); - return index; - } - - /** Get the names of the {@link LayoutComponentTag}s that are parent tags of the render tag. */ - public List getComponentPath() { - return componentPath; - } - - /** Get the index (zero-based) of the combined render page and component path within the page. */ - public int getIndex() { - return index; - } - - /** - * True if the specified tag is a component that must execute so that the current component tag - * can execute. That is, this tag is a parent of the current component. - * - * @param tag The tag to check to see if it is part of this path. - */ - public boolean isPathComponent(LayoutComponentTag tag) throws StripesJspException { - List path = getComponentPath(); - return path == null ? false : isPathComponent(tag, path.iterator()); - } - - /** - * Recursive method called from {@link #isPathComponent(LayoutComponentTag)} that returns true - * if the specified tag's name is present in the component path iterator at the same position - * where this tag occurs in the render/component tag tree. For example, if the path iterator - * contains the component names {@code ["foo", "bar"]} then this method will return true if the - * tag's name is {@code "bar"} and it is a child of a render tag that is a child of a component - * tag whose name is {@code "foo"}. - * - * @param tag The tag to check - * @param path The path to the check the tag against - */ - protected boolean isPathComponent(LayoutComponentTag tag, Iterator path) { - LayoutTag parent = tag.getLayoutParent(); - if (parent instanceof LayoutRenderTag) { - parent = parent.getLayoutParent(); - if (!(parent instanceof LayoutComponentTag) || parent instanceof LayoutComponentTag - && isPathComponent((LayoutComponentTag) parent, path) && path.hasNext()) { - return tag.getName().equals(path.next()); - } - } - - return false; - } - - @Override - public boolean equals(Object obj) { - LayoutRenderTagPath that = (LayoutRenderTagPath) obj; - if (index != that.index) - return false; - if (componentPath == that.componentPath) - return true; - if (componentPath == null || that.componentPath == null) - return false; - return componentPath.equals(that.componentPath); - } - - @Override - public int hashCode() { - return toString().hashCode(); + private List componentPath; + private int index; + + /** Construct a new instance to identify the specified tag. */ + public LayoutRenderTagPath(LayoutRenderTag tag) { + this.componentPath = calculateComponentPath(tag); + this.index = incrementIndex(tag.getPageContext()); + } + + /** + * Calculate the path to a render tag. The path is a list of names of components that must + * execute, in order, so that the specified render tag can execute. + * + * @param tag The render tag. + * @return A list of component names or null if the render tag is not a child of a component. + */ + protected List calculateComponentPath(LayoutRenderTag tag) { + LinkedList path = null; + + for (LayoutTag parent = tag.getLayoutParent(); parent instanceof LayoutComponentTag; ) { + if (path == null) path = new LinkedList(); + + path.addFirst(((LayoutComponentTag) parent).getName()); + + parent = parent.getLayoutParent(); + parent = parent instanceof LayoutRenderTag ? parent.getLayoutParent() : null; } - @Override - public String toString() { - return toStringWithoutIndex() + '[' + index + ']'; + return path; + } + + /** Get the next index for this path from the specified page context. */ + protected int incrementIndex(PageContext pageContext) { + String key = getClass().getName() + "#" + toStringWithoutIndex(); + Integer index = (Integer) pageContext.getAttribute(key); + if (index == null) index = 0; + else ++index; + pageContext.setAttribute(key, index); + return index; + } + + /** Get the names of the {@link LayoutComponentTag}s that are parent tags of the render tag. */ + public List getComponentPath() { + return componentPath; + } + + /** Get the index (zero-based) of the combined render page and component path within the page. */ + public int getIndex() { + return index; + } + + /** + * True if the specified tag is a component that must execute so that the current component tag + * can execute. That is, this tag is a parent of the current component. + * + * @param tag The tag to check to see if it is part of this path. + */ + public boolean isPathComponent(LayoutComponentTag tag) throws StripesJspException { + List path = getComponentPath(); + return path == null ? false : isPathComponent(tag, path.iterator()); + } + + /** + * Recursive method called from {@link #isPathComponent(LayoutComponentTag)} that returns true if + * the specified tag's name is present in the component path iterator at the same position where + * this tag occurs in the render/component tag tree. For example, if the path iterator contains + * the component names {@code ["foo", "bar"]} then this method will return true if the tag's name + * is {@code "bar"} and it is a child of a render tag that is a child of a component tag whose + * name is {@code "foo"}. + * + * @param tag The tag to check + * @param path The path to the check the tag against + */ + protected boolean isPathComponent(LayoutComponentTag tag, Iterator path) { + LayoutTag parent = tag.getLayoutParent(); + if (parent instanceof LayoutRenderTag) { + parent = parent.getLayoutParent(); + if (!(parent instanceof LayoutComponentTag) + || parent instanceof LayoutComponentTag + && isPathComponent((LayoutComponentTag) parent, path) + && path.hasNext()) { + return tag.getName().equals(path.next()); + } } - /** Get a string representation of this instance without including the index. */ - public String toStringWithoutIndex() { - if (componentPath == null) - return ""; - - StringBuilder s = new StringBuilder(); - for (Iterator it = componentPath.iterator(); it.hasNext();) { - s.append(it.next()); - if (it.hasNext()) - s.append('>'); - } - return s.toString(); + return false; + } + + @Override + public boolean equals(Object obj) { + LayoutRenderTagPath that = (LayoutRenderTagPath) obj; + if (index != that.index) return false; + if (componentPath == that.componentPath) return true; + if (componentPath == null || that.componentPath == null) return false; + return componentPath.equals(that.componentPath); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public String toString() { + return toStringWithoutIndex() + '[' + index + ']'; + } + + /** Get a string representation of this instance without including the index. */ + public String toStringWithoutIndex() { + if (componentPath == null) return ""; + + StringBuilder s = new StringBuilder(); + for (Iterator it = componentPath.iterator(); it.hasNext(); ) { + s.append(it.next()); + if (it.hasNext()) s.append('>'); } + return s.toString(); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutTag.java b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutTag.java index 4e79eda8d..d53c28bc7 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutTag.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutTag.java @@ -14,87 +14,88 @@ */ package net.sourceforge.stripes.tag.layout; +import jakarta.servlet.http.HttpServletRequest; import java.util.Map.Entry; - -import javax.servlet.http.HttpServletRequest; - import net.sourceforge.stripes.controller.StripesConstants; import net.sourceforge.stripes.tag.StripesTagSupport; import net.sourceforge.stripes.util.HttpUtil; /** * Abstract base class for the tags that handle rendering of layouts. - * + * * @author Ben Gunter * @since Stripes 1.5.4 */ public abstract class LayoutTag extends StripesTagSupport { - /** Get the context-relative path of the page that invoked this tag. */ - public String getCurrentPagePath() { - HttpServletRequest request = (HttpServletRequest) pageContext.getRequest(); - String path = (String) request.getAttribute(StripesConstants.REQ_ATTR_INCLUDE_PATH); - if (path == null) - path = HttpUtil.getRequestedPath(request); - return path; - } + /** Get the context-relative path of the page that invoked this tag. */ + public String getCurrentPagePath() { + HttpServletRequest request = (HttpServletRequest) pageContext.getRequest(); + String path = (String) request.getAttribute(StripesConstants.REQ_ATTR_INCLUDE_PATH); + if (path == null) path = HttpUtil.getRequestedPath(request); + return path; + } - /** - * True if the nearest ancestor of this tag that is an instance of {@link LayoutTag} is also an - * instance of {@link LayoutRenderTag}. - */ - public boolean isChildOfRender() { - LayoutTag parent = getLayoutParent(); - return parent instanceof LayoutRenderTag; - } + /** + * True if the nearest ancestor of this tag that is an instance of {@link LayoutTag} is also an + * instance of {@link LayoutRenderTag}. + */ + public boolean isChildOfRender() { + LayoutTag parent = getLayoutParent(); + return parent instanceof LayoutRenderTag; + } - /** - * True if the nearest ancestor of this tag that is an instance of {@link LayoutTag} is also an - * instance of {@link LayoutDefinitionTag}. - */ - public boolean isChildOfDefinition() { - LayoutTag parent = getLayoutParent(); - return parent instanceof LayoutDefinitionTag; - } + /** + * True if the nearest ancestor of this tag that is an instance of {@link LayoutTag} is also an + * instance of {@link LayoutDefinitionTag}. + */ + public boolean isChildOfDefinition() { + LayoutTag parent = getLayoutParent(); + return parent instanceof LayoutDefinitionTag; + } - /** - * True if the nearest ancestor of this tag that is an instance of {@link LayoutTag} is also an - * instance of {@link LayoutComponentTag}. - */ - public boolean isChildOfComponent() { - LayoutTag parent = getLayoutParent(); - return parent instanceof LayoutComponentTag; - } + /** + * True if the nearest ancestor of this tag that is an instance of {@link LayoutTag} is also an + * instance of {@link LayoutComponentTag}. + */ + public boolean isChildOfComponent() { + LayoutTag parent = getLayoutParent(); + return parent instanceof LayoutComponentTag; + } - /** - * Get the nearest ancestor of this tag that is an instance of {@link LayoutTag}. If no ancestor - * of that type is found then null. - */ - @SuppressWarnings("unchecked") - public T getLayoutParent() { - return (T) getParentTag(LayoutTag.class); - } + /** + * Get the nearest ancestor of this tag that is an instance of {@link LayoutTag}. If no ancestor + * of that type is found then null. + */ + @SuppressWarnings("unchecked") + public T getLayoutParent() { + return (T) getParentTag(LayoutTag.class); + } - /** - * Starting from the outer-most context and working up the stack, put a reference to each - * component renderer by name into the page context and push this tag's page context onto the - * renderer's page context stack. Working from the bottom of the stack up ensures that newly - * defined components override any that might have been defined previously by the same name. - */ - public void exportComponentRenderers() { - for (LayoutContext c = LayoutContext.lookup(pageContext).getFirst(); c != null; c = c.getNext()) { - for (Entry entry : c.getComponents().entrySet()) { - entry.getValue().pushPageContext(pageContext); - pageContext.setAttribute(entry.getKey(), entry.getValue()); - } - } + /** + * Starting from the outer-most context and working up the stack, put a reference to each + * component renderer by name into the page context and push this tag's page context onto the + * renderer's page context stack. Working from the bottom of the stack up ensures that newly + * defined components override any that might have been defined previously by the same name. + */ + public void exportComponentRenderers() { + for (LayoutContext c = LayoutContext.lookup(pageContext).getFirst(); + c != null; + c = c.getNext()) { + for (Entry entry : c.getComponents().entrySet()) { + entry.getValue().pushPageContext(pageContext); + pageContext.setAttribute(entry.getKey(), entry.getValue()); + } } + } - /** Pop this tag's page context off each of the component renderers' page context stacks. */ - public void cleanUpComponentRenderers() { - for (LayoutContext c = LayoutContext.lookup(pageContext).getLast(); c != null; c = c.getPrevious()) { - for (LayoutComponentRenderer renderer : c.getComponents().values()) { - renderer.popPageContext(); - } - } + /** Pop this tag's page context off each of the component renderers' page context stacks. */ + public void cleanUpComponentRenderers() { + for (LayoutContext c = LayoutContext.lookup(pageContext).getLast(); + c != null; + c = c.getPrevious()) { + for (LayoutComponentRenderer renderer : c.getComponents().values()) { + renderer.popPageContext(); + } } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutWriter.java b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutWriter.java index b37a8c4c4..b67cca5b1 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutWriter.java +++ b/stripes/src/main/java/net/sourceforge/stripes/tag/layout/LayoutWriter.java @@ -14,14 +14,12 @@ */ package net.sourceforge.stripes.tag.layout; +import jakarta.servlet.jsp.JspWriter; +import jakarta.servlet.jsp.PageContext; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.LinkedList; - -import javax.servlet.jsp.JspWriter; -import javax.servlet.jsp.PageContext; - import net.sourceforge.stripes.exception.StripesRuntimeException; import net.sourceforge.stripes.util.Log; @@ -31,142 +29,135 @@ * from rendering more than once when {@link LayoutRenderTag}s and {@link LayoutComponentTag}s are * nested within it. The definition tag silences output during a component render phase, and the * component that wishes to render turns output back on during its body evaluation. - * + * * @author Ben Gunter * @since Stripes 1.5.4 */ public class LayoutWriter extends Writer { - private static final Log log = Log.getInstance(LayoutWriter.class); - - /** The control character that, when encountered in the output stream, toggles the silent state. */ - private static final char TOGGLE = 0; - - private LinkedList writers = new LinkedList(); - private boolean silent, silentState; - - /** - * Create a new layout writer that wraps the given JSP writer. - * - * @param out The JSP writer to which output will be written. - */ - public LayoutWriter(JspWriter out) { - log.debug("Create layout writer wrapped around ", out); - this.writers.addFirst(out); - } - - /** Get the writer to which output is currently being written. */ - protected Writer getOut() { - return writers.peek(); - } - - /** If true, then discard all output. If false, then resume sending output to the JSP writer. */ - public boolean isSilent() { - return silent; - } - - /** - * Enable or disable silent mode. The output buffer for the given page context will be flushed - * before silent mode is enabled to ensure all buffered data are written. - * - * @param silent True to silence output, false to enable output. - * @param pageContext The page context in use at the time output is to be silenced. - * @throws IOException If an error occurs writing to output. - */ - public void setSilent(boolean silent, PageContext pageContext) throws IOException { - if (silent != this.silent) { - pageContext.getOut().write(TOGGLE); - this.silent = silent; - log.trace("Output is ", (silent ? "DISABLED" : "ENABLED")); - } - } - - /** - * Flush the page context's output buffer and redirect output into a buffer. The buffer can be - * closed and its contents retrieved by calling {@link #closeBuffer(PageContext)}. - */ - public void openBuffer(PageContext pageContext) { - log.trace("Open buffer"); - tryFlush(pageContext); - writers.addFirst(new StringWriter(1024)); + private static final Log log = Log.getInstance(LayoutWriter.class); + + /** + * The control character that, when encountered in the output stream, toggles the silent state. + */ + private static final char TOGGLE = 0; + + private LinkedList writers = new LinkedList(); + private boolean silent, silentState; + + /** + * Create a new layout writer that wraps the given JSP writer. + * + * @param out The JSP writer to which output will be written. + */ + public LayoutWriter(JspWriter out) { + log.debug("Create layout writer wrapped around ", out); + this.writers.addFirst(out); + } + + /** Get the writer to which output is currently being written. */ + protected Writer getOut() { + return writers.peek(); + } + + /** If true, then discard all output. If false, then resume sending output to the JSP writer. */ + public boolean isSilent() { + return silent; + } + + /** + * Enable or disable silent mode. The output buffer for the given page context will be flushed + * before silent mode is enabled to ensure all buffered data are written. + * + * @param silent True to silence output, false to enable output. + * @param pageContext The page context in use at the time output is to be silenced. + * @throws IOException If an error occurs writing to output. + */ + public void setSilent(boolean silent, PageContext pageContext) throws IOException { + if (silent != this.silent) { + pageContext.getOut().write(TOGGLE); + this.silent = silent; + log.trace("Output is ", (silent ? "DISABLED" : "ENABLED")); } - - /** - * Flush the page context's output buffer and resume sending output to the writer that was - * receiving output prior to calling {@link #openBuffer(PageContext)}. - * - * @return The buffer's contents. - */ - public String closeBuffer(PageContext pageContext) { - if (getOut() instanceof StringWriter) { - tryFlush(pageContext); - String contents = ((StringWriter) writers.poll()).toString(); - log.trace("Closed buffer: \"", contents, "\""); - return contents; - } - else { - throw new StripesRuntimeException( - "Attempt to close a buffer without having first called openBuffer(..)!"); - } + } + + /** + * Flush the page context's output buffer and redirect output into a buffer. The buffer can be + * closed and its contents retrieved by calling {@link #closeBuffer(PageContext)}. + */ + public void openBuffer(PageContext pageContext) { + log.trace("Open buffer"); + tryFlush(pageContext); + writers.addFirst(new StringWriter(1024)); + } + + /** + * Flush the page context's output buffer and resume sending output to the writer that was + * receiving output prior to calling {@link #openBuffer(PageContext)}. + * + * @return The buffer's contents. + */ + public String closeBuffer(PageContext pageContext) { + if (getOut() instanceof StringWriter) { + tryFlush(pageContext); + String contents = ((StringWriter) writers.poll()).toString(); + log.trace("Closed buffer: \"", contents, "\""); + return contents; + } else { + throw new StripesRuntimeException( + "Attempt to close a buffer without having first called openBuffer(..)!"); } - - /** Try to flush the page context's output buffer. If an exception is thrown, just log it. */ - protected void tryFlush(PageContext pageContext) { - try { - if (pageContext != null) - pageContext.getOut().flush(); - } - catch (IOException e) { - // This seems to happen once at the beginning and once at the end. Don't know why. - log.debug("Failed to flush buffer: ", e.getMessage()); - } + } + + /** Try to flush the page context's output buffer. If an exception is thrown, just log it. */ + protected void tryFlush(PageContext pageContext) { + try { + if (pageContext != null) pageContext.getOut().flush(); + } catch (IOException e) { + // This seems to happen once at the beginning and once at the end. Don't know why. + log.debug("Failed to flush buffer: ", e.getMessage()); } - - @Override - public void close() throws IOException { - getOut().close(); + } + + @Override + public void close() throws IOException { + getOut().close(); + } + + @Override + public void flush() throws IOException { + getOut().flush(); + } + + /** + * Calls {@link JspWriter#clear()} on the wrapped JSP writer. + * + * @throws IOException + */ + public void clear() throws IOException { + Writer out = getOut(); + if (out instanceof JspWriter) { + ((JspWriter) out).clear(); + } else if (out instanceof StringWriter) { + ((StringWriter) out).getBuffer().setLength(0); + } else { + throw new StripesRuntimeException( + "How did I get a writer of type " + out.getClass().getName() + "??"); } - - @Override - public void flush() throws IOException { - getOut().flush(); - } - - /** - * Calls {@link JspWriter#clear()} on the wrapped JSP writer. - * - * @throws IOException - */ - public void clear() throws IOException { - Writer out = getOut(); - if (out instanceof JspWriter) { - ((JspWriter) out).clear(); - } - else if (out instanceof StringWriter) { - ((StringWriter) out).getBuffer().setLength(0); - } - else { - throw new StripesRuntimeException("How did I get a writer of type " - + out.getClass().getName() + "??"); - } - } - - @Override - public void write(char[] cbuf, int off, int len) throws IOException { - for (int i = off, mark = i, n = i + len; i < n; ++i) { - switch (cbuf[i]) { - case TOGGLE: - if (this.silentState) - mark = i + 1; - else if (i > mark) - getOut().write(cbuf, mark, i - mark); - this.silentState = !this.silentState; - break; - default: - if (this.silentState) - ++mark; - else if (i >= mark && i == n - 1) - getOut().write(cbuf, mark, i - mark + 1); - } - } + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + for (int i = off, mark = i, n = i + len; i < n; ++i) { + switch (cbuf[i]) { + case TOGGLE: + if (this.silentState) mark = i + 1; + else if (i > mark) getOut().write(cbuf, mark, i - mark); + this.silentState = !this.silentState; + break; + default: + if (this.silentState) ++mark; + else if (i >= mark && i == n - 1) getOut().write(cbuf, mark, i - mark + 1); + } } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/util/Base64.java b/stripes/src/main/java/net/sourceforge/stripes/util/Base64.java index 2c0ce8d62..e695e130e 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/util/Base64.java +++ b/stripes/src/main/java/net/sourceforge/stripes/util/Base64.java @@ -1,1816 +1,1899 @@ package net.sourceforge.stripes.util; /** - *

    Encodes and decodes to and from Base64 notation.

    - *

    Homepage: http://iharder.net/base64.

    + * Encodes and decodes to and from Base64 notation. * - *

    The options parameter, which appears in a few places, is used to pass - * several pieces of information to the encoder. In the "higher level" methods such as - * encodeBytes( bytes, options ) the options parameter can be used to indicate such - * things as first gzipping the bytes before encoding them, not inserting linefeeds - * (though that breaks strict Base64 compatibility), and encoding using the URL-safe - * and Ordered dialects.

    + *

    Homepage: http://iharder.net/base64. * - *

    The constants defined in Base64 can be OR-ed together to combine options, so you - * might make a call like this:

    + *

    The options parameter, which appears in a few places, is used to pass several pieces + * of information to the encoder. In the "higher level" methods such as encodeBytes( bytes, options + * ) the options parameter can be used to indicate such things as first gzipping the bytes before + * encoding them, not inserting linefeeds (though that breaks strict Base64 compatibility), and + * encoding using the URL-safe and Ordered dialects. * - * String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DONT_BREAK_LINES ); + *

    The constants defined in Base64 can be OR-ed together to combine options, so you might make a + * call like this: + * String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DONT_BREAK_LINES ); * - *

    to compress the data before encoding it and then making the output have no newline characters.

    + *

    to compress the data before encoding it and then making the output have no newline characters. * + *

    Change Log: * - *

    - * Change Log: - *

    *
      - *
    • v2.2.2 - Fixed encodeFileToFile and decodeFileToFile to use the - * Base64.InputStream class to encode and decode on the fly which uses - * less memory than encoding/decoding an entire file into memory before writing.
    • - *
    • v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug - * when using very small files (~< 40 bytes).
    • - *
    • v2.2 - Added some helper methods for encoding/decoding directly from - * one file to the next. Also added a main() method to support command line - * encoding/decoding from one file to the next. Also added these Base64 dialects: - *
        - *
      1. The default is RFC3548 format.
      2. - *
      3. Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates - * URL and file name friendly format as described in Section 4 of RFC3548. - * http://www.faqs.org/rfcs/rfc3548.html
      4. - *
      5. Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates - * URL and file name friendly format that preserves lexical ordering as described - * in http://www.faqs.org/qa/rfcc-1940.html
      6. - *
      - * Special thanks to Jim Kellerman at http://www.powerset.com/ - * for contributing the new Base64 dialects. - *
    • - * - *
    • v2.1 - Cleaned up javadoc comments and unused variables and methods. Added - * some convenience methods for reading and writing to and from files.
    • - *
    • v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems - * with other encodings (like EBCDIC).
    • - *
    • v2.0.1 - Fixed an error when decoding a single byte, that is, when the - * encoded data was a single byte.
    • - *
    • v2.0 - I got rid of methods that used booleans to set options. - * Now everything is more consolidated and cleaner. The code now detects - * when data that's being decoded is gzip-compressed and will decompress it - * automatically. Generally things are cleaner. You'll probably have to - * change some method calls that you were making to support the new - * options format (ints that you "OR" together).
    • - *
    • v1.5.1 - Fixed bug when decompressing and decoding to a - * byte[] using decode( String s, boolean gzipCompressed ). - * Added the ability to "suspend" encoding in the Output Stream so - * you can turn on and off the encoding if you need to embed base64 - * data in an otherwise "normal" stream (like an XML file).
    • - *
    • v1.5 - Output stream pases on flush() command but doesn't do anything itself. - * This helps when using GZIP streams. - * Added the ability to GZip-compress objects before encoding them.
    • - *
    • v1.4 - Added helper methods to read/write files.
    • - *
    • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
    • - *
    • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream - * where last buffer being read, if not completely full, was not returned.
    • - *
    • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.
    • - *
    • v1.3.3 - Fixed I/O streams which were totally messed up.
    • + *
    • v2.2.2 - Fixed encodeFileToFile and decodeFileToFile to use the Base64.InputStream class to + * encode and decode on the fly which uses less memory than encoding/decoding an entire file + * into memory before writing. + *
    • v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug when using very small + * files (~< 40 bytes). + *
    • v2.2 - Added some helper methods for encoding/decoding directly from one file to the next. + * Also added a main() method to support command line encoding/decoding from one file to the + * next. Also added these Base64 dialects: + *
        + *
      1. The default is RFC3548 format. + *
      2. Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates URL and file + * name friendly format as described in Section 4 of RFC3548. + * http://www.faqs.org/rfcs/rfc3548.html + *
      3. Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates URL and file + * name friendly format that preserves lexical ordering as described in + * http://www.faqs.org/qa/rfcc-1940.html + *
      + * Special thanks to Jim Kellerman at http://www.powerset.com/ for contributing the new + * Base64 dialects. + *
    • v2.1 - Cleaned up javadoc comments and unused variables and methods. Added some convenience + * methods for reading and writing to and from files. + *
    • v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems with other + * encodings (like EBCDIC). + *
    • v2.0.1 - Fixed an error when decoding a single byte, that is, when the encoded data was a + * single byte. + *
    • v2.0 - I got rid of methods that used booleans to set options. Now everything is more + * consolidated and cleaner. The code now detects when data that's being decoded is + * gzip-compressed and will decompress it automatically. Generally things are cleaner. You'll + * probably have to change some method calls that you were making to support the new options + * format (ints that you "OR" together). + *
    • v1.5.1 - Fixed bug when decompressing and decoding to a byte[] using decode( String s, + * boolean gzipCompressed ). Added the ability to "suspend" encoding in the Output Stream + * so you can turn on and off the encoding if you need to embed base64 data in an otherwise + * "normal" stream (like an XML file). + *
    • v1.5 - Output stream pases on flush() command but doesn't do anything itself. This helps + * when using GZIP streams. Added the ability to GZip-compress objects before encoding them. + *
    • v1.4 - Added helper methods to read/write files. + *
    • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset. + *
    • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream where last + * buffer being read, if not completely full, was not returned. + *
    • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time. + *
    • v1.3.3 - Fixed I/O streams which were totally messed up. *
    * - *

    - * I am placing this code in the Public Domain. Do with it as you will. - * This software comes with no guarantees or warranties but with - * plenty of well-wishing instead! - * Please visit http://iharder.net/base64 - * periodically to check for updates or to contribute improvements. - *

    + *

    I am placing this code in the Public Domain. Do with it as you will. This software comes with + * no guarantees or warranties but with plenty of well-wishing instead! Please visit http://iharder.net/base64 periodically to check for updates + * or to contribute improvements. * * @author Robert Harder * @author rob@iharder.net * @version 2.2.2 */ -public class Base64 -{ - -/* ******** P U B L I C F I E L D S ******** */ - - - /** No options specified. Value is zero. */ - public final static int NO_OPTIONS = 0; - - /** Specify encoding. */ - public final static int ENCODE = 1; - - - /** Specify decoding. */ - public final static int DECODE = 0; - - - /** Specify that data should be gzip-compressed. */ - public final static int GZIP = 2; - - - /** Don't break lines when encoding (violates strict Base64 specification) */ - public final static int DONT_BREAK_LINES = 8; - - /** - * Encode using Base64-like encoding that is URL- and Filename-safe as described - * in Section 4 of RFC3548: - * http://www.faqs.org/rfcs/rfc3548.html. - * It is important to note that data encoded this way is not officially valid Base64, - * or at the very least should not be called Base64 without also specifying that is - * was encoded using the URL- and Filename-safe dialect. - */ - public final static int URL_SAFE = 16; - - - /** - * Encode using the special "ordered" dialect of Base64 described here: - * http://www.faqs.org/qa/rfcc-1940.html. - */ - public final static int ORDERED = 32; - - -/* ******** P R I V A T E F I E L D S ******** */ - - - /** Maximum line length (76) of Base64 output. */ - private final static int MAX_LINE_LENGTH = 76; - - - /** The equals sign (=) as a byte. */ - private final static byte EQUALS_SIGN = (byte)'='; - - - /** The new line character (\n) as a byte. */ - private final static byte NEW_LINE = (byte)'\n'; - - - /** Preferred encoding. */ - private final static String PREFERRED_ENCODING = "UTF-8"; - - - // I think I end up not using the BAD_ENCODING indicator. - //private final static byte BAD_ENCODING = -9; // Indicates error in encoding - private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding - private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding - - -/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ - - /** The 64 valid Base64 values. */ - //private final static byte[] ALPHABET; - /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ - private final static byte[] _STANDARD_ALPHABET = - { - (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', - (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', - (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', - (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', - (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', - (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', - (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', - (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', - (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/' - }; - - - /** - * Translates a Base64 value to either its 6-bit reconstruction value - * or a negative number indicating some other meaning. - **/ - private final static byte[] _STANDARD_DECODABET = - { - -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 - -5,-5, // Whitespace: Tab and Linefeed - -9,-9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 - -9,-9,-9,-9,-9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 - 62, // Plus sign at decimal 43 - -9,-9,-9, // Decimal 44 - 46 - 63, // Slash at decimal 47 - 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine - -9,-9,-9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9,-9,-9, // Decimal 62 - 64 - 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' - 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' - -9,-9,-9,-9,-9,-9, // Decimal 91 - 96 - 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' - 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' - -9,-9,-9,-9 // Decimal 123 - 126 - /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - -/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ - - /** - * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: - * http://www.faqs.org/rfcs/rfc3548.html. - * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash." - */ - private final static byte[] _URL_SAFE_ALPHABET = - { - (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', - (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', - (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', - (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', - (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', - (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', - (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', - (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', - (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_' - }; - - /** - * Used in decoding URL- and Filename-safe dialects of Base64. - */ - private final static byte[] _URL_SAFE_DECODABET = - { - -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 - -5,-5, // Whitespace: Tab and Linefeed - -9,-9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 - -9,-9,-9,-9,-9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 62, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine - -9,-9,-9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9,-9,-9, // Decimal 62 - 64 - 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' - 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' - -9,-9,-9,-9, // Decimal 91 - 94 - 63, // Underscore at decimal 95 - -9, // Decimal 96 - 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' - 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' - -9,-9,-9,-9 // Decimal 123 - 126 - /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - - -/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ - - /** - * I don't get the point of this technique, but it is described here: - * http://www.faqs.org/qa/rfcc-1940.html. - */ - private final static byte[] _ORDERED_ALPHABET = - { - (byte)'-', - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', - (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', - (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', - (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', - (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', - (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', - (byte)'_', - (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', - (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', - (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', - (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z' - }; - - /** - * Used in decoding the "ordered" dialect of Base64. - */ - private final static byte[] _ORDERED_DECODABET = - { - -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 - -5,-5, // Whitespace: Tab and Linefeed - -9,-9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 - -9,-9,-9,-9,-9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 0, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 1,2,3,4,5,6,7,8,9,10, // Numbers zero through nine - -9,-9,-9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9,-9,-9, // Decimal 62 - 64 - 11,12,13,14,15,16,17,18,19,20,21,22,23, // Letters 'A' through 'M' - 24,25,26,27,28,29,30,31,32,33,34,35,36, // Letters 'N' through 'Z' - -9,-9,-9,-9, // Decimal 91 - 94 - 37, // Underscore at decimal 95 - -9, // Decimal 96 - 38,39,40,41,42,43,44,45,46,47,48,49,50, // Letters 'a' through 'm' - 51,52,53,54,55,56,57,58,59,60,61,62,63, // Letters 'n' through 'z' - -9,-9,-9,-9 // Decimal 123 - 126 - /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - -/* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ - - - /** - * Returns one of the _SOMETHING_ALPHABET byte arrays depending on - * the options specified. - * It's possible, though silly, to specify ORDERED and URLSAFE - * in which case one of them will be picked, though there is - * no guarantee as to which one will be picked. - */ - private final static byte[] getAlphabet( int options ) - { - if( (options & URL_SAFE) == URL_SAFE ) return _URL_SAFE_ALPHABET; - else if( (options & ORDERED) == ORDERED ) return _ORDERED_ALPHABET; - else return _STANDARD_ALPHABET; - - } // end getAlphabet - - - /** - * Returns one of the _SOMETHING_DECODABET byte arrays depending on - * the options specified. - * It's possible, though silly, to specify ORDERED and URL_SAFE - * in which case one of them will be picked, though there is - * no guarantee as to which one will be picked. - */ - private final static byte[] getDecodabet( int options ) - { - if( (options & URL_SAFE) == URL_SAFE ) return _URL_SAFE_DECODABET; - else if( (options & ORDERED) == ORDERED ) return _ORDERED_DECODABET; - else return _STANDARD_DECODABET; - - } // end getAlphabet - - - - /** Defeats instantiation. */ - private Base64(){} - +public class Base64 { - /** - * Encodes or decodes two files from the command line; - * feel free to delete this method (in fact you probably should) - * if you're embedding this code into a larger program. - */ - public final static void main( String[] args ) - { - if( args.length < 3 ){ - usage("Not enough arguments."); - } // end if: args.length < 3 - else { - String flag = args[0]; - String infile = args[1]; - String outfile = args[2]; - if( flag.equals( "-e" ) ){ - Base64.encodeFileToFile( infile, outfile ); - } // end if: encode - else if( flag.equals( "-d" ) ) { - Base64.decodeFileToFile( infile, outfile ); - } // end else if: decode - else { - usage( "Unknown flag: " + flag ); - } // end else - } // end else - } // end main + /* ******** P U B L I C F I E L D S ******** */ - /** - * Prints command line usage. - * - * @param msg A message to include with usage info. - */ - private final static void usage( String msg ) - { - System.err.println( msg ); - System.err.println( "Usage: java Base64 -e|-d inputfile outputfile" ); - } // end usage - - -/* ******** E N C O D I N G M E T H O D S ******** */ - - - /** - * Encodes up to the first three bytes of array threeBytes - * and returns a four-byte array in Base64 notation. - * The actual number of significant bytes in your array is - * given by numSigBytes. - * The array threeBytes needs only be as big as - * numSigBytes. - * Code can reuse a byte array by passing a four-byte array as b4. - * - * @param b4 A reusable byte array to reduce array instantiation - * @param threeBytes the array to convert - * @param numSigBytes the number of significant bytes in your array - * @return four byte array in Base64 notation. - * @since 1.5.1 - */ - private static byte[] encode3to4( byte[] b4, byte[] threeBytes, int numSigBytes, int options ) - { - encode3to4( threeBytes, 0, numSigBytes, b4, 0, options ); - return b4; - } // end encode3to4 + /** No options specified. Value is zero. */ + public static final int NO_OPTIONS = 0; + + /** Specify encoding. */ + public static final int ENCODE = 1; + + /** Specify decoding. */ + public static final int DECODE = 0; + + /** Specify that data should be gzip-compressed. */ + public static final int GZIP = 2; + + /** Don't break lines when encoding (violates strict Base64 specification) */ + public static final int DONT_BREAK_LINES = 8; + + /** + * Encode using Base64-like encoding that is URL- and Filename-safe as described in Section 4 of + * RFC3548: http://www.faqs.org/rfcs/rfc3548.html. It is + * important to note that data encoded this way is not officially valid Base64, or at the + * very least should not be called Base64 without also specifying that is was encoded using the + * URL- and Filename-safe dialect. + */ + public static final int URL_SAFE = 16; + + /** + * Encode using the special "ordered" dialect of Base64 described here: http://www.faqs.org/qa/rfcc-1940.html. + */ + public static final int ORDERED = 32; + + /* ******** P R I V A T E F I E L D S ******** */ + + /** Maximum line length (76) of Base64 output. */ + private static final int MAX_LINE_LENGTH = 76; + + /** The equals sign (=) as a byte. */ + private static final byte EQUALS_SIGN = (byte) '='; + + /** The new line character (\n) as a byte. */ + private static final byte NEW_LINE = (byte) '\n'; + + /** Preferred encoding. */ + private static final String PREFERRED_ENCODING = "UTF-8"; + + // I think I end up not using the BAD_ENCODING indicator. + // private final static byte BAD_ENCODING = -9; // Indicates error in encoding + private static final byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding + private static final byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding + + /* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ + + /** The 64 valid Base64 values. */ + // private final static byte[] ALPHABET; + /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ + private static final byte[] _STANDARD_ALPHABET = { + (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', (byte) 'H', + (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', + (byte) 'Y', (byte) 'Z', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', + (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', + (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', + (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '+', (byte) '/' + }; + + /** + * Translates a Base64 value to either its 6-bit reconstruction value or a negative number + * indicating some other meaning. + */ + private static final byte[] _STANDARD_DECODABET = { + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, // Decimal 0 - 8 + -5, + -5, // Whitespace: Tab and Linefeed + -9, + -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, // Decimal 14 - 26 + -9, + -9, + -9, + -9, + -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9, + -9, + -9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, // Numbers zero through nine + -9, + -9, + -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, + -9, + -9, // Decimal 62 - 64 + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, // Letters 'A' through 'N' + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, // Letters 'O' through 'Z' + -9, + -9, + -9, + -9, + -9, + -9, // Decimal 91 - 96 + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, // Letters 'a' through 'm' + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, // Letters 'n' through 'z' + -9, + -9, + -9, + -9 // Decimal 123 - 126 + /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + /* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ + + /** + * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: http://www.faqs.org/rfcs/rfc3548.html. Notice + * that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash." + */ + private static final byte[] _URL_SAFE_ALPHABET = { + (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', (byte) 'H', + (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', + (byte) 'Y', (byte) 'Z', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', + (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', + (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', + (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '-', (byte) '_' + }; + + /** Used in decoding URL- and Filename-safe dialects of Base64. */ + private static final byte[] _URL_SAFE_DECODABET = { + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, // Decimal 0 - 8 + -5, + -5, // Whitespace: Tab and Linefeed + -9, + -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, // Decimal 14 - 26 + -9, + -9, + -9, + -9, + -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 62, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, // Numbers zero through nine + -9, + -9, + -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, + -9, + -9, // Decimal 62 - 64 + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, // Letters 'A' through 'N' + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, // Letters 'O' through 'Z' + -9, + -9, + -9, + -9, // Decimal 91 - 94 + 63, // Underscore at decimal 95 + -9, // Decimal 96 + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, // Letters 'a' through 'm' + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, // Letters 'n' through 'z' + -9, + -9, + -9, + -9 // Decimal 123 - 126 + /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + /* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ + + /** + * I don't get the point of this technique, but it is described here: http://www.faqs.org/qa/rfcc-1940.html. + */ + private static final byte[] _ORDERED_ALPHABET = { + (byte) '-', (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', + (byte) '7', (byte) '8', (byte) '9', (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', + (byte) 'F', (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', + (byte) 'N', (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) '_', (byte) 'a', (byte) 'b', + (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', + (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', + (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z' + }; + + /** Used in decoding the "ordered" dialect of Base64. */ + private static final byte[] _ORDERED_DECODABET = { + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, // Decimal 0 - 8 + -5, + -5, // Whitespace: Tab and Linefeed + -9, + -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, // Decimal 14 - 26 + -9, + -9, + -9, + -9, + -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, + -9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 0, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, // Numbers zero through nine + -9, + -9, + -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, + -9, + -9, // Decimal 62 - 64 + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, // Letters 'A' through 'M' + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, // Letters 'N' through 'Z' + -9, + -9, + -9, + -9, // Decimal 91 - 94 + 37, // Underscore at decimal 95 + -9, // Decimal 96 + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, // Letters 'a' through 'm' + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, // Letters 'n' through 'z' + -9, + -9, + -9, + -9 // Decimal 123 - 126 + /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + /* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ + + /** + * Returns one of the _SOMETHING_ALPHABET byte arrays depending on the options specified. It's + * possible, though silly, to specify ORDERED and URLSAFE in which case one of them will be + * picked, though there is no guarantee as to which one will be picked. + */ + private static final byte[] getAlphabet(int options) { + if ((options & URL_SAFE) == URL_SAFE) return _URL_SAFE_ALPHABET; + else if ((options & ORDERED) == ORDERED) return _ORDERED_ALPHABET; + else return _STANDARD_ALPHABET; + } // end getAlphabet + + /** + * Returns one of the _SOMETHING_DECODABET byte arrays depending on the options specified. It's + * possible, though silly, to specify ORDERED and URL_SAFE in which case one of them will be + * picked, though there is no guarantee as to which one will be picked. + */ + private static final byte[] getDecodabet(int options) { + if ((options & URL_SAFE) == URL_SAFE) return _URL_SAFE_DECODABET; + else if ((options & ORDERED) == ORDERED) return _ORDERED_DECODABET; + else return _STANDARD_DECODABET; + } // end getAlphabet + + /** Defeats instantiation. */ + private Base64() {} + + /** + * Encodes or decodes two files from the command line; feel free to delete this method (in + * fact you probably should) if you're embedding this code into a larger program. + */ + public static final void main(String[] args) { + if (args.length < 3) { + usage("Not enough arguments."); + } // end if: args.length < 3 + else { + String flag = args[0]; + String infile = args[1]; + String outfile = args[2]; + if (flag.equals("-e")) { + Base64.encodeFileToFile(infile, outfile); + } // end if: encode + else if (flag.equals("-d")) { + Base64.decodeFileToFile(infile, outfile); + } // end else if: decode + else { + usage("Unknown flag: " + flag); + } // end else + } // end else + } // end main + + /** + * Prints command line usage. + * + * @param msg A message to include with usage info. + */ + private static final void usage(String msg) { + System.err.println(msg); + System.err.println("Usage: java Base64 -e|-d inputfile outputfile"); + } // end usage + + /* ******** E N C O D I N G M E T H O D S ******** */ + + /** + * Encodes up to the first three bytes of array threeBytes and returns a four-byte + * array in Base64 notation. The actual number of significant bytes in your array is given by + * numSigBytes. The array threeBytes needs only be as big as + * numSigBytes. Code can reuse a byte array by passing a four-byte array as + * b4. + * + * @param b4 A reusable byte array to reduce array instantiation + * @param threeBytes the array to convert + * @param numSigBytes the number of significant bytes in your array + * @return four byte array in Base64 notation. + * @since 1.5.1 + */ + private static byte[] encode3to4(byte[] b4, byte[] threeBytes, int numSigBytes, int options) { + encode3to4(threeBytes, 0, numSigBytes, b4, 0, options); + return b4; + } // end encode3to4 + + /** + * Encodes up to three bytes of the array source and writes the resulting four Base64 + * bytes to destination. The source and destination arrays can be manipulated anywhere + * along their length by specifying srcOffset and destOffset. This method + * does not check to make sure your arrays are large enough to accomodate srcOffset + 3 + * for the source array or destOffset + 4 for the destination + * array. The actual number of significant bytes in your array is given by numSigBytes. + * + *

    This is the lowest level of the encoding methods with all possible parameters. + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @return the destination array + * @since 1.3 + */ + private static byte[] encode3to4( + byte[] source, + int srcOffset, + int numSigBytes, + byte[] destination, + int destOffset, + int options) { + byte[] ALPHABET = getAlphabet(options); + + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index ALPHABET + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = + (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) + | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) + | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + + switch (numSigBytes) { + case 3: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f]; + return destination; + + case 2: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + case 1: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = EQUALS_SIGN; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + default: + return destination; + } // end switch + } // end encode3to4 + + /** + * Serializes an object and returns the Base64-encoded version of that serialized object. If the + * object cannot be serialized or there is another error, the method will return null. + * The object is not GZip-compressed before being encoded. + * + * @param serializableObject The object to encode + * @return The Base64-encoded object + * @since 1.4 + */ + public static String encodeObject(java.io.Serializable serializableObject) { + return encodeObject(serializableObject, NO_OPTIONS); + } // end encodeObject + + /** + * Serializes an object and returns the Base64-encoded version of that serialized object. If the + * object cannot be serialized or there is another error, the method will return null. + * + *

    Valid options: + * + *

    +   *   GZIP: gzip-compresses object before encoding it.
    +   *   DONT_BREAK_LINES: don't break lines at 76 characters
    +   *     Note: Technically, this makes your encoding non-compliant.
    +   * 
    + * + *

    Example: encodeObject( myObj, Base64.GZIP ) or + * + *

    Example: encodeObject( myObj, Base64.GZIP | Base64.DONT_BREAK_LINES ) + * + * @param serializableObject The object to encode + * @param options Specified options + * @return The Base64-encoded object + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeObject(java.io.Serializable serializableObject, int options) { + // Streams + java.io.ByteArrayOutputStream baos = null; + java.io.OutputStream b64os = null; + java.io.ObjectOutputStream oos = null; + java.util.zip.GZIPOutputStream gzos = null; + + // Isolate options + int gzip = (options & GZIP); + @SuppressWarnings("unused") + int dontBreakLines = (options & DONT_BREAK_LINES); + + try { + // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | options); + + // GZip? + if (gzip == GZIP) { + gzos = new java.util.zip.GZIPOutputStream(b64os); + oos = new java.io.ObjectOutputStream(gzos); + } // end if: gzip + else oos = new java.io.ObjectOutputStream(b64os); + + oos.writeObject(serializableObject); + } // end try + catch (java.io.IOException e) { + e.printStackTrace(); + return null; + } // end catch + finally { + try { + oos.close(); + } catch (Exception e) { + } + try { + gzos.close(); + } catch (Exception e) { + } + try { + b64os.close(); + } catch (Exception e) { + } + try { + baos.close(); + } catch (Exception e) { + } + } // end finally + + // Return value according to relevant encoding. + try { + return new String(baos.toByteArray(), PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String(baos.toByteArray()); + } // end catch + } // end encode + + /** + * Encodes a byte array into Base64 notation. Does not GZip-compress data. + * + * @param source The data to convert + * @since 1.4 + */ + public static String encodeBytes(byte[] source) { + return encodeBytes(source, 0, source.length, NO_OPTIONS); + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation. + * + *

    Valid options: + * + *

    +   *   GZIP: gzip-compresses object before encoding it.
    +   *   DONT_BREAK_LINES: don't break lines at 76 characters
    +   *     Note: Technically, this makes your encoding non-compliant.
    +   * 
    + * + *

    Example: encodeBytes( myData, Base64.GZIP ) or + * + *

    Example: encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES ) + * + * @param source The data to convert + * @param options Specified options + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes(byte[] source, int options) { + return encodeBytes(source, 0, source.length, options); + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation. Does not GZip-compress data. + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @since 1.4 + */ + public static String encodeBytes(byte[] source, int off, int len) { + return encodeBytes(source, off, len, NO_OPTIONS); + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation. + * + *

    Valid options: + * + *

    +   *   GZIP: gzip-compresses object before encoding it.
    +   *   DONT_BREAK_LINES: don't break lines at 76 characters
    +   *     Note: Technically, this makes your encoding non-compliant.
    +   * 
    + * + *

    Example: encodeBytes( myData, Base64.GZIP ) or + * + *

    Example: encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES ) + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes(byte[] source, int off, int len, int options) { + // Isolate options + int dontBreakLines = (options & DONT_BREAK_LINES); + int gzip = (options & GZIP); + + // Compress? + if (gzip == GZIP) { + java.io.ByteArrayOutputStream baos = null; + java.util.zip.GZIPOutputStream gzos = null; + Base64.OutputStream b64os = null; + + try { + // GZip -> Base64 -> ByteArray + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | options); + gzos = new java.util.zip.GZIPOutputStream(b64os); + + gzos.write(source, off, len); + gzos.close(); + } // end try + catch (java.io.IOException e) { + e.printStackTrace(); + return null; + } // end catch + finally { + try { + gzos.close(); + } catch (Exception e) { + } + try { + b64os.close(); + } catch (Exception e) { + } + try { + baos.close(); + } catch (Exception e) { + } + } // end finally + + // Return value according to relevant encoding. + try { + return new String(baos.toByteArray(), PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String(baos.toByteArray()); + } // end catch + } // end if: compress + + // Else, don't compress. Better not to use streams at all then. + else { + // Convert option to boolean in way that code likes it. + boolean breakLines = dontBreakLines == 0; + + int len43 = len * 4 / 3; + byte[] outBuff = + new byte + [(len43) // Main 4:3 + + ((len % 3) > 0 ? 4 : 0) // Account for padding + + (breakLines ? (len43 / MAX_LINE_LENGTH) : 0)]; // New lines + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for (; d < len2; d += 3, e += 4) { + encode3to4(source, d + off, 3, outBuff, e, options); + + lineLength += 4; + if (breakLines && lineLength == MAX_LINE_LENGTH) { + outBuff[e + 4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // en dfor: each piece of array + + if (d < len) { + encode3to4(source, d + off, len - d, outBuff, e, options); + e += 4; + } // end if: some padding needed + + // Return value according to relevant encoding. + try { + return new String(outBuff, 0, e, PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String(outBuff, 0, e); + } // end catch + } // end else: don't compress + } // end encodeBytes + + /* ******** D E C O D I N G M E T H O D S ******** */ + + /** + * Decodes four bytes from array source and writes the resulting bytes (up to three of + * them) to destination. The source and destination arrays can be manipulated anywhere + * along their length by specifying srcOffset and destOffset. This method + * does not check to make sure your arrays are large enough to accomodate srcOffset + 4 + * for the source array or destOffset + 3 for the destination + * array. This method returns the actual number of bytes that were converted from the Base64 + * encoding. + * + *

    This is the lowest level of the decoding methods with all possible parameters. + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param options alphabet type is pulled from this (standard, url-safe, ordered) + * @return the number of decoded bytes converted + * @since 1.3 + */ + private static int decode4to3( + byte[] source, int srcOffset, byte[] destination, int destOffset, int options) { + byte[] DECODABET = getDecodabet(options); + + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); + int outBuff = + ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12); + + destination[destOffset] = (byte) (outBuff >>> 16); + return 1; + } + + // Example: DkL= + else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); + int outBuff = + ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6); + + destination[destOffset] = (byte) (outBuff >>> 16); + destination[destOffset + 1] = (byte) (outBuff >>> 8); + return 2; + } + + // Example: DkLE + else { + try { + // Two ways to do the same thing. Don't know which way I like best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) + // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); + int outBuff = + ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6) + | ((DECODABET[source[srcOffset + 3]] & 0xFF)); + + destination[destOffset] = (byte) (outBuff >> 16); + destination[destOffset + 1] = (byte) (outBuff >> 8); + destination[destOffset + 2] = (byte) (outBuff); + + return 3; + } catch (Exception e) { + System.out.println("" + source[srcOffset] + ": " + (DECODABET[source[srcOffset]])); + System.out.println("" + source[srcOffset + 1] + ": " + (DECODABET[source[srcOffset + 1]])); + System.out.println("" + source[srcOffset + 2] + ": " + (DECODABET[source[srcOffset + 2]])); + System.out.println("" + source[srcOffset + 3] + ": " + (DECODABET[source[srcOffset + 3]])); + return -1; + } // end catch + } + } // end decodeToBytes + + /** + * Very low-level access to decoding ASCII characters in the form of a byte array. Does not + * support automatically gunzipping or any other "fancy" features. + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @return decoded data + * @since 1.3 + */ + public static byte[] decode(byte[] source, int off, int len, int options) { + byte[] DECODABET = getDecodabet(options); + + int len34 = len * 3 / 4; + byte[] outBuff = new byte[len34]; // Upper limit on size of output + int outBuffPosn = 0; + + byte[] b4 = new byte[4]; + int b4Posn = 0; + int i = 0; + byte sbiCrop = 0; + byte sbiDecode = 0; + for (i = off; i < off + len; i++) { + sbiCrop = (byte) (source[i] & 0x7f); // Only the low seven bits + sbiDecode = DECODABET[sbiCrop]; + + if (sbiDecode >= WHITE_SPACE_ENC) // White space, Equals sign or better + { + if (sbiDecode >= EQUALS_SIGN_ENC) { + b4[b4Posn++] = sbiCrop; + if (b4Posn > 3) { + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, options); + b4Posn = 0; + + // If that was the equals sign, break out of 'for' loop + if (sbiCrop == EQUALS_SIGN) break; + } // end if: quartet built + } // end if: equals sign or better + + } // end if: white space, equals sign or better + else { + System.err.println("Bad Base64 input character at " + i + ": " + source[i] + "(decimal)"); + return null; + } // end else: + } // each input character + + byte[] out = new byte[outBuffPosn]; + System.arraycopy(outBuff, 0, out, 0, outBuffPosn); + return out; + } // end decode + + /** + * Decodes data from Base64 notation, automatically detecting gzip-compressed data and + * decompressing it. + * + * @param s the string to decode + * @return the decoded data + * @since 1.4 + */ + public static byte[] decode(String s) { + return decode(s, NO_OPTIONS); + } + + /** + * Decodes data from Base64 notation, automatically detecting gzip-compressed data and + * decompressing it. + * + * @param s the string to decode + * @param options encode options such as URL_SAFE + * @return the decoded data + * @since 1.4 + */ + public static byte[] decode(String s, int options) { + byte[] bytes; + try { + bytes = s.getBytes(PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uee) { + bytes = s.getBytes(); + } // end catch + // + + // Decode + bytes = decode(bytes, 0, bytes.length, options); + + // Check to see if it's gzip-compressed + // GZIP Magic Two-Byte Number: 0x8b1f (35615) + if (bytes != null && bytes.length >= 4) { + + int head = ((int) bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); + if (java.util.zip.GZIPInputStream.GZIP_MAGIC == head) { + java.io.ByteArrayInputStream bais = null; + java.util.zip.GZIPInputStream gzis = null; + java.io.ByteArrayOutputStream baos = null; + byte[] buffer = new byte[2048]; + int length = 0; + + try { + baos = new java.io.ByteArrayOutputStream(); + bais = new java.io.ByteArrayInputStream(bytes); + gzis = new java.util.zip.GZIPInputStream(bais); + + while ((length = gzis.read(buffer)) >= 0) { + baos.write(buffer, 0, length); + } // end while: reading input + + // No error? Get new bytes. + bytes = baos.toByteArray(); + + } // end try + catch (java.io.IOException e) { + // Just return originally-decoded bytes + } // end catch + finally { + try { + baos.close(); + } catch (Exception e) { + } + try { + gzis.close(); + } catch (Exception e) { + } + try { + bais.close(); + } catch (Exception e) { + } + } // end finally + } // end if: gzipped + } // end if: bytes.length >= 2 + + return bytes; + } // end decode + + /** + * Attempts to decode Base64 data and deserialize a Java Object within. Returns null if + * there was an error. + * + * @param encodedObject The Base64 data to decode + * @return The decoded and deserialized object + * @since 1.5 + */ + public static Object decodeToObject(String encodedObject) { + // Decode and gunzip if necessary + byte[] objBytes = decode(encodedObject); + + java.io.ByteArrayInputStream bais = null; + java.io.ObjectInputStream ois = null; + Object obj = null; + + try { + bais = new java.io.ByteArrayInputStream(objBytes); + ois = new java.io.ObjectInputStream(bais); + + obj = ois.readObject(); + } // end try + catch (java.io.IOException e) { + e.printStackTrace(); + obj = null; + } // end catch + catch (java.lang.ClassNotFoundException e) { + e.printStackTrace(); + obj = null; + } // end catch + finally { + try { + bais.close(); + } catch (Exception e) { + } + try { + ois.close(); + } catch (Exception e) { + } + } // end finally + + return obj; + } // end decodeObject + + /** + * Convenience method for encoding data to a file. + * + * @param dataToEncode byte array of data to encode in base64 form + * @param filename Filename for saving encoded data + * @return true if successful, false otherwise + * @since 2.1 + */ + public static boolean encodeToFile(byte[] dataToEncode, String filename) { + boolean success = false; + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream(new java.io.FileOutputStream(filename), Base64.ENCODE); + bos.write(dataToEncode); + success = true; + } // end try + catch (java.io.IOException e) { + + success = false; + } // end catch: IOException + finally { + try { + bos.close(); + } catch (Exception e) { + } + } // end finally + + return success; + } // end encodeToFile + + /** + * Convenience method for decoding data to a file. + * + * @param dataToDecode Base64-encoded data as a string + * @param filename Filename for saving decoded data + * @return true if successful, false otherwise + * @since 2.1 + */ + public static boolean decodeToFile(String dataToDecode, String filename) { + boolean success = false; + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream(new java.io.FileOutputStream(filename), Base64.DECODE); + bos.write(dataToDecode.getBytes(PREFERRED_ENCODING)); + success = true; + } // end try + catch (java.io.IOException e) { + success = false; + } // end catch: IOException + finally { + try { + bos.close(); + } catch (Exception e) { + } + } // end finally + + return success; + } // end decodeToFile + + /** + * Convenience method for reading a base64-encoded file and decoding it. + * + * @param filename Filename for reading encoded data + * @return decoded byte array or null if unsuccessful + * @since 2.1 + */ + public static byte[] decodeFromFile(String filename) { + byte[] decodedData = null; + Base64.InputStream bis = null; + try { + // Set up some useful variables + java.io.File file = new java.io.File(filename); + byte[] buffer = null; + int length = 0; + int numBytes = 0; + + // Check for size of file + if (file.length() > Integer.MAX_VALUE) { + System.err.println( + "File is too big for this convenience method (" + file.length() + " bytes)."); + return null; + } // end if: file too big for int index + buffer = new byte[(int) file.length()]; + + // Open a stream + bis = + new Base64.InputStream( + new java.io.BufferedInputStream(new java.io.FileInputStream(file)), Base64.DECODE); + + // Read until done + while ((numBytes = bis.read(buffer, length, 4096)) >= 0) length += numBytes; + + // Save in a variable to return + decodedData = new byte[length]; + System.arraycopy(buffer, 0, decodedData, 0, length); + + } // end try + catch (java.io.IOException e) { + System.err.println("Error decoding from file " + filename); + } // end catch: IOException + finally { + try { + bis.close(); + } catch (Exception e) { + } + } // end finally + + return decodedData; + } // end decodeFromFile + + /** + * Convenience method for reading a binary file and base64-encoding it. + * + * @param filename Filename for reading binary data + * @return base64-encoded string or null if unsuccessful + * @since 2.1 + */ + public static String encodeFromFile(String filename) { + String encodedData = null; + Base64.InputStream bis = null; + try { + // Set up some useful variables + java.io.File file = new java.io.File(filename); + byte[] buffer = + new byte + [Math.max( + (int) (file.length() * 1.4), 40)]; // Need max() for math on small files (v2.2.1) + int length = 0; + int numBytes = 0; + + // Open a stream + bis = + new Base64.InputStream( + new java.io.BufferedInputStream(new java.io.FileInputStream(file)), Base64.ENCODE); + + // Read until done + while ((numBytes = bis.read(buffer, length, 4096)) >= 0) length += numBytes; + + // Save in a variable to return + encodedData = new String(buffer, 0, length, Base64.PREFERRED_ENCODING); + + } // end try + catch (java.io.IOException e) { + System.err.println("Error encoding from file " + filename); + } // end catch: IOException + finally { + try { + bis.close(); + } catch (Exception e) { + } + } // end finally + + return encodedData; + } // end encodeFromFile + + /** + * Reads infile and encodes it to outfile. + * + * @param infile Input file + * @param outfile Output file + * @return true if the operation is successful + * @since 2.2 + */ + public static boolean encodeFileToFile(String infile, String outfile) { + boolean success = false; + java.io.InputStream in = null; + java.io.OutputStream out = null; + try { + in = + new Base64.InputStream( + new java.io.BufferedInputStream(new java.io.FileInputStream(infile)), Base64.ENCODE); + out = new java.io.BufferedOutputStream(new java.io.FileOutputStream(outfile)); + byte[] buffer = new byte[65536]; // 64K + int read = -1; + while ((read = in.read(buffer)) >= 0) { + out.write(buffer, 0, read); + } // end while: through file + success = true; + } catch (java.io.IOException exc) { + exc.printStackTrace(); + } finally { + try { + in.close(); + } catch (Exception exc) { + } + try { + out.close(); + } catch (Exception exc) { + } + } // end finally + + return success; + } // end encodeFileToFile + + /** + * Reads infile and decodes it to outfile. + * + * @param infile Input file + * @param outfile Output file + * @return true if the operation is successful + * @since 2.2 + */ + public static boolean decodeFileToFile(String infile, String outfile) { + boolean success = false; + java.io.InputStream in = null; + java.io.OutputStream out = null; + try { + in = + new Base64.InputStream( + new java.io.BufferedInputStream(new java.io.FileInputStream(infile)), Base64.DECODE); + out = new java.io.BufferedOutputStream(new java.io.FileOutputStream(outfile)); + byte[] buffer = new byte[65536]; // 64K + int read = -1; + while ((read = in.read(buffer)) >= 0) { + out.write(buffer, 0, read); + } // end while: through file + success = true; + } catch (java.io.IOException exc) { + exc.printStackTrace(); + } finally { + try { + in.close(); + } catch (Exception exc) { + } + try { + out.close(); + } catch (Exception exc) { + } + } // end finally + + return success; + } // end decodeFileToFile + + /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ + + /** + * A {@link Base64.InputStream} will read data from another java.io.InputStream, given in + * the constructor, and encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class InputStream extends java.io.FilterInputStream { + private boolean encode; // Encoding or decoding + private int position; // Current position in the buffer + private byte[] buffer; // Small buffer holding converted data + private int bufferLength; // Length of buffer (3 or 4) + private int numSigBytes; // Number of meaningful bytes in the buffer + private int lineLength; + private boolean breakLines; // Break lines at less than 80 characters + private int options; // Record options used to create the stream. + + @SuppressWarnings("unused") + private byte[] alphabet; // Local copies to avoid extra method calls + + private byte[] decodabet; // Local copies to avoid extra method calls - /** - *

    Encodes up to three bytes of the array source - * and writes the resulting four Base64 bytes to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accomodate srcOffset + 3 for - * the source array or destOffset + 4 for - * the destination array. - * The actual number of significant bytes in your array is - * given by numSigBytes.

    - *

    This is the lowest level of the encoding methods with - * all possible parameters.

    + * Constructs a {@link Base64.InputStream} in DECODE mode. * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param numSigBytes the number of significant bytes in your array - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @return the destination array + * @param in the java.io.InputStream from which to read data. * @since 1.3 */ - private static byte[] encode3to4( - byte[] source, int srcOffset, int numSigBytes, - byte[] destination, int destOffset, int options ) - { - byte[] ALPHABET = getAlphabet( options ); - - // 1 2 3 - // 01234567890123456789012345678901 Bit position - // --------000000001111111122222222 Array position from threeBytes - // --------| || || || | Six bit groups to index ALPHABET - // >>18 >>12 >> 6 >> 0 Right shift necessary - // 0x3f 0x3f 0x3f Additional AND - - // Create buffer with zero-padding if there are only one or two - // significant bytes passed in the array. - // We have to shift left 24 in order to flush out the 1's that appear - // when Java treats a value as negative that is cast from a byte to an int. - int inBuff = ( numSigBytes > 0 ? ((source[ srcOffset ] << 24) >>> 8) : 0 ) - | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 ) - | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 ); - - switch( numSigBytes ) - { - case 3: - destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; - destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; - destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; - destination[ destOffset + 3 ] = ALPHABET[ (inBuff ) & 0x3f ]; - return destination; - - case 2: - destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; - destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; - destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; - destination[ destOffset + 3 ] = EQUALS_SIGN; - return destination; - - case 1: - destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; - destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; - destination[ destOffset + 2 ] = EQUALS_SIGN; - destination[ destOffset + 3 ] = EQUALS_SIGN; - return destination; - - default: - return destination; - } // end switch - } // end encode3to4 - - - - /** - * Serializes an object and returns the Base64-encoded - * version of that serialized object. If the object - * cannot be serialized or there is another error, - * the method will return null. - * The object is not GZip-compressed before being encoded. - * - * @param serializableObject The object to encode - * @return The Base64-encoded object - * @since 1.4 - */ - public static String encodeObject( java.io.Serializable serializableObject ) - { - return encodeObject( serializableObject, NO_OPTIONS ); - } // end encodeObject - - + public InputStream(java.io.InputStream in) { + this(in, DECODE); + } // end constructor /** - * Serializes an object and returns the Base64-encoded - * version of that serialized object. If the object - * cannot be serialized or there is another error, - * the method will return null. - *

    - * Valid options:

    -     *   GZIP: gzip-compresses object before encoding it.
    +     * Constructs a {@link Base64.InputStream} in either ENCODE or DECODE mode.
    +     *
    +     * 

    Valid options: + * + *

    +     *   ENCODE or DECODE: Encode or Decode as data is read.
          *   DONT_BREAK_LINES: don't break lines at 76 characters
    +     *     (only meaningful when encoding)
          *     Note: Technically, this makes your encoding non-compliant.
          * 
    - *

    - * Example: encodeObject( myObj, Base64.GZIP ) or - *

    - * Example: encodeObject( myObj, Base64.GZIP | Base64.DONT_BREAK_LINES ) * - * @param serializableObject The object to encode + *

    Example: new Base64.InputStream( in, Base64.DECODE ) + * + * @param in the java.io.InputStream from which to read data. * @param options Specified options - * @return The Base64-encoded object - * @see Base64#GZIP + * @see Base64#ENCODE + * @see Base64#DECODE * @see Base64#DONT_BREAK_LINES * @since 2.0 */ - public static String encodeObject( java.io.Serializable serializableObject, int options ) - { - // Streams - java.io.ByteArrayOutputStream baos = null; - java.io.OutputStream b64os = null; - java.io.ObjectOutputStream oos = null; - java.util.zip.GZIPOutputStream gzos = null; - - // Isolate options - int gzip = (options & GZIP); - @SuppressWarnings("unused") - int dontBreakLines = (options & DONT_BREAK_LINES); - - try - { - // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream - baos = new java.io.ByteArrayOutputStream(); - b64os = new Base64.OutputStream( baos, ENCODE | options ); - - // GZip? - if( gzip == GZIP ) - { - gzos = new java.util.zip.GZIPOutputStream( b64os ); - oos = new java.io.ObjectOutputStream( gzos ); - } // end if: gzip - else - oos = new java.io.ObjectOutputStream( b64os ); - - oos.writeObject( serializableObject ); - } // end try - catch( java.io.IOException e ) - { - e.printStackTrace(); - return null; - } // end catch - finally - { - try{ oos.close(); } catch( Exception e ){} - try{ gzos.close(); } catch( Exception e ){} - try{ b64os.close(); } catch( Exception e ){} - try{ baos.close(); } catch( Exception e ){} - } // end finally - - // Return value according to relevant encoding. - try - { - return new String( baos.toByteArray(), PREFERRED_ENCODING ); - } // end try - catch (java.io.UnsupportedEncodingException uue) - { - return new String( baos.toByteArray() ); - } // end catch - - } // end encode - - + public InputStream(java.io.InputStream in, int options) { + super(in); + this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES; + this.encode = (options & ENCODE) == ENCODE; + this.bufferLength = encode ? 4 : 3; + this.buffer = new byte[bufferLength]; + this.position = -1; + this.lineLength = 0; + this.options = options; // Record for later, mostly to determine which alphabet to use + this.alphabet = getAlphabet(options); + this.decodabet = getDecodabet(options); + } // end constructor /** - * Encodes a byte array into Base64 notation. - * Does not GZip-compress data. + * Reads enough of the input stream to convert to/from Base64 and returns the next byte. * - * @param source The data to convert - * @since 1.4 + * @return next byte + * @since 1.3 */ - public static String encodeBytes( byte[] source ) - { - return encodeBytes( source, 0, source.length, NO_OPTIONS ); - } // end encodeBytes - + @Override + public int read() throws java.io.IOException { + // Do we need to get data? + if (position < 0) { + if (encode) { + byte[] b3 = new byte[3]; + int numBinaryBytes = 0; + for (int i = 0; i < 3; i++) { + try { + int b = in.read(); + + // If end of stream, b is -1. + if (b >= 0) { + b3[i] = (byte) b; + numBinaryBytes++; + } // end if: not end of stream + + } // end try: read + catch (java.io.IOException e) { + // Only a problem if we got no data at all. + if (i == 0) throw e; + } // end catch + } // end for: each needed input byte + + if (numBinaryBytes > 0) { + encode3to4(b3, 0, numBinaryBytes, buffer, 0, options); + position = 0; + numSigBytes = 4; + } // end if: got data + else { + return -1; + } // end else + } // end if: encoding + // Else decoding + else { + byte[] b4 = new byte[4]; + int i = 0; + for (i = 0; i < 4; i++) { + // Read four "meaningful" bytes: + int b = 0; + do { + b = in.read(); + } while (b >= 0 && decodabet[b & 0x7f] <= WHITE_SPACE_ENC); + + if (b < 0) break; // Reads a -1 if end of stream + + b4[i] = (byte) b; + } // end for: each needed input byte + + if (i == 4) { + numSigBytes = decode4to3(b4, 0, buffer, 0, options); + position = 0; + } // end if: got four characters + else if (i == 0) { + return -1; + } // end else if: also padded correctly + else { + // Must have broken out from above. + throw new java.io.IOException("Improperly padded Base64 input."); + } // end + } // end else: decode + } // end else: get data + + // Got data? + if (position >= 0) { + // End of relevant data? + if (/*!encode &&*/ position >= numSigBytes) return -1; + + if (encode && breakLines && lineLength >= MAX_LINE_LENGTH) { + lineLength = 0; + return '\n'; + } // end if + else { + lineLength++; // This isn't important when decoding + // but throwing an extra "if" seems + // just as wasteful. + + int b = buffer[position++]; + + if (position >= bufferLength) position = -1; + + return b & 0xFF; // This is how you "cast" a byte that's + // intended to be unsigned. + } // end else + } // end if: position >= 0 + + // Else error + else { + // When JDK1.4 is more accepted, use an assertion here. + throw new java.io.IOException("Error in Base64 code reading stream."); + } // end else + } // end read /** - * Encodes a byte array into Base64 notation. - *

    - * Valid options:

    -     *   GZIP: gzip-compresses object before encoding it.
    -     *   DONT_BREAK_LINES: don't break lines at 76 characters
    -     *     Note: Technically, this makes your encoding non-compliant.
    -     * 
    - *

    - * Example: encodeBytes( myData, Base64.GZIP ) or - *

    - * Example: encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES ) + * Calls {@link #read()} repeatedly until the end of stream is reached or len bytes + * are read. Returns number of bytes read into array or -1 if end of stream is encountered. * - * - * @param source The data to convert - * @param options Specified options - * @see Base64#GZIP - * @see Base64#DONT_BREAK_LINES - * @since 2.0 + * @param dest array to hold values + * @param off offset for array + * @param len max number of bytes to read into array + * @return bytes read into array or -1 if end of stream is encountered. + * @since 1.3 */ - public static String encodeBytes( byte[] source, int options ) - { - return encodeBytes( source, 0, source.length, options ); - } // end encodeBytes - - + @Override + public int read(byte[] dest, int off, int len) throws java.io.IOException { + int i; + int b; + for (i = 0; i < len; i++) { + b = read(); + + // if( b < 0 && i == 0 ) + // return -1; + + if (b >= 0) dest[off + i] = (byte) b; + else if (i == 0) return -1; + else break; // Out of 'for' loop + } // end for: each byte read + return i; + } // end read + } // end inner class InputStream + + /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ + + /** + * A {@link Base64.OutputStream} will write data to another java.io.OutputStream, given + * in the constructor, and encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class OutputStream extends java.io.FilterOutputStream { + private boolean encode; + private int position; + private byte[] buffer; + private int bufferLength; + private int lineLength; + private boolean breakLines; + private byte[] b4; // Scratch used in a few places + private boolean suspendEncoding; + private int options; // Record for later + + @SuppressWarnings("unused") + private byte[] alphabet; // Local copies to avoid extra method calls + + private byte[] decodabet; // Local copies to avoid extra method calls + /** - * Encodes a byte array into Base64 notation. - * Does not GZip-compress data. + * Constructs a {@link Base64.OutputStream} in ENCODE mode. * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @since 1.4 + * @param out the java.io.OutputStream to which data will be written. + * @since 1.3 */ - public static String encodeBytes( byte[] source, int off, int len ) - { - return encodeBytes( source, off, len, NO_OPTIONS ); - } // end encodeBytes - - + public OutputStream(java.io.OutputStream out) { + this(out, ENCODE); + } // end constructor /** - * Encodes a byte array into Base64 notation. - *

    - * Valid options:

    -     *   GZIP: gzip-compresses object before encoding it.
    +     * Constructs a {@link Base64.OutputStream} in either ENCODE or DECODE mode.
    +     *
    +     * 

    Valid options: + * + *

    +     *   ENCODE or DECODE: Encode or Decode as data is read.
          *   DONT_BREAK_LINES: don't break lines at 76 characters
    +     *     (only meaningful when encoding)
          *     Note: Technically, this makes your encoding non-compliant.
          * 
    - *

    - * Example: encodeBytes( myData, Base64.GZIP ) or - *

    - * Example: encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES ) * + *

    Example: new Base64.OutputStream( out, Base64.ENCODE ) * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param options Specified options - * @see Base64#GZIP + * @param out the java.io.OutputStream to which data will be written. + * @param options Specified options. + * @see Base64#ENCODE + * @see Base64#DECODE * @see Base64#DONT_BREAK_LINES - * @since 2.0 - */ - public static String encodeBytes( byte[] source, int off, int len, int options ) - { - // Isolate options - int dontBreakLines = ( options & DONT_BREAK_LINES ); - int gzip = ( options & GZIP ); - - // Compress? - if( gzip == GZIP ) - { - java.io.ByteArrayOutputStream baos = null; - java.util.zip.GZIPOutputStream gzos = null; - Base64.OutputStream b64os = null; - - - try - { - // GZip -> Base64 -> ByteArray - baos = new java.io.ByteArrayOutputStream(); - b64os = new Base64.OutputStream( baos, ENCODE | options ); - gzos = new java.util.zip.GZIPOutputStream( b64os ); - - gzos.write( source, off, len ); - gzos.close(); - } // end try - catch( java.io.IOException e ) - { - e.printStackTrace(); - return null; - } // end catch - finally - { - try{ gzos.close(); } catch( Exception e ){} - try{ b64os.close(); } catch( Exception e ){} - try{ baos.close(); } catch( Exception e ){} - } // end finally - - // Return value according to relevant encoding. - try - { - return new String( baos.toByteArray(), PREFERRED_ENCODING ); - } // end try - catch (java.io.UnsupportedEncodingException uue) - { - return new String( baos.toByteArray() ); - } // end catch - } // end if: compress - - // Else, don't compress. Better not to use streams at all then. - else - { - // Convert option to boolean in way that code likes it. - boolean breakLines = dontBreakLines == 0; - - int len43 = len * 4 / 3; - byte[] outBuff = new byte[ ( len43 ) // Main 4:3 - + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding - + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines - int d = 0; - int e = 0; - int len2 = len - 2; - int lineLength = 0; - for( ; d < len2; d+=3, e+=4 ) - { - encode3to4( source, d+off, 3, outBuff, e, options ); - - lineLength += 4; - if( breakLines && lineLength == MAX_LINE_LENGTH ) - { - outBuff[e+4] = NEW_LINE; - e++; - lineLength = 0; - } // end if: end of line - } // en dfor: each piece of array - - if( d < len ) - { - encode3to4( source, d+off, len - d, outBuff, e, options ); - e += 4; - } // end if: some padding needed - - - // Return value according to relevant encoding. - try - { - return new String( outBuff, 0, e, PREFERRED_ENCODING ); - } // end try - catch (java.io.UnsupportedEncodingException uue) - { - return new String( outBuff, 0, e ); - } // end catch - - } // end else: don't compress - - } // end encodeBytes - - - - - -/* ******** D E C O D I N G M E T H O D S ******** */ - - - /** - * Decodes four bytes from array source - * and writes the resulting bytes (up to three of them) - * to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accomodate srcOffset + 4 for - * the source array or destOffset + 3 for - * the destination array. - * This method returns the actual number of bytes that - * were converted from the Base64 encoding. - *

    This is the lowest level of the decoding methods with - * all possible parameters.

    - * - * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @param options alphabet type is pulled from this (standard, url-safe, ordered) - * @return the number of decoded bytes converted * @since 1.3 */ - private static int decode4to3( byte[] source, int srcOffset, byte[] destination, int destOffset, int options ) - { - byte[] DECODABET = getDecodabet( options ); - - // Example: Dk== - if( source[ srcOffset + 2] == EQUALS_SIGN ) - { - // Two ways to do the same thing. Don't know which way I like best. - //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); - int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) - | ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 ); - - destination[ destOffset ] = (byte)( outBuff >>> 16 ); - return 1; - } - - // Example: DkL= - else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) - { - // Two ways to do the same thing. Don't know which way I like best. - //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); - int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) - | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) - | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6 ); - - destination[ destOffset ] = (byte)( outBuff >>> 16 ); - destination[ destOffset + 1 ] = (byte)( outBuff >>> 8 ); - return 2; - } - - // Example: DkLE - else - { - try{ - // Two ways to do the same thing. Don't know which way I like best. - //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) - // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); - int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) - | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) - | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6) - | ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF ) ); - - - destination[ destOffset ] = (byte)( outBuff >> 16 ); - destination[ destOffset + 1 ] = (byte)( outBuff >> 8 ); - destination[ destOffset + 2 ] = (byte)( outBuff ); - - return 3; - }catch( Exception e){ - System.out.println(""+source[srcOffset]+ ": " + ( DECODABET[ source[ srcOffset ] ] ) ); - System.out.println(""+source[srcOffset+1]+ ": " + ( DECODABET[ source[ srcOffset + 1 ] ] ) ); - System.out.println(""+source[srcOffset+2]+ ": " + ( DECODABET[ source[ srcOffset + 2 ] ] ) ); - System.out.println(""+source[srcOffset+3]+ ": " + ( DECODABET[ source[ srcOffset + 3 ] ] ) ); - return -1; - } // end catch - } - } // end decodeToBytes - - - - + public OutputStream(java.io.OutputStream out, int options) { + super(out); + this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES; + this.encode = (options & ENCODE) == ENCODE; + this.bufferLength = encode ? 3 : 4; + this.buffer = new byte[bufferLength]; + this.position = 0; + this.lineLength = 0; + this.suspendEncoding = false; + this.b4 = new byte[4]; + this.options = options; + this.alphabet = getAlphabet(options); + this.decodabet = getDecodabet(options); + } // end constructor + /** - * Very low-level access to decoding ASCII characters in - * the form of a byte array. Does not support automatically - * gunzipping or any other "fancy" features. + * Writes the byte to the output stream after converting to/from Base64 notation. When encoding, + * bytes are buffered three at a time before the output stream actually gets a write() call. + * When decoding, bytes are buffered four at a time. * - * @param source The Base64 encoded data - * @param off The offset of where to begin decoding - * @param len The length of characters to decode - * @return decoded data + * @param theByte the byte to write * @since 1.3 */ - public static byte[] decode( byte[] source, int off, int len, int options ) - { - byte[] DECODABET = getDecodabet( options ); - - int len34 = len * 3 / 4; - byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output - int outBuffPosn = 0; - - byte[] b4 = new byte[4]; - int b4Posn = 0; - int i = 0; - byte sbiCrop = 0; - byte sbiDecode = 0; - for( i = off; i < off+len; i++ ) - { - sbiCrop = (byte)(source[i] & 0x7f); // Only the low seven bits - sbiDecode = DECODABET[ sbiCrop ]; - - if( sbiDecode >= WHITE_SPACE_ENC ) // White space, Equals sign or better - { - if( sbiDecode >= EQUALS_SIGN_ENC ) - { - b4[ b4Posn++ ] = sbiCrop; - if( b4Posn > 3 ) - { - outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn, options ); - b4Posn = 0; - - // If that was the equals sign, break out of 'for' loop - if( sbiCrop == EQUALS_SIGN ) - break; - } // end if: quartet built - - } // end if: equals sign or better - - } // end if: white space, equals sign or better - else - { - System.err.println( "Bad Base64 input character at " + i + ": " + source[i] + "(decimal)" ); - return null; - } // end else: - } // each input character - - byte[] out = new byte[ outBuffPosn ]; - System.arraycopy( outBuff, 0, out, 0, outBuffPosn ); - return out; - } // end decode - - - - - /** - * Decodes data from Base64 notation, automatically - * detecting gzip-compressed data and decompressing it. - * - * @param s the string to decode - * @return the decoded data - * @since 1.4 - */ - public static byte[] decode( String s ) - { - return decode( s, NO_OPTIONS ); - } - - - /** - * Decodes data from Base64 notation, automatically - * detecting gzip-compressed data and decompressing it. - * - * @param s the string to decode - * @param options encode options such as URL_SAFE - * @return the decoded data - * @since 1.4 - */ - public static byte[] decode( String s, int options ) - { - byte[] bytes; - try - { - bytes = s.getBytes( PREFERRED_ENCODING ); - } // end try - catch( java.io.UnsupportedEncodingException uee ) - { - bytes = s.getBytes(); - } // end catch - // - - // Decode - bytes = decode( bytes, 0, bytes.length, options ); - - - // Check to see if it's gzip-compressed - // GZIP Magic Two-Byte Number: 0x8b1f (35615) - if( bytes != null && bytes.length >= 4 ) - { - - int head = ((int)bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); - if( java.util.zip.GZIPInputStream.GZIP_MAGIC == head ) - { - java.io.ByteArrayInputStream bais = null; - java.util.zip.GZIPInputStream gzis = null; - java.io.ByteArrayOutputStream baos = null; - byte[] buffer = new byte[2048]; - int length = 0; - - try - { - baos = new java.io.ByteArrayOutputStream(); - bais = new java.io.ByteArrayInputStream( bytes ); - gzis = new java.util.zip.GZIPInputStream( bais ); - - while( ( length = gzis.read( buffer ) ) >= 0 ) - { - baos.write(buffer,0,length); - } // end while: reading input - - // No error? Get new bytes. - bytes = baos.toByteArray(); - - } // end try - catch( java.io.IOException e ) - { - // Just return originally-decoded bytes - } // end catch - finally - { - try{ baos.close(); } catch( Exception e ){} - try{ gzis.close(); } catch( Exception e ){} - try{ bais.close(); } catch( Exception e ){} - } // end finally - - } // end if: gzipped - } // end if: bytes.length >= 2 - - return bytes; - } // end decode - - - + @Override + public void write(int theByte) throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + super.out.write(theByte); + return; + } // end if: supsended - /** - * Attempts to decode Base64 data and deserialize a Java - * Object within. Returns null if there was an error. - * - * @param encodedObject The Base64 data to decode - * @return The decoded and deserialized object - * @since 1.5 - */ - public static Object decodeToObject( String encodedObject ) - { - // Decode and gunzip if necessary - byte[] objBytes = decode( encodedObject ); - - java.io.ByteArrayInputStream bais = null; - java.io.ObjectInputStream ois = null; - Object obj = null; - - try - { - bais = new java.io.ByteArrayInputStream( objBytes ); - ois = new java.io.ObjectInputStream( bais ); - - obj = ois.readObject(); - } // end try - catch( java.io.IOException e ) - { - e.printStackTrace(); - obj = null; - } // end catch - catch( java.lang.ClassNotFoundException e ) - { - e.printStackTrace(); - obj = null; - } // end catch - finally - { - try{ bais.close(); } catch( Exception e ){} - try{ ois.close(); } catch( Exception e ){} - } // end finally - - return obj; - } // end decodeObject - - - - /** - * Convenience method for encoding data to a file. - * - * @param dataToEncode byte array of data to encode in base64 form - * @param filename Filename for saving encoded data - * @return true if successful, false otherwise - * - * @since 2.1 - */ - public static boolean encodeToFile( byte[] dataToEncode, String filename ) - { - boolean success = false; - Base64.OutputStream bos = null; - try - { - bos = new Base64.OutputStream( - new java.io.FileOutputStream( filename ), Base64.ENCODE ); - bos.write( dataToEncode ); - success = true; - } // end try - catch( java.io.IOException e ) - { - - success = false; - } // end catch: IOException - finally - { - try{ bos.close(); } catch( Exception e ){} - } // end finally - - return success; - } // end encodeToFile - - - /** - * Convenience method for decoding data to a file. - * - * @param dataToDecode Base64-encoded data as a string - * @param filename Filename for saving decoded data - * @return true if successful, false otherwise - * - * @since 2.1 - */ - public static boolean decodeToFile( String dataToDecode, String filename ) - { - boolean success = false; - Base64.OutputStream bos = null; - try - { - bos = new Base64.OutputStream( - new java.io.FileOutputStream( filename ), Base64.DECODE ); - bos.write( dataToDecode.getBytes( PREFERRED_ENCODING ) ); - success = true; - } // end try - catch( java.io.IOException e ) - { - success = false; - } // end catch: IOException - finally - { - try{ bos.close(); } catch( Exception e ){} - } // end finally - - return success; - } // end decodeToFile - - - - - /** - * Convenience method for reading a base64-encoded - * file and decoding it. - * - * @param filename Filename for reading encoded data - * @return decoded byte array or null if unsuccessful - * - * @since 2.1 - */ - public static byte[] decodeFromFile( String filename ) - { - byte[] decodedData = null; - Base64.InputStream bis = null; - try - { - // Set up some useful variables - java.io.File file = new java.io.File( filename ); - byte[] buffer = null; - int length = 0; - int numBytes = 0; - - // Check for size of file - if( file.length() > Integer.MAX_VALUE ) - { - System.err.println( "File is too big for this convenience method (" + file.length() + " bytes)." ); - return null; - } // end if: file too big for int index - buffer = new byte[ (int)file.length() ]; - - // Open a stream - bis = new Base64.InputStream( - new java.io.BufferedInputStream( - new java.io.FileInputStream( file ) ), Base64.DECODE ); - - // Read until done - while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) - length += numBytes; - - // Save in a variable to return - decodedData = new byte[ length ]; - System.arraycopy( buffer, 0, decodedData, 0, length ); - - } // end try - catch( java.io.IOException e ) + // Encode? + if (encode) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) // Enough to encode. { - System.err.println( "Error decoding from file " + filename ); - } // end catch: IOException - finally - { - try{ bis.close(); } catch( Exception e) {} - } // end finally - - return decodedData; - } // end decodeFromFile - - - + out.write(encode3to4(b4, buffer, bufferLength, options)); + + lineLength += 4; + if (breakLines && lineLength >= MAX_LINE_LENGTH) { + out.write(NEW_LINE); + lineLength = 0; + } // end if: end of line + + position = 0; + } // end if: enough to output + } // end if: encoding + + // Else, Decoding + else { + // Meaningful Base64 character? + if (decodabet[theByte & 0x7f] > WHITE_SPACE_ENC) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) // Enough to output. + { + int len = Base64.decode4to3(buffer, 0, b4, 0, options); + out.write(b4, 0, len); + // out.write( Base64.decode4to3( buffer ) ); + position = 0; + } // end if: enough to output + } // end if: meaningful base64 character + else if (decodabet[theByte & 0x7f] != WHITE_SPACE_ENC) { + throw new java.io.IOException("Invalid character in Base64 data."); + } // end else: not white space either + } // end else: decoding + } // end write + /** - * Convenience method for reading a binary file - * and base64-encoding it. - * - * @param filename Filename for reading binary data - * @return base64-encoded string or null if unsuccessful + * Calls {@link #write(int)} repeatedly until len bytes are written. * - * @since 2.1 + * @param theBytes array from which to read bytes + * @param off offset for array + * @param len max number of bytes to read into array + * @since 1.3 */ - public static String encodeFromFile( String filename ) - { - String encodedData = null; - Base64.InputStream bis = null; - try - { - // Set up some useful variables - java.io.File file = new java.io.File( filename ); - byte[] buffer = new byte[ Math.max((int)(file.length() * 1.4),40) ]; // Need max() for math on small files (v2.2.1) - int length = 0; - int numBytes = 0; - - // Open a stream - bis = new Base64.InputStream( - new java.io.BufferedInputStream( - new java.io.FileInputStream( file ) ), Base64.ENCODE ); - - // Read until done - while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) - length += numBytes; - - // Save in a variable to return - encodedData = new String( buffer, 0, length, Base64.PREFERRED_ENCODING ); - - } // end try - catch( java.io.IOException e ) - { - System.err.println( "Error encoding from file " + filename ); - } // end catch: IOException - finally - { - try{ bis.close(); } catch( Exception e) {} - } // end finally - - return encodedData; - } // end encodeFromFile - - - - + @Override + public void write(byte[] theBytes, int off, int len) throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + super.out.write(theBytes, off, len); + return; + } // end if: supsended + + for (int i = 0; i < len; i++) { + write(theBytes[off + i]); + } // end for: each byte written + } // end write + /** - * Reads infile and encodes it to outfile. - * - * @param infile Input file - * @param outfile Output file - * @return true if the operation is successful - * @since 2.2 + * Method added by PHIL. [Thanks, PHIL. -Rob] This pads the buffer without closing the stream. */ - public static boolean encodeFileToFile( String infile, String outfile ) - { - boolean success = false; - java.io.InputStream in = null; - java.io.OutputStream out = null; - try{ - in = new Base64.InputStream( - new java.io.BufferedInputStream( - new java.io.FileInputStream( infile ) ), - Base64.ENCODE ); - out = new java.io.BufferedOutputStream( new java.io.FileOutputStream( outfile ) ); - byte[] buffer = new byte[65536]; // 64K - int read = -1; - while( ( read = in.read(buffer) ) >= 0 ){ - out.write( buffer,0,read ); - } // end while: through file - success = true; - } catch( java.io.IOException exc ){ - exc.printStackTrace(); - } finally{ - try{ in.close(); } catch( Exception exc ){} - try{ out.close(); } catch( Exception exc ){} - } // end finally - - return success; - } // end encodeFileToFile - - - + public void flushBase64() throws java.io.IOException { + if (position > 0) { + if (encode) { + out.write(encode3to4(b4, buffer, position, options)); + position = 0; + } // end if: encoding + else { + throw new java.io.IOException("Base64 input not properly padded."); + } // end else: decoding + } // end if: buffer partially full + } // end flush + /** - * Reads infile and decodes it to outfile. + * Flushes and closes (I think, in the superclass) the stream. * - * @param infile Input file - * @param outfile Output file - * @return true if the operation is successful - * @since 2.2 + * @since 1.3 */ - public static boolean decodeFileToFile( String infile, String outfile ) - { - boolean success = false; - java.io.InputStream in = null; - java.io.OutputStream out = null; - try{ - in = new Base64.InputStream( - new java.io.BufferedInputStream( - new java.io.FileInputStream( infile ) ), - Base64.DECODE ); - out = new java.io.BufferedOutputStream( new java.io.FileOutputStream( outfile ) ); - byte[] buffer = new byte[65536]; // 64K - int read = -1; - while( ( read = in.read(buffer) ) >= 0 ){ - out.write( buffer,0,read ); - } // end while: through file - success = true; - } catch( java.io.IOException exc ){ - exc.printStackTrace(); - } finally{ - try{ in.close(); } catch( Exception exc ){} - try{ out.close(); } catch( Exception exc ){} - } // end finally - - return success; - } // end decodeFileToFile - - - /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ - - - + @Override + public void close() throws java.io.IOException { + // 1. Ensure that pending characters are written + flushBase64(); + + // 2. Actually close the stream + // Base class both flushes and closes. + super.close(); + + buffer = null; + out = null; + } // end close + /** - * A {@link Base64.InputStream} will read data from another - * java.io.InputStream, given in the constructor, - * and encode/decode to/from Base64 notation on the fly. + * Suspends encoding of the stream. May be helpful if you need to embed a piece of + * base640-encoded data in a stream. * - * @see Base64 - * @since 1.3 + * @since 1.5.1 */ - public static class InputStream extends java.io.FilterInputStream - { - private boolean encode; // Encoding or decoding - private int position; // Current position in the buffer - private byte[] buffer; // Small buffer holding converted data - private int bufferLength; // Length of buffer (3 or 4) - private int numSigBytes; // Number of meaningful bytes in the buffer - private int lineLength; - private boolean breakLines; // Break lines at less than 80 characters - private int options; // Record options used to create the stream. - @SuppressWarnings("unused") - private byte[] alphabet; // Local copies to avoid extra method calls - private byte[] decodabet; // Local copies to avoid extra method calls - - - /** - * Constructs a {@link Base64.InputStream} in DECODE mode. - * - * @param in the java.io.InputStream from which to read data. - * @since 1.3 - */ - public InputStream( java.io.InputStream in ) - { - this( in, DECODE ); - } // end constructor - - - /** - * Constructs a {@link Base64.InputStream} in - * either ENCODE or DECODE mode. - *

    - * Valid options:

    -         *   ENCODE or DECODE: Encode or Decode as data is read.
    -         *   DONT_BREAK_LINES: don't break lines at 76 characters
    -         *     (only meaningful when encoding)
    -         *     Note: Technically, this makes your encoding non-compliant.
    -         * 
    - *

    - * Example: new Base64.InputStream( in, Base64.DECODE ) - * - * - * @param in the java.io.InputStream from which to read data. - * @param options Specified options - * @see Base64#ENCODE - * @see Base64#DECODE - * @see Base64#DONT_BREAK_LINES - * @since 2.0 - */ - public InputStream( java.io.InputStream in, int options ) - { - super( in ); - this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES; - this.encode = (options & ENCODE) == ENCODE; - this.bufferLength = encode ? 4 : 3; - this.buffer = new byte[ bufferLength ]; - this.position = -1; - this.lineLength = 0; - this.options = options; // Record for later, mostly to determine which alphabet to use - this.alphabet = getAlphabet(options); - this.decodabet = getDecodabet(options); - } // end constructor - - /** - * Reads enough of the input stream to convert - * to/from Base64 and returns the next byte. - * - * @return next byte - * @since 1.3 - */ - @Override - public int read() throws java.io.IOException - { - // Do we need to get data? - if( position < 0 ) - { - if( encode ) - { - byte[] b3 = new byte[3]; - int numBinaryBytes = 0; - for( int i = 0; i < 3; i++ ) - { - try - { - int b = in.read(); - - // If end of stream, b is -1. - if( b >= 0 ) - { - b3[i] = (byte)b; - numBinaryBytes++; - } // end if: not end of stream - - } // end try: read - catch( java.io.IOException e ) - { - // Only a problem if we got no data at all. - if( i == 0 ) - throw e; - - } // end catch - } // end for: each needed input byte - - if( numBinaryBytes > 0 ) - { - encode3to4( b3, 0, numBinaryBytes, buffer, 0, options ); - position = 0; - numSigBytes = 4; - } // end if: got data - else - { - return -1; - } // end else - } // end if: encoding - - // Else decoding - else - { - byte[] b4 = new byte[4]; - int i = 0; - for( i = 0; i < 4; i++ ) - { - // Read four "meaningful" bytes: - int b = 0; - do{ b = in.read(); } - while( b >= 0 && decodabet[ b & 0x7f ] <= WHITE_SPACE_ENC ); - - if( b < 0 ) - break; // Reads a -1 if end of stream - - b4[i] = (byte)b; - } // end for: each needed input byte - - if( i == 4 ) - { - numSigBytes = decode4to3( b4, 0, buffer, 0, options ); - position = 0; - } // end if: got four characters - else if( i == 0 ){ - return -1; - } // end else if: also padded correctly - else - { - // Must have broken out from above. - throw new java.io.IOException( "Improperly padded Base64 input." ); - } // end - - } // end else: decode - } // end else: get data - - // Got data? - if( position >= 0 ) - { - // End of relevant data? - if( /*!encode &&*/ position >= numSigBytes ) - return -1; - - if( encode && breakLines && lineLength >= MAX_LINE_LENGTH ) - { - lineLength = 0; - return '\n'; - } // end if - else - { - lineLength++; // This isn't important when decoding - // but throwing an extra "if" seems - // just as wasteful. - - int b = buffer[ position++ ]; - - if( position >= bufferLength ) - position = -1; - - return b & 0xFF; // This is how you "cast" a byte that's - // intended to be unsigned. - } // end else - } // end if: position >= 0 - - // Else error - else - { - // When JDK1.4 is more accepted, use an assertion here. - throw new java.io.IOException( "Error in Base64 code reading stream." ); - } // end else - } // end read - - - /** - * Calls {@link #read()} repeatedly until the end of stream - * is reached or len bytes are read. - * Returns number of bytes read into array or -1 if - * end of stream is encountered. - * - * @param dest array to hold values - * @param off offset for array - * @param len max number of bytes to read into array - * @return bytes read into array or -1 if end of stream is encountered. - * @since 1.3 - */ - @Override - public int read( byte[] dest, int off, int len ) throws java.io.IOException - { - int i; - int b; - for( i = 0; i < len; i++ ) - { - b = read(); - - //if( b < 0 && i == 0 ) - // return -1; - - if( b >= 0 ) - dest[off + i] = (byte)b; - else if( i == 0 ) - return -1; - else - break; // Out of 'for' loop - } // end for: each byte read - return i; - } // end read - - } // end inner class InputStream - - - - - - - /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ - - - + public void suspendEncoding() throws java.io.IOException { + flushBase64(); + this.suspendEncoding = true; + } // end suspendEncoding + /** - * A {@link Base64.OutputStream} will write data to another - * java.io.OutputStream, given in the constructor, - * and encode/decode to/from Base64 notation on the fly. + * Resumes encoding of the stream. May be helpful if you need to embed a piece of + * base640-encoded data in a stream. * - * @see Base64 - * @since 1.3 + * @since 1.5.1 */ - public static class OutputStream extends java.io.FilterOutputStream - { - private boolean encode; - private int position; - private byte[] buffer; - private int bufferLength; - private int lineLength; - private boolean breakLines; - private byte[] b4; // Scratch used in a few places - private boolean suspendEncoding; - private int options; // Record for later - @SuppressWarnings("unused") - private byte[] alphabet; // Local copies to avoid extra method calls - private byte[] decodabet; // Local copies to avoid extra method calls - - /** - * Constructs a {@link Base64.OutputStream} in ENCODE mode. - * - * @param out the java.io.OutputStream to which data will be written. - * @since 1.3 - */ - public OutputStream( java.io.OutputStream out ) - { - this( out, ENCODE ); - } // end constructor - - - /** - * Constructs a {@link Base64.OutputStream} in - * either ENCODE or DECODE mode. - *

    - * Valid options:

    -         *   ENCODE or DECODE: Encode or Decode as data is read.
    -         *   DONT_BREAK_LINES: don't break lines at 76 characters
    -         *     (only meaningful when encoding)
    -         *     Note: Technically, this makes your encoding non-compliant.
    -         * 
    - *

    - * Example: new Base64.OutputStream( out, Base64.ENCODE ) - * - * @param out the java.io.OutputStream to which data will be written. - * @param options Specified options. - * @see Base64#ENCODE - * @see Base64#DECODE - * @see Base64#DONT_BREAK_LINES - * @since 1.3 - */ - public OutputStream( java.io.OutputStream out, int options ) - { - super( out ); - this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES; - this.encode = (options & ENCODE) == ENCODE; - this.bufferLength = encode ? 3 : 4; - this.buffer = new byte[ bufferLength ]; - this.position = 0; - this.lineLength = 0; - this.suspendEncoding = false; - this.b4 = new byte[4]; - this.options = options; - this.alphabet = getAlphabet(options); - this.decodabet = getDecodabet(options); - } // end constructor - - - /** - * Writes the byte to the output stream after - * converting to/from Base64 notation. - * When encoding, bytes are buffered three - * at a time before the output stream actually - * gets a write() call. - * When decoding, bytes are buffered four - * at a time. - * - * @param theByte the byte to write - * @since 1.3 - */ - @Override - public void write(int theByte) throws java.io.IOException - { - // Encoding suspended? - if( suspendEncoding ) - { - super.out.write( theByte ); - return; - } // end if: supsended - - // Encode? - if( encode ) - { - buffer[ position++ ] = (byte)theByte; - if( position >= bufferLength ) // Enough to encode. - { - out.write( encode3to4( b4, buffer, bufferLength, options ) ); - - lineLength += 4; - if( breakLines && lineLength >= MAX_LINE_LENGTH ) - { - out.write( NEW_LINE ); - lineLength = 0; - } // end if: end of line - - position = 0; - } // end if: enough to output - } // end if: encoding - - // Else, Decoding - else - { - // Meaningful Base64 character? - if( decodabet[ theByte & 0x7f ] > WHITE_SPACE_ENC ) - { - buffer[ position++ ] = (byte)theByte; - if( position >= bufferLength ) // Enough to output. - { - int len = Base64.decode4to3( buffer, 0, b4, 0, options ); - out.write( b4, 0, len ); - //out.write( Base64.decode4to3( buffer ) ); - position = 0; - } // end if: enough to output - } // end if: meaningful base64 character - else if( decodabet[ theByte & 0x7f ] != WHITE_SPACE_ENC ) - { - throw new java.io.IOException( "Invalid character in Base64 data." ); - } // end else: not white space either - } // end else: decoding - } // end write - - - - /** - * Calls {@link #write(int)} repeatedly until len - * bytes are written. - * - * @param theBytes array from which to read bytes - * @param off offset for array - * @param len max number of bytes to read into array - * @since 1.3 - */ - @Override - public void write( byte[] theBytes, int off, int len ) throws java.io.IOException - { - // Encoding suspended? - if( suspendEncoding ) - { - super.out.write( theBytes, off, len ); - return; - } // end if: supsended - - for( int i = 0; i < len; i++ ) - { - write( theBytes[ off + i ] ); - } // end for: each byte written - - } // end write - - - - /** - * Method added by PHIL. [Thanks, PHIL. -Rob] - * This pads the buffer without closing the stream. - */ - public void flushBase64() throws java.io.IOException - { - if( position > 0 ) - { - if( encode ) - { - out.write( encode3to4( b4, buffer, position, options ) ); - position = 0; - } // end if: encoding - else - { - throw new java.io.IOException( "Base64 input not properly padded." ); - } // end else: decoding - } // end if: buffer partially full - - } // end flush - - - /** - * Flushes and closes (I think, in the superclass) the stream. - * - * @since 1.3 - */ - @Override - public void close() throws java.io.IOException - { - // 1. Ensure that pending characters are written - flushBase64(); - - // 2. Actually close the stream - // Base class both flushes and closes. - super.close(); - - buffer = null; - out = null; - } // end close - - - - /** - * Suspends encoding of the stream. - * May be helpful if you need to embed a piece of - * base640-encoded data in a stream. - * - * @since 1.5.1 - */ - public void suspendEncoding() throws java.io.IOException - { - flushBase64(); - this.suspendEncoding = true; - } // end suspendEncoding - - - /** - * Resumes encoding of the stream. - * May be helpful if you need to embed a piece of - * base640-encoded data in a stream. - * - * @since 1.5.1 - */ - public void resumeEncoding() - { - this.suspendEncoding = false; - } // end resumeEncoding - - - - } // end inner class OutputStream - - -} // end class Base64 + public void resumeEncoding() { + this.suspendEncoding = false; + } // end resumeEncoding + } // end inner class OutputStream +} // end class Base64 diff --git a/stripes/src/main/java/net/sourceforge/stripes/util/CollectionUtil.java b/stripes/src/main/java/net/sourceforge/stripes/util/CollectionUtil.java index 9d5382ee3..bd22b6f19 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/util/CollectionUtil.java +++ b/stripes/src/main/java/net/sourceforge/stripes/util/CollectionUtil.java @@ -15,8 +15,8 @@ package net.sourceforge.stripes.util; import java.lang.reflect.Array; -import java.util.List; import java.util.LinkedList; +import java.util.List; /** * Utility methods for working with Collections and Arrays. @@ -25,138 +25,133 @@ * @since Stripes 1.4 */ public class CollectionUtil { - /** - * Checks to see if an array contains an item. Works on unsorted arrays. If the array is - * null this method will always return false. If the item is null, will return true if the - * array contains a null entry, false otherwise. In all other cases, item.equals() is used - * to determine equality. - * - * @param arr the array to scan for the item. - * @param item the item to be looked for - * @return true if item is contained in the array, false otherwise - */ - public static boolean contains(Object[] arr, Object item) { - if (arr == null) return false; - - for (int i=0; iChecks to see if an event is applicable given an array of event names. The array is - * usually derived from the on attribute of one of the Stripes annotations - * (e.g. {@link net.sourceforge.stripes.validation.ValidationMethod}). The array can - * be composed of positive event names (e.g. {"foo", "bar"}) in which case the event - * must be contained in the array, or negative event names (e.g. {"!splat", "!whee"}) in - * which case the event must not be contained in the array.

    - * - *

    Calling this method with a null or zero length array will always return true.

    - * - * @param events an array containing event names or event names prefixed with bangs - * @param event the event name to check for applicability given the array - * @return true if the array indicates the event is applicable, false otherwise - */ - public static boolean applies(String events[], String event) { - if (events == null || events.length == 0) return true; - boolean isPositive = events[0].charAt(0) != '!'; + return true; + } - if (isPositive) return contains(events, event); - else return !contains(events, "!" + event); - } + /** + * Checks to see if an event is applicable given an array of event names. The array is usually + * derived from the on attribute of one of the Stripes annotations (e.g. {@link + * net.sourceforge.stripes.validation.ValidationMethod}). The array can be composed of + * positive event names (e.g. {"foo", "bar"}) in which case the event must be contained in + * the array, or negative event names (e.g. {"!splat", "!whee"}) in which case the event must not + * be contained in the array. + * + *

    Calling this method with a null or zero length array will always return true. + * + * @param events an array containing event names or event names prefixed with bangs + * @param event the event name to check for applicability given the array + * @return true if the array indicates the event is applicable, false otherwise + */ + public static boolean applies(String events[], String event) { + if (events == null || events.length == 0) return true; + boolean isPositive = events[0].charAt(0) != '!'; + + if (isPositive) return contains(events, event); + else return !contains(events, "!" + event); + } - /** - * Converts an Object reference that is known to be an array into an Object[]. If the array - * is assignable to Object[], the array passed in is simply cast and returned. Otherwise a - * new Object[] of equal size is constructed and the elements are wrapped and inserted into - * the new array before being returned. - * - * @param in an array of Objects or primitives - * @return an Object[], either the array passed in, or in the case of primitives, a new - * Object[] containing a wrapper for each element in the input array - * @throws IllegalArgumentException thrown if the in parameter is null or not an array - */ - public static Object[] asObjectArray(Object in) { - if (in == null || !in.getClass().isArray()) { - throw new IllegalArgumentException("Parameter to asObjectArray must be a non-null array."); - } - else if (in instanceof Object[]) { - return (Object[]) in; - } - else { - int length = Array.getLength(in); - Object[] out = new Object[length]; - for (int i=0; iConverts an Object reference that is known to be an array into a List. Semantically - * very similar to {@link java.util.Arrays#asList(Object[])} except that this method - * can deal with arrays of primitives in the same manner as arrays as objects.

    - * - *

    A new List is created of the same size as the array, and elements are copied from - * the array into the List. If elements are primitives then they are converted to the - * appropriate wrapper types in order to return a List.

    - * - * @param in an array of Objects or primitives (null values are not allowed) - * @return a List containing an element for each element in the input array - * @throws IllegalArgumentException thrown if the in parameter is null or not an array - */ - public static List asList(Object in) { - if (in == null || !in.getClass().isArray()) { - throw new IllegalArgumentException("Parameter to asObjectArray must be a non-null array."); - } - else { - int length = Array.getLength(in); - LinkedList list = new LinkedList(); - for (int i=0; iA new List is created of the same size as the array, and elements are copied from the array + * into the List. If elements are primitives then they are converted to the appropriate wrapper + * types in order to return a List. + * + * @param in an array of Objects or primitives (null values are not allowed) + * @return a List containing an element for each element in the input array + * @throws IllegalArgumentException thrown if the in parameter is null or not an array + */ + public static List asList(Object in) { + if (in == null || !in.getClass().isArray()) { + throw new IllegalArgumentException("Parameter to asObjectArray must be a non-null array."); + } else { + int length = Array.getLength(in); + LinkedList list = new LinkedList(); + for (int i = 0; i < length; ++i) { + list.add(i, Array.get(in, i)); + } - return list; - } + return list; } + } - /** - * Converts an Iterable into a List that can be navigated in ways other than simple - * iteration. If the underlying implementation of the Iterable is a List, it is cast - * to List and returned. Otherwise it is iterated and the items placed, in order, - * into a new List. - * - * @param in an Iterable to serve as the source for a List - * @return either the Iterable itself if it is a List, or a new List with the same elements - */ - public static List asList(Iterable in) { - if (in instanceof List) return (List) in; - else { - LinkedList list = new LinkedList(); - for (T item : in) { - list.add(item); - } + /** + * Converts an Iterable into a List that can be navigated in ways other than simple iteration. If + * the underlying implementation of the Iterable is a List, it is cast to List and returned. + * Otherwise it is iterated and the items placed, in order, into a new List. + * + * @param in an Iterable to serve as the source for a List + * @return either the Iterable itself if it is a List, or a new List with the same elements + */ + public static List asList(Iterable in) { + if (in instanceof List) return (List) in; + else { + LinkedList list = new LinkedList(); + for (T item : in) { + list.add(item); + } - return list; - } + return list; } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/util/ConcurrentHashSet.java b/stripes/src/main/java/net/sourceforge/stripes/util/ConcurrentHashSet.java index 19aac991c..2b094e012 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/util/ConcurrentHashSet.java +++ b/stripes/src/main/java/net/sourceforge/stripes/util/ConcurrentHashSet.java @@ -25,117 +25,117 @@ * copied from the Java 1.5 Javadoc for {@link ConcurrentHashMap} and changed to reflect that this * is a Set and not a Map. See the Javadoc for {@link ConcurrentHashMap} for information on * performance characteristics, etc. - * + * * @author Ben Gunter */ public class ConcurrentHashSet implements Set { - /** The value object that will be put in the map since it does not accept null values. */ - private static final Object VALUE = new Object(); - - /** The map that backs this set. */ - private ConcurrentMap map; - - /** - * Creates a new, empty map with a default initial capacity, load factor, and concurrencyLevel. - */ - public ConcurrentHashSet() { - map = new ConcurrentHashMap(); - } - - /** - * Creates a new, empty map with the specified initial capacity, and with default load factor - * and concurrencyLevel. - * - * @param initialCapacity the initial capacity. The implementation performs internal sizing to - * accommodate this many elements. - * @throws IllegalArgumentException if the initial capacity of elements is negative. - */ - public ConcurrentHashSet(int initialCapacity) { - map = new ConcurrentHashMap(initialCapacity); - } - - /** - * Creates a new, empty map with the specified initial capacity, load factor, and concurrency - * level. - * - * @param initialCapacity the initial capacity. The implementation performs internal sizing to - * accommodate this many elements. - * @param loadFactor the load factor threshold, used to control resizing. Resizing may be - * performed when the average number of elements per bin exceeds this threshold. - * @param concurrencyLevel - the estimated number of concurrently updating threads. The - * implementation performs internal sizing to try to accommodate this many threads. - * @throws IllegalArgumentException if the initial capacity is negative or the load factor or - * concurrencyLevel are nonpositive. - */ - public ConcurrentHashSet(int initialCapacity, float loadFactor, int concurrencyLevel) { - map = new ConcurrentHashMap(initialCapacity, loadFactor, concurrencyLevel); - } - - /** - * Creates a new set with the same elements as the given set. The set is created with a capacity - * of twice the number of elements in the given set or 11 (whichever is greater), and a default - * load factor and concurrencyLevel. - * - * @param set The set - */ - public ConcurrentHashSet(Set set) { - this(Math.max(set.size() * 2, 11)); - addAll(set); - } - - public boolean add(T e) { - return map.putIfAbsent(e, VALUE) == null; - } - - public boolean addAll(Collection c) { - boolean b = false; - for (T t : c) { - b = b || map.putIfAbsent(t, VALUE) == null; - } - return b; - } - - public void clear() { - map.clear(); - } - - public boolean contains(Object o) { - return map.keySet().contains(o); - } - - public boolean containsAll(Collection c) { - return map.keySet().containsAll(c); - } - - public boolean isEmpty() { - return map.isEmpty(); - } - - public Iterator iterator() { - return map.keySet().iterator(); - } - - public boolean remove(Object o) { - return map.remove(o) != null; - } - - public boolean removeAll(Collection c) { - return map.keySet().removeAll(c); - } - - public boolean retainAll(Collection c) { - return map.keySet().retainAll(c); - } - - public int size() { - return map.size(); - } - - public Object[] toArray() { - return map.keySet().toArray(); - } - - public E[] toArray(E[] a) { - return map.keySet().toArray(a); - } + /** The value object that will be put in the map since it does not accept null values. */ + private static final Object VALUE = new Object(); + + /** The map that backs this set. */ + private ConcurrentMap map; + + /** + * Creates a new, empty map with a default initial capacity, load factor, and concurrencyLevel. + */ + public ConcurrentHashSet() { + map = new ConcurrentHashMap(); + } + + /** + * Creates a new, empty map with the specified initial capacity, and with default load factor and + * concurrencyLevel. + * + * @param initialCapacity the initial capacity. The implementation performs internal sizing to + * accommodate this many elements. + * @throws IllegalArgumentException if the initial capacity of elements is negative. + */ + public ConcurrentHashSet(int initialCapacity) { + map = new ConcurrentHashMap(initialCapacity); + } + + /** + * Creates a new, empty map with the specified initial capacity, load factor, and concurrency + * level. + * + * @param initialCapacity the initial capacity. The implementation performs internal sizing to + * accommodate this many elements. + * @param loadFactor the load factor threshold, used to control resizing. Resizing may be + * performed when the average number of elements per bin exceeds this threshold. + * @param concurrencyLevel - the estimated number of concurrently updating threads. The + * implementation performs internal sizing to try to accommodate this many threads. + * @throws IllegalArgumentException if the initial capacity is negative or the load factor or + * concurrencyLevel are nonpositive. + */ + public ConcurrentHashSet(int initialCapacity, float loadFactor, int concurrencyLevel) { + map = new ConcurrentHashMap(initialCapacity, loadFactor, concurrencyLevel); + } + + /** + * Creates a new set with the same elements as the given set. The set is created with a capacity + * of twice the number of elements in the given set or 11 (whichever is greater), and a default + * load factor and concurrencyLevel. + * + * @param set The set + */ + public ConcurrentHashSet(Set set) { + this(Math.max(set.size() * 2, 11)); + addAll(set); + } + + public boolean add(T e) { + return map.putIfAbsent(e, VALUE) == null; + } + + public boolean addAll(Collection c) { + boolean b = false; + for (T t : c) { + b = b || map.putIfAbsent(t, VALUE) == null; + } + return b; + } + + public void clear() { + map.clear(); + } + + public boolean contains(Object o) { + return map.keySet().contains(o); + } + + public boolean containsAll(Collection c) { + return map.keySet().containsAll(c); + } + + public boolean isEmpty() { + return map.isEmpty(); + } + + public Iterator iterator() { + return map.keySet().iterator(); + } + + public boolean remove(Object o) { + return map.remove(o) != null; + } + + public boolean removeAll(Collection c) { + return map.keySet().removeAll(c); + } + + public boolean retainAll(Collection c) { + return map.keySet().retainAll(c); + } + + public int size() { + return map.size(); + } + + public Object[] toArray() { + return map.keySet().toArray(); + } + + public E[] toArray(E[] a) { + return map.keySet().toArray(a); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/util/CryptoUtil.java b/stripes/src/main/java/net/sourceforge/stripes/util/CryptoUtil.java index 4e6df99d8..29d8c0325 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/util/CryptoUtil.java +++ b/stripes/src/main/java/net/sourceforge/stripes/util/CryptoUtil.java @@ -16,7 +16,6 @@ import java.security.MessageDigest; import java.security.SecureRandom; - import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; @@ -25,365 +24,365 @@ import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESedeKeySpec; import javax.crypto.spec.IvParameterSpec; - import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.controller.StripesFilter; import net.sourceforge.stripes.exception.StripesRuntimeException; /** - *

    Cryptographic utility that can encrypt and decrypt Strings using a key stored in - * HttpSession. Strings are encrypted by default using a 168bit DEDede (triple DES) key - * and then Base 64 encoded in a way that is compatible with being inserte into web pages.

    + * Cryptographic utility that can encrypt and decrypt Strings using a key stored in HttpSession. + * Strings are encrypted by default using a 168bit DEDede (triple DES) key and then Base 64 encoded + * in a way that is compatible with being inserte into web pages. * - *

    A single encryption key is used to encrypt values for all sessions in the web application. - * The key can come from multiple sources. Without any configuration the key will be generated - * using a SecureRandom the first time it is needed. Note: this will result in encrypted - * values that are not decryptable across application restarts or across nodes in a cluster. - * Alternatively specific key material can be specified using the configuration parameter - * Stripes.EncryptionKey in web.xml. This key is text that is used to generate - * a secret key, and ideally should be quite long (at least 20 characters). If a key is - * configured this way the same key will be used across all nodes in a cluster and across - * restarts.

    + *

    A single encryption key is used to encrypt values for all sessions in the web application. The + * key can come from multiple sources. Without any configuration the key will be generated using a + * SecureRandom the first time it is needed. Note: this will result in encrypted values that are + * not decryptable across application restarts or across nodes in a cluster. Alternatively + * specific key material can be specified using the configuration parameter + * Stripes.EncryptionKey in web.xml. This key is text that is used to generate a secret key, + * and ideally should be quite long (at least 20 characters). If a key is configured this way the + * same key will be used across all nodes in a cluster and across restarts. * *

    Finally a key can be specified by calling {@link #setSecretKey(javax.crypto.SecretKey)} and - * providing your own {@link SecretKey} instance. This method allows the specification of any - * key from any source. In addition the provided key can be for any algorithm supported by - * the JVM in which it is constructed. CryptoUtil will then use the algorithm returned by - * {@link javax.crypto.SecretKey#getAlgorithm()}. If using this method, the key should be set - * before any requests are made, e.g. in a {@link javax.servlet.ServletContextListener}.

    + * providing your own {@link SecretKey} instance. This method allows the specification of any key + * from any source. In addition the provided key can be for any algorithm supported by the JVM in + * which it is constructed. CryptoUtil will then use the algorithm returned by {@link + * javax.crypto.SecretKey#getAlgorithm()}. If using this method, the key should be set before any + * requests are made, e.g. in a {@link jakarta.servlet.ServletContextListener}. * - *

    Stripes originally performed a broken authentication scheme. It was rewritten in STS-934 - * to perform the Encrypt-then-Mac pattern. Also the encryption mode was changed from ECB to CBC.

    + *

    Stripes originally performed a broken authentication scheme. It was rewritten in STS-934 to + * perform the Encrypt-then-Mac pattern. Also the encryption mode was changed from ECB to CBC. * * @author Tim Fennell * @since Stripes 1.2 * @see https://en.wikipedia.org/wiki/Authenticated_encryption */ public class CryptoUtil { - private static final Log log = Log.getInstance(CryptoUtil.class); - - /** The algorithm that is used to encrypt values. */ - protected static final String ALGORITHM = "DESede"; - protected static final String CIPHER_MODE_MODIFIER = "/CBC/PKCS5Padding"; - protected static final int CIPHER_BLOCK_LENGTH = 8; - private static final String CIPHER_HMAC_ALGORITHM = "HmacSHA256"; - private static final int CIPHER_HMAC_LENGTH = 32; - - /** Key used to look up the location of a secret key. */ - public static final String CONFIG_ENCRYPTION_KEY = "Stripes.EncryptionKey"; - - /** Minimum number of bytes to raise the key material to before generating a key. */ - private static final int MIN_KEY_BYTES = 128; - - /** The options used for Base64 Encoding. */ - private static final int BASE64_OPTIONS = Base64.URL_SAFE | Base64.DONT_BREAK_LINES; - - /** Secret key to be used o encrypt and decrypt values. */ - private static SecretKey secretKey; - - /** - * Takes in a String, encrypts it and then base64 encodes the resulting byte[] so that it can be - * transmitted and stored as a String. Can be decrypted by a subsequent call to - * {@link #decrypt(String)}. Because, null and "" are equivalent to the Stripes binding engine, - * if {@code input} is null, then it will be encrypted as if it were "". - * - * @param input the String to encrypt and encode - * @return the encrypted, base64 encoded String - */ - public static String encrypt(String input) { - if (input == null) - input = ""; - - // encryption is disabled in debug mode - Configuration configuration = StripesFilter.getConfiguration(); - if (configuration != null && configuration.isDebugMode()) - return input; - - try { - byte[] inbytes = input.getBytes(); - final int inputLength = inbytes.length; - byte[] output = new byte[ calculateCipherbytes(inputLength) + CIPHER_HMAC_LENGTH ]; - - //key required by cipher and hmac - SecretKey key = getSecretKey(); - - /* - * Generate an initialization vector required by block cipher modes - */ - byte[] iv = generateInitializationVector(); - System.arraycopy(iv, 0, output, 0, CIPHER_BLOCK_LENGTH); - - /* - * Encrypt-then-Mac (EtM) pattern, first encrypt plaintext - */ - - Cipher cipher = getCipher(key, Cipher.ENCRYPT_MODE, iv, 0, CIPHER_BLOCK_LENGTH); - cipher.doFinal(inbytes, 0, inbytes.length, output, CIPHER_BLOCK_LENGTH); - - /* - * Encrypt-then-Mac (EtM) pattern, authenticate ciphertext - */ - hmac(key, output, 0, output.length - CIPHER_HMAC_LENGTH, output, output.length - CIPHER_HMAC_LENGTH); - - // Then base64 encode the bytes - return Base64.encodeBytes(output, BASE64_OPTIONS); - } - catch (Exception e) { - throw new StripesRuntimeException("Could not encrypt value.", e); - } + private static final Log log = Log.getInstance(CryptoUtil.class); + + /** The algorithm that is used to encrypt values. */ + protected static final String ALGORITHM = "DESede"; + + protected static final String CIPHER_MODE_MODIFIER = "/CBC/PKCS5Padding"; + protected static final int CIPHER_BLOCK_LENGTH = 8; + private static final String CIPHER_HMAC_ALGORITHM = "HmacSHA256"; + private static final int CIPHER_HMAC_LENGTH = 32; + + /** Key used to look up the location of a secret key. */ + public static final String CONFIG_ENCRYPTION_KEY = "Stripes.EncryptionKey"; + + /** Minimum number of bytes to raise the key material to before generating a key. */ + private static final int MIN_KEY_BYTES = 128; + + /** The options used for Base64 Encoding. */ + private static final int BASE64_OPTIONS = Base64.URL_SAFE | Base64.DONT_BREAK_LINES; + + /** Secret key to be used o encrypt and decrypt values. */ + private static SecretKey secretKey; + + /** + * Takes in a String, encrypts it and then base64 encodes the resulting byte[] so that it can be + * transmitted and stored as a String. Can be decrypted by a subsequent call to {@link + * #decrypt(String)}. Because, null and "" are equivalent to the Stripes binding engine, if {@code + * input} is null, then it will be encrypted as if it were "". + * + * @param input the String to encrypt and encode + * @return the encrypted, base64 encoded String + */ + public static String encrypt(String input) { + if (input == null) input = ""; + + // encryption is disabled in debug mode + Configuration configuration = StripesFilter.getConfiguration(); + if (configuration != null && configuration.isDebugMode()) return input; + + try { + byte[] inbytes = input.getBytes(); + final int inputLength = inbytes.length; + byte[] output = new byte[calculateCipherbytes(inputLength) + CIPHER_HMAC_LENGTH]; + + // key required by cipher and hmac + SecretKey key = getSecretKey(); + + /* + * Generate an initialization vector required by block cipher modes + */ + byte[] iv = generateInitializationVector(); + System.arraycopy(iv, 0, output, 0, CIPHER_BLOCK_LENGTH); + + /* + * Encrypt-then-Mac (EtM) pattern, first encrypt plaintext + */ + + Cipher cipher = getCipher(key, Cipher.ENCRYPT_MODE, iv, 0, CIPHER_BLOCK_LENGTH); + cipher.doFinal(inbytes, 0, inbytes.length, output, CIPHER_BLOCK_LENGTH); + + /* + * Encrypt-then-Mac (EtM) pattern, authenticate ciphertext + */ + hmac( + key, + output, + 0, + output.length - CIPHER_HMAC_LENGTH, + output, + output.length - CIPHER_HMAC_LENGTH); + + // Then base64 encode the bytes + return Base64.encodeBytes(output, BASE64_OPTIONS); + } catch (Exception e) { + throw new StripesRuntimeException("Could not encrypt value.", e); + } + } + + /** + * Generates IV, random start bytes required by most block cipher modes, which is intended to + * prevent analyzing a cipher mode as an xor substitution cipher. + * + * @return CIPHER_BLOCK_LENGTH bytes of random data + */ + private static byte[] generateInitializationVector() { + // always create a new SecureRandom; get new OS/JVM random bytes instead of cycling the prng. + SecureRandom random = new SecureRandom(); + byte[] iv = new byte[CIPHER_BLOCK_LENGTH]; + random.nextBytes(iv); + return iv; + } + + /** + * Performs keyed authentication using HMAC. Note: When building ciphertext+hmac array, data and + * mac will be the same array, and dataLength == macPos. + * + * @param key the authentication key + * @param data the data to be authenticated + * @param dataPos start of data to be authenticated + * @param dataLength the number of bytes to be authenticated + * @param mac the array which holds the resulting hmac + * @param macPos the position to write the hmac to + */ + private static void hmac( + SecretKey key, byte[] data, int dataPos, int dataLength, byte[] mac, int macPos) + throws Exception { + Mac m = Mac.getInstance(CIPHER_HMAC_ALGORITHM); + m.init(key); + m.update(data, dataPos, dataLength); + m.doFinal(mac, macPos); + } + + /** + * Returns the ciphertext length for a given plaintext, + * + * @param inputLength the length of plaintext + * @return the length of ciphertext, calculated based on blockcipher block size + */ + private static int calculateCipherbytes(int inputLength) { + // 2 = IV + last block (including padding) + int blocks = 2 + (inputLength / CIPHER_BLOCK_LENGTH); + return blocks * CIPHER_BLOCK_LENGTH; + } + + /** + * Takes in a base64 encoded and encrypted String that was generated by a call to {@link + * #encrypt(String)} and decrypts it. If {@code input} is null, then null will be returned. + * + * @param input the base64 String to decode and decrypt + * @return the decrypted String + */ + public static String decrypt(String input) { + if (input == null) return null; + + // encryption is disabled in debug mode + Configuration configuration = StripesFilter.getConfiguration(); + if (configuration != null && configuration.isDebugMode()) return input; + + // First un-base64 the String + byte[] bytes = Base64.decode(input, BASE64_OPTIONS); + if (bytes == null || bytes.length < 1) { + log.warn("Input is not Base64 encoded: ", input); + return null; } - /** - * Generates IV, random start bytes required by most block cipher modes, - * which is intended to prevent analyzing a cipher mode as an xor substitution cipher. - * - * @return CIPHER_BLOCK_LENGTH bytes of random data - */ - private static byte[] generateInitializationVector() { - // always create a new SecureRandom; get new OS/JVM random bytes instead of cycling the prng. - SecureRandom random = new SecureRandom(); - byte[] iv = new byte[CIPHER_BLOCK_LENGTH]; - random.nextBytes(iv); - return iv; - } - - /** - * Performs keyed authentication using HMAC. - * Note: When building ciphertext+hmac array, data and mac will be the same array, and dataLength == macPos. - * @param key the authentication key - * @param data the data to be authenticated - * @param dataPos start of data to be authenticated - * @param dataLength the number of bytes to be authenticated - * @param mac the array which holds the resulting hmac - * @param macPos the position to write the hmac to - */ - private static void hmac(SecretKey key, byte[] data, int dataPos, int dataLength, byte[] mac, int macPos) throws Exception { - Mac m = Mac.getInstance(CIPHER_HMAC_ALGORITHM); - m.init(key); - m.update(data, dataPos, dataLength); - m.doFinal(mac, macPos); - } - - /** - * Returns the ciphertext length for a given plaintext, - * @param inputLength the length of plaintext - * @return the length of ciphertext, calculated based on blockcipher block size - */ - private static int calculateCipherbytes(int inputLength) { - // 2 = IV + last block (including padding) - int blocks = 2 + (inputLength/CIPHER_BLOCK_LENGTH); - return blocks * CIPHER_BLOCK_LENGTH; - } - - /** - * Takes in a base64 encoded and encrypted String that was generated by a call to - * {@link #encrypt(String)} and decrypts it. If {@code input} is null, then null will be - * returned. - * - * @param input the base64 String to decode and decrypt - * @return the decrypted String - */ - public static String decrypt(String input) { - if (input == null) - return null; - - // encryption is disabled in debug mode - Configuration configuration = StripesFilter.getConfiguration(); - if (configuration != null && configuration.isDebugMode()) - return input; - - // First un-base64 the String - byte[] bytes = Base64.decode(input, BASE64_OPTIONS); - if (bytes == null || bytes.length < 1) { - log.warn("Input is not Base64 encoded: ", input); - return null; - } - - if (bytes.length < CIPHER_BLOCK_LENGTH * 2 + CIPHER_HMAC_LENGTH) { - log.warn("Input is too short: ", input); - return null; - } + if (bytes.length < CIPHER_BLOCK_LENGTH * 2 + CIPHER_HMAC_LENGTH) { + log.warn("Input is too short: ", input); + return null; + } - SecretKey key = getSecretKey(); - - /* - * HMAC: validate ciphertext integrity. - * invalid hmac = choosen ciphertext attack against system. - * - * Encrypt-then-Mac (EtM) pattern, HMAC must be validated before the dangerous decrypt operation. - * - */ - - byte[] mac = new byte[CIPHER_HMAC_LENGTH]; - try { - hmac(key, bytes, 0, bytes.length - CIPHER_HMAC_LENGTH, mac, 0); - } catch (Exception e1) { - log.warn("Unexpected error performing hmac on: ", input); - return null; - } - - boolean validCiphertext; - try { - validCiphertext = hmacEquals(key, bytes, bytes.length - CIPHER_HMAC_LENGTH, mac, 0); - } catch (Exception e1) { - log.warn("Unexpected error validating hmac of: ", input); - return null; - } - if (!validCiphertext) { - log.warn("Input was not encrypted with the current encryption key (bad HMAC): ", input); - return null; - } - - /* - * Encrypt-then-Mac pattern; - * If validation success, ciphertext is assumed to be friendly and safe to process. - * Padding attacks, wrong blocklength etc is not expected from this point. - * - */ - - // Then fetch a cipher and decrypt the bytes - Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE, bytes, 0, CIPHER_BLOCK_LENGTH); - byte[] output; - try { - output = cipher.doFinal(bytes, CIPHER_BLOCK_LENGTH, bytes.length - CIPHER_HMAC_LENGTH - CIPHER_BLOCK_LENGTH); - } - catch (IllegalBlockSizeException e) { - log.warn("Unexpected IllegalBlockSizeException on: ", input); - return null; - } - catch (BadPaddingException e) { - log.warn("Unexpected BadPaddingException on: ", input); - return null; - } + SecretKey key = getSecretKey(); - return new String(output); + /* + * HMAC: validate ciphertext integrity. + * invalid hmac = choosen ciphertext attack against system. + * + * Encrypt-then-Mac (EtM) pattern, HMAC must be validated before the dangerous decrypt operation. + * + */ + + byte[] mac = new byte[CIPHER_HMAC_LENGTH]; + try { + hmac(key, bytes, 0, bytes.length - CIPHER_HMAC_LENGTH, mac, 0); + } catch (Exception e1) { + log.warn("Unexpected error performing hmac on: ", input); + return null; } - /** - * Compares HMAC in a manner secured against timing attacks, as per NCC Group "Double Hmac Verification" recepie. - * Destructive compare, the hmac's will be replaced with the hmac's of themselves as a side effect. - * @param key the hmac crypto key - * @param mac1 the array which contains the hmac - * @param mac1pos the position of the hmac in mac1 array. - * @param mac2 the array which contains the hmac - * @param mac2pos the position of the hmac in mac2 array. - * @return true if hmacs are equal, otherwise false - * @see double hmac as per https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/ - */ - private static boolean hmacEquals(SecretKey key, byte[] mac1, int mac1pos, - byte[] mac2, int mac2pos) throws Exception { - hmac(key, mac1, mac1pos, CIPHER_HMAC_LENGTH, mac1, mac1pos); - hmac(key, mac2, mac2pos, CIPHER_HMAC_LENGTH, mac2, mac2pos); - for(int i = 0; i < CIPHER_HMAC_LENGTH; i++) - if (mac1[mac1pos+i] != mac2[mac2pos+i]) - return false; - return true; - } - - /** - * Generates a cipher based on a key and an initialization vector - * @param key the crypto key - * @param mode Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE - * @param iv the initialization vector - * @param ivpos the start position of the initialization vector, typically 0 - * @param ivlength the length of the initialization vector - * @return the cipher object - * @see Cipher#ENCRYPT_MODE - * @see Cipher#DECRYPT_MODE - */ - protected static Cipher getCipher(SecretKey key, int mode, byte[] iv, int ivpos, int ivlength) { - try { - // Then build a cipher for the correct mode - Cipher cipher = Cipher.getInstance(key.getAlgorithm() + CIPHER_MODE_MODIFIER); - IvParameterSpec ivps = new IvParameterSpec(iv, ivpos, ivlength); - cipher.init(mode, key, ivps); - return cipher; - } - catch (Exception e) { - throw new StripesRuntimeException("Could not generate a Cipher.", e); - } + boolean validCiphertext; + try { + validCiphertext = hmacEquals(key, bytes, bytes.length - CIPHER_HMAC_LENGTH, mac, 0); + } catch (Exception e1) { + log.warn("Unexpected error validating hmac of: ", input); + return null; + } + if (!validCiphertext) { + log.warn("Input was not encrypted with the current encryption key (bad HMAC): ", input); + return null; } - /** - * Returns the secret key to be used to encrypt and decrypt values. The key will be generated - * the first time it is requested. Will look for source material for the key in config and - * use that if found. Otherwise will generate key material using a SecureRandom and then - * manufacture the key. Once the key is created it is cached locally and the - * same key instance will be returned until the application is shutdown or restarted. + /* + * Encrypt-then-Mac pattern; + * If validation success, ciphertext is assumed to be friendly and safe to process. + * Padding attacks, wrong blocklength etc is not expected from this point. * - * @return SecretKey the secret key used to encrypt and decrypt values */ - protected static synchronized SecretKey getSecretKey() { - try { - if (CryptoUtil.secretKey == null) { - // Check to see if a key location was specified in config - byte[] material = getKeyMaterialFromConfig(); - - // If there wasn't a key string in config, make one - if (material == null) { - material = new byte[MIN_KEY_BYTES]; - new SecureRandom().nextBytes(material); - } - // Hash the key string given in config - else { - MessageDigest digest = MessageDigest.getInstance("SHA1"); - int length = digest.getDigestLength(); - byte[] hashed = new byte[MIN_KEY_BYTES]; - for (int i = 0; i < hashed.length; i += length) { - material = digest.digest(material); - System.arraycopy(material, 0, hashed, i, - Math.min(length, MIN_KEY_BYTES - i)); - } - material = hashed; - } - - // Now manufacture the actual Secret Key instance - SecretKeyFactory factory = SecretKeyFactory.getInstance(CryptoUtil.ALGORITHM); - CryptoUtil.secretKey = factory.generateSecret(new DESedeKeySpec(material)); - } - } - catch (Exception e) { - throw new StripesRuntimeException("Could not generate a secret key.", e); - } - return CryptoUtil.secretKey; + // Then fetch a cipher and decrypt the bytes + Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE, bytes, 0, CIPHER_BLOCK_LENGTH); + byte[] output; + try { + output = + cipher.doFinal( + bytes, CIPHER_BLOCK_LENGTH, bytes.length - CIPHER_HMAC_LENGTH - CIPHER_BLOCK_LENGTH); + } catch (IllegalBlockSizeException e) { + log.warn("Unexpected IllegalBlockSizeException on: ", input); + return null; + } catch (BadPaddingException e) { + log.warn("Unexpected BadPaddingException on: ", input); + return null; } - /** - * Attempts to load material from which to manufacture a secret key from the Stripes - * Configuration. If config is unavailable or there is no material configured null - * will be returned. - * - * @return a byte[] of key material, or null - */ - protected static byte[] getKeyMaterialFromConfig() { - try { - Configuration config = StripesFilter.getConfiguration(); - if (config != null) { - String key = config.getBootstrapPropertyResolver().getProperty(CONFIG_ENCRYPTION_KEY); - if (key != null) { - return key.getBytes(); - } - } + return new String(output); + } + + /** + * Compares HMAC in a manner secured against timing attacks, as per NCC Group "Double Hmac + * Verification" recepie. Destructive compare, the hmac's will be replaced with the hmac's of + * themselves as a side effect. + * + * @param key the hmac crypto key + * @param mac1 the array which contains the hmac + * @param mac1pos the position of the hmac in mac1 array. + * @param mac2 the array which contains the hmac + * @param mac2pos the position of the hmac in mac2 array. + * @return true if hmacs are equal, otherwise false + * @see double hmac as per + * https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/ + */ + private static boolean hmacEquals( + SecretKey key, byte[] mac1, int mac1pos, byte[] mac2, int mac2pos) throws Exception { + hmac(key, mac1, mac1pos, CIPHER_HMAC_LENGTH, mac1, mac1pos); + hmac(key, mac2, mac2pos, CIPHER_HMAC_LENGTH, mac2, mac2pos); + for (int i = 0; i < CIPHER_HMAC_LENGTH; i++) + if (mac1[mac1pos + i] != mac2[mac2pos + i]) return false; + return true; + } + + /** + * Generates a cipher based on a key and an initialization vector + * + * @param key the crypto key + * @param mode Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE + * @param iv the initialization vector + * @param ivpos the start position of the initialization vector, typically 0 + * @param ivlength the length of the initialization vector + * @return the cipher object + * @see Cipher#ENCRYPT_MODE + * @see Cipher#DECRYPT_MODE + */ + protected static Cipher getCipher(SecretKey key, int mode, byte[] iv, int ivpos, int ivlength) { + try { + // Then build a cipher for the correct mode + Cipher cipher = Cipher.getInstance(key.getAlgorithm() + CIPHER_MODE_MODIFIER); + IvParameterSpec ivps = new IvParameterSpec(iv, ivpos, ivlength); + cipher.init(mode, key, ivps); + return cipher; + } catch (Exception e) { + throw new StripesRuntimeException("Could not generate a Cipher.", e); + } + } + + /** + * Returns the secret key to be used to encrypt and decrypt values. The key will be generated the + * first time it is requested. Will look for source material for the key in config and use that if + * found. Otherwise will generate key material using a SecureRandom and then manufacture the key. + * Once the key is created it is cached locally and the same key instance will be returned until + * the application is shutdown or restarted. + * + * @return SecretKey the secret key used to encrypt and decrypt values + */ + protected static synchronized SecretKey getSecretKey() { + try { + if (CryptoUtil.secretKey == null) { + // Check to see if a key location was specified in config + byte[] material = getKeyMaterialFromConfig(); + + // If there wasn't a key string in config, make one + if (material == null) { + material = new byte[MIN_KEY_BYTES]; + new SecureRandom().nextBytes(material); } - catch (Exception e) { - log.warn("Could not load key material from configuration.", e); + // Hash the key string given in config + else { + MessageDigest digest = MessageDigest.getInstance("SHA1"); + int length = digest.getDigestLength(); + byte[] hashed = new byte[MIN_KEY_BYTES]; + for (int i = 0; i < hashed.length; i += length) { + material = digest.digest(material); + System.arraycopy(material, 0, hashed, i, Math.min(length, MIN_KEY_BYTES - i)); + } + material = hashed; } - return null; + // Now manufacture the actual Secret Key instance + SecretKeyFactory factory = SecretKeyFactory.getInstance(CryptoUtil.ALGORITHM); + CryptoUtil.secretKey = factory.generateSecret(new DESedeKeySpec(material)); + } + } catch (Exception e) { + throw new StripesRuntimeException("Could not generate a secret key.", e); } - /** - * Sets the secret key that will be used by the CryptoUtil to perform encryption - * and decryption. In general the use of the config property (Stripes.EncryptionKey) - * should be preferred, but if specific encryption methods are required, this method - * allows the caller to set a SecretKey suitable to any symmetric encryption algorithm - * available in the JVM. - * - * @param key the secret key to be used to encrypt and decrypt values going forward - */ - public static synchronized void setSecretKey(SecretKey key) { - CryptoUtil.secretKey = key; + return CryptoUtil.secretKey; + } + + /** + * Attempts to load material from which to manufacture a secret key from the Stripes + * Configuration. If config is unavailable or there is no material configured null will be + * returned. + * + * @return a byte[] of key material, or null + */ + protected static byte[] getKeyMaterialFromConfig() { + try { + Configuration config = StripesFilter.getConfiguration(); + if (config != null) { + String key = config.getBootstrapPropertyResolver().getProperty(CONFIG_ENCRYPTION_KEY); + if (key != null) { + return key.getBytes(); + } + } + } catch (Exception e) { + log.warn("Could not load key material from configuration.", e); } + return null; + } + + /** + * Sets the secret key that will be used by the CryptoUtil to perform encryption and decryption. + * In general the use of the config property (Stripes.EncryptionKey) should be preferred, but if + * specific encryption methods are required, this method allows the caller to set a SecretKey + * suitable to any symmetric encryption algorithm available in the JVM. + * + * @param key the secret key to be used to encrypt and decrypt values going forward + */ + public static synchronized void setSecretKey(SecretKey key) { + CryptoUtil.secretKey = key; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/util/HtmlUtil.java b/stripes/src/main/java/net/sourceforge/stripes/util/HtmlUtil.java index 1df0ff93c..8daa2939d 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/util/HtmlUtil.java +++ b/stripes/src/main/java/net/sourceforge/stripes/util/HtmlUtil.java @@ -25,80 +25,85 @@ * @author Tim Fennell */ public class HtmlUtil { - private static final String FIELD_DELIMITER_STRING = "||"; - private static final Pattern FIELD_DELIMITER_PATTERN = Pattern.compile("\\|\\|"); + private static final String FIELD_DELIMITER_STRING = "||"; + private static final Pattern FIELD_DELIMITER_PATTERN = Pattern.compile("\\|\\|"); - /** - * Replaces special HTML characters from the set {@literal [<, >, ", ', &]} with their HTML - * escape codes. Note that because the escape codes are multi-character that the returned - * String could be longer than the one passed in. - * - * @param fragment a String fragment that might have HTML special characters in it - * @return the fragment with special characters escaped - */ - public static String encode(String fragment) { - // If the input is null, then the output is null - if (fragment == null) return null; + /** + * Replaces special HTML characters from the set {@literal [<, >, ", ', &]} with their HTML escape + * codes. Note that because the escape codes are multi-character that the returned String could be + * longer than the one passed in. + * + * @param fragment a String fragment that might have HTML special characters in it + * @return the fragment with special characters escaped + */ + public static String encode(String fragment) { + // If the input is null, then the output is null + if (fragment == null) return null; - StringBuilder builder = new StringBuilder(fragment.length() + 10); // a little wiggle room - char[] characters = fragment.toCharArray(); + StringBuilder builder = new StringBuilder(fragment.length() + 10); // a little wiggle room + char[] characters = fragment.toCharArray(); - // This loop used to also look for and replace single ticks with ' but it - // turns out that it's not strictly necessary since Stripes uses double-quotes - // around all form fields, and stupid IE6 will render ' verbatim instead - // of as a single quote. - for (int i=0; i' : builder.append(">"); break; - case '"' : builder.append("""); break; - case '&' : builder.append("&"); break; - default: builder.append(characters[i]); - } - } - - return builder.toString(); + // This loop used to also look for and replace single ticks with ' but it + // turns out that it's not strictly necessary since Stripes uses double-quotes + // around all form fields, and stupid IE6 will render ' verbatim instead + // of as a single quote. + for (int i = 0; i < characters.length; ++i) { + switch (characters[i]) { + case '<': + builder.append("<"); + break; + case '>': + builder.append(">"); + break; + case '"': + builder.append("""); + break; + case '&': + builder.append("&"); + break; + default: + builder.append(characters[i]); + } } - /** - * One of a pair of methods (the other is splitValues) that is used to combine several - * un-encoded values into a single delimited, encoded value for placement into a - * hidden field. - * - * @param values One or more values which are to be combined - * @return a single HTML-encoded String that contains all the values in such a way that - * they can be converted back into a Collection of Strings with splitValues(). - */ - public static String combineValues(Collection values) { - if (values == null || values.size() == 0) { - return ""; - } - else { - StringBuilder builder = new StringBuilder(values.size() * 30); - for (String value : values) { - builder.append(value).append(FIELD_DELIMITER_STRING); - } - - return encode(builder.toString()); - } + return builder.toString(); + } + + /** + * One of a pair of methods (the other is splitValues) that is used to combine several un-encoded + * values into a single delimited, encoded value for placement into a hidden field. + * + * @param values One or more values which are to be combined + * @return a single HTML-encoded String that contains all the values in such a way that they can + * be converted back into a Collection of Strings with splitValues(). + */ + public static String combineValues(Collection values) { + if (values == null || values.size() == 0) { + return ""; + } else { + StringBuilder builder = new StringBuilder(values.size() * 30); + for (String value : values) { + builder.append(value).append(FIELD_DELIMITER_STRING); + } + + return encode(builder.toString()); } + } - /** - * Takes in a String produced by combineValues and returns a Collection of values that - * contains the same values as originally supplied to combineValues. Note that the order - * or items in the collection (and indeed the type of Collection used) are not guaranteed - * to be the same. - * - * @param value a String value produced by - * @return a Collection of zero or more Strings - */ - public static Collection splitValues(String value) { - if (value == null || value.length() == 0) { - return Collections.emptyList(); - } - else { - String[] splits = FIELD_DELIMITER_PATTERN.split(value); - return Arrays.asList(splits); - } + /** + * Takes in a String produced by combineValues and returns a Collection of values that contains + * the same values as originally supplied to combineValues. Note that the order or items in the + * collection (and indeed the type of Collection used) are not guaranteed to be the same. + * + * @param value a String value produced by + * @return a Collection of zero or more Strings + */ + public static Collection splitValues(String value) { + if (value == null || value.length() == 0) { + return Collections.emptyList(); + } else { + String[] splits = FIELD_DELIMITER_PATTERN.split(value); + return Arrays.asList(splits); } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/util/HttpUtil.java b/stripes/src/main/java/net/sourceforge/stripes/util/HttpUtil.java index ec7ef94ee..690611e7e 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/util/HttpUtil.java +++ b/stripes/src/main/java/net/sourceforge/stripes/util/HttpUtil.java @@ -14,79 +14,73 @@ */ package net.sourceforge.stripes.util; -import javax.servlet.http.HttpServletRequest; - +import jakarta.servlet.http.HttpServletRequest; import net.sourceforge.stripes.controller.StripesConstants; /** * Provides helper methods for working with HTTP requests and responses. - * + * * @author Ben Gunter * @since Stripes 1.5.1 */ public class HttpUtil { - /** - *

    - * Get the path from the given request. This method is different from - * {@link HttpServletRequest#getRequestURI()} in that it concatenates and returns the servlet - * path plus the path info from the request. These are usually the same, but in some cases they - * are not. - *

    - *

    - * One case where they are known to differ is when a request for a directory is forwarded by the - * servlet container to a welcome file. In that case, {@link HttpServletRequest#getRequestURI()} - * returns the path that was actually requested (e.g., {@code "/"}), whereas the servlet path - * plus path info is the path to the welcome file (e.g. {@code "/index.jsp"}). - *

    - */ - public static String getRequestedPath(HttpServletRequest request) { - String servletPath, pathInfo; - - // Check to see if the request is processing an include, and pull the path - // information from the appropriate source. - // only request attributes need decoding, not servletPath and pathInfo - // see http://www.stripesframework.org/jira/browse/STS-899 + /** + * Get the path from the given request. This method is different from {@link + * HttpServletRequest#getRequestURI()} in that it concatenates and returns the servlet path plus + * the path info from the request. These are usually the same, but in some cases they are not. + * + *

    One case where they are known to differ is when a request for a directory is forwarded by + * the servlet container to a welcome file. In that case, {@link + * HttpServletRequest#getRequestURI()} returns the path that was actually requested (e.g., {@code + * "/"}), whereas the servlet path plus path info is the path to the welcome file (e.g. {@code + * "/index.jsp"}). + */ + public static String getRequestedPath(HttpServletRequest request) { + String servletPath, pathInfo; - servletPath = urlDecodeNullSafe((String) request.getAttribute(StripesConstants.REQ_ATTR_INCLUDE_PATH)); - if (servletPath != null) { - pathInfo = urlDecodeNullSafe((String) request.getAttribute(StripesConstants.REQ_ATTR_INCLUDE_PATH_INFO)); - } - else { - servletPath = request.getServletPath(); - pathInfo = request.getPathInfo(); - } + // Check to see if the request is processing an include, and pull the path + // information from the appropriate source. + // only request attributes need decoding, not servletPath and pathInfo + // see http://www.stripesframework.org/jira/browse/STS-899 - if (servletPath == null) - return pathInfo == null ? "" : pathInfo; - else if (pathInfo == null) - return servletPath; - else - return servletPath + pathInfo; + servletPath = + urlDecodeNullSafe((String) request.getAttribute(StripesConstants.REQ_ATTR_INCLUDE_PATH)); + if (servletPath != null) { + pathInfo = + urlDecodeNullSafe( + (String) request.getAttribute(StripesConstants.REQ_ATTR_INCLUDE_PATH_INFO)); + } else { + servletPath = request.getServletPath(); + pathInfo = request.getPathInfo(); } - /** - * Get the servlet path of the current request. The value returned by this method may differ - * from {@link HttpServletRequest#getServletPath()}. If the given request is an include, then - * the servlet path of the included resource is returned. - */ - public static String getRequestedServletPath(HttpServletRequest request) { - // Check to see if the request is processing an include, and pull the path - // information from the appropriate source. - String path = (String) request.getAttribute(StripesConstants.REQ_ATTR_INCLUDE_PATH); - if (path == null) { - path = request.getServletPath(); - } - return path == null ? "" : path; - } + if (servletPath == null) return pathInfo == null ? "" : pathInfo; + else if (pathInfo == null) return servletPath; + else return servletPath + pathInfo; + } - /** No instances */ - private HttpUtil() { + /** + * Get the servlet path of the current request. The value returned by this method may differ from + * {@link HttpServletRequest#getServletPath()}. If the given request is an include, then the + * servlet path of the included resource is returned. + */ + public static String getRequestedServletPath(HttpServletRequest request) { + // Check to see if the request is processing an include, and pull the path + // information from the appropriate source. + String path = (String) request.getAttribute(StripesConstants.REQ_ATTR_INCLUDE_PATH); + if (path == null) { + path = request.getServletPath(); } + return path == null ? "" : path; + } + + /** No instances */ + private HttpUtil() {} - private static String urlDecodeNullSafe(String url) { - if (url==null) { - return null; - } - return StringUtil.urlDecode(url); + private static String urlDecodeNullSafe(String url) { + if (url == null) { + return null; } + return StringUtil.urlDecode(url); + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/util/Literal.java b/stripes/src/main/java/net/sourceforge/stripes/util/Literal.java index 0a49bbd0d..acdab9f79 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/util/Literal.java +++ b/stripes/src/main/java/net/sourceforge/stripes/util/Literal.java @@ -14,68 +14,86 @@ */ package net.sourceforge.stripes.util; -import java.util.List; -import java.util.Collections; import java.util.ArrayList; -import java.util.Set; +import java.util.Collections; import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; /** - * Utility class that makes it easy to construct Collection literals, and also provides - * a nicer syntax for creating array literals. + * Utility class that makes it easy to construct Collection literals, and also provides a nicer + * syntax for creating array literals. * * @author Tim Fennell * @since Stripes 1.1.2 */ public class Literal { - /** Returns an array containing all the elements supplied. */ - public static T[] array(T... elements) { return elements; } + /** Returns an array containing all the elements supplied. */ + public static T[] array(T... elements) { + return elements; + } - /** Returns an array containing all the elements supplied. */ - public static boolean[] array(boolean... elements) { return elements; } + /** Returns an array containing all the elements supplied. */ + public static boolean[] array(boolean... elements) { + return elements; + } - /** Returns an array containing all the elements supplied. */ - public static byte[] array(byte... elements) { return elements; } + /** Returns an array containing all the elements supplied. */ + public static byte[] array(byte... elements) { + return elements; + } - /** Returns an array containing all the elements supplied. */ - public static char[] array(char... elements) { return elements; } + /** Returns an array containing all the elements supplied. */ + public static char[] array(char... elements) { + return elements; + } - /** Returns an array containing all the elements supplied. */ - public static short[] array(short... elements) { return elements; } + /** Returns an array containing all the elements supplied. */ + public static short[] array(short... elements) { + return elements; + } - /** Returns an array containing all the elements supplied. */ - public static int[] array(int... elements) { return elements; } + /** Returns an array containing all the elements supplied. */ + public static int[] array(int... elements) { + return elements; + } - /** Returns an array containing all the elements supplied. */ - public static long[] array(long... elements) { return elements; } + /** Returns an array containing all the elements supplied. */ + public static long[] array(long... elements) { + return elements; + } - /** Returns an array containing all the elements supplied. */ - public static float[] array(float... elements) { return elements; } + /** Returns an array containing all the elements supplied. */ + public static float[] array(float... elements) { + return elements; + } - /** Returns an array containing all the elements supplied. */ - public static double[] array(double... elements) { return elements; } + /** Returns an array containing all the elements supplied. */ + public static double[] array(double... elements) { + return elements; + } - /** Returns a new List instance containing the supplied elements. */ - public static List list(T... elements) { - List list = new ArrayList(); - Collections.addAll(list, elements); - return list; - } + /** Returns a new List instance containing the supplied elements. */ + public static List list(T... elements) { + List list = new ArrayList(); + Collections.addAll(list, elements); + return list; + } - /** Returns a new Set instance containing the supplied elements. */ - public static Set set(T... elements) { - Set set = new HashSet(); - Collections.addAll(set, elements); - return set; - } + /** Returns a new Set instance containing the supplied elements. */ + public static Set set(T... elements) { + Set set = new HashSet(); + Collections.addAll(set, elements); + return set; + } - /** Returns a new SortedSet instance containing the supplied elements. */ - public static > SortedSet sortedSet(T... elements) { - SortedSet set = new TreeSet(); - Collections.addAll(set, elements); - return set; - } -} \ No newline at end of file + /** Returns a new SortedSet instance containing the supplied elements. */ + public static > SortedSet sortedSet(T... elements) { + SortedSet set = new TreeSet(); + Collections.addAll(set, elements); + return set; + } +} diff --git a/stripes/src/main/java/net/sourceforge/stripes/util/Log.java b/stripes/src/main/java/net/sourceforge/stripes/util/Log.java index 813528af6..b1687a6c0 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/util/Log.java +++ b/stripes/src/main/java/net/sourceforge/stripes/util/Log.java @@ -17,185 +17,197 @@ import org.apache.commons.logging.LogFactory; /** - *

    A wafer thin wrapper around Commons logging that uses var-args to make it - * much more efficient to call the logging methods in commons logging without having to - * surround every call site with calls to Log.isXXXEnabled(). All the methods on this - * class take a variable length list of arguments and, only if logging is enabled for - * the level and channel being logged to, will those arguments be toString()'d and - * appended together.

    + * A wafer thin wrapper around Commons logging that uses var-args to make it much more + * efficient to call the logging methods in commons logging without having to surround every call + * site with calls to Log.isXXXEnabled(). All the methods on this class take a variable length list + * of arguments and, only if logging is enabled for the level and channel being logged to, will + * those arguments be toString()'d and appended together. * * @author Tim Fennell */ public final class Log { - private org.apache.commons.logging.Log realLog; - - public org.apache.commons.logging.Log getRealLog() { - return realLog; - } - - /** - * Get a Log instance to perform logging within the Class specified. Returns an instance - * of this class which wraps an instance of the commons logging Log class. - * @param clazz the Class which is going to be doing the logging - * @return a Log instance with which to log - */ - public static Log getInstance(Class clazz) { - return new Log( LogFactory.getLog(clazz) ); - } - - /** - * Forces Log to cleanup any cached resources. This is called by the StripesFilter when - * it is destroyed, but can be called from user code as well if necessary. - */ - public static void cleanup() { - LogFactory.release(Thread.currentThread().getContextClassLoader()); - } - - /** - * Private constructor which creates a new Log instance wrapping the commons Log instance - * provided. Only used by the static getInstance() method on this class. - */ - private Log(org.apache.commons.logging.Log realLog) { - this.realLog = realLog; - } - - /** - * Logs a Throwable and optional message parts at level fatal. - * @param throwable an instance of Throwable that should be logged with stack trace - * @param messageParts zero or more objects which should be combined, by calling toString() - * to form the log message. - */ - public final void fatal(Throwable throwable, Object... messageParts) { - if (this.realLog.isFatalEnabled()) { - this.realLog.fatal(StringUtil.combineParts(messageParts), throwable); - } - } - - /** - * Logs a Throwable and optional message parts at level error. - * @param throwable an instance of Throwable that should be logged with stack trace - * @param messageParts zero or more objects which should be combined, by calling toString() - * to form the log message. - */ - public final void error(Throwable throwable, Object... messageParts) { - if (this.realLog.isErrorEnabled()) { - this.realLog.error(StringUtil.combineParts(messageParts), throwable); - } - } - - /** - * Logs a Throwable and optional message parts at level warn. - * @param throwable an instance of Throwable that should be logged with stack trace - * @param messageParts zero or more objects which should be combined, by calling toString() - * to form the log message. - */ - public final void warn(Throwable throwable, Object... messageParts) { - if (this.realLog.isWarnEnabled()) { - this.realLog.warn(StringUtil.combineParts(messageParts), throwable); - } - } - - /** - * Logs a Throwable and optional message parts at level info. - * @param throwable an instance of Throwable that should be logged with stack trace - * @param messageParts zero or more objects which should be combined, by calling toString() - * to form the log message. - */ - public final void info(Throwable throwable, Object... messageParts) { - if (this.realLog.isInfoEnabled()) { - this.realLog.info(StringUtil.combineParts(messageParts), throwable); - } - } - - /** - * Logs a Throwable and optional message parts at level debug. - * @param throwable an instance of Throwable that should be logged with stack trace - * @param messageParts zero or more objects which should be combined, by calling toString() - * to form the log message. - */ - public final void debug(Throwable throwable, Object... messageParts) { - if (this.realLog.isDebugEnabled()) { - this.realLog.debug(StringUtil.combineParts(messageParts), throwable); - } - } - - /** - * Logs a Throwable and optional message parts at level trace. - * @param throwable an instance of Throwable that should be logged with stack trace - * @param messageParts zero or more objects which should be combined, by calling toString() - * to form the log message. - */ - public final void trace(Throwable throwable, Object... messageParts) { - if (this.realLog.isTraceEnabled()) { - this.realLog.trace(StringUtil.combineParts(messageParts), throwable); - } - } - - // Similar methods, but without Throwables, follow - - /** - * Logs one or more message parts at level fatal. - * @param messageParts one or more objects which should be combined, by calling toString() - * to form the log message. - */ - public final void fatal(Object... messageParts) { - if (this.realLog.isFatalEnabled()) { - this.realLog.fatal(StringUtil.combineParts(messageParts)); - } - } - - /** - * Logs one or more message parts at level error. - * @param messageParts one or more objects which should be combined, by calling toString() - * to form the log message. - */ - public final void error(Object... messageParts) { - if (this.realLog.isErrorEnabled()) { - this.realLog.error(StringUtil.combineParts(messageParts)); - } - } - - /** - * Logs one or more message parts at level warn. - * @param messageParts one or more objects which should be combined, by calling toString() - * to form the log message. - */ - public final void warn(Object... messageParts) { - if (this.realLog.isWarnEnabled()) { - this.realLog.warn(StringUtil.combineParts(messageParts)); - } - } - - /** - * Logs one or more message parts at level info. - * @param messageParts one or more objects which should be combined, by calling toString() - * to form the log message. - */ - public final void info(Object... messageParts) { - if (this.realLog.isInfoEnabled()) { - this.realLog.info(StringUtil.combineParts(messageParts)); - } - } - - /** - * Logs one or more message parts at level debug. - * @param messageParts one or more objects which should be combined, by calling toString() - * to form the log message. - */ - public final void debug(Object... messageParts) { - if (this.realLog.isDebugEnabled()) { - this.realLog.debug(StringUtil.combineParts(messageParts)); - } - } - - /** - * Logs one or more message parts at level trace. - * @param messageParts one or more objects which should be combined, by calling toString() - * to form the log message. - */ - public final void trace(Object... messageParts) { - if (this.realLog.isTraceEnabled()) { - this.realLog.trace(StringUtil.combineParts(messageParts)); - } - } + private org.apache.commons.logging.Log realLog; + + public org.apache.commons.logging.Log getRealLog() { + return realLog; + } + + /** + * Get a Log instance to perform logging within the Class specified. Returns an instance of this + * class which wraps an instance of the commons logging Log class. + * + * @param clazz the Class which is going to be doing the logging + * @return a Log instance with which to log + */ + public static Log getInstance(Class clazz) { + return new Log(LogFactory.getLog(clazz)); + } + + /** + * Forces Log to cleanup any cached resources. This is called by the StripesFilter when it is + * destroyed, but can be called from user code as well if necessary. + */ + public static void cleanup() { + LogFactory.release(Thread.currentThread().getContextClassLoader()); + } + + /** + * Private constructor which creates a new Log instance wrapping the commons Log instance + * provided. Only used by the static getInstance() method on this class. + */ + private Log(org.apache.commons.logging.Log realLog) { + this.realLog = realLog; + } + + /** + * Logs a Throwable and optional message parts at level fatal. + * + * @param throwable an instance of Throwable that should be logged with stack trace + * @param messageParts zero or more objects which should be combined, by calling toString() to + * form the log message. + */ + public final void fatal(Throwable throwable, Object... messageParts) { + if (this.realLog.isFatalEnabled()) { + this.realLog.fatal(StringUtil.combineParts(messageParts), throwable); + } + } + + /** + * Logs a Throwable and optional message parts at level error. + * + * @param throwable an instance of Throwable that should be logged with stack trace + * @param messageParts zero or more objects which should be combined, by calling toString() to + * form the log message. + */ + public final void error(Throwable throwable, Object... messageParts) { + if (this.realLog.isErrorEnabled()) { + this.realLog.error(StringUtil.combineParts(messageParts), throwable); + } + } + + /** + * Logs a Throwable and optional message parts at level warn. + * + * @param throwable an instance of Throwable that should be logged with stack trace + * @param messageParts zero or more objects which should be combined, by calling toString() to + * form the log message. + */ + public final void warn(Throwable throwable, Object... messageParts) { + if (this.realLog.isWarnEnabled()) { + this.realLog.warn(StringUtil.combineParts(messageParts), throwable); + } + } + + /** + * Logs a Throwable and optional message parts at level info. + * + * @param throwable an instance of Throwable that should be logged with stack trace + * @param messageParts zero or more objects which should be combined, by calling toString() to + * form the log message. + */ + public final void info(Throwable throwable, Object... messageParts) { + if (this.realLog.isInfoEnabled()) { + this.realLog.info(StringUtil.combineParts(messageParts), throwable); + } + } + + /** + * Logs a Throwable and optional message parts at level debug. + * + * @param throwable an instance of Throwable that should be logged with stack trace + * @param messageParts zero or more objects which should be combined, by calling toString() to + * form the log message. + */ + public final void debug(Throwable throwable, Object... messageParts) { + if (this.realLog.isDebugEnabled()) { + this.realLog.debug(StringUtil.combineParts(messageParts), throwable); + } + } + + /** + * Logs a Throwable and optional message parts at level trace. + * + * @param throwable an instance of Throwable that should be logged with stack trace + * @param messageParts zero or more objects which should be combined, by calling toString() to + * form the log message. + */ + public final void trace(Throwable throwable, Object... messageParts) { + if (this.realLog.isTraceEnabled()) { + this.realLog.trace(StringUtil.combineParts(messageParts), throwable); + } + } + + // Similar methods, but without Throwables, follow + + /** + * Logs one or more message parts at level fatal. + * + * @param messageParts one or more objects which should be combined, by calling toString() to form + * the log message. + */ + public final void fatal(Object... messageParts) { + if (this.realLog.isFatalEnabled()) { + this.realLog.fatal(StringUtil.combineParts(messageParts)); + } + } + + /** + * Logs one or more message parts at level error. + * + * @param messageParts one or more objects which should be combined, by calling toString() to form + * the log message. + */ + public final void error(Object... messageParts) { + if (this.realLog.isErrorEnabled()) { + this.realLog.error(StringUtil.combineParts(messageParts)); + } + } + + /** + * Logs one or more message parts at level warn. + * + * @param messageParts one or more objects which should be combined, by calling toString() to form + * the log message. + */ + public final void warn(Object... messageParts) { + if (this.realLog.isWarnEnabled()) { + this.realLog.warn(StringUtil.combineParts(messageParts)); + } + } + + /** + * Logs one or more message parts at level info. + * + * @param messageParts one or more objects which should be combined, by calling toString() to form + * the log message. + */ + public final void info(Object... messageParts) { + if (this.realLog.isInfoEnabled()) { + this.realLog.info(StringUtil.combineParts(messageParts)); + } + } + + /** + * Logs one or more message parts at level debug. + * + * @param messageParts one or more objects which should be combined, by calling toString() to form + * the log message. + */ + public final void debug(Object... messageParts) { + if (this.realLog.isDebugEnabled()) { + this.realLog.debug(StringUtil.combineParts(messageParts)); + } + } + + /** + * Logs one or more message parts at level trace. + * + * @param messageParts one or more objects which should be combined, by calling toString() to form + * the log message. + */ + public final void trace(Object... messageParts) { + if (this.realLog.isTraceEnabled()) { + this.realLog.trace(StringUtil.combineParts(messageParts)); + } + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/util/Log4JLogger.java b/stripes/src/main/java/net/sourceforge/stripes/util/Log4JLogger.java deleted file mode 100644 index cadb32d9c..000000000 --- a/stripes/src/main/java/net/sourceforge/stripes/util/Log4JLogger.java +++ /dev/null @@ -1,339 +0,0 @@ -/* - * Copyright 2001-2004 The Apache Software Foundation. - * - * 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 net.sourceforge.stripes.util; - -import java.io.Serializable; -import org.apache.commons.logging.Log; -import org.apache.log4j.Logger; -import org.apache.log4j.Priority; -import org.apache.log4j.Level; - -/** - * Implementation of {@link Log} that maps directly to a - * Logger for log4J version 1.2. - *

    - * Initial configuration of the corresponding Logger instances should be done - * in the usual manner, as outlined in the Log4J documentation. - *

    - * The reason this logger is distinct from the 1.3 logger is that in version 1.2 - * of Log4J: - *

      - *
    • class Logger takes Priority parameters not Level parameters. - *
    • class Level extends Priority - *
    - * Log4J1.3 is expected to change Level so it no longer extends Priority, which is - * a non-binary-compatible change. The class generated by compiling this code against - * log4j 1.2 will therefore not run against log4j 1.3. - * - * @author Scott Sanders - * @author Rod Waldhoff - * @author Robert Burrell Donkin - * @version $Id: Log4JLogger.java 370672 2006-01-19 23:52:23Z skitching $ - */ - -@SuppressWarnings({ "serial", "deprecation" }) -public class Log4JLogger implements Log, Serializable { - - // ------------------------------------------------------------- Attributes - - /** The fully qualified name of the Log4JLogger class. - * - * ADP - This is the only change from the original in commons-logging-1.1-src */ - private static final String FQCN = net.sourceforge.stripes.util.Log.class.getName(); - - /** Log to this logger */ - private transient Logger logger = null; - - /** Logger name */ - private String name = null; - - private static Priority traceLevel; - - // ------------------------------------------------------------ - // Static Initializer. - // - // Note that this must come after the static variable declarations - // otherwise initialiser expressions associated with those variables - // will override any settings done here. - // - // Verify that log4j is available, and that it is version 1.2. - // If an ExceptionInInitializerError is generated, then LogFactoryImpl - // will treat that as meaning that the appropriate underlying logging - // library is just not present - if discovery is in progress then - // discovery will continue. - // ------------------------------------------------------------ - - static { - if (!Priority.class.isAssignableFrom(Level.class)) { - // nope, this is log4j 1.3, so force an ExceptionInInitializerError - throw new InstantiationError("Log4J 1.2 not available"); - } - - // Releases of log4j1.2 >= 1.2.12 have Priority.TRACE available, earlier - // versions do not. If TRACE is not available, then we have to map - // calls to Log.trace(...) onto the DEBUG level. - - try { - traceLevel = (Priority) Level.class.getDeclaredField("TRACE").get(null); - } catch(Exception ex) { - // ok, trace not available - traceLevel = Priority.DEBUG; - } - } - - - // ------------------------------------------------------------ Constructor - - public Log4JLogger() { - } - - - /** - * Base constructor. - */ - public Log4JLogger(String name) { - this.name = name; - this.logger = getLogger(); - } - - /** For use with a log4j factory. - */ - public Log4JLogger(Logger logger ) { - this.name = logger.getName(); - this.logger=logger; - } - - - // --------------------------------------------------------- - // Implementation - // - // Note that in the methods below the Priority class is used to define - // levels even though the Level class is supported in 1.2. This is done - // so that at compile time the call definitely resolves to a call to - // a method that takes a Priority rather than one that takes a Level. - // - // The Category class (and hence its subclass Logger) in version 1.2 only - // has methods that take Priority objects. The Category class (and hence - // Logger class) in version 1.3 has methods that take both Priority and - // Level objects. This means that if we use Level here, and compile - // against log4j 1.3 then calls would be bound to the versions of - // methods taking Level objects and then would fail to run against - // version 1.2 of log4j. - // --------------------------------------------------------- - - - /** - * Logs a message with org.apache.log4j.Priority.TRACE. - * When using a log4j version that does not support the TRACE - * level, the message will be logged at the DEBUG level. - * - * @param message to log - * @see org.apache.commons.logging.Log#trace(Object) - */ - public void trace(Object message) { - getLogger().log(FQCN, traceLevel, message, null ); - } - - - /** - * Logs a message with org.apache.log4j.Priority.TRACE. - * When using a log4j version that does not support the TRACE - * level, the message will be logged at the DEBUG level. - * - * @param message to log - * @param t log this cause - * @see org.apache.commons.logging.Log#trace(Object, Throwable) - */ - public void trace(Object message, Throwable t) { - getLogger().log(FQCN, traceLevel, message, t ); - } - - - /** - * Logs a message with org.apache.log4j.Priority.DEBUG. - * - * @param message to log - * @see org.apache.commons.logging.Log#debug(Object) - */ - public void debug(Object message) { - getLogger().log(FQCN, Priority.DEBUG, message, null ); - } - - /** - * Logs a message with org.apache.log4j.Priority.DEBUG. - * - * @param message to log - * @param t log this cause - * @see org.apache.commons.logging.Log#debug(Object, Throwable) - */ - public void debug(Object message, Throwable t) { - getLogger().log(FQCN, Priority.DEBUG, message, t ); - } - - - /** - * Logs a message with org.apache.log4j.Priority.INFO. - * - * @param message to log - * @see org.apache.commons.logging.Log#info(Object) - */ - public void info(Object message) { - getLogger().log(FQCN, Priority.INFO, message, null ); - } - - - /** - * Logs a message with org.apache.log4j.Priority.INFO. - * - * @param message to log - * @param t log this cause - * @see org.apache.commons.logging.Log#info(Object, Throwable) - */ - public void info(Object message, Throwable t) { - getLogger().log(FQCN, Priority.INFO, message, t ); - } - - - /** - * Logs a message with org.apache.log4j.Priority.WARN. - * - * @param message to log - * @see org.apache.commons.logging.Log#warn(Object) - */ - public void warn(Object message) { - getLogger().log(FQCN, Priority.WARN, message, null ); - } - - - /** - * Logs a message with org.apache.log4j.Priority.WARN. - * - * @param message to log - * @param t log this cause - * @see org.apache.commons.logging.Log#warn(Object, Throwable) - */ - public void warn(Object message, Throwable t) { - getLogger().log(FQCN, Priority.WARN, message, t ); - } - - - /** - * Logs a message with org.apache.log4j.Priority.ERROR. - * - * @param message to log - * @see org.apache.commons.logging.Log#error(Object) - */ - public void error(Object message) { - getLogger().log(FQCN, Priority.ERROR, message, null ); - } - - - /** - * Logs a message with org.apache.log4j.Priority.ERROR. - * - * @param message to log - * @param t log this cause - * @see org.apache.commons.logging.Log#error(Object, Throwable) - */ - public void error(Object message, Throwable t) { - getLogger().log(FQCN, Priority.ERROR, message, t ); - } - - - /** - * Logs a message with org.apache.log4j.Priority.FATAL. - * - * @param message to log - * @see org.apache.commons.logging.Log#fatal(Object) - */ - public void fatal(Object message) { - getLogger().log(FQCN, Priority.FATAL, message, null ); - } - - - /** - * Logs a message with org.apache.log4j.Priority.FATAL. - * - * @param message to log - * @param t log this cause - * @see org.apache.commons.logging.Log#fatal(Object, Throwable) - */ - public void fatal(Object message, Throwable t) { - getLogger().log(FQCN, Priority.FATAL, message, t ); - } - - - /** - * Return the native Logger instance we are using. - */ - public Logger getLogger() { - if (logger == null) { - logger = Logger.getLogger(name); - } - return (this.logger); - } - - - /** - * Check whether the Log4j Logger used is enabled for DEBUG priority. - */ - public boolean isDebugEnabled() { - return getLogger().isDebugEnabled(); - } - - - /** - * Check whether the Log4j Logger used is enabled for ERROR priority. - */ - public boolean isErrorEnabled() { - return getLogger().isEnabledFor(Priority.ERROR); - } - - - /** - * Check whether the Log4j Logger used is enabled for FATAL priority. - */ - public boolean isFatalEnabled() { - return getLogger().isEnabledFor(Priority.FATAL); - } - - - /** - * Check whether the Log4j Logger used is enabled for INFO priority. - */ - public boolean isInfoEnabled() { - return getLogger().isInfoEnabled(); - } - - - /** - * Check whether the Log4j Logger used is enabled for TRACE priority. - * When using a log4j version that does not support the TRACE level, this call - * will report whether DEBUG is enabled or not. - */ - public boolean isTraceEnabled() { - return getLogger().isEnabledFor(traceLevel); - } - - /** - * Check whether the Log4j Logger used is enabled for WARN priority. - */ - public boolean isWarnEnabled() { - return getLogger().isEnabledFor(Priority.WARN); - } -} diff --git a/stripes/src/main/java/net/sourceforge/stripes/util/Range.java b/stripes/src/main/java/net/sourceforge/stripes/util/Range.java index c4dc1afa1..b4803ad39 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/util/Range.java +++ b/stripes/src/main/java/net/sourceforge/stripes/util/Range.java @@ -16,96 +16,99 @@ /** * Utility class for working with ranges, ranging from start to end (both inclusive). - * + * * @author Ward van Wanrooij * @since Stripes 1.6 */ public class Range> implements Comparable> { - private T start, end; + private T start, end; - /** - * Constructor for range from start to end (both inclusive). Start and end may not be null. - * - * @param start Start of the range - * @param end End of the range - */ - public Range(T start, T end) { - setStart(start); - setEnd(end); - } + /** + * Constructor for range from start to end (both inclusive). Start and end may not be null. + * + * @param start Start of the range + * @param end End of the range + */ + public Range(T start, T end) { + setStart(start); + setEnd(end); + } - /** - * Retrieves start of the range. - * - * @return Start of the range - */ - public T getStart() { - return start; - } + /** + * Retrieves start of the range. + * + * @return Start of the range + */ + public T getStart() { + return start; + } - /** - * Sets start of the range. Start may not be null. - * - * @param start Start of the range - */ - public void setStart(T start) { - if (start == null) - throw new NullPointerException(); - this.start = start; - } + /** + * Sets start of the range. Start may not be null. + * + * @param start Start of the range + */ + public void setStart(T start) { + if (start == null) throw new NullPointerException(); + this.start = start; + } - /** - * Retrieves end of the range. - * - * @return End of the range - */ - public T getEnd() { - return end; - } + /** + * Retrieves end of the range. + * + * @return End of the range + */ + public T getEnd() { + return end; + } - /** - * Sets end of the range. End may not be null. - * - * @param end End of the range - */ - public void setEnd(T end) { - if (end == null) - throw new NullPointerException(); - this.end = end; - } + /** + * Sets end of the range. End may not be null. + * + * @param end End of the range + */ + public void setEnd(T end) { + if (end == null) throw new NullPointerException(); + this.end = end; + } - /** - * Checks whether an item is contained in this range. - * - * @param item Item to check - * @return True if item is in range - */ - public boolean contains(T item) { - return (start.compareTo(item) <= 0) && (end.compareTo(item) >= 0); - } + /** + * Checks whether an item is contained in this range. + * + * @param item Item to check + * @return True if item is in range + */ + public boolean contains(T item) { + return (start.compareTo(item) <= 0) && (end.compareTo(item) >= 0); + } - public int compareTo(Range o) { - int res; + public int compareTo(Range o) { + int res; - if ((res = start.compareTo(o.getStart())) == 0) - res = end.compareTo(o.getEnd()); - return res; - } + if ((res = start.compareTo(o.getStart())) == 0) res = end.compareTo(o.getEnd()); + return res; + } - @SuppressWarnings("unchecked") - @Override - public boolean equals(Object o) { - return (o instanceof Range) && ((this == o) || (compareTo((Range) o) == 0)); - } + @SuppressWarnings("unchecked") + @Override + public boolean equals(Object o) { + return (o instanceof Range) && ((this == o) || (compareTo((Range) o) == 0)); + } - @Override - public int hashCode() { - return start.hashCode() ^ end.hashCode(); - } + @Override + public int hashCode() { + return start.hashCode() ^ end.hashCode(); + } - @Override - public String toString() { - return getClass().getName() + " { type: " + start.getClass().getName() + ", start: " - + start.toString() + ", end: " + end.toString() + " }"; - } + @Override + public String toString() { + return getClass().getName() + + " { type: " + + start.getClass().getName() + + ", start: " + + start.toString() + + ", end: " + + end.toString() + + " }"; + } } diff --git a/stripes/src/main/java/net/sourceforge/stripes/util/ReflectUtil.java b/stripes/src/main/java/net/sourceforge/stripes/util/ReflectUtil.java index 17ff193ba..8acd1aed9 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/util/ReflectUtil.java +++ b/stripes/src/main/java/net/sourceforge/stripes/util/ReflectUtil.java @@ -14,37 +14,36 @@ */ package net.sourceforge.stripes.util; -import net.sourceforge.stripes.exception.StripesRuntimeException; +import static java.lang.reflect.Modifier.isPublic; +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; -import java.util.Map; -import java.util.HashMap; -import java.util.Collection; -import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; -import java.util.Set; -import java.util.HashSet; -import java.util.SortedSet; -import java.util.TreeSet; +import java.util.Map; import java.util.Queue; -import java.util.LinkedList; +import java.util.Set; import java.util.SortedMap; +import java.util.SortedSet; import java.util.TreeMap; -import java.util.Arrays; +import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; - -import static java.lang.reflect.Modifier.isPublic; -import java.beans.PropertyDescriptor; -import java.beans.BeanInfo; -import java.beans.Introspector; -import java.beans.IntrospectionException; +import net.sourceforge.stripes.exception.StripesRuntimeException; /** * Common utilty methods that are useful when working with reflection. @@ -52,591 +51,576 @@ * @author Tim Fennell */ public class ReflectUtil { - private static final Log log = Log.getInstance(ReflectUtil.class); - - /** A cache of property descriptors by class and property name */ - private static Map, Map> propertyDescriptors = - new ConcurrentHashMap, Map>(); - - /** Static helper class, shouldn't be constructed. */ - private ReflectUtil() {} - - /** - * Holds a map of commonly used interface types (mostly collections) to a class that - * implements the interface and will, by default, be instantiated when an instance - * of the interface is needed. - */ - protected static final Map,Class> interfaceImplementations = new HashMap,Class>(); - - /** - * Holds a map of primitive type to the default value for that primitive type. Isn't it - * odd that there's no way to get this programmatically from the Class objects? - */ - protected static final Map,Object> primitiveDefaults = new HashMap,Object>(); - - static { - interfaceImplementations.put(Collection.class, ArrayList.class); - interfaceImplementations.put(List.class, ArrayList.class); - interfaceImplementations.put(Set.class, HashSet.class); - interfaceImplementations.put(SortedSet.class, TreeSet.class); - interfaceImplementations.put(Queue.class, LinkedList.class); - interfaceImplementations.put(Map.class, HashMap.class); - interfaceImplementations.put(SortedMap.class, TreeMap.class); - - primitiveDefaults.put(Boolean.TYPE, false); - primitiveDefaults.put(Character.TYPE, '\0'); - primitiveDefaults.put(Byte.TYPE, new Byte("0")); - primitiveDefaults.put(Short.TYPE, new Short("0")); - primitiveDefaults.put(Integer.TYPE, new Integer(0)); - primitiveDefaults.put(Long.TYPE, new Long(0l)); - primitiveDefaults.put(Float.TYPE, new Float(0f)); - primitiveDefaults.put(Double.TYPE, new Double(0.0)); - } - - /** - * The set of method that annotation classes inherit, and should be avoided when - * toString()ing an annotation class. - */ - private static final Set INHERITED_ANNOTATION_METHODS = - Literal.set("toString", "equals", "hashCode", "annotationType"); - - /** - * Utility method used to load a class. Any time that Stripes needs to load of find a - * class by name it uses this method. As a result any time the classloading strategy - * needs to change it can be done in one place! Currently uses - * {@code Thread.currentThread().getContextClassLoader().loadClass(String)}. - * - * @param name the fully qualified (binary) name of the class to find or load - * @return the Class object representing the class - * @throws ClassNotFoundException if the class cannot be loaded - */ - @SuppressWarnings("rawtypes") // this allows us to assign without casting - public static Class findClass(String name) throws ClassNotFoundException { - return Thread.currentThread().getContextClassLoader().loadClass(name); - } - - /** - *

    A better (more concise) toString method for annotation types that yields a String - * that should look more like the actual usage of the annotation in a class. The String produced - * is similar to that produced by calling toString() on the annotation directly, with the - * following differences:

    - * - *