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}