View Javadoc

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