001: /* ***** BEGIN LICENSE BLOCK *****
002: * Version: MPL 1.1
003: * The contents of this file are subject to the Mozilla Public License Version
004: * 1.1 (the "License"); you may not use this file except in compliance with
005: * the License. You may obtain a copy of the License at
006: * http://www.mozilla.org/MPL/
007: *
008: * Software distributed under the License is distributed on an "AS IS" basis,
009: * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
010: * for the specific language governing rights and limitations under the
011: * License.
012: *
013: * The Original Code is Riot.
014: *
015: * The Initial Developer of the Original Code is
016: * Neteye GmbH.
017: * Portions created by the Initial Developer are Copyright (C) 2007
018: * the Initial Developer. All Rights Reserved.
019: *
020: * Contributor(s):
021: * Felix Gnass [fgnass at neteye dot de]
022: *
023: * ***** END LICENSE BLOCK ***** */
024: package org.riotfamily.common.web.collaboration;
025:
026: import java.util.HashMap;
027: import java.util.Iterator;
028: import java.util.Map;
029:
030: import javax.servlet.http.HttpServletRequest;
031:
032: import org.springframework.util.Assert;
033: import org.springframework.util.ObjectUtils;
034:
035: /**
036: * Class that facilitates collaboration between controllers.
037: * <p>
038: * The Spring DispatcherServlet performs an attribute clean-up after an include
039: * so included controllers can't expose request attributes to other controllers
040: * unless you turn this feature off, which usually isn't a good idea.
041: * <p>
042: * Another level of complexity comes into play when you want to cache the output
043: * of a controller that contributes information needed by other collaborators.
044: * If a cached version is served, the controller's handleRequest() method is not
045: * invoked (which after all is the purpose of a cache), therefore the controller
046: * has no chance to set any request attributes.
047: * <p>
048: * One solution would be to cache the dependent controller too. But in this case
049: * we would have to ensure that the cached version of the first controller is
050: * invalidated as soon as the dependent cache item is removed from the cache.
051: * As there is no easy way to achieve this, the SharedProperites class uses
052: * another approach:
053: * <p>
054: * The {@link SharedPropertiesInterceptor} exposes a HashMap as request
055: * attribute before the top-level handler is executed. A controller that wants
056: * to hand data on to a subsequent controller can use the
057: * {@link #setProperty(HttpServletRequest, String, String)} method to add
058: * entries to this map. Because the map is already present <em>before</em> the
059: * the include is performed, it won't be cleaned up by the DispatcherServlet.
060: * Another controller can now invoke
061: * {@link #getProperty(HttpServletRequest, String)} to retrieve the previously
062: * set values.
063: * <p>
064: * Currently shared properties are limited to Strings. The reason for this is
065: * that Cachius is aware of shared properties and caches them along with the
066: * actual output. It's basically a precaution to prevent users from caching
067: * their business objects. The fact that Cachius knows about shared properties
068: * allows us to serve cached content and still expose data to others.
069: * <p>
070: * A common use case for this is when you want to add information to the
071: * document title (or a meta-tag) that is provided by a controller which is
072: * deeply nested inside a template and would be processed too late. An elegant
073: * solution is to use the push-up feature of the TemplateController, which
074: * takes the nested controller out of the regular rendering flow and processes
075: * it first. This pushed-up controller can now expose shared properties which
076: * are then available everywhere else on the page, even if the pushed-up content
077: * comes from the cache.
078: * <p>
079: * Note: You can easily access shared properties in your FreeMarker views via
080: * the <code>common.getSharedProperty()</code> function and the
081: * <code>common.setSharedProperty()</code> macro.
082: *
083: * @author Felix Gnass [fgnass at neteye dot de]
084: * @since 6.5
085: */
086: public class SharedProperties {
087:
088: /** Name of the request attribute under which properties are exposed */
089: private static final String PROPERTIES_ATTRIBUTE = SharedProperties.class
090: .getName()
091: + ".properties";
092:
093: /**
094: * Checks whether a properties map is present in the request and creates
095: * a new one if no map is found.
096: * @see SharedPropertiesInterceptor
097: */
098: static void exposeTo(HttpServletRequest request) {
099: if (request.getAttribute(PROPERTIES_ATTRIBUTE) == null) {
100: request.setAttribute(PROPERTIES_ATTRIBUTE, new HashMap());
101: }
102: }
103:
104: /**
105: * Returns the properties map for the given request.
106: * @throws IllegalStateException if no properties map is found in the request
107: */
108: private static Map getProperties(HttpServletRequest request) {
109: Map properties = (Map) request
110: .getAttribute(PROPERTIES_ATTRIBUTE);
111: Assert
112: .state(
113: properties != null,
114: "No shared properties found in the "
115: + "request. Please register a SharedPropertiesInterceptor "
116: + "in order to use this feature.");
117:
118: return properties;
119: }
120:
121: /**
122: * Sets a shared property.
123: * @throws IllegalStateException if no properties map is found in the request
124: */
125: public static void setProperty(HttpServletRequest request,
126: String key, String value) {
127: getProperties(request).put(key, value);
128: }
129:
130: /**
131: * Sets multiple shared property at once. The given map must contain String
132: * keys and String (or null) values.
133: */
134: public static void setProperties(HttpServletRequest request,
135: Map properties) {
136: if (properties != null) {
137: Map map = (Map) request.getAttribute(PROPERTIES_ATTRIBUTE);
138: if (map != null) {
139: Iterator it = properties.entrySet().iterator();
140: while (it.hasNext()) {
141: Map.Entry entry = (Map.Entry) it.next();
142: Object key = entry.getKey();
143: Object value = entry.getValue();
144: Assert.isInstanceOf(String.class, key,
145: "Map must only contain String keys");
146:
147: Assert
148: .isTrue(value == null
149: || value instanceof String,
150: "Map must only contain String (or null) values.");
151:
152: map.put(key, value);
153: }
154: }
155: }
156: }
157:
158: /**
159: * Retrieves a shared property.
160: * @throws IllegalStateException if no properties map is found in the request
161: */
162: public static String getProperty(HttpServletRequest request,
163: String key) {
164: return (String) getProperties(request).get(key);
165: }
166:
167: /**
168: * Returns all shared properties currently set.
169: */
170: public static Map getSnapshot(HttpServletRequest request) {
171: Map properties = (Map) request
172: .getAttribute(PROPERTIES_ATTRIBUTE);
173: return properties != null ? new HashMap(properties) : null;
174: }
175:
176: /**
177: * Returns a map containing all properties that have been added or modified
178: * since the snapshot was made.
179: */
180: public static Map getDiff(HttpServletRequest request, Map snapshot) {
181: Map diff = getSnapshot(request);
182: if (diff == null || diff.isEmpty()) {
183: return null;
184: }
185: if (snapshot != null) {
186: Iterator it = diff.entrySet().iterator();
187: while (it.hasNext()) {
188: Map.Entry entry = (Map.Entry) it.next();
189: String key = (String) entry.getKey();
190: String value = (String) entry.getValue();
191: String oldValue = (String) snapshot.get(key);
192: if (ObjectUtils.nullSafeEquals(value, oldValue)) {
193: it.remove();
194: }
195: }
196: }
197: return diff;
198: }
199: }
|