001: package org.apache.velocity.tools.view.servlet;
002:
003: /*
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021:
022: import java.io.InputStream;
023: import java.io.PrintWriter;
024: import java.io.StringWriter;
025: import java.util.ArrayList;
026: import java.util.HashMap;
027: import java.util.Iterator;
028: import java.util.Map;
029:
030: import javax.servlet.ServletContext;
031: import javax.servlet.http.HttpSession;
032:
033: import org.apache.commons.digester.RuleSet;
034: import org.apache.commons.logging.Log;
035: import org.apache.commons.logging.LogFactory;
036: import org.apache.velocity.tools.view.ToolInfo;
037: import org.apache.velocity.tools.view.XMLToolboxManager;
038: import org.apache.velocity.tools.view.context.ViewContext;
039:
040: /**
041: * <p>A toolbox manager for the servlet environment.</p>
042: *
043: * <p>A toolbox manager is responsible for automatically filling the Velocity
044: * context with a set of view tools. This class provides the following
045: * features:</p>
046: * <ul>
047: * <li>configurable through an XML-based configuration file</li>
048: * <li>assembles a set of view tools (the toolbox) on request</li>
049: * <li>handles different tool scopes (request, session, application)</li>
050: * <li>supports any class with a public constructor without parameters
051: * to be used as a view tool</li>
052: * <li>supports adding primitive data values to the context(String,Number,Boolean)</li>
053: * </ul>
054: *
055: *
056: * <p><strong>Configuration</strong></p>
057: * <p>The toolbox manager is configured through an XML-based configuration
058: * file. The configuration file is passed to the {@link #load(java.io.InputStream input)}
059: * method. The format is shown in the following example:</p>
060: * <pre>
061: * <?xml version="1.0"?>
062: *
063: * <toolbox>
064: * <tool>
065: * <key>link</key>
066: * <scope>request</scope>
067: * <class>org.apache.velocity.tools.view.tools.LinkTool</class>
068: * </tool>
069: * <tool>
070: * <key>date</key>
071: * <scope>application</scope>
072: * <class>org.apache.velocity.tools.generic.DateTool</class>
073: * </tool>
074: * <data type="number">
075: * <key>luckynumber</key>
076: * <value>1.37</value>
077: * </data>
078: * <data type="string">
079: * <key>greeting</key>
080: * <value>Hello World!</value>
081: * </data>
082: * <xhtml>true</xhtml>
083: * </toolbox>
084: * </pre>
085: * <p>The recommended location for the configuration file is the WEB-INF directory of the
086: * web application.</p>
087: *
088: * @author <a href="mailto:sidler@teamup.com">Gabriel Sidler</a>
089: * @author Nathan Bubna
090: * @author <a href="mailto:geirm@apache.org">Geir Magnusson Jr.</a>
091: * @author <a href="mailto:henning@schmiedehausen.org">Henning P. Schmiedehausen</a>
092: * @version $Id: ServletToolboxManager.java 488460 2006-12-19 00:00:35Z nbubna $
093: */
094: public class ServletToolboxManager extends XMLToolboxManager {
095:
096: // --------------------------------------------------- Properties ---------
097:
098: public static final String SESSION_TOOLS_KEY = ServletToolboxManager.class
099: .getName()
100: + ":session-tools";
101:
102: protected static final Log LOG = LogFactory
103: .getLog(ServletToolboxManager.class);
104:
105: private ServletContext servletContext;
106: private Map appTools;
107: private ArrayList sessionToolInfo;
108: private ArrayList requestToolInfo;
109: private boolean createSession;
110:
111: private static HashMap managersMap = new HashMap();
112: private static RuleSet servletRuleSet = new ServletToolboxRuleSet();
113:
114: // --------------------------------------------------- Constructor --------
115:
116: /**
117: * Use getInstance(ServletContext,String) instead
118: * to ensure there is exactly one ServletToolboxManager
119: * per xml toolbox configuration file.
120: */
121: private ServletToolboxManager(ServletContext servletContext) {
122: this .servletContext = servletContext;
123: appTools = new HashMap();
124: sessionToolInfo = new ArrayList();
125: requestToolInfo = new ArrayList();
126: createSession = true;
127: }
128:
129: // -------------------------------------------- Public Methods ------------
130:
131: /**
132: * ServletToolboxManager factory method.
133: * This method will ensure there is exactly one ServletToolboxManager
134: * per xml toolbox configuration file.
135: */
136: public static synchronized ServletToolboxManager getInstance(
137: ServletContext servletContext, String toolboxFile) {
138: // little fix up
139: if (!toolboxFile.startsWith("/")) {
140: toolboxFile = "/" + toolboxFile;
141: }
142:
143: // get the unique key for this toolbox file in this servlet context
144: String uniqueKey = servletContext.hashCode() + ':'
145: + toolboxFile;
146:
147: // check if an instance already exists
148: ServletToolboxManager toolboxManager = (ServletToolboxManager) managersMap
149: .get(uniqueKey);
150:
151: if (toolboxManager == null) {
152: // if not, build one
153: InputStream is = null;
154: try {
155: // get the bits
156: is = servletContext.getResourceAsStream(toolboxFile);
157:
158: if (is != null) {
159: LOG.info("Using config file '" + toolboxFile + "'");
160:
161: toolboxManager = new ServletToolboxManager(
162: servletContext);
163: toolboxManager.load(is);
164:
165: // remember it
166: managersMap.put(uniqueKey, toolboxManager);
167:
168: LOG.debug("Toolbox setup complete.");
169: } else {
170: LOG.debug("No toolbox was found at '" + toolboxFile
171: + "'");
172: }
173: } catch (Exception e) {
174: LOG.error("Problem loading toolbox '" + toolboxFile
175: + "'", e);
176: } finally {
177: try {
178: if (is != null) {
179: is.close();
180: }
181: } catch (Exception ee) {
182: }
183: }
184: }
185: return toolboxManager;
186: }
187:
188: /**
189: * <p>Sets whether or not to create a new session when none exists for the
190: * current request and session-scoped tools have been defined for this
191: * toolbox.</p>
192: *
193: * <p>If true, then a call to {@link #getToolbox(Object)} will
194: * create a new session if none currently exists for this request and
195: * the toolbox has one or more session-scoped tools designed.</p>
196: *
197: * <p>If false, then a call to getToolbox(Object) will never
198: * create a new session for the current request.
199: * This effectively means that no session-scoped tools will be added to
200: * the ToolboxContext for a request that does not have a session object.
201: * </p>
202: *
203: * The default value is true.
204: */
205: public void setCreateSession(boolean b) {
206: createSession = b;
207: LOG.debug("create-session is set to " + b);
208: }
209:
210: /**
211: * <p>Sets an application attribute to tell velocimacros and tools
212: * (especially the LinkTool) whether they should output XHTML or HTML.</p>
213: *
214: * @see ViewContext#XHTML
215: * @since VelocityTools 1.1
216: */
217: public void setXhtml(Boolean value) {
218: servletContext.setAttribute(ViewContext.XHTML, value);
219: LOG.info(ViewContext.XHTML + " is set to " + value);
220: }
221:
222: // ------------------------------ XMLToolboxManager Overrides -------------
223:
224: /**
225: * <p>Retrieves the rule set Digester should use to parse and load
226: * the toolbox for this manager.</p>
227: *
228: * <p>The DTD corresponding to the ServletToolboxRuleSet is:
229: * <pre>
230: * <?xml version="1.0"?>
231: * <!ELEMENT toolbox (create-session?,xhtml?,tool*,data*,#PCDATA)>
232: * <!ELEMENT create-session (#CDATA)>
233: * <!ELEMENT xhtml (#CDATA)>
234: * <!ELEMENT tool (key,scope?,class,parameter*,#PCDATA)>
235: * <!ELEMENT data (key,value)>
236: * <!ATTLIST data type (string|number|boolean) "string">
237: * <!ELEMENT key (#CDATA)>
238: * <!ELEMENT scope (#CDATA)>
239: * <!ELEMENT class (#CDATA)>
240: * <!ELEMENT parameter (EMPTY)>
241: * <!ATTLIST parameter name CDATA #REQUIRED>
242: * <!ATTLIST parameter value CDATA #REQUIRED>
243: * <!ELEMENT value (#CDATA)>
244: * </pre></p>
245: *
246: * @since VelocityTools 1.1
247: */
248: protected RuleSet getRuleSet() {
249: return servletRuleSet;
250: }
251:
252: /**
253: * Ensures that application-scoped tools do not have request path
254: * restrictions set for them, as those will not be enforced.
255: *
256: * @param info a ToolInfo object
257: * @return true if the ToolInfo is valid
258: * @since VelocityTools 1.3
259: */
260: protected boolean validateToolInfo(ToolInfo info) {
261: if (!super .validateToolInfo(info)) {
262: return false;
263: }
264: if (info instanceof ServletToolInfo) {
265: ServletToolInfo sti = (ServletToolInfo) info;
266: if (sti.getRequestPath() != null
267: && !ViewContext.REQUEST.equalsIgnoreCase(sti
268: .getScope())) {
269: LOG
270: .error(sti.getKey()
271: + " must be a request-scoped tool to have a request path restriction!");
272: return false;
273: }
274: }
275: return true;
276: }
277:
278: /**
279: * Overrides XMLToolboxManager to separate tools by scope.
280: * For this to work, we obviously override getToolbox(Object) as well.
281: */
282: public void addTool(ToolInfo info) {
283: if (validateToolInfo(info)) {
284: if (info instanceof ServletToolInfo) {
285: ServletToolInfo sti = (ServletToolInfo) info;
286:
287: if (ViewContext.REQUEST
288: .equalsIgnoreCase(sti.getScope())) {
289: requestToolInfo.add(sti);
290: return;
291: } else if (ViewContext.SESSION.equalsIgnoreCase(sti
292: .getScope())) {
293: sessionToolInfo.add(sti);
294: return;
295: } else if (ViewContext.APPLICATION.equalsIgnoreCase(sti
296: .getScope())) {
297: /* add application scoped tools to appTools and
298: * initialize them with the ServletContext */
299: appTools.put(sti.getKey(), sti
300: .getInstance(servletContext));
301: return;
302: } else {
303: LOG.warn("Unknown scope '" + sti.getScope()
304: + "' - " + sti.getKey()
305: + " will be request scoped.");
306:
307: //default is request scope
308: requestToolInfo.add(info);
309: }
310: } else {
311: //default is request scope
312: requestToolInfo.add(info);
313: }
314: }
315: }
316:
317: /**
318: * Overrides XMLToolboxManager to put data into appTools map
319: */
320: public void addData(ToolInfo info) {
321: if (validateToolInfo(info)) {
322: appTools.put(info.getKey(), info.getInstance(null));
323: }
324: }
325:
326: /**
327: * Overrides XMLToolboxManager to handle the separate
328: * scopes.
329: *
330: * Application scope tools were initialized when the toolbox was loaded.
331: * Session scope tools are initialized once per session and stored in a
332: * map in the session attributes.
333: * Request scope tools are initialized on every request.
334: *
335: * @param initData the {@link ViewContext} for the current servlet request
336: */
337: public Map getToolbox(Object initData) {
338: //we know the initData is a ViewContext
339: ViewContext ctx = (ViewContext) initData;
340: String requestPath = ServletUtils.getPath(ctx.getRequest());
341:
342: //create the toolbox map with the application tools in it
343: Map toolbox = new HashMap(appTools);
344:
345: if (!sessionToolInfo.isEmpty()) {
346: HttpSession session = ctx.getRequest().getSession(
347: createSession);
348: if (session != null) {
349: // allow only one thread per session at a time
350: synchronized (getMutex(session)) {
351: // get the session tools
352: Map stmap = (Map) session
353: .getAttribute(SESSION_TOOLS_KEY);
354: if (stmap == null) {
355: // init and store session tools map
356: stmap = new HashMap(sessionToolInfo.size());
357: Iterator i = sessionToolInfo.iterator();
358: while (i.hasNext()) {
359: ServletToolInfo sti = (ServletToolInfo) i
360: .next();
361: stmap.put(sti.getKey(), sti
362: .getInstance(ctx));
363: }
364: session.setAttribute(SESSION_TOOLS_KEY, stmap);
365: }
366: // add them to the toolbox
367: toolbox.putAll(stmap);
368: }
369: }
370: }
371:
372: //add and initialize request tools
373: Iterator i = requestToolInfo.iterator();
374: while (i.hasNext()) {
375: ToolInfo info = (ToolInfo) i.next();
376: if (info instanceof ServletToolInfo) {
377: ServletToolInfo sti = (ServletToolInfo) info;
378: if (!sti.allowsRequestPath(requestPath)) {
379: continue;
380: }
381: }
382: toolbox.put(info.getKey(), info.getInstance(ctx));
383: }
384:
385: return toolbox;
386: }
387:
388: /**
389: * Returns a mutex (lock object) unique to the specified session
390: * to allow for reliable synchronization on the session.
391: */
392: protected Object getMutex(HttpSession session) {
393: // yes, this uses double-checked locking, but it is safe here
394: // since partial initialization of the lock is not an issue
395: Object lock = session.getAttribute("session.mutex");
396: if (lock == null) {
397: // one thread per toolbox manager at a time
398: synchronized (this ) {
399: // in case another thread already came thru
400: lock = session.getAttribute("session.mutex");
401: if (lock == null) {
402: // use a Boolean because it is serializable and small
403: lock = new Boolean(true);
404: session.setAttribute("session.mutex", lock);
405: }
406: }
407: }
408: return lock;
409: }
410:
411: }
|