View Javadoc

1   /*
2    * $Id: TagUtils.java 421129 2006-07-12 05:13:54Z wsmoak $
3    *
4    * Copyright 1999-2006 The Apache Software Foundation.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *      http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.struts.taglib;
19  
20  import org.apache.commons.beanutils.PropertyUtils;
21  import org.apache.commons.logging.Log;
22  import org.apache.commons.logging.LogFactory;
23  import org.apache.struts.Globals;
24  import org.apache.struts.action.ActionErrors;
25  import org.apache.struts.action.ActionMessage;
26  import org.apache.struts.action.ActionMessages;
27  import org.apache.struts.config.ForwardConfig;
28  import org.apache.struts.config.ModuleConfig;
29  import org.apache.struts.taglib.html.Constants;
30  import org.apache.struts.util.MessageResources;
31  import org.apache.struts.util.ModuleUtils;
32  import org.apache.struts.util.RequestUtils;
33  import org.apache.struts.util.ResponseUtils;
34  
35  import javax.servlet.http.HttpServletRequest;
36  import javax.servlet.http.HttpServletResponse;
37  import javax.servlet.http.HttpSession;
38  import javax.servlet.jsp.JspException;
39  import javax.servlet.jsp.JspWriter;
40  import javax.servlet.jsp.PageContext;
41  import javax.servlet.jsp.tagext.BodyContent;
42  
43  import java.io.IOException;
44  
45  import java.lang.reflect.InvocationTargetException;
46  
47  import java.net.MalformedURLException;
48  
49  import java.util.HashMap;
50  import java.util.Iterator;
51  import java.util.Locale;
52  import java.util.Map;
53  
54  /***
55   * Provides helper methods for JSP tags.
56   *
57   * @version $Rev: 421129 $
58   * @since Struts 1.2
59   */
60  public class TagUtils {
61      /***
62       * The Singleton instance.
63       * @since 1.3.5 Changed to non-final so it may be overridden, use at your own risk (you've been warned!!)
64       */
65      private static TagUtils instance = new TagUtils();
66  
67      /***
68       * Commons logging instance.
69       */
70      private static final Log log = LogFactory.getLog(TagUtils.class);
71  
72      /***
73       * The message resources for this package. TODO We need to move the
74       * relevant messages out of this properties file.
75       */
76      private static final MessageResources messages =
77          MessageResources.getMessageResources(
78              "org.apache.struts.taglib.LocalStrings");
79  
80      /***
81       * Maps lowercase JSP scope names to their PageContext integer constant
82       * values.
83       */
84      private static final Map scopes = new HashMap();
85  
86      /***
87       * Initialize the scope names map and the encode variable with the
88       * Java 1.4 method if available.
89       */
90      static {
91          scopes.put("page", new Integer(PageContext.PAGE_SCOPE));
92          scopes.put("request", new Integer(PageContext.REQUEST_SCOPE));
93          scopes.put("session", new Integer(PageContext.SESSION_SCOPE));
94          scopes.put("application", new Integer(PageContext.APPLICATION_SCOPE));
95      }
96  
97      /***
98       * Constructor for TagUtils.
99       */
100     protected TagUtils() {
101         super();
102     }
103 
104     /***
105      * Returns the Singleton instance of TagUtils.
106      */
107     public static TagUtils getInstance() {
108         return instance;
109     }
110 
111     /***
112      * Set the instance.
113      * This blatently violates the Singleton pattern, but then some say Singletons are an anti-pattern.
114      * @since 1.3.5 Changed to non-final and added setInstance() so TagUtils may be overridden, use at your own risk (you've been warned!!)
115      * @param instance The instance to set.
116      */
117     public static void setInstance(TagUtils instance){
118       TagUtils.instance = instance;
119     }
120 
121     /***
122      * Compute a set of query parameters that will be dynamically added to a
123      * generated URL.  The returned Map is keyed by parameter name, and the
124      * values are either null (no value specified), a String (single value
125      * specified), or a String[] array (multiple values specified).  Parameter
126      * names correspond to the corresponding attributes of the
127      * <code>&lt;html:link&gt;</code> tag.  If no query parameters are
128      * identified, return <code>null</code>.
129      *
130      * @param pageContext   PageContext we are operating in
131      * @param paramId       Single-value request parameter name (if any)
132      * @param paramName     Bean containing single-value parameter value
133      * @param paramProperty Property (of bean named by <code>paramName</code>
134      *                      containing single-value parameter value
135      * @param paramScope    Scope containing bean named by <code>paramName</code>
136      * @param name          Bean containing multi-value parameters Map (if
137      *                      any)
138      * @param property      Property (of bean named by <code>name</code>
139      *                      containing multi-value parameters Map
140      * @param scope         Scope containing bean named by <code>name</code>
141      * @param transaction   Should we add our transaction control token?
142      * @return Map of query parameters
143      * @throws JspException if we cannot look up the required beans
144      * @throws JspException if a class cast exception occurs on a looked-up
145      *                      bean or property
146      */
147     public Map computeParameters(PageContext pageContext, String paramId,
148         String paramName, String paramProperty, String paramScope, String name,
149         String property, String scope, boolean transaction)
150         throws JspException {
151         // Short circuit if no parameters are specified
152         if ((paramId == null) && (name == null) && !transaction) {
153             return (null);
154         }
155 
156         // Locate the Map containing our multi-value parameters map
157         Map map = null;
158 
159         try {
160             if (name != null) {
161                 map = (Map) getInstance().lookup(pageContext, name, property,
162                         scope);
163             }
164 
165             // @TODO - remove this - it is never thrown
166             //        } catch (ClassCastException e) {
167             //            saveException(pageContext, e);
168             //            throw new JspException(
169             //                    messages.getMessage("parameters.multi", name, property, scope));
170         } catch (JspException e) {
171             saveException(pageContext, e);
172             throw e;
173         }
174 
175         // Create a Map to contain our results from the multi-value parameters
176         Map results = null;
177 
178         if (map != null) {
179             results = new HashMap(map);
180         } else {
181             results = new HashMap();
182         }
183 
184         // Add the single-value parameter (if any)
185         if ((paramId != null) && (paramName != null)) {
186             Object paramValue = null;
187 
188             try {
189                 paramValue =
190                     TagUtils.getInstance().lookup(pageContext, paramName,
191                         paramProperty, paramScope);
192             } catch (JspException e) {
193                 saveException(pageContext, e);
194                 throw e;
195             }
196 
197             if (paramValue != null) {
198                 String paramString = null;
199 
200                 if (paramValue instanceof String) {
201                     paramString = (String) paramValue;
202                 } else {
203                     paramString = paramValue.toString();
204                 }
205 
206                 Object mapValue = results.get(paramId);
207 
208                 if (mapValue == null) {
209                     results.put(paramId, paramString);
210                 } else if (mapValue instanceof String[]) {
211                     String[] oldValues = (String[]) mapValue;
212                     String[] newValues = new String[oldValues.length + 1];
213 
214                     System.arraycopy(oldValues, 0, newValues, 0,
215                         oldValues.length);
216                     newValues[oldValues.length] = paramString;
217                     results.put(paramId, newValues);
218                 } else {
219                     String[] newValues = new String[2];
220 
221                     newValues[0] = mapValue.toString();
222                     newValues[1] = paramString;
223                     results.put(paramId, newValues);
224                 }
225             }
226         }
227 
228         // Add our transaction control token (if requested)
229         if (transaction) {
230             HttpSession session = pageContext.getSession();
231             String token = null;
232 
233             if (session != null) {
234                 token =
235                     (String) session.getAttribute(Globals.TRANSACTION_TOKEN_KEY);
236             }
237 
238             if (token != null) {
239                 results.put(Constants.TOKEN_KEY, token);
240             }
241         }
242 
243         // Return the completed Map
244         return (results);
245     }
246 
247     public String computeURL(PageContext pageContext, String forward,
248         String href, String page, String action, String module, Map params,
249         String anchor, boolean redirect)
250         throws MalformedURLException {
251         return this.computeURLWithCharEncoding(pageContext, forward, href,
252             page, action, module, params, anchor, redirect, false);
253     }
254 
255     /***
256      * Compute a hyperlink URL based on the <code>forward</code>,
257      * <code>href</code>, <code>action</code> or <code>page</code> parameter
258      * that is not null. The returned URL will have already been passed to
259      * <code>response.encodeURL()</code> for adding a session identifier.
260      *
261      * @param pageContext PageContext for the tag making this call
262      * @param forward     Logical forward name for which to look up the
263      *                    context-relative URI (if specified)
264      * @param href        URL to be utilized unmodified (if specified)
265      * @param page        Module-relative page for which a URL should be
266      *                    created (if specified)
267      * @param action      Logical action name for which to look up the
268      *                    context-relative URI (if specified)
269      * @param params      Map of parameters to be dynamically included (if
270      *                    any)
271      * @param anchor      Anchor to be dynamically included (if any)
272      * @param redirect    Is this URL for a <code>response.sendRedirect()</code>?
273      * @return URL with session identifier
274      * @throws java.net.MalformedURLException if a URL cannot be created for
275      *                                        the specified parameters
276      */
277     public String computeURLWithCharEncoding(PageContext pageContext,
278         String forward, String href, String page, String action, String module,
279         Map params, String anchor, boolean redirect, boolean useLocalEncoding)
280         throws MalformedURLException {
281         return computeURLWithCharEncoding(pageContext, forward, href, page,
282             action, module, params, anchor, redirect, true, useLocalEncoding);
283     }
284 
285     public String computeURL(PageContext pageContext, String forward,
286         String href, String page, String action, String module, Map params,
287         String anchor, boolean redirect, boolean encodeSeparator)
288         throws MalformedURLException {
289         return computeURLWithCharEncoding(pageContext, forward, href, page,
290             action, module, params, anchor, redirect, encodeSeparator, false);
291     }
292 
293     /***
294      * Compute a hyperlink URL based on the <code>forward</code>,
295      * <code>href</code>, <code>action</code> or <code>page</code> parameter
296      * that is not null. The returned URL will have already been passed to
297      * <code>response.encodeURL()</code> for adding a session identifier.
298      *
299      * @param pageContext      PageContext for the tag making this call
300      * @param forward          Logical forward name for which to look up the
301      *                         context-relative URI (if specified)
302      * @param href             URL to be utilized unmodified (if specified)
303      * @param page             Module-relative page for which a URL should be
304      *                         created (if specified)
305      * @param action           Logical action name for which to look up the
306      *                         context-relative URI (if specified)
307      * @param params           Map of parameters to be dynamically included
308      *                         (if any)
309      * @param anchor           Anchor to be dynamically included (if any)
310      * @param redirect         Is this URL for a <code>response.sendRedirect()</code>?
311      * @param encodeSeparator  This is only checked if redirect is set to
312      *                         false (never encoded for a redirect).  If true,
313      *                         query string parameter separators are encoded
314      *                         as &gt;amp;, else &amp; is used.
315      * @param useLocalEncoding If set to true, urlencoding is done on the
316      *                         bytes of character encoding from
317      *                         ServletResponse#getCharacterEncoding. Use UTF-8
318      *                         otherwise.
319      * @return URL with session identifier
320      * @throws java.net.MalformedURLException if a URL cannot be created for
321      *                                        the specified parameters
322      */
323     public String computeURLWithCharEncoding(PageContext pageContext,
324         String forward, String href, String page, String action, String module,
325         Map params, String anchor, boolean redirect, boolean encodeSeparator,
326         boolean useLocalEncoding)
327         throws MalformedURLException {
328         String charEncoding = "UTF-8";
329 
330         if (useLocalEncoding) {
331             charEncoding = pageContext.getResponse().getCharacterEncoding();
332         }
333 
334         // TODO All the computeURL() methods need refactoring!
335         // Validate that exactly one specifier was included
336         int n = 0;
337 
338         if (forward != null) {
339             n++;
340         }
341 
342         if (href != null) {
343             n++;
344         }
345 
346         if (page != null) {
347             n++;
348         }
349 
350         if (action != null) {
351             n++;
352         }
353 
354         if (n != 1) {
355             throw new MalformedURLException(messages.getMessage(
356                     "computeURL.specifier"));
357         }
358 
359         // Look up the module configuration for this request
360         ModuleConfig moduleConfig = getModuleConfig(module, pageContext);
361 
362         // Calculate the appropriate URL
363         StringBuffer url = new StringBuffer();
364         HttpServletRequest request =
365             (HttpServletRequest) pageContext.getRequest();
366 
367         if (forward != null) {
368             ForwardConfig forwardConfig =
369                 moduleConfig.findForwardConfig(forward);
370 
371             if (forwardConfig == null) {
372                 throw new MalformedURLException(messages.getMessage(
373                         "computeURL.forward", forward));
374             }
375 
376             // **** removed - see bug 37817 ****
377             //  if (forwardConfig.getRedirect()) {
378             //      redirect = true;
379             //  }
380 
381             if (forwardConfig.getPath().startsWith("/")) {
382                 url.append(request.getContextPath());
383                 url.append(RequestUtils.forwardURL(request, forwardConfig,
384                         moduleConfig));
385             } else {
386                 url.append(forwardConfig.getPath());
387             }
388         } else if (href != null) {
389             url.append(href);
390         } else if (action != null) {
391             url.append(instance.getActionMappingURL(action, module,
392                     pageContext, false));
393         } else /* if (page != null) */
394          {
395             url.append(request.getContextPath());
396             url.append(this.pageURL(request, page, moduleConfig));
397         }
398 
399         // Add anchor if requested (replacing any existing anchor)
400         if (anchor != null) {
401             String temp = url.toString();
402             int hash = temp.indexOf('#');
403 
404             if (hash >= 0) {
405                 url.setLength(hash);
406             }
407 
408             url.append('#');
409             url.append(this.encodeURL(anchor, charEncoding));
410         }
411 
412         // Add dynamic parameters if requested
413         if ((params != null) && (params.size() > 0)) {
414             // Save any existing anchor
415             String temp = url.toString();
416             int hash = temp.indexOf('#');
417 
418             if (hash >= 0) {
419                 anchor = temp.substring(hash + 1);
420                 url.setLength(hash);
421                 temp = url.toString();
422             } else {
423                 anchor = null;
424             }
425 
426             // Define the parameter separator
427             String separator = null;
428 
429             if (redirect) {
430                 separator = "&";
431             } else if (encodeSeparator) {
432                 separator = "&amp;";
433             } else {
434                 separator = "&";
435             }
436 
437             // Add the required request parameters
438             boolean question = temp.indexOf('?') >= 0;
439             Iterator keys = params.keySet().iterator();
440 
441             while (keys.hasNext()) {
442                 String key = (String) keys.next();
443                 Object value = params.get(key);
444 
445                 if (value == null) {
446                     if (!question) {
447                         url.append('?');
448                         question = true;
449                     } else {
450                         url.append(separator);
451                     }
452 
453                     url.append(this.encodeURL(key, charEncoding));
454                     url.append('='); // Interpret null as "no value"
455                 } else if (value instanceof String) {
456                     if (!question) {
457                         url.append('?');
458                         question = true;
459                     } else {
460                         url.append(separator);
461                     }
462 
463                     url.append(this.encodeURL(key, charEncoding));
464                     url.append('=');
465                     url.append(this.encodeURL((String) value, charEncoding));
466                 } else if (value instanceof String[]) {
467                     String[] values = (String[]) value;
468 
469                     for (int i = 0; i < values.length; i++) {
470                         if (!question) {
471                             url.append('?');
472                             question = true;
473                         } else {
474                             url.append(separator);
475                         }
476 
477                         url.append(this.encodeURL(key, charEncoding));
478                         url.append('=');
479                         url.append(this.encodeURL(values[i], charEncoding));
480                     }
481                 } else /* Convert other objects to a string */
482                  {
483                     if (!question) {
484                         url.append('?');
485                         question = true;
486                     } else {
487                         url.append(separator);
488                     }
489 
490                     url.append(this.encodeURL(key, charEncoding));
491                     url.append('=');
492                     url.append(this.encodeURL(value.toString(), charEncoding));
493                 }
494             }
495 
496             // Re-add the saved anchor (if any)
497             if (anchor != null) {
498                 url.append('#');
499                 url.append(this.encodeURL(anchor, charEncoding));
500             }
501         }
502 
503         // Perform URL rewriting to include our session ID (if any)
504         // but only if url is not an external URL
505         if ((href == null) && (pageContext.getSession() != null)) {
506             HttpServletResponse response =
507                 (HttpServletResponse) pageContext.getResponse();
508 
509             if (redirect) {
510                 return (response.encodeRedirectURL(url.toString()));
511             }
512 
513             return (response.encodeURL(url.toString()));
514         }
515 
516         return (url.toString());
517     }
518 
519     /***
520      * URLencodes a string assuming the character encoding is UTF-8.
521      *
522      * @param url
523      * @return String The encoded url in UTF-8
524      */
525     public String encodeURL(String url) {
526         return encodeURL(url, "UTF-8");
527     }
528 
529     /***
530      * Use the new URLEncoder.encode() method from Java 1.4 if available, else
531      * use the old deprecated version.  This method uses reflection to find
532      * the appropriate method; if the reflection operations throw exceptions,
533      * this will return the url encoded with the old URLEncoder.encode()
534      * method.
535      *
536      * @param enc The character encoding the urlencode is performed on.
537      * @return String The encoded url.
538      */
539     public String encodeURL(String url, String enc) {
540         return ResponseUtils.encodeURL(url, enc);
541     }
542 
543     /***
544      * Filter the specified string for characters that are senstive to HTML
545      * interpreters, returning the string with these characters replaced by
546      * the corresponding character entities.
547      *
548      * @param value The string to be filtered and returned
549      */
550     public String filter(String value) {
551         return ResponseUtils.filter(value);
552     }
553 
554     /***
555      * Return the form action converted into an action mapping path.  The
556      * value of the <code>action</code> property is manipulated as follows in
557      * computing the name of the requested mapping:
558      *
559      * <ul>
560      *
561      * <li>Any filename extension is removed (on the theory that extension
562      * mapping is being used to select the controller servlet).</li>
563      *
564      * <li>If the resulting value does not start with a slash, then a slash is
565      * prepended.</li>
566      *
567      * </ul>
568      */
569     public String getActionMappingName(String action) {
570         String value = action;
571         int question = action.indexOf("?");
572 
573         if (question >= 0) {
574             value = value.substring(0, question);
575         }
576 
577         int pound = value.indexOf("#");
578 
579         if (pound >= 0) {
580             value = value.substring(0, pound);
581         }
582 
583         int slash = value.lastIndexOf("/");
584         int period = value.lastIndexOf(".");
585 
586         if ((period >= 0) && (period > slash)) {
587             value = value.substring(0, period);
588         }
589 
590         return value.startsWith("/") ? value : ("/" + value);
591     }
592 
593     /***
594      * Return the form action converted into a server-relative URL.
595      */
596     public String getActionMappingURL(String action, PageContext pageContext) {
597         return getActionMappingURL(action, null, pageContext, false);
598     }
599 
600     /***
601      * Return the form action converted into a server-relative URL.
602      */
603     public String getActionMappingURL(String action, String module,
604         PageContext pageContext, boolean contextRelative) {
605         HttpServletRequest request =
606             (HttpServletRequest) pageContext.getRequest();
607 
608         String contextPath = request.getContextPath();
609         StringBuffer value = new StringBuffer();
610 
611         // Avoid setting two slashes at the beginning of an action:
612         //  the length of contextPath should be more than 1
613         //  in case of non-root context, otherwise length==1 (the slash)
614         if (contextPath.length() > 1) {
615             value.append(contextPath);
616         }
617 
618         ModuleConfig moduleConfig = getModuleConfig(module, pageContext);
619 
620         if ((moduleConfig != null) && (!contextRelative)) {
621             value.append(moduleConfig.getPrefix());
622         }
623 
624         // Use our servlet mapping, if one is specified
625         String servletMapping =
626             (String) pageContext.getAttribute(Globals.SERVLET_KEY,
627                 PageContext.APPLICATION_SCOPE);
628 
629         if (servletMapping != null) {
630             String queryString = null;
631             int question = action.indexOf("?");
632 
633             if (question >= 0) {
634                 queryString = action.substring(question);
635             }
636 
637             String actionMapping = getActionMappingName(action);
638 
639             if (servletMapping.startsWith("*.")) {
640                 value.append(actionMapping);
641                 value.append(servletMapping.substring(1));
642             } else if (servletMapping.endsWith("/*")) {
643                 value.append(servletMapping.substring(0,
644                         servletMapping.length() - 2));
645                 value.append(actionMapping);
646             } else if (servletMapping.equals("/")) {
647                 value.append(actionMapping);
648             }
649 
650             if (queryString != null) {
651                 value.append(queryString);
652             }
653         }
654         // Otherwise, assume extension mapping is in use and extension is
655         // already included in the action property
656         else {
657             if (!action.startsWith("/")) {
658                 value.append("/");
659             }
660 
661             value.append(action);
662         }
663 
664         return value.toString();
665     }
666 
667     /***
668      * Retrieves the value from request scope and if it isn't already an
669      * <code>ActionMessages</code>, some classes are converted to one.
670      *
671      * @param pageContext The PageContext for the current page
672      * @param paramName   Key for parameter value
673      * @return ActionErrors in page context.
674      * @throws JspException
675      */
676     public ActionMessages getActionMessages(PageContext pageContext,
677         String paramName) throws JspException {
678         ActionMessages am = new ActionMessages();
679 
680         Object value = pageContext.findAttribute(paramName);
681 
682         if (value != null) {
683             try {
684                 if (value instanceof String) {
685                     am.add(ActionMessages.GLOBAL_MESSAGE,
686                         new ActionMessage((String) value));
687                 } else if (value instanceof String[]) {
688                     String[] keys = (String[]) value;
689 
690                     for (int i = 0; i < keys.length; i++) {
691                         am.add(ActionMessages.GLOBAL_MESSAGE,
692                             new ActionMessage(keys[i]));
693                     }
694                 } else if (value instanceof ActionErrors) {
695                     ActionMessages m = (ActionMessages) value;
696 
697                     am.add(m);
698                 } else if (value instanceof ActionMessages) {
699                     am = (ActionMessages) value;
700                 } else {
701                     throw new JspException(messages.getMessage(
702                             "actionMessages.errors", value.getClass().getName()));
703                 }
704             } catch (JspException e) {
705                 throw e;
706             } catch (Exception e) {
707                 log.warn("Unable to retieve ActionMessage for paramName : "
708                     + paramName, e);
709             }
710         }
711 
712         return am;
713     }
714 
715     /***
716      * Return the default ModuleConfig object if it exists, null if
717      * otherwise.
718      *
719      * @param pageContext The page context.
720      * @return the ModuleConfig object
721      */
722     public ModuleConfig getModuleConfig(PageContext pageContext) {
723         return getModuleConfig(null, pageContext);
724     }
725 
726     /***
727      * Return the specified ModuleConfig object for the given prefix if it
728      * exists, otherwise a NullPointerException will be thrown.
729      *
730      * @param module      The module prefix
731      * @param pageContext The page context.
732      * @return the ModuleConfig object
733      * @throws NullPointerException Thrown when module cannot be found
734      */
735     public ModuleConfig getModuleConfig(String module, PageContext pageContext) {
736         ModuleConfig config =
737             ModuleUtils.getInstance().getModuleConfig(module,
738                 (HttpServletRequest) pageContext.getRequest(),
739                 pageContext.getServletContext());
740 
741         // ModuleConfig not found
742         if (config == null) {
743             throw new NullPointerException("Module '" + module + "' not found.");
744         }
745 
746         return config;
747     }
748 
749     /***
750      * Converts the scope name into its corresponding PageContext constant
751      * value.
752      *
753      * @param scopeName Can be "page", "request", "session", or "application"
754      *                  in any case.
755      * @return The constant representing the scope (ie. PageContext.REQUEST_SCOPE).
756      * @throws JspException if the scopeName is not a valid name.
757      */
758     public int getScope(String scopeName)
759         throws JspException {
760         Integer scope = (Integer) scopes.get(scopeName.toLowerCase());
761 
762         if (scope == null) {
763             throw new JspException(messages.getMessage("lookup.scope", scope));
764         }
765 
766         return scope.intValue();
767     }
768 
769     /***
770      * Look up and return current user locale, based on the specified
771      * parameters.
772      *
773      * @param pageContext The PageContext associated with this request
774      * @param locale      Name of the session attribute for our user's Locale.
775      *                    If this is <code>null</code>, the default locale key
776      *                    is used for the lookup.
777      * @return current user locale
778      */
779     public Locale getUserLocale(PageContext pageContext, String locale) {
780         return RequestUtils.getUserLocale((HttpServletRequest) pageContext
781             .getRequest(), locale);
782     }
783 
784     /***
785      * Returns true if the custom tags are in XHTML mode.
786      */
787     public boolean isXhtml(PageContext pageContext) {
788         String xhtml =
789             (String) pageContext.getAttribute(Globals.XHTML_KEY,
790                 PageContext.PAGE_SCOPE);
791 
792         return "true".equalsIgnoreCase(xhtml);
793     }
794 
795     /***
796      * Locate and return the specified bean, from an optionally specified
797      * scope, in the specified page context.  If no such bean is found, return
798      * <code>null</code> instead.  If an exception is thrown, it will have
799      * already been saved via a call to <code>saveException()</code>.
800      *
801      * @param pageContext Page context to be searched
802      * @param name        Name of the bean to be retrieved
803      * @param scopeName   Scope to be searched (page, request, session,
804      *                    application) or <code>null</code> to use
805      *                    <code>findAttribute()</code> instead
806      * @return JavaBean in the specified page context
807      * @throws JspException if an invalid scope name is requested
808      */
809     public Object lookup(PageContext pageContext, String name, String scopeName)
810         throws JspException {
811         if (scopeName == null) {
812             return pageContext.findAttribute(name);
813         }
814 
815         try {
816             return pageContext.getAttribute(name, instance.getScope(scopeName));
817         } catch (JspException e) {
818             saveException(pageContext, e);
819             throw e;
820         }
821     }
822 
823     /***
824      * Locate and return the specified property of the specified bean, from an
825      * optionally specified scope, in the specified page context.  If an
826      * exception is thrown, it will have already been saved via a call to
827      * <code>saveException()</code>.
828      *
829      * @param pageContext Page context to be searched
830      * @param name        Name of the bean to be retrieved
831      * @param property    Name of the property to be retrieved, or
832      *                    <code>null</code> to retrieve the bean itself
833      * @param scope       Scope to be searched (page, request, session,
834      *                    application) or <code>null</code> to use
835      *                    <code>findAttribute()</code> instead
836      * @return property of specified JavaBean
837      * @throws JspException if an invalid scope name is requested
838      * @throws JspException if the specified bean is not found
839      * @throws JspException if accessing this property causes an
840      *                      IllegalAccessException, IllegalArgumentException,
841      *                      InvocationTargetException, or NoSuchMethodException
842      */
843     public Object lookup(PageContext pageContext, String name, String property,
844         String scope) throws JspException {
845         // Look up the requested bean, and return if requested
846         Object bean = lookup(pageContext, name, scope);
847 
848         if (bean == null) {
849             JspException e = null;
850 
851             if (scope == null) {
852                 e = new JspException(messages.getMessage("lookup.bean.any", name));
853             } else {
854                 e = new JspException(messages.getMessage("lookup.bean", name,
855                             scope));
856             }
857 
858             saveException(pageContext, e);
859             throw e;
860         }
861 
862         if (property == null) {
863             return bean;
864         }
865 
866         // Locate and return the specified property
867         try {
868             return PropertyUtils.getProperty(bean, property);
869         } catch (IllegalAccessException e) {
870             saveException(pageContext, e);
871             throw new JspException(messages.getMessage("lookup.access",
872                     property, name));
873         } catch (IllegalArgumentException e) {
874             saveException(pageContext, e);
875             throw new JspException(messages.getMessage("lookup.argument",
876                     property, name));
877         } catch (InvocationTargetException e) {
878             Throwable t = e.getTargetException();
879 
880             if (t == null) {
881                 t = e;
882             }
883 
884             saveException(pageContext, t);
885             throw new JspException(messages.getMessage("lookup.target",
886                     property, name));
887         } catch (NoSuchMethodException e) {
888             saveException(pageContext, e);
889 
890             String beanName = name;
891 
892             // Name defaults to Contants.BEAN_KEY if no name is specified by
893             // an input tag. Thus lookup the bean under the key and use
894             // its class name for the exception message.
895             if (Constants.BEAN_KEY.equals(name)) {
896                 Object obj = pageContext.findAttribute(Constants.BEAN_KEY);
897 
898                 if (obj != null) {
899                     beanName = obj.getClass().getName();
900                 }
901             }
902 
903             throw new JspException(messages.getMessage("lookup.method",
904                     property, beanName));
905         }
906     }
907 
908     /***
909      * Look up and return a message string, based on the specified
910      * parameters.
911      *
912      * @param pageContext The PageContext associated with this request
913      * @param bundle      Name of the servlet context attribute for our
914      *                    message resources bundle
915      * @param locale      Name of the session attribute for our user's Locale
916      * @param key         Message key to be looked up and returned
917      * @return message string
918      * @throws JspException if a lookup error occurs (will have been saved in
919      *                      the request already)
920      */
921     public String message(PageContext pageContext, String bundle,
922         String locale, String key)
923         throws JspException {
924         return message(pageContext, bundle, locale, key, null);
925     }
926 
927     /***
928      * Look up and return a message string, based on the specified
929      * parameters.
930      *
931      * @param pageContext The PageContext associated with this request
932      * @param bundle      Name of the servlet context attribute for our
933      *                    message resources bundle
934      * @param locale      Name of the session attribute for our user's Locale
935      * @param key         Message key to be looked up and returned
936      * @param args        Replacement parameters for this message
937      * @return message string
938      * @throws JspException if a lookup error occurs (will have been saved in
939      *                      the request already)
940      */
941     public String message(PageContext pageContext, String bundle,
942         String locale, String key, Object[] args)
943         throws JspException {
944         MessageResources resources =
945             retrieveMessageResources(pageContext, bundle, false);
946 
947         Locale userLocale = getUserLocale(pageContext, locale);
948         String message = null;
949 
950         if (args == null) {
951             message = resources.getMessage(userLocale, key);
952         } else {
953             message = resources.getMessage(userLocale, key, args);
954         }
955 
956         if ((message == null) && log.isDebugEnabled()) {
957             // log missing key to ease debugging
958             log.debug(resources.getMessage("message.resources", key, bundle,
959                     locale));
960         }
961 
962         return message;
963     }
964 
965     /***
966      * <p>Return the context-relative URL that corresponds to the specified
967      * <code>page</code> attribute value, calculated based on the
968      * <code>pagePattern</code> property of the current module's {@link
969      * ModuleConfig}.</p>
970      *
971      * @param request The servlet request we are processing
972      * @param page    The module-relative URL to be substituted in to the
973      *                <code>pagePattern</code> pattern for the current module
974      *                (<strong>MUST</strong> start with a slash)
975      * @return context-relative URL
976      */
977     public String pageURL(HttpServletRequest request, String page,
978         ModuleConfig moduleConfig) {
979         StringBuffer sb = new StringBuffer();
980         String pagePattern =
981             moduleConfig.getControllerConfig().getPagePattern();
982 
983         if (pagePattern == null) {
984             sb.append(moduleConfig.getPrefix());
985             sb.append(page);
986         } else {
987             boolean dollar = false;
988 
989             for (int i = 0; i < pagePattern.length(); i++) {
990                 char ch = pagePattern.charAt(i);
991 
992                 if (dollar) {
993                     switch (ch) {
994                     case 'M':
995                         sb.append(moduleConfig.getPrefix());
996 
997                         break;
998 
999                     case 'P':
1000                         sb.append(page);
1001 
1002                         break;
1003 
1004                     case '$':
1005                         sb.append('$');
1006 
1007                         break;
1008 
1009                     default:
1010                         ; // Silently swallow
1011                     }
1012 
1013                     dollar = false;
1014 
1015                     continue;
1016                 } else if (ch == '$') {
1017                     dollar = true;
1018                 } else {
1019                     sb.append(ch);
1020                 }
1021             }
1022         }
1023 
1024         return sb.toString();
1025     }
1026 
1027     /***
1028      * Return true if a message string for the specified message key is
1029      * present for the specified <code>Locale</code> and bundle.
1030      *
1031      * @param pageContext The PageContext associated with this request
1032      * @param bundle      Name of the servlet context attribute for our
1033      *                    message resources bundle
1034      * @param locale      Name of the session attribute for our user's Locale
1035      * @param key         Message key to be looked up and returned
1036      * @return true if a message string for message key exists
1037      * @throws JspException if a lookup error occurs (will have been saved in
1038      *                      the request already)
1039      */
1040     public boolean present(PageContext pageContext, String bundle,
1041         String locale, String key)
1042         throws JspException {
1043         MessageResources resources =
1044             retrieveMessageResources(pageContext, bundle, true);
1045 
1046         Locale userLocale = getUserLocale(pageContext, locale);
1047 
1048         return resources.isPresent(userLocale, key);
1049     }
1050 
1051     /***
1052      * Returns the appropriate MessageResources object for the current module
1053      * and the given bundle.
1054      *
1055      * @param pageContext    Search the context's scopes for the resources.
1056      * @param bundle         The bundle name to look for.  If this is
1057      *                       <code>null</code>, the default bundle name is
1058      *                       used.
1059      * @param checkPageScope Whether to check page scope
1060      * @return MessageResources The bundle's resources stored in some scope.
1061      * @throws JspException if the MessageResources object could not be
1062      *                      found.
1063      */
1064     public MessageResources retrieveMessageResources(PageContext pageContext,
1065         String bundle, boolean checkPageScope)
1066         throws JspException {
1067         MessageResources resources = null;
1068 
1069         if (bundle == null) {
1070             bundle = Globals.MESSAGES_KEY;
1071         }
1072 
1073         if (checkPageScope) {
1074             resources =
1075                 (MessageResources) pageContext.getAttribute(bundle,
1076                     PageContext.PAGE_SCOPE);
1077         }
1078 
1079         if (resources == null) {
1080             resources =
1081                 (MessageResources) pageContext.getAttribute(bundle,
1082                     PageContext.REQUEST_SCOPE);
1083         }
1084 
1085         if (resources == null) {
1086             ModuleConfig moduleConfig = getModuleConfig(pageContext);
1087 
1088             resources =
1089                 (MessageResources) pageContext.getAttribute(bundle
1090                     + moduleConfig.getPrefix(), PageContext.APPLICATION_SCOPE);
1091         }
1092 
1093         if (resources == null) {
1094             resources =
1095                 (MessageResources) pageContext.getAttribute(bundle,
1096                     PageContext.APPLICATION_SCOPE);
1097         }
1098 
1099         if (resources == null) {
1100             JspException e =
1101                 new JspException(messages.getMessage("message.bundle", bundle));
1102 
1103             saveException(pageContext, e);
1104             throw e;
1105         }
1106 
1107         return resources;
1108     }
1109 
1110     /***
1111      * Save the specified exception as a request attribute for later use.
1112      *
1113      * @param pageContext The PageContext for the current page
1114      * @param exception   The exception to be saved
1115      */
1116     public void saveException(PageContext pageContext, Throwable exception) {
1117         pageContext.setAttribute(Globals.EXCEPTION_KEY, exception,
1118             PageContext.REQUEST_SCOPE);
1119     }
1120 
1121     /***
1122      * Write the specified text as the response to the writer associated with
1123      * this page.  <strong>WARNING</strong> - If you are writing body content
1124      * from the <code>doAfterBody()</code> method of a custom tag class that
1125      * implements <code>BodyTag</code>, you should be calling
1126      * <code>writePrevious()</code> instead.
1127      *
1128      * @param pageContext The PageContext object for this page
1129      * @param text        The text to be written
1130      * @throws JspException if an input/output error occurs (already saved)
1131      */
1132     public void write(PageContext pageContext, String text)
1133         throws JspException {
1134         JspWriter writer = pageContext.getOut();
1135 
1136         try {
1137             writer.print(text);
1138         } catch (IOException e) {
1139             saveException(pageContext, e);
1140             throw new JspException(messages.getMessage("write.io", e.toString()));
1141         }
1142     }
1143 
1144     /***
1145      * Write the specified text as the response to the writer associated with
1146      * the body content for the tag within which we are currently nested.
1147      *
1148      * @param pageContext The PageContext object for this page
1149      * @param text        The text to be written
1150      * @throws JspException if an input/output error occurs (already saved)
1151      */
1152     public void writePrevious(PageContext pageContext, String text)
1153         throws JspException {
1154         JspWriter writer = pageContext.getOut();
1155 
1156         if (writer instanceof BodyContent) {
1157             writer = ((BodyContent) writer).getEnclosingWriter();
1158         }
1159 
1160         try {
1161             writer.print(text);
1162         } catch (IOException e) {
1163             saveException(pageContext, e);
1164             throw new JspException(messages.getMessage("write.io", e.toString()));
1165         }
1166     }
1167 }