View Javadoc
1   /*
2    * $Id$
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  package org.apache.struts.extras.actions;
22  
23  import java.util.HashMap;
24  import java.util.Locale;
25  import java.util.Map;
26  
27  import jakarta.servlet.ServletException;
28  import jakarta.servlet.http.HttpServletRequest;
29  import jakarta.servlet.http.HttpServletResponse;
30  
31  import org.apache.struts.Globals;
32  import org.apache.struts.action.ActionForm;
33  import org.apache.struts.action.ActionForward;
34  import org.apache.struts.action.ActionMapping;
35  import org.apache.struts.config.MessageResourcesConfig;
36  import org.apache.struts.config.ModuleConfig;
37  import org.apache.struts.util.MessageResources;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  /**
42   * <p> An abstract <strong>Action</strong> that dispatches to the subclass
43   * mapped <code>execute</code> method. This is useful in cases where an HTML
44   * form has multiple submit buttons with the same name. The button name is
45   * specified by the <code>parameter</code> property of the corresponding
46   * ActionMapping. To configure the use of this action in your
47   * <code>struts-config.xml</code> file, create an entry like this:</p> <pre>
48   *   &lt;action path="/test"
49   *           type="org.example.MyAction"
50   *           name="MyForm"
51   *          scope="request"
52   *          input="/test.jsp"
53   *      parameter="method"/&gt;
54   * </pre> <p>
55   *
56   * which will use the value of the request parameter named "method" to locate
57   * the corresponding key in ApplicationResources. For example, you might have
58   * the following ApplicationResources.properties:</p> <pre>
59   *    button.add=Add Record
60   *    button.delete=Delete Record
61   *  </pre><p>
62   *
63   * And your JSP would have the following format for submit buttons:</p> <pre>
64   *   &lt;html:form action="/test"&gt;
65   *    &lt;html:submit property="method"&gt;
66   *      &lt;bean:message key="button.add"/&gt;
67   *    &lt;/html:submit&gt;
68   *    &lt;html:submit property="method"&gt;
69   *      &lt;bean:message key="button.delete"/&gt;
70   *    &lt;/html:submit&gt;
71   *  &lt;/html:form&gt;
72   *  </pre> <p>
73   *
74   * Your subclass must implement both getKeyMethodMap and the methods defined
75   * in the map. An example of such implementations are:</p>
76   * <pre>
77   *  protected Map getKeyMethodMap() {
78   *      Map map = new HashMap();
79   *      map.put("button.add", "add");
80   *      map.put("button.delete", "delete");
81   *      return map;
82   *  }
83   *
84   *  public ActionForward add(ActionMapping mapping,
85   *          ActionForm form,
86   *          HttpServletRequest request,
87   *          HttpServletResponse response)
88   *          throws IOException, ServletException {
89   *      // do add
90   *      return mapping.findForward("success");
91   *  }
92   *
93   *  public ActionForward delete(ActionMapping mapping,
94   *          ActionForm form,
95   *          HttpServletRequest request,
96   *          HttpServletResponse response)
97   *          throws IOException, ServletException {
98   *      // do delete
99   *      return mapping.findForward("success");
100  *  }
101  * </pre>
102  * <p> <strong>Notes</strong> - If duplicate values exist for the keys
103  * returned by getKeys, only the first one found will be returned. If no
104  * corresponding key is found then an exception will be thrown. You can
105  * override the method <code>unspecified</code> to provide a custom handler.
106  * If the submit was cancelled (a <code>html:cancel</code> button was
107  * pressed), the custom handler <code>cancelled</code> will be used instead.
108  */
109 public abstract class LookupDispatchAction extends DispatchAction {
110     private static final long serialVersionUID = 8608624951935780151L;
111 
112     /**
113      * The {@code Log} instance for this class.
114      */
115     private transient final Logger log =
116         LoggerFactory.getLogger(LookupDispatchAction.class);
117 
118     /**
119      * Reverse lookup map from resource value to resource key.
120      */
121     protected HashMap<Locale, Map<String, String>> localeMap = new HashMap<>();
122 
123     /**
124      * Resource key to method name lookup.
125      */
126     protected Map<String, String> keyMethodMap = null;
127 
128     // ---------------------------------------------------------- Public Methods
129 
130     /**
131      * Process the specified HTTP request, and create the corresponding HTTP
132      * response (or forward to another web component that will create it).
133      * Return an <code>ActionForward</code> instance describing where and how
134      * control should be forwarded, or <code>null</code> if the response has
135      * already been completed.
136      *
137      * @param mapping  The ActionMapping used to select this instance
138      * @param request  The HTTP request we are processing
139      * @param response The HTTP response we are creating
140      * @param form     The optional ActionForm bean for this request (if any)
141      * @return Describes where and how control should be forwarded.
142      * @throws Exception if an error occurs
143      */
144     public ActionForward execute(ActionMapping mapping, ActionForm form,
145         HttpServletRequest request, HttpServletResponse response)
146         throws Exception {
147         return super.execute(mapping, form, request, response);
148     }
149 
150     /**
151      * This is the first time this Locale is used so build the reverse lookup
152      * Map. Search for message keys in all configured MessageResources for the
153      * current module.
154      *
155      * @param request    The HTTP request we are processing
156      * @param userLocale The locale for this request
157      * @return The reverse lookup map for the specified locale.
158      */
159     private Map<String, String> initLookupMap(HttpServletRequest request, Locale userLocale) {
160         Map<String, String> lookupMap = new HashMap<>();
161 
162         this.keyMethodMap = this.getKeyMethodMap();
163 
164         ModuleConfig moduleConfig =
165             (ModuleConfig) request.getAttribute(Globals.MODULE_KEY);
166 
167         MessageResourcesConfig[] mrc =
168             moduleConfig.findMessageResourcesConfigs();
169 
170         // Look through all module's MessageResources
171         for (int i = 0; i < mrc.length; i++) {
172             MessageResources resources =
173                 this.getResources(request, mrc[i].getKey());
174 
175             // Look for key in MessageResources
176             for (String key : this.keyMethodMap.keySet()) {
177                 String text = resources.getMessage(userLocale, key);
178 
179                 // Found key and haven't added to Map yet, so add the text
180                 if ((text != null) && !lookupMap.containsKey(text)) {
181                     lookupMap.put(text, key);
182                 }
183             }
184         }
185 
186         return lookupMap;
187     }
188 
189     /**
190      * Provides the mapping from resource key to method name.
191      *
192      * @return Resource key / method name map.
193      */
194     protected abstract Map<String, String> getKeyMethodMap();
195 
196     /**
197      * Lookup the method name corresponding to the client request's locale.
198      *
199      * @param request The HTTP request we are processing
200      * @param keyName The parameter name to use as the properties key
201      * @param mapping The ActionMapping used to select this instance
202      * @return The method's localized name.
203      * @throws ServletException if keyName cannot be resolved
204      * @since Struts 1.2.0
205      */
206     protected String getLookupMapName(HttpServletRequest request,
207         String keyName, ActionMapping mapping)
208         throws ServletException {
209         // Based on this request's Locale get the lookupMap
210         Map<String, String> lookupMap = null;
211 
212         synchronized (localeMap) {
213             Locale userLocale = this.getLocale(request);
214 
215             lookupMap = this.localeMap.get(userLocale);
216 
217             if (lookupMap == null) {
218                 lookupMap = this.initLookupMap(request, userLocale);
219                 this.localeMap.put(userLocale, lookupMap);
220             }
221         }
222 
223         // Find the key for the resource
224         String key = lookupMap.get(keyName);
225 
226         if (key == null) {
227             String message =
228                 messages.getMessage("dispatch.resource", mapping.getPath());
229             log.error("{} '{}'", message, keyName);
230             throw new ServletException(message);
231         }
232 
233         // Find the method name
234         String methodName = keyMethodMap.get(key);
235 
236         if (methodName == null) {
237             String message =
238                 messages.getMessage("dispatch.lookup", mapping.getPath(), key);
239 
240             throw new ServletException(message);
241         }
242 
243         return methodName;
244     }
245 
246     /**
247      * Returns the method name, given a parameter's value.
248      *
249      * @param mapping   The ActionMapping used to select this instance
250      * @param form      The optional ActionForm bean for this request (if
251      *                  any)
252      * @param request   The HTTP request we are processing
253      * @param response  The HTTP response we are creating
254      * @param parameter The <code>ActionMapping</code> parameter's name
255      * @return The method's name.
256      * @throws Exception if an error occurs
257      * @since Struts 1.2.0
258      */
259     protected String getMethodName(ActionMapping mapping, ActionForm form,
260         HttpServletRequest request, HttpServletResponse response,
261         String parameter) throws Exception {
262         // Identify the method name to be dispatched to.
263         // dispatchMethod() will call unspecified() if name is null
264         String keyName = request.getParameter(parameter);
265 
266         if ((keyName == null) || (keyName.length() == 0)) {
267             return null;
268         }
269 
270         String methodName = getLookupMapName(request, keyName, mapping);
271 
272         return methodName;
273     }
274 }