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