001/*
002 * Copyright 2002-2012 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 *      https://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
017package org.springframework.web.portlet.util;
018
019import java.io.File;
020import java.io.FileNotFoundException;
021import java.io.IOException;
022import java.util.Enumeration;
023import java.util.HashMap;
024import java.util.Map;
025import java.util.TreeMap;
026import javax.portlet.ActionRequest;
027import javax.portlet.ActionResponse;
028import javax.portlet.PortletContext;
029import javax.portlet.PortletException;
030import javax.portlet.PortletRequest;
031import javax.portlet.PortletRequestDispatcher;
032import javax.portlet.PortletResponse;
033import javax.portlet.PortletSession;
034import javax.portlet.ResourceRequest;
035import javax.portlet.ResourceResponse;
036import javax.portlet.filter.PortletRequestWrapper;
037import javax.portlet.filter.PortletResponseWrapper;
038import javax.servlet.http.Cookie;
039
040import org.springframework.util.Assert;
041import org.springframework.util.StringUtils;
042import org.springframework.web.util.WebUtils;
043
044/**
045 * Miscellaneous utilities for portlet applications.
046 * Used by various framework classes.
047 *
048 * @author Juergen Hoeller
049 * @author William G. Thompson, Jr.
050 * @author John A. Lewis
051 * @since 2.0
052 */
053public abstract class PortletUtils {
054
055        /**
056         * Return the temporary directory for the current web application,
057         * as provided by the portlet container.
058         * @param portletContext the portlet context of the web application
059         * @return the File representing the temporary directory
060         */
061        public static File getTempDir(PortletContext portletContext) {
062                Assert.notNull(portletContext, "PortletContext must not be null");
063                return (File) portletContext.getAttribute(WebUtils.TEMP_DIR_CONTEXT_ATTRIBUTE);
064        }
065
066        /**
067         * Return the real path of the given path within the web application,
068         * as provided by the portlet container.
069         * <p>Prepends a slash if the path does not already start with a slash,
070         * and throws a {@link java.io.FileNotFoundException} if the path cannot
071         * be resolved to a resource (in contrast to
072         * {@link javax.portlet.PortletContext#getRealPath PortletContext's {@code getRealPath}},
073         * which simply returns {@code null}).
074         * @param portletContext the portlet context of the web application
075         * @param path the relative path within the web application
076         * @return the corresponding real path
077         * @throws FileNotFoundException if the path cannot be resolved to a resource
078         * @see javax.portlet.PortletContext#getRealPath
079         */
080        public static String getRealPath(PortletContext portletContext, String path) throws FileNotFoundException {
081                Assert.notNull(portletContext, "PortletContext must not be null");
082                // Interpret location as relative to the web application root directory.
083                if (!path.startsWith("/")) {
084                        path = "/" + path;
085                }
086                String realPath = portletContext.getRealPath(path);
087                if (realPath == null) {
088                        throw new FileNotFoundException(
089                                        "PortletContext resource [" + path + "] cannot be resolved to absolute file path - " +
090                                        "web application archive not expanded?");
091                }
092                return realPath;
093        }
094
095
096        /**
097         * Check the given request for a session attribute of the given name under the
098         * {@link javax.portlet.PortletSession#PORTLET_SCOPE}.
099         * Returns {@code null} if there is no session or if the session has no such attribute in that scope.
100         * Does not create a new session if none has existed before!
101         * @param request current portlet request
102         * @param name the name of the session attribute
103         * @return the value of the session attribute, or {@code null} if not found
104         */
105        public static Object getSessionAttribute(PortletRequest request, String name) {
106                return getSessionAttribute(request, name, PortletSession.PORTLET_SCOPE);
107        }
108
109        /**
110         * Check the given request for a session attribute of the given name in the given scope.
111         * Returns {@code null} if there is no session or if the session has no such attribute in that scope.
112         * Does not create a new session if none has existed before!
113         * @param request current portlet request
114         * @param name the name of the session attribute
115         * @param scope session scope of this attribute
116         * @return the value of the session attribute, or {@code null} if not found
117         */
118        public static Object getSessionAttribute(PortletRequest request, String name, int scope) {
119                Assert.notNull(request, "Request must not be null");
120                PortletSession session = request.getPortletSession(false);
121                return (session != null ? session.getAttribute(name, scope) : null);
122        }
123
124        /**
125         * Check the given request for a session attribute of the given name
126         * under the {@link javax.portlet.PortletSession#PORTLET_SCOPE}.
127         * Throws an exception if there is no session or if the session has
128         * no such attribute in that scope.
129         * <p>Does not create a new session if none has existed before!
130         * @param request current portlet request
131         * @param name the name of the session attribute
132         * @return the value of the session attribute
133         * @throws IllegalStateException if the session attribute could not be found
134         */
135        public static Object getRequiredSessionAttribute(PortletRequest request, String name)
136                        throws IllegalStateException {
137
138                return getRequiredSessionAttribute(request, name, PortletSession.PORTLET_SCOPE);
139        }
140
141        /**
142         * Check the given request for a session attribute of the given name in the given scope.
143         * Throws an exception if there is no session or if the session has no such attribute
144         * in that scope.
145         * <p>Does not create a new session if none has existed before!
146         * @param request current portlet request
147         * @param name the name of the session attribute
148         * @param scope session scope of this attribute
149         * @return the value of the session attribute
150         * @throws IllegalStateException if the session attribute could not be found
151         */
152        public static Object getRequiredSessionAttribute(PortletRequest request, String name, int scope)
153                        throws IllegalStateException {
154                Object attr = getSessionAttribute(request, name, scope);
155                if (attr == null) {
156                        throw new IllegalStateException("No session attribute '" + name + "' found");
157                }
158                return attr;
159        }
160
161        /**
162         * Set the session attribute with the given name to the given value under the {@link javax.portlet.PortletSession#PORTLET_SCOPE}.
163         * Removes the session attribute if value is {@code null}, if a session existed at all.
164         * Does not create a new session if not necessary!
165         * @param request current portlet request
166         * @param name the name of the session attribute
167         * @param value the value of the session attribute
168         */
169        public static void setSessionAttribute(PortletRequest request, String name, Object value) {
170                setSessionAttribute(request, name, value, PortletSession.PORTLET_SCOPE);
171        }
172
173        /**
174         * Set the session attribute with the given name to the given value in the given scope.
175         * Removes the session attribute if value is {@code null}, if a session existed at all.
176         * Does not create a new session if not necessary!
177         * @param request current portlet request
178         * @param name the name of the session attribute
179         * @param value the value of the session attribute
180         * @param scope session scope of this attribute
181         */
182        public static void setSessionAttribute(PortletRequest request, String name, Object value, int scope) {
183                Assert.notNull(request, "Request must not be null");
184                if (value != null) {
185                        request.getPortletSession().setAttribute(name, value, scope);
186                }
187                else {
188                        PortletSession session = request.getPortletSession(false);
189                        if (session != null) {
190                                session.removeAttribute(name, scope);
191                        }
192                }
193        }
194
195        /**
196         * Get the specified session attribute under the {@link javax.portlet.PortletSession#PORTLET_SCOPE},
197         * creating and setting a new attribute if no existing found. The given class
198         * needs to have a public no-arg constructor.
199         * Useful for on-demand state objects in a web tier, like shopping carts.
200         * @param session current portlet session
201         * @param name the name of the session attribute
202         * @param clazz the class to instantiate for a new attribute
203         * @return the value of the session attribute, newly created if not found
204         * @throws IllegalArgumentException if the session attribute could not be instantiated
205         */
206        public static Object getOrCreateSessionAttribute(PortletSession session, String name, Class<?> clazz)
207                        throws IllegalArgumentException {
208
209                return getOrCreateSessionAttribute(session, name, clazz, PortletSession.PORTLET_SCOPE);
210        }
211
212        /**
213         * Get the specified session attribute in the given scope,
214         * creating and setting a new attribute if no existing found. The given class
215         * needs to have a public no-arg constructor.
216         * Useful for on-demand state objects in a web tier, like shopping carts.
217         * @param session current portlet session
218         * @param name the name of the session attribute
219         * @param clazz the class to instantiate for a new attribute
220         * @param scope the session scope of this attribute
221         * @return the value of the session attribute, newly created if not found
222         * @throws IllegalArgumentException if the session attribute could not be instantiated
223         */
224        public static Object getOrCreateSessionAttribute(PortletSession session, String name, Class<?> clazz, int scope)
225                        throws IllegalArgumentException {
226
227                Assert.notNull(session, "Session must not be null");
228                Object sessionObject = session.getAttribute(name, scope);
229                if (sessionObject == null) {
230                        Assert.notNull(clazz, "Class must not be null if attribute value is to be instantiated");
231                        try {
232                                sessionObject = clazz.newInstance();
233                        }
234                        catch (InstantiationException ex) {
235                                throw new IllegalArgumentException(
236                                                "Could not instantiate class [" + clazz.getName() +
237                                                "] for session attribute '" + name + "': " + ex.getMessage());
238                        }
239                        catch (IllegalAccessException ex) {
240                                throw new IllegalArgumentException(
241                                                "Could not access default constructor of class [" + clazz.getName() +
242                                                "] for session attribute '" + name + "': " + ex.getMessage());
243                        }
244                        session.setAttribute(name, sessionObject, scope);
245                }
246                return sessionObject;
247        }
248
249        /**
250         * Return the best available mutex for the given session:
251         * that is, an object to synchronize on for the given session.
252         * <p>Returns the session mutex attribute if available; usually,
253         * this means that the
254         * {@link org.springframework.web.util.HttpSessionMutexListener}
255         * needs to be defined in {@code web.xml}. Falls back to the
256         * {@link javax.portlet.PortletSession} itself if no mutex attribute found.
257         * <p>The session mutex is guaranteed to be the same object during
258         * the entire lifetime of the session, available under the key defined
259         * by the {@link org.springframework.web.util.WebUtils#SESSION_MUTEX_ATTRIBUTE}
260         * constant. It serves as a safe reference to synchronize on for locking
261         * on the current session.
262         * <p>In many cases, the {@link javax.portlet.PortletSession} reference
263         * itself is a safe mutex as well, since it will always be the same
264         * object reference for the same active logical session. However, this is
265         * not guaranteed across different servlet containers; the only 100% safe
266         * way is a session mutex.
267         * @param session the HttpSession to find a mutex for
268         * @return the mutex object (never {@code null})
269         * @see org.springframework.web.util.WebUtils#SESSION_MUTEX_ATTRIBUTE
270         * @see org.springframework.web.util.HttpSessionMutexListener
271         */
272        public static Object getSessionMutex(PortletSession session) {
273                Assert.notNull(session, "Session must not be null");
274                Object mutex = session.getAttribute(WebUtils.SESSION_MUTEX_ATTRIBUTE, PortletSession.APPLICATION_SCOPE);
275                if (mutex == null) {
276                        mutex = session;
277                }
278                return mutex;
279        }
280
281
282        /**
283         * Return an appropriate request object of the specified type, if available,
284         * unwrapping the given request as far as necessary.
285         * @param request the portlet request to introspect
286         * @param requiredType the desired type of request object
287         * @return the matching request object, or {@code null} if none
288         * of that type is available
289         */
290        @SuppressWarnings("unchecked")
291        public static <T> T getNativeRequest(PortletRequest request, Class<T> requiredType) {
292                if (requiredType != null) {
293                        if (requiredType.isInstance(request)) {
294                                return (T) request;
295                        }
296                        else if (request instanceof PortletRequestWrapper) {
297                                return getNativeRequest(((PortletRequestWrapper) request).getRequest(), requiredType);
298                        }
299                }
300                return null;
301        }
302
303        /**
304         * Return an appropriate response object of the specified type, if available,
305         * unwrapping the given response as far as necessary.
306         * @param response the portlet response to introspect
307         * @param requiredType the desired type of response object
308         * @return the matching response object, or {@code null} if none
309         * of that type is available
310         */
311        @SuppressWarnings("unchecked")
312        public static <T> T getNativeResponse(PortletResponse response, Class<T> requiredType) {
313                if (requiredType != null) {
314                        if (requiredType.isInstance(response)) {
315                                return (T) response;
316                        }
317                        else if (response instanceof PortletResponseWrapper) {
318                                return getNativeResponse(((PortletResponseWrapper) response).getResponse(), requiredType);
319                        }
320                }
321                return null;
322        }
323
324        /**
325         * Expose the given Map as request attributes, using the keys as attribute names
326         * and the values as corresponding attribute values. Keys must be Strings.
327         * @param request current portlet request
328         * @param attributes the attributes Map
329         */
330        public static void exposeRequestAttributes(PortletRequest request, Map<String, ?> attributes) {
331                Assert.notNull(request, "Request must not be null");
332                Assert.notNull(attributes, "Attributes Map must not be null");
333                for (Map.Entry<String, ?> entry : attributes.entrySet()) {
334                        request.setAttribute(entry.getKey(), entry.getValue());
335                }
336        }
337
338        /**
339         * Retrieve the first cookie with the given name. Note that multiple
340         * cookies can have the same name but different paths or domains.
341         * @param request current portlet request
342         * @param name cookie name
343         * @return the first cookie with the given name, or {@code null} if none is found
344         */
345        public static Cookie getCookie(PortletRequest request, String name) {
346                Assert.notNull(request, "Request must not be null");
347                Cookie cookies[] = request.getCookies();
348                if (cookies != null) {
349                        for (Cookie cookie : cookies) {
350                                if (name.equals(cookie.getName())) {
351                                        return cookie;
352                                }
353                        }
354                }
355                return null;
356        }
357
358        /**
359         * Check if a specific input type="submit" parameter was sent in the request,
360         * either via a button (directly with name) or via an image (name + ".x" or
361         * name + ".y").
362         * @param request current portlet request
363         * @param name name of the parameter
364         * @return if the parameter was sent
365         * @see org.springframework.web.util.WebUtils#SUBMIT_IMAGE_SUFFIXES
366         */
367        public static boolean hasSubmitParameter(PortletRequest request, String name) {
368                return getSubmitParameter(request, name) != null;
369        }
370
371        /**
372         * Return the full name of a specific input type="submit" parameter
373         * if it was sent in the request, either via a button (directly with name)
374         * or via an image (name + ".x" or name + ".y").
375         * @param request current portlet request
376         * @param name name of the parameter
377         * @return the actual parameter name with suffix if needed - null if not present
378         * @see org.springframework.web.util.WebUtils#SUBMIT_IMAGE_SUFFIXES
379         */
380        public static String getSubmitParameter(PortletRequest request, String name) {
381                Assert.notNull(request, "Request must not be null");
382                if (request.getParameter(name) != null) {
383                        return name;
384                }
385                for (int i = 0; i < WebUtils.SUBMIT_IMAGE_SUFFIXES.length; i++) {
386                        String suffix = WebUtils.SUBMIT_IMAGE_SUFFIXES[i];
387                        String parameter = name + suffix;
388                        if (request.getParameter(parameter) != null) {
389                                return parameter;
390                        }
391                }
392                return null;
393        }
394
395        /**
396         * Return a map containing all parameters with the given prefix.
397         * Maps single values to String and multiple values to String array.
398         * <p>For example, with a prefix of "spring_", "spring_param1" and
399         * "spring_param2" result in a Map with "param1" and "param2" as keys.
400         * <p>Similar to portlet
401         * {@link javax.portlet.PortletRequest#getParameterMap()},
402         * but more flexible.
403         * @param request portlet request in which to look for parameters
404         * @param prefix the beginning of parameter names
405         * (if this is {@code null} or the empty string, all parameters will match)
406         * @return map containing request parameters <b>without the prefix</b>,
407         * containing either a String or a String array as values
408         * @see javax.portlet.PortletRequest#getParameterNames
409         * @see javax.portlet.PortletRequest#getParameterValues
410         * @see javax.portlet.PortletRequest#getParameterMap
411         */
412        public static Map<String, Object> getParametersStartingWith(PortletRequest request, String prefix) {
413                Assert.notNull(request, "Request must not be null");
414                Enumeration<String> paramNames = request.getParameterNames();
415                Map<String, Object> params = new TreeMap<String, Object>();
416                if (prefix == null) {
417                        prefix = "";
418                }
419                while (paramNames != null && paramNames.hasMoreElements()) {
420                        String paramName = paramNames.nextElement();
421                        if ("".equals(prefix) || paramName.startsWith(prefix)) {
422                                String unprefixed = paramName.substring(prefix.length());
423                                String[] values = request.getParameterValues(paramName);
424                                if (values == null || values.length == 0) {
425                                        // Do nothing, no values found at all.
426                                }
427                                else if (values.length > 1) {
428                                        params.put(unprefixed, values);
429                                }
430                                else {
431                                        params.put(unprefixed, values[0]);
432                                }
433                        }
434                }
435                return params;
436        }
437
438        /**
439         * Return the target page specified in the request.
440         * @param request current portlet request
441         * @param paramPrefix the parameter prefix to check for
442         * (e.g. "_target" for parameters like "_target1" or "_target2")
443         * @param currentPage the current page, to be returned as fallback
444         * if no target page specified
445         * @return the page specified in the request, or current page if not found
446         */
447        public static int getTargetPage(PortletRequest request, String paramPrefix, int currentPage) {
448                Enumeration<String> paramNames = request.getParameterNames();
449                while (paramNames.hasMoreElements()) {
450                        String paramName = paramNames.nextElement();
451                        if (paramName.startsWith(paramPrefix)) {
452                                for (int i = 0; i < WebUtils.SUBMIT_IMAGE_SUFFIXES.length; i++) {
453                                        String suffix = WebUtils.SUBMIT_IMAGE_SUFFIXES[i];
454                                        if (paramName.endsWith(suffix)) {
455                                                paramName = paramName.substring(0, paramName.length() - suffix.length());
456                                        }
457                                }
458                                return Integer.parseInt(paramName.substring(paramPrefix.length()));
459                        }
460                }
461                return currentPage;
462        }
463
464
465        /**
466         * Pass all the action request parameters to the render phase by putting them into
467         * the action response object. This may not be called when the action will call
468         * {@link javax.portlet.ActionResponse#sendRedirect sendRedirect}.
469         * @param request the current action request
470         * @param response the current action response
471         * @see javax.portlet.ActionResponse#setRenderParameter
472         */
473        public static void passAllParametersToRenderPhase(ActionRequest request, ActionResponse response) {
474                try {
475                        Enumeration<String> en = request.getParameterNames();
476                        while (en.hasMoreElements()) {
477                                String param = en.nextElement();
478                                String values[] = request.getParameterValues(param);
479                                response.setRenderParameter(param, values);
480                        }
481                }
482                catch (IllegalStateException ex) {
483                        // Ignore in case sendRedirect was already set.
484                }
485        }
486
487        /**
488         * Clear all the render parameters from the {@link javax.portlet.ActionResponse}.
489         * This may not be called when the action will call
490         * {@link ActionResponse#sendRedirect sendRedirect}.
491         * @param response the current action response
492         * @see ActionResponse#setRenderParameters
493         */
494        public static void clearAllRenderParameters(ActionResponse response) {
495                try {
496                        response.setRenderParameters(new HashMap<String, String[]>(0));
497                }
498                catch (IllegalStateException ex) {
499                        // Ignore in case sendRedirect was already set.
500                }
501        }
502
503        /**
504         * Serve the resource as specified in the given request to the given response,
505         * using the PortletContext's request dispatcher.
506         * <p>This is roughly equivalent to Portlet 2.0 GenericPortlet.
507         * @param request the current resource request
508         * @param response the current resource response
509         * @param context the current Portlet's PortletContext
510         * @throws PortletException propagated from Portlet API's forward method
511         * @throws IOException propagated from Portlet API's forward method
512         */
513        public static void serveResource(ResourceRequest request, ResourceResponse response, PortletContext context)
514                        throws PortletException, IOException {
515
516                String id = request.getResourceID();
517                if (id != null) {
518                        if (!PortletUtils.isProtectedResource(id)) {
519                                PortletRequestDispatcher rd = context.getRequestDispatcher(id);
520                                if (rd != null) {
521                                        rd.forward(request, response);
522                                        return;
523                                }
524                        }
525                        response.setProperty(ResourceResponse.HTTP_STATUS_CODE, "404");
526                }
527        }
528
529        /**
530         * Check whether the specified path indicates a resource in the protected
531         * WEB-INF or META-INF directories.
532         * @param path the path to check
533         */
534        private static boolean isProtectedResource(String path) {
535                return (StringUtils.startsWithIgnoreCase(path, "/WEB-INF") ||
536                                StringUtils.startsWithIgnoreCase(path, "/META-INF"));
537        }
538
539}