001// --------------------------------------------------------------------------------
002// Copyright 2002-2024 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017/*
018Copyright 2005-2006 Seth Fitzsimmons <seth@note.amherst.edu>
019
020Licensed under the Apache License, Version 2.0 (the "License");
021you may not use this file except in compliance with the License.
022You may obtain a copy of the License at
023
024    http://www.apache.org/licenses/LICENSE-2.0
025
026Unless required by applicable law or agreed to in writing, software
027distributed under the License is distributed on an "AS IS" BASIS,
028WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
029See the License for the specific language governing permissions and
030limitations under the License.
031*/
032package com.echothree.view.client.web.struts.sprout;
033
034import java.io.IOException;
035import java.lang.annotation.ElementType;
036import java.lang.annotation.Retention;
037import java.lang.annotation.RetentionPolicy;
038import java.lang.annotation.Target;
039import javax.servlet.ServletException;
040import javax.servlet.http.HttpServletRequest;
041import javax.servlet.http.HttpServletResponse;
042import org.apache.log4j.Logger;
043import org.apache.struts.Globals;
044import org.apache.struts.action.ActionForm;
045import org.apache.struts.action.ActionForward;
046import org.apache.struts.action.ActionMapping;
047import org.apache.struts.action.ActionMessages;
048import org.apache.struts.action.DynaActionForm;
049import org.apache.struts.actions.MappingDispatchAction;
050
051/**
052 * <p>One goal is to make web application development in Java more
053 * fun. Sprouts require Java 5 (1.5) due to their use of annotations.  They
054 * also require a servlet engine conforming to the Servlet 2.4 and JSP 2.0
055 * specs (Tomcat 5.x, for example) due to the use of filters, listeners, and
056 * tag files.</p>
057 * 
058 * <p>Sprouts obviate the need to write Struts action-mappings as
059 * they use information gleaned from the initial Sprout, class
060 * properties (name and package), as well as annotations to appropriately
061 * self-register on URLs defined by convention or specification with
062 * properties obtained the same way.</p>
063 * 
064 * <p>The path is determined by the package name where the components to the
065 * right of "action" are converted into directories.  Thus,
066 * <code>whatever.action.*</code> will correspond to <code>/*</code> and
067 * <code>whatever.action.help.*</code> to <code>/help/*</code></p>
068 * 
069 * <p>The <em>file</em> (or <em>action</em>) is determined by the method name.
070 * Thus, <code>index()</code> will correspond to <code>index.do</code> and
071 * <code>submitChange()</code> to <code>submit_change</code>
072 * (CamelCased method names are converted).</p>
073 * 
074 * <p>(<strong>NOTE:</strong> <code>publick()</code> maps to public.do as
075 * <em>public</em> is a Java keyword.)</p>
076 * 
077 * <p>Form names are created from the class name appended with <em>Form</em>,
078 * so <code>TestAction</code> would default to <code>TestActionForm</code>.
079 * This behavior can be overridden using the
080 * <code>@FormName("AlternateForm")</code> annotation.</p>
081 * 
082 * <p>Input, validate, and scope properties can be overridden
083 * with <code>@Input</code>, <code>@Validate</code>, and <code>@Scope</code>
084 * respectively.</p>
085 * 
086 * <p><code>f(key)</code>, <code>F(key)</code>, and <code>s(key,value)</code>
087 * are helper methods that manipulate DynaActionForms (if used) and obviate
088 * the need to cast excessively.  <code>f()</code> is the equivalent of
089 * calling <code>getString()</code>, <code>F()</code> <code>get()</code>, and
090 * <code>s()</code> <code>set()</code>.</p>
091 * 
092 * <p><code>getMessages()</code>, <code>getErrors()</code>,
093 * <code>saveMessages()</code>, and <code>saveErrors()</code> have been
094 * modified to store state in the user's session allowing them to be used more
095 * simply and effectively.  Rather than using this:
096 * <pre>
097 *   ActionMessages errors = new ActionMessages();
098 *   ...
099 *   saveErrors(request, errors);
100 * </pre>
101 * You should use getErrors() to initialize the errors ActionMessages object:
102 * <pre>
103 *   ActionMessages errors = getErrors( request );
104 *   ...
105 * </pre>
106 * This way, messages and errors can be stacked up (while being kept separate)
107 * until they are displayed using the sprout:notifications taglib (see
108 * WEB-INF/tags/sprout/notifications.tag).</p>
109 * 
110 * <p>
111 * TODO add additional reserved words<br />
112 * TODO add a default ActionForm with just an "id" field (as a String) as "SproutForm"<br />
113 * TODO add a GlobalForward annotation with "redirect" property to add to the<br />
114 *      list of global-forwards.<br />
115 * TODO add some measure of SiteMesh integration for AJAX partials<br />
116 * TODO add some form of ActionForwardBuilder</p>
117 * 
118 * @author Seth Fitzsimmons
119 */
120public abstract class Sprout
121        extends MappingDispatchAction {
122
123    private final static Logger log = Logger.getLogger( Sprout.class );
124
125    static final String DEFAULT_SCOPE = "request";
126    static final String DEFAULT_FORM_SUFFIX = "Form";
127    static final String DEFAULT_VIEW_EXTENSION = ".jsp";
128    public static final String SPROUT_DEFAULT_ACTION_FORM_NAME = "form"; 
129
130    /** Default forward key. */
131    public static final String FWD_SUCCESS = "success";
132    
133    private String beanName;
134    private static final ThreadLocal<DynaActionForm> formHolder = new ThreadLocal<>();
135    
136    public final void init(final ActionMapping mapping, final ActionForm form, final HttpServletRequest request, final HttpServletResponse response) {
137        if ( form instanceof DynaActionForm )
138            formHolder.set( (DynaActionForm) form );
139        else
140            // clean up
141            formHolder.remove();
142        
143        onInit( mapping, form, request, response );
144    }
145    
146    /**
147     * Callback for subclass-specific initialization.
148     */
149    protected void onInit(final ActionMapping mapping, final ActionForm form, final HttpServletRequest request, final HttpServletResponse response) {}
150    
151    /**
152     * Shortcut for ((DynaActionForm) form).getString(key).
153     */
154    protected String f(final String key) {
155        if ( null == formHolder.get() )
156            throw new UnsupportedOperationException("Active form is not a DynaActionForm.");
157        
158        return formHolder.get().getString( key );
159    }
160    
161    /**
162     * Shortcut for ((DynaActionForm) form).get(key).
163     */
164    protected Object F(final String key) {
165        if ( null == formHolder.get() )
166            throw new UnsupportedOperationException("Active form is not a DynaActionForm.");
167        
168        return formHolder.get().get( key );
169    }
170    
171    /**
172     * Shortcut for ((DynaActionForm) form).set(key, value).
173     */
174    protected void s(final String key, final Object value) {
175        if ( null == formHolder.get() )
176            throw new UnsupportedOperationException("Active form is not a DynaActionForm.");
177        
178        formHolder.get().set( key, value );
179    }
180    
181    /**
182     * Add errors to the session.
183     */
184    @Override
185    protected void addErrors(final HttpServletRequest request, final ActionMessages msgs) {
186        saveErrors( request, msgs );
187    }
188    
189    /**
190     * Gets undisplayed errors from both the request and the session.
191     */
192    @Override
193    protected ActionMessages getErrors(final HttpServletRequest request) {
194        final ActionMessages errors = super.getErrors( request );
195        errors.add( (ActionMessages) request.getSession().getAttribute( Globals.ERROR_KEY ) );
196        return errors;
197    }
198    
199    /**
200     * Saves errors to the session scope so that they may be picked up by the
201     * next action that accesses errors.
202     */
203    @Override
204    protected void saveErrors(final HttpServletRequest request, final ActionMessages msgs) {
205        saveErrors( request.getSession(), msgs );
206    }
207    
208    /**
209     * Add messages to the session.
210     */
211    @Override
212    protected void addMessages(final HttpServletRequest request, final ActionMessages msgs) {
213        saveMessages( request, msgs );
214    }
215    
216    /**
217     * Gets undisplayed messages from both the request and the session.
218     */
219    @Override
220    protected ActionMessages getMessages(final HttpServletRequest request) {
221        final ActionMessages msgs = super.getMessages( request );
222        msgs.add( (ActionMessages) request.getSession().getAttribute( Globals.MESSAGE_KEY ) );
223        return msgs;
224    }
225    
226    /**
227     * Saves messages to the session scope so that they may be picked up by the
228     * next action that accesses messages.
229     */
230    @Override
231    protected void saveMessages(final HttpServletRequest request, final ActionMessages msgs) {
232        saveMessages( request.getSession(), msgs );
233    }
234    
235    /**
236     * Helper method to display index.jsp in response to a request for
237     * /index.do
238     */
239    public ActionForward index(final ActionMapping mapping, final ActionForm form, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException {
240        return mapping.findForward( FWD_SUCCESS );
241    }
242    
243    /**
244     * Override the default form name for this action.  Equivalent to setting
245     * <em>name</em> property in an <em>action</em> mapping in
246     * <code>struts-config.xml</code>.
247     */
248    @Target(ElementType.METHOD)
249    @Retention(RetentionPolicy.RUNTIME)
250    protected @interface FormName {
251        /**
252         * Form name. Corresponds to a <em>form-bean</em> mapping in
253         * <code>struts-config.xml</code>.
254         */
255        String value();
256    }
257    
258    /**
259     * <p>Specifies a local forward.  Equivalent to adding a
260     * <em>forward</em> mapping within an <em>action</em> mapping in
261     * <code>struts-config.xml</code>.</p>
262     * 
263     * <p>It is possible to define multiple forwards by providing parameters
264     * as arrays.</p>
265     */
266    @Target(ElementType.METHOD)
267    @Retention(RetentionPolicy.RUNTIME)
268    protected @interface Forward {
269        /**
270         * Forward name.  Corresponds to <em>name</em> property.
271         */
272        String[] name();
273        /**
274         * Whether this forward is a redirect.  Corresponds to
275         * <em>redirect</em> property.
276         */
277        boolean[] redirect() default {};
278        /**
279         * Forward path.  Corresponds to <em>path</em> property.
280         */
281        String[] path();
282    }
283    
284    /**
285     * Specifies the "input" property for this action.
286     */
287    @Target(ElementType.METHOD)
288    @Retention(RetentionPolicy.RUNTIME)
289    protected @interface Input {
290        /**
291         * Path to source JSP.
292         */
293        String value();
294    }
295
296    /**
297     * Specifies the "scope" property for this action.
298     */
299    @Target(ElementType.METHOD)
300    @Retention(RetentionPolicy.RUNTIME)
301    protected @interface Scope {
302        /**
303         * <em>request</em> (default) or <em>session</em>
304         */
305        String value();
306    }
307    
308    /**
309     * Instruct Struts to validate the form provided to this method. Equivalent
310     * to setting <em>validate</em> property to <em>true</em>.
311     */
312    @Target(ElementType.METHOD)
313    @Retention(RetentionPolicy.RUNTIME)
314    protected @interface Validate {
315        /**
316         * Unnecessary to specify this, as it defaults to <em>true</em> if
317         * present, <em>false</em> otherwise.
318         */
319        boolean value() default true;
320    }
321}