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}