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) 2006
018: * the Initial Developer. All Rights Reserved.
019: *
020: * Contributor(s):
021: * Felix Gnass [fgnass at neteye dot de]
022: * Alf Werder <alf.werder@glonz.com>
023: *
024: * ***** END LICENSE BLOCK ***** */
025: package org.riotfamily.website.template;
026:
027: import java.util.ArrayList;
028: import java.util.Collection;
029: import java.util.HashMap;
030: import java.util.Iterator;
031: import java.util.List;
032: import java.util.Map;
033:
034: import javax.servlet.http.HttpServletRequest;
035: import javax.servlet.http.HttpServletResponse;
036:
037: import org.riotfamily.common.web.util.ServletUtils;
038: import org.springframework.beans.factory.InitializingBean;
039: import org.springframework.web.servlet.DispatcherServlet;
040: import org.springframework.web.servlet.ModelAndView;
041: import org.springframework.web.servlet.mvc.AbstractController;
042:
043: /**
044: * Controller that passes a map of URLs to it's view (the template). The view
045: * is responsible for including the URLs (using a RequestDispatcher) at the
046: * right place.
047: * <p>
048: * The most simple way to achieve this is to use a JSTL view that contains
049: * a <code><c:import value="${<i>slotname</i>}" /></code> tag for each
050: * slot, where <code><i>slotname</i></code> has to be one of the keys present
051: * in the controllers configuration map.
052: * </p>
053: * <p>
054: * You may extend existing template configurations by setting the
055: * {@link #setParent(TemplateController) parent} property to another
056: * TemplateController. The local configuration will then be merged with the
057: * one of the parent, overriding previously defined URLs.
058: * </p>
059: * <p>
060: * Additionally the controller supports nested templates, i.e. the URL of a
061: * slot may in turn map to another TemplateController. These nested structures
062: * are taken into account when configurations are merged. When extending a
063: * parent you may also override URLs defined in nested templates.
064: * </p>
065: * <p>
066: * Let's say Template A has two slots, <i>left</i> and <i>right</i>. The right
067: * slot includes another Template B which also has two slots <i>top</i> and
068: * <i>bottom</i>, where <i>top</i> contains the URL <code>/foo.html</code>.
069: * </p>
070: * We can now define a third TemplateController A2 which extends A:
071: * <pre>
072: * <template:definition name="A2" parent="A">
073: * <template:insert slot="right.top" url="/bar.html" />
074: * </template:definition>
075: * </pre>
076: * <p>
077: * The syntax above makes use of the Spring 2.0 namespace support. See
078: * {@link org.riotfamily.website.template.config.TemplateNamespaceHandler}
079: * for more information.
080: * </p>
081: *
082: * @author Alf Werder
083: * @author Felix Gnass
084: */
085: public class TemplateController extends AbstractController implements
086: InitializingBean {
087:
088: /** NOTE: The DispatcherServlet class name prefix forces an attribute
089: * cleanup to be performed after an include, regardless of the servlet's
090: * cleanupAfterIncludes setting.
091: */
092: protected static final String SLOTS_CONFIGURATION_ATTRIBUTE = DispatcherServlet.class
093: .getName()
094: + "#" + TemplateController.class.getName() + ".slots";
095:
096: protected static final String SLOT_PATH_ATTRIBUTE = DispatcherServlet.class
097: .getName()
098: + "#" + TemplateController.class.getName() + ".slotPath";
099:
100: protected static final String SLOT_PARAMETER = TemplateController.class
101: .getName()
102: + ".SLOT";
103:
104: private TemplateController parent;
105:
106: private String viewName;
107:
108: private Map configuration;
109:
110: private Map mergedConfiguration;
111:
112: private boolean session;
113:
114: public TemplateController getParent() {
115: return parent;
116: }
117:
118: public void setParent(TemplateController parent) {
119: this .parent = parent;
120: }
121:
122: public String getViewName() {
123: return viewName;
124: }
125:
126: public void setViewName(String view) {
127: this .viewName = view;
128: }
129:
130: public Map getConfiguration() {
131: return configuration;
132: }
133:
134: public void setConfiguration(Map configuration) {
135: this .configuration = configuration;
136: }
137:
138: public void setSession(boolean session) {
139: this .session = session;
140: }
141:
142: /**
143: * Initializes the controller after all properties have been set. If a
144: * parent controller is set the view will be inherited (if not set locally).
145: */
146: public final void afterPropertiesSet() throws Exception {
147: inheritView();
148: inheritConfiguration();
149: initController();
150: }
151:
152: /**
153: * Subclasses may overwrite this method to perform initialization tasks
154: * after all properties have been set. The default implementation
155: * does nothing.
156: */
157: protected void initController() {
158: }
159:
160: /**
161: * Sets the view to the parent view if it has not been set locally.
162: */
163: private void inheritView() {
164: if (viewName == null && getParent() != null) {
165: viewName = getParent().getViewName();
166: }
167: }
168:
169: /**
170: * Merges the configuration map with the ones defined by ancestors.
171: */
172: protected void inheritConfiguration() {
173: mergedConfiguration = new HashMap();
174: if (parent != null) {
175: mergedConfiguration.putAll(parent.getMergedConfiguration());
176: }
177: if (configuration != null) {
178: mergedConfiguration.putAll(configuration);
179: }
180: }
181:
182: protected Map getMergedConfiguration() {
183: return this .mergedConfiguration;
184: }
185:
186: /**
187: * Gets the effective configuration in case the template is nested within
188: * another template. The surrounding template(s) may override the local
189: * slot configuration.
190: */
191: private Map getEffectiveConfiguration(HttpServletRequest request) {
192: Map effectiveConfiguration = new HashMap(
193: getMergedConfiguration());
194: applyOverrides(effectiveConfiguration, request);
195: return effectiveConfiguration;
196: }
197:
198: /**
199: * Applies the overrides defined by surrounding templates.
200: */
201: protected void applyOverrides(Map config, HttpServletRequest request) {
202: Map slotsConfiguration = (Map) request
203: .getAttribute(SLOTS_CONFIGURATION_ATTRIBUTE);
204:
205: if (slotsConfiguration != null) {
206: String slot = request.getParameter(SLOT_PARAMETER);
207: if (slot != null) {
208: String prefix = slot + '.';
209: config
210: .putAll(selectEntries(slotsConfiguration,
211: prefix));
212: } else {
213: config.putAll(slotsConfiguration);
214: }
215: }
216: }
217:
218: /**
219: * Creates a new map containing all entries starting with the given prefix.
220: * The prefix is stripped from the keys of the new map.
221: */
222: private static Map selectEntries(Map map, String prefix) {
223: Map result = new HashMap();
224: int prefixLength = prefix.length();
225: Iterator i = map.entrySet().iterator();
226: while (i.hasNext()) {
227: Map.Entry entry = (Map.Entry) i.next();
228: String key = (String) entry.getKey();
229: if (key.startsWith(prefix)) {
230: result.put(key.substring(prefixLength), entry
231: .getValue());
232: }
233: }
234: return result;
235: }
236:
237: /**
238: * Builds a map of URLs that is used as model for the template view.
239: */
240: protected Map buildUrlMap(Map config) {
241: Map model = new HashMap();
242: Iterator i = config.entrySet().iterator();
243: while (i.hasNext()) {
244: Map.Entry entry = (Map.Entry) i.next();
245: String slot = (String) entry.getKey();
246: if (slot.indexOf('.') == -1) {
247: if (entry.getValue() != null) {
248: model.put(slot, getUrlMapValue(entry.getValue(),
249: slot));
250: } else {
251: model.remove(slot);
252: }
253: }
254: }
255: return model;
256: }
257:
258: /**
259: * Returns either a single String or a list of URLs.
260: */
261: protected Object getUrlMapValue(Object value, String slot) {
262: if (value instanceof Collection) {
263: ArrayList urls = new ArrayList();
264: Iterator it = ((Collection) value).iterator();
265: while (it.hasNext()) {
266: String location = (String) it.next();
267: urls.add(getSlotUrl(location, slot));
268: }
269: return urls;
270: } else {
271: return getSlotUrl((String) value, slot);
272: }
273: }
274:
275: /**
276: * Returns the include URL for the given location and slot. By default
277: * <code>SLOT_REQUEST_PARAMETER_NAME</code> is appended, containing the
278: * given slot name.
279: */
280: protected String getSlotUrl(String location, String slot) {
281: if (location.startsWith("data://")) {
282: return location.substring(7);
283: } else {
284: StringBuffer url = new StringBuffer();
285: url.append(location);
286: url.append((url.indexOf("?") != -1) ? '&' : '?');
287: url.append(SLOT_PARAMETER);
288: url.append('=');
289: url.append(slot);
290: return url.toString();
291: }
292: }
293:
294: protected ModelAndView handleRequestInternal(
295: HttpServletRequest request, HttpServletResponse response)
296: throws Exception {
297:
298: if (session) {
299: request.getSession();
300: }
301: Map config = getEffectiveConfiguration(request);
302: request.setAttribute(SLOTS_CONFIGURATION_ATTRIBUTE, config);
303: request.setAttribute(SLOT_PATH_ATTRIBUTE, getSlotPath(request));
304: return new ModelAndView(getViewName(), buildUrlMap(config));
305: }
306:
307: /**
308: * Returns the fully qualified slot-path for the given request.
309: */
310: private static String getSlotPath(HttpServletRequest request) {
311: String slotPath = (String) request
312: .getAttribute(SLOT_PATH_ATTRIBUTE);
313: String slot = request.getParameter(SLOT_PARAMETER);
314: if (slot != null) {
315: if (slotPath != null) {
316: slotPath = slotPath + '.' + slot;
317: } else {
318: slotPath = slot;
319: }
320: }
321: return slotPath;
322: }
323:
324: /**
325: * Returns the fully qualified slot-name for the given request
326: * or <code>null</code> if the current request was not included via a
327: * TemplateController. If the slot contains multiple URLs, the index (+1)
328: * of the current URL is appended to the returned name.
329: */
330: public static String getFullSlotName(HttpServletRequest request) {
331: Map slots = (Map) request
332: .getAttribute(SLOTS_CONFIGURATION_ATTRIBUTE);
333: String slotName = request.getParameter(SLOT_PARAMETER);
334: if (slots == null || slotName == null) {
335: return null;
336: }
337: Object content = slots.get(slotName);
338: String uri = ServletUtils.getPathWithinApplication(request);
339: if (content instanceof List) {
340: List list = (List) content;
341: int i = list.indexOf(uri);
342: if (i == -1) {
343: return null;
344: }
345: return getSlotPath(request) + "#" + (i + 1);
346:
347: } else if (uri.equals(content)) {
348: return getSlotPath(request);
349: }
350: return null;
351: }
352: }
|