001: /*
002: * Copyright 2002-2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.springframework.web.util;
018:
019: import java.io.File;
020: import java.io.FileNotFoundException;
021: import java.util.Enumeration;
022: import java.util.Iterator;
023: import java.util.Map;
024: import java.util.TreeMap;
025:
026: import javax.servlet.ServletContext;
027: import javax.servlet.ServletRequest;
028: import javax.servlet.http.Cookie;
029: import javax.servlet.http.HttpServletRequest;
030: import javax.servlet.http.HttpSession;
031:
032: import org.springframework.util.Assert;
033: import org.springframework.util.StringUtils;
034:
035: /**
036: * Miscellaneous utilities for web applications.
037: * Used by various framework classes.
038: *
039: * @author Rod Johnson
040: * @author Juergen Hoeller
041: */
042: public abstract class WebUtils {
043:
044: /**
045: * Standard Servlet 2.3+ spec request attributes for include URI and paths.
046: * <p>If included via a RequestDispatcher, the current resource will see the
047: * originating request. Its own URI and paths are exposed as request attributes.
048: */
049: public static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri";
050: public static final String INCLUDE_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.include.context_path";
051: public static final String INCLUDE_SERVLET_PATH_ATTRIBUTE = "javax.servlet.include.servlet_path";
052: public static final String INCLUDE_PATH_INFO_ATTRIBUTE = "javax.servlet.include.path_info";
053: public static final String INCLUDE_QUERY_STRING_ATTRIBUTE = "javax.servlet.include.query_string";
054:
055: /**
056: * Standard Servlet 2.4+ spec request attributes for forward URI and paths.
057: * <p>If forwarded to via a RequestDispatcher, the current resource will see its
058: * own URI and paths. The originating URI and paths are exposed as request attributes.
059: */
060: public static final String FORWARD_REQUEST_URI_ATTRIBUTE = "javax.servlet.forward.request_uri";
061: public static final String FORWARD_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.forward.context_path";
062: public static final String FORWARD_SERVLET_PATH_ATTRIBUTE = "javax.servlet.forward.servlet_path";
063: public static final String FORWARD_PATH_INFO_ATTRIBUTE = "javax.servlet.forward.path_info";
064: public static final String FORWARD_QUERY_STRING_ATTRIBUTE = "javax.servlet.forward.query_string";
065:
066: /**
067: * Prefix of the charset clause in a content type String: ";charset="
068: */
069: public static final String CONTENT_TYPE_CHARSET_PREFIX = ";charset=";
070:
071: /**
072: * Default character encoding to use when <code>request.getCharacterEncoding</code>
073: * returns <code>null</code>, according to the Servlet spec.
074: * @see ServletRequest#getCharacterEncoding
075: */
076: public static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1";
077:
078: /**
079: * Standard Servlet spec context attribute that specifies a temporary
080: * directory for the current web application, of type <code>java.io.File</code>.
081: */
082: public static final String TEMP_DIR_CONTEXT_ATTRIBUTE = "javax.servlet.context.tempdir";
083:
084: /**
085: * HTML escape parameter at the servlet context level
086: * (i.e. a context-param in <code>web.xml</code>): "defaultHtmlEscape".
087: */
088: public static final String HTML_ESCAPE_CONTEXT_PARAM = "defaultHtmlEscape";
089:
090: /**
091: * Web app root key parameter at the servlet context level
092: * (i.e. a context-param in <code>web.xml</code>): "webAppRootKey".
093: */
094: public static final String WEB_APP_ROOT_KEY_PARAM = "webAppRootKey";
095:
096: /** Default web app root key: "webapp.root" */
097: public static final String DEFAULT_WEB_APP_ROOT_KEY = "webapp.root";
098:
099: /** Name suffixes in case of image buttons */
100: public static final String[] SUBMIT_IMAGE_SUFFIXES = { ".x", ".y" };
101:
102: /** Key for the mutex session attribute */
103: public static final String SESSION_MUTEX_ATTRIBUTE = WebUtils.class
104: .getName()
105: + ".MUTEX";
106:
107: /**
108: * Set a system property to the web application root directory.
109: * The key of the system property can be defined with the "webAppRootKey"
110: * context-param in <code>web.xml</code>. Default is "webapp.root".
111: * <p>Can be used for tools that support substition with <code>System.getProperty</code>
112: * values, like Log4J's "${key}" syntax within log file locations.
113: * @param servletContext the servlet context of the web application
114: * @throws IllegalStateException if the system property is already set,
115: * or if the WAR file is not expanded
116: * @see #WEB_APP_ROOT_KEY_PARAM
117: * @see #DEFAULT_WEB_APP_ROOT_KEY
118: * @see WebAppRootListener
119: * @see Log4jWebConfigurer
120: */
121: public static void setWebAppRootSystemProperty(
122: ServletContext servletContext) throws IllegalStateException {
123: Assert.notNull(servletContext,
124: "ServletContext must not be null");
125: String root = servletContext.getRealPath("/");
126: if (root == null) {
127: throw new IllegalStateException(
128: "Cannot set web app root system property when WAR file is not expanded");
129: }
130: String param = servletContext
131: .getInitParameter(WEB_APP_ROOT_KEY_PARAM);
132: String key = (param != null ? param : DEFAULT_WEB_APP_ROOT_KEY);
133: String oldValue = System.getProperty(key);
134: if (oldValue != null && !StringUtils.pathEquals(oldValue, root)) {
135: throw new IllegalStateException(
136: "Web app root system property already set to different value: '"
137: + key
138: + "' = ["
139: + oldValue
140: + "] instead of ["
141: + root
142: + "] - "
143: + "Choose unique values for the 'webAppRootKey' context-param in your web.xml files!");
144: }
145: System.setProperty(key, root);
146: servletContext.log("Set web app root system property: '" + key
147: + "' = [" + root + "]");
148: }
149:
150: /**
151: * Remove the system property that points to the web app root directory.
152: * To be called on shutdown of the web application.
153: * @param servletContext the servlet context of the web application
154: * @see #setWebAppRootSystemProperty
155: */
156: public static void removeWebAppRootSystemProperty(
157: ServletContext servletContext) {
158: Assert.notNull(servletContext,
159: "ServletContext must not be null");
160: String param = servletContext
161: .getInitParameter(WEB_APP_ROOT_KEY_PARAM);
162: String key = (param != null ? param : DEFAULT_WEB_APP_ROOT_KEY);
163: System.getProperties().remove(key);
164: }
165:
166: /**
167: * Return whether default HTML escaping is enabled for the web application,
168: * i.e. the value of the "defaultHtmlEscape" context-param in <code>web.xml</code>
169: * (if any). Falls back to <code>false</code> in case of no explicit default given.
170: * @param servletContext the servlet context of the web application
171: * @return whether default HTML escaping is enabled (default is false)
172: */
173: public static boolean isDefaultHtmlEscape(
174: ServletContext servletContext) {
175: Assert.notNull(servletContext,
176: "ServletContext must not be null");
177: String param = servletContext
178: .getInitParameter(HTML_ESCAPE_CONTEXT_PARAM);
179: return Boolean.valueOf(param).booleanValue();
180: }
181:
182: /**
183: * Return whether default HTML escaping is enabled for the web application,
184: * i.e. the value of the "defaultHtmlEscape" context-param in <code>web.xml</code>
185: * (if any).
186: * <p>This method differentiates between no param specified at all and
187: * an actual boolean value specified, allowing to have a context-specific
188: * default in case of no setting at the global level.
189: * @param servletContext the servlet context of the web application
190: * @return whether default HTML escaping is enabled (null = no explicit default)
191: */
192: public static Boolean getDefaultHtmlEscape(
193: ServletContext servletContext) {
194: Assert.notNull(servletContext,
195: "ServletContext must not be null");
196: String param = servletContext
197: .getInitParameter(HTML_ESCAPE_CONTEXT_PARAM);
198: return (StringUtils.hasText(param) ? Boolean.valueOf(param)
199: : null);
200: }
201:
202: /**
203: * Return the temporary directory for the current web application,
204: * as provided by the servlet container.
205: * @param servletContext the servlet context of the web application
206: * @return the File representing the temporary directory
207: */
208: public static File getTempDir(ServletContext servletContext) {
209: Assert.notNull(servletContext,
210: "ServletContext must not be null");
211: return (File) servletContext
212: .getAttribute(TEMP_DIR_CONTEXT_ATTRIBUTE);
213: }
214:
215: /**
216: * Return the real path of the given path within the web application,
217: * as provided by the servlet container.
218: * <p>Prepends a slash if the path does not already start with a slash,
219: * and throws a FileNotFoundException if the path cannot be resolved to
220: * a resource (in contrast to ServletContext's <code>getRealPath</code>,
221: * which returns null).
222: * @param servletContext the servlet context of the web application
223: * @param path the path within the web application
224: * @return the corresponding real path
225: * @throws FileNotFoundException if the path cannot be resolved to a resource
226: * @see javax.servlet.ServletContext#getRealPath
227: */
228: public static String getRealPath(ServletContext servletContext,
229: String path) throws FileNotFoundException {
230: Assert.notNull(servletContext,
231: "ServletContext must not be null");
232: // Interpret location as relative to the web application root directory.
233: if (!path.startsWith("/")) {
234: path = "/" + path;
235: }
236: String realPath = servletContext.getRealPath(path);
237: if (realPath == null) {
238: throw new FileNotFoundException("ServletContext resource ["
239: + path
240: + "] cannot be resolved to absolute file path - "
241: + "web application archive not expanded?");
242: }
243: return realPath;
244: }
245:
246: /**
247: * Determine the session id of the given request, if any.
248: * @param request current HTTP request
249: * @return the session id, or <code>null</code> if none
250: */
251: public static String getSessionId(HttpServletRequest request) {
252: Assert.notNull(request, "Request must not be null");
253: HttpSession session = request.getSession(false);
254: return (session != null ? session.getId() : null);
255: }
256:
257: /**
258: * Check the given request for a session attribute of the given name.
259: * Returns null if there is no session or if the session has no such attribute.
260: * Does not create a new session if none has existed before!
261: * @param request current HTTP request
262: * @param name the name of the session attribute
263: * @return the value of the session attribute, or <code>null</code> if not found
264: */
265: public static Object getSessionAttribute(
266: HttpServletRequest request, String name) {
267: Assert.notNull(request, "Request must not be null");
268: HttpSession session = request.getSession(false);
269: return (session != null ? session.getAttribute(name) : null);
270: }
271:
272: /**
273: * Check the given request for a session attribute of the given name.
274: * Throws an exception if there is no session or if the session has no such
275: * attribute. Does not create a new session if none has existed before!
276: * @param request current HTTP request
277: * @param name the name of the session attribute
278: * @return the value of the session attribute, or <code>null</code> if not found
279: * @throws IllegalStateException if the session attribute could not be found
280: */
281: public static Object getRequiredSessionAttribute(
282: HttpServletRequest request, String name)
283: throws IllegalStateException {
284:
285: Object attr = getSessionAttribute(request, name);
286: if (attr == null) {
287: throw new IllegalStateException("No session attribute '"
288: + name + "' found");
289: }
290: return attr;
291: }
292:
293: /**
294: * Set the session attribute with the given name to the given value.
295: * Removes the session attribute if value is null, if a session existed at all.
296: * Does not create a new session if not necessary!
297: * @param request current HTTP request
298: * @param name the name of the session attribute
299: * @param value the value of the session attribute
300: */
301: public static void setSessionAttribute(HttpServletRequest request,
302: String name, Object value) {
303: Assert.notNull(request, "Request must not be null");
304: if (value != null) {
305: request.getSession().setAttribute(name, value);
306: } else {
307: HttpSession session = request.getSession(false);
308: if (session != null) {
309: session.removeAttribute(name);
310: }
311: }
312: }
313:
314: /**
315: * Get the specified session attribute, creating and setting a new attribute if
316: * no existing found. The given class needs to have a public no-arg constructor.
317: * Useful for on-demand state objects in a web tier, like shopping carts.
318: * @param session current HTTP session
319: * @param name the name of the session attribute
320: * @param clazz the class to instantiate for a new attribute
321: * @return the value of the session attribute, newly created if not found
322: * @throws IllegalArgumentException if the session attribute could not be instantiated
323: */
324: public static Object getOrCreateSessionAttribute(
325: HttpSession session, String name, Class clazz)
326: throws IllegalArgumentException {
327:
328: Assert.notNull(session, "Session must not be null");
329: Object sessionObject = session.getAttribute(name);
330: if (sessionObject == null) {
331: try {
332: sessionObject = clazz.newInstance();
333: } catch (InstantiationException ex) {
334: throw new IllegalArgumentException(
335: "Could not instantiate class ["
336: + clazz.getName()
337: + "] for session attribute '" + name
338: + "': " + ex.getMessage());
339: } catch (IllegalAccessException ex) {
340: throw new IllegalArgumentException(
341: "Could not access default constructor of class ["
342: + clazz.getName()
343: + "] for session attribute '" + name
344: + "': " + ex.getMessage());
345: }
346: session.setAttribute(name, sessionObject);
347: }
348: return sessionObject;
349: }
350:
351: /**
352: * Return the best available mutex for the given session:
353: * that is, an object to synchronize on for the given session.
354: * <p>Returns the session mutex attribute if available; usually,
355: * this means that the HttpSessionMutexListener needs to be defined
356: * in <code>web.xml</code>. Falls back to the HttpSession itself
357: * if no mutex attribute found.
358: * <p>The session mutex is guaranteed to be the same object during
359: * the entire lifetime of the session, available under the key defined
360: * by the <code>SESSION_MUTEX_ATTRIBUTE</code> constant. It serves as a
361: * safe reference to synchronize on for locking on the current session.
362: * <p>In many cases, the HttpSession reference itself is a safe mutex
363: * as well, since it will always be the same object reference for the
364: * same active logical session. However, this is not guaranteed across
365: * different servlet containers; the only 100% safe way is a session mutex.
366: * @param session the HttpSession to find a mutex for
367: * @return the mutex object (never <code>null</code>)
368: * @see #SESSION_MUTEX_ATTRIBUTE
369: * @see HttpSessionMutexListener
370: */
371: public static Object getSessionMutex(HttpSession session) {
372: Assert.notNull(session, "Session must not be null");
373: Object mutex = session.getAttribute(SESSION_MUTEX_ATTRIBUTE);
374: if (mutex == null) {
375: mutex = session;
376: }
377: return mutex;
378: }
379:
380: /**
381: * Determine whether the given request is an include request,
382: * that is, not a top-level HTTP request coming in from the outside.
383: * <p>Checks the presence of the "javax.servlet.include.request_uri"
384: * request attribute. Could check any request attribute that is only
385: * present in an include request.
386: * @param request current servlet request
387: * @return whether the given request is an include request
388: */
389: public static boolean isIncludeRequest(ServletRequest request) {
390: return (request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE) != null);
391: }
392:
393: /**
394: * Expose the current request URI and paths as {@link javax.servlet.http.HttpServletRequest}
395: * attributes under the keys defined in the Servlet 2.4 specification,
396: * for containers that implement 2.3 or an earlier version of the Servlet API:
397: * <code>javax.servlet.forward.request_uri</code>,
398: * <code>javax.servlet.forward.context_path</code>,
399: * <code>javax.servlet.forward.servlet_path</code>,
400: * <code>javax.servlet.forward.path_info</code>,
401: * <code>javax.servlet.forward.query_string</code>.
402: * <p>Does not override values if already present, to not cause conflicts
403: * with the attributes exposed by Servlet 2.4+ containers themselves.
404: * @param request current servlet request
405: */
406: public static void exposeForwardRequestAttributes(
407: HttpServletRequest request) {
408: if (request.getAttribute(FORWARD_REQUEST_URI_ATTRIBUTE) == null) {
409: request.setAttribute(FORWARD_REQUEST_URI_ATTRIBUTE, request
410: .getRequestURI());
411: }
412: if (request.getAttribute(FORWARD_CONTEXT_PATH_ATTRIBUTE) == null) {
413: request.setAttribute(FORWARD_CONTEXT_PATH_ATTRIBUTE,
414: request.getContextPath());
415: }
416: if (request.getAttribute(FORWARD_SERVLET_PATH_ATTRIBUTE) == null) {
417: request.setAttribute(FORWARD_SERVLET_PATH_ATTRIBUTE,
418: request.getServletPath());
419: }
420: if (request.getAttribute(FORWARD_PATH_INFO_ATTRIBUTE) == null) {
421: request.setAttribute(FORWARD_PATH_INFO_ATTRIBUTE, request
422: .getPathInfo());
423: }
424: if (request.getAttribute(FORWARD_QUERY_STRING_ATTRIBUTE) == null) {
425: request.setAttribute(FORWARD_QUERY_STRING_ATTRIBUTE,
426: request.getQueryString());
427: }
428: }
429:
430: /**
431: * Expose the given Map as request attributes, using the keys as attribute names
432: * and the values as corresponding attribute values. Keys need to be Strings.
433: * @param request current HTTP request
434: * @param attributes the attributes Map
435: */
436: public static void exposeRequestAttributes(ServletRequest request,
437: Map attributes) {
438: Assert.notNull(request, "Request must not be null");
439: Iterator it = attributes.entrySet().iterator();
440: while (it.hasNext()) {
441: Map.Entry entry = (Map.Entry) it.next();
442: if (!(entry.getKey() instanceof String)) {
443: throw new IllegalArgumentException(
444: "Invalid key ["
445: + entry.getKey()
446: + "] in attributes Map - only Strings allowed as attribute keys");
447: }
448: request.setAttribute((String) entry.getKey(), entry
449: .getValue());
450: }
451: }
452:
453: /**
454: * Retrieve the first cookie with the given name. Note that multiple
455: * cookies can have the same name but different paths or domains.
456: * @param request current servlet request
457: * @param name cookie name
458: * @return the first cookie with the given name, or <code>null</code> if none is found
459: */
460: public static Cookie getCookie(HttpServletRequest request,
461: String name) {
462: Assert.notNull(request, "Request must not be null");
463: Cookie cookies[] = request.getCookies();
464: if (cookies != null) {
465: for (int i = 0; i < cookies.length; i++) {
466: if (name.equals(cookies[i].getName())) {
467: return cookies[i];
468: }
469: }
470: }
471: return null;
472: }
473:
474: /**
475: * Check if a specific input type="submit" parameter was sent in the request,
476: * either via a button (directly with name) or via an image (name + ".x" or
477: * name + ".y").
478: * @param request current HTTP request
479: * @param name name of the parameter
480: * @return if the parameter was sent
481: * @see #SUBMIT_IMAGE_SUFFIXES
482: */
483: public static boolean hasSubmitParameter(ServletRequest request,
484: String name) {
485: Assert.notNull(request, "Request must not be null");
486: if (request.getParameter(name) != null) {
487: return true;
488: }
489: for (int i = 0; i < SUBMIT_IMAGE_SUFFIXES.length; i++) {
490: String suffix = SUBMIT_IMAGE_SUFFIXES[i];
491: if (request.getParameter(name + suffix) != null) {
492: return true;
493: }
494: }
495: return false;
496: }
497:
498: /**
499: * Return a map containing all parameters with the given prefix.
500: * Maps single values to String and multiple values to String array.
501: * <p>For example, with a prefix of "spring_", "spring_param1" and
502: * "spring_param2" result in a Map with "param1" and "param2" as keys.
503: * @param request HTTP request in which to look for parameters
504: * @param prefix the beginning of parameter names
505: * (if this is null or the empty string, all parameters will match)
506: * @return map containing request parameters <b>without the prefix</b>,
507: * containing either a String or a String array as values
508: * @see javax.servlet.ServletRequest#getParameterNames
509: * @see javax.servlet.ServletRequest#getParameterValues
510: * @see javax.servlet.ServletRequest#getParameterMap
511: */
512: public static Map getParametersStartingWith(ServletRequest request,
513: String prefix) {
514: Assert.notNull(request, "Request must not be null");
515: Enumeration paramNames = request.getParameterNames();
516: Map params = new TreeMap();
517: if (prefix == null) {
518: prefix = "";
519: }
520: while (paramNames != null && paramNames.hasMoreElements()) {
521: String paramName = (String) paramNames.nextElement();
522: if ("".equals(prefix) || paramName.startsWith(prefix)) {
523: String unprefixed = paramName
524: .substring(prefix.length());
525: String[] values = request.getParameterValues(paramName);
526: if (values == null || values.length == 0) {
527: // Do nothing, no values found at all.
528: } else if (values.length > 1) {
529: params.put(unprefixed, values);
530: } else {
531: params.put(unprefixed, values[0]);
532: }
533: }
534: }
535: return params;
536: }
537:
538: /**
539: * Return the target page specified in the request.
540: * @param request current servlet request
541: * @param paramPrefix the parameter prefix to check for
542: * (e.g. "_target" for parameters like "_target1" or "_target2")
543: * @param currentPage the current page, to be returned as fallback
544: * if no target page specified
545: * @return the page specified in the request, or current page if not found
546: */
547: public static int getTargetPage(ServletRequest request,
548: String paramPrefix, int currentPage) {
549: Enumeration paramNames = request.getParameterNames();
550: while (paramNames.hasMoreElements()) {
551: String paramName = (String) paramNames.nextElement();
552: if (paramName.startsWith(paramPrefix)) {
553: for (int i = 0; i < WebUtils.SUBMIT_IMAGE_SUFFIXES.length; i++) {
554: String suffix = WebUtils.SUBMIT_IMAGE_SUFFIXES[i];
555: if (paramName.endsWith(suffix)) {
556: paramName = paramName.substring(0, paramName
557: .length()
558: - suffix.length());
559: }
560: }
561: return Integer.parseInt(paramName.substring(paramPrefix
562: .length()));
563: }
564: }
565: return currentPage;
566: }
567:
568: /**
569: * Extract the URL filename from the given request URL path.
570: * Correctly resolves nested paths such as "/products/view.html" as well.
571: * @param urlPath the request URL path (e.g. "/index.html")
572: * @return the extracted URI filename (e.g. "index")
573: */
574: public static String extractFilenameFromUrlPath(String urlPath) {
575: int begin = urlPath.lastIndexOf('/') + 1;
576: int end = urlPath.indexOf(';');
577: if (end == -1) {
578: end = urlPath.indexOf('?');
579: if (end == -1) {
580: end = urlPath.length();
581: }
582: }
583: String filename = urlPath.substring(begin, end);
584: int dotIndex = filename.lastIndexOf('.');
585: if (dotIndex != -1) {
586: filename = filename.substring(0, dotIndex);
587: }
588: return filename;
589: }
590:
591: }
|