001: /*
002: * $Id: XSLTCallingConvention.java,v 1.55 2007/09/18 08:45:06 agoubard Exp $
003: *
004: * Copyright 2003-2007 Orange Nederland Breedband B.V.
005: * See the COPYRIGHT file for redistribution and use restrictions.
006: */
007: package org.xins.server;
008:
009: import java.io.File;
010: import java.io.IOException;
011: import java.io.PrintWriter;
012: import java.io.StringReader;
013: import java.io.StringWriter;
014: import java.io.Writer;
015: import java.util.HashMap;
016: import java.util.Map;
017: import java.util.Properties;
018:
019: import javax.xml.transform.Result;
020: import javax.xml.transform.Source;
021: import javax.xml.transform.Templates;
022: import javax.xml.transform.Transformer;
023: import javax.xml.transform.TransformerFactory;
024: import javax.xml.transform.stream.StreamResult;
025: import javax.xml.transform.stream.StreamSource;
026:
027: import javax.servlet.http.HttpServletRequest;
028: import javax.servlet.http.HttpServletResponse;
029:
030: import org.xins.common.Utils;
031: import org.xins.common.collections.InvalidPropertyValueException;
032: import org.xins.common.collections.MissingRequiredPropertyException;
033: import org.xins.common.collections.PropertyReader;
034: import org.xins.common.manageable.InitializationException;
035: import org.xins.common.text.TextUtils;
036: import org.xins.logdoc.ExceptionUtils;
037:
038: /**
039: * XSLT calling convention.
040: * The XSLT calling convention input is the same as for the standard calling
041: * convention. The XSLT calling convention output is the result of the XML
042: * normally returned by the standard calling convention and the specified
043: * XSLT.
044: * The Mime type of the return data can be specified in the XSLT using the
045: * media-type or method attribute of the XSL output element.
046: * More information about the XSLT calling convention can be found in the
047: * <a href="http://www.xins.org/docs/index.html">user guide</a>.
048: *
049: * @version $Revision: 1.55 $ $Date: 2007/09/18 08:45:06 $
050: * @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
051: * @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
052: */
053: public class XSLTCallingConvention extends StandardCallingConvention {
054:
055: /**
056: * The name of the runtime property that defines if the templates should be
057: * cached. Should be either <code>"true"</code> or <code>"false"</code>.
058: * By default the cache is enabled.
059: */
060: protected static final String TEMPLATES_CACHE_PROPERTY = "templates.cache";
061:
062: /**
063: * The name of the input parameter that specifies the location of the XSLT
064: * template to use.
065: */
066: protected static final String TEMPLATE_PARAMETER = "_template";
067:
068: /**
069: * The name of the input parameter used to clear the template cache.
070: */
071: protected static final String CLEAR_TEMPLATE_CACHE_PARAMETER = "_cleartemplatecache";
072:
073: /**
074: * The XSLT transformer. Never <code>null</code>.
075: */
076: private final TransformerFactory _factory;
077:
078: /**
079: * Flag that indicates whether the templates should be cached. This field
080: * is set during initialization.
081: */
082: private boolean _cacheTemplates;
083:
084: /**
085: * The prefix to use for the _template parameter.
086: * This field is set during initialization.
087: * If the value is <code>null</code> the _template parameter is not allowed.
088: */
089: private String _templatesPrefix;
090:
091: /**
092: * Location of the XSLT templates. This field is initially
093: * <code>null</code> and set during initialization.
094: */
095: private String _location;
096:
097: /**
098: * Cache for the XSLT templates. Never <code>null</code>.
099: */
100: private Map _templateCache;
101:
102: /**
103: * Constructs a new <code>XSLTCallingConvention</code> object.
104: */
105: public XSLTCallingConvention() {
106:
107: // Create the transformer factory
108: _factory = TransformerFactory.newInstance();
109:
110: // Initialize the template cache
111: _templateCache = new HashMap(89);
112: }
113:
114: protected void initImpl(PropertyReader runtimeProperties)
115: throws MissingRequiredPropertyException,
116: InvalidPropertyValueException, InitializationException {
117:
118: // Determine if the template cache should be enabled
119: String cacheEnabled = runtimeProperties
120: .get(TEMPLATES_CACHE_PROPERTY);
121: initCacheEnabled(cacheEnabled);
122:
123: // Get the base directory of the style sheets.
124: _location = getXSLTLocation(runtimeProperties, "source");
125:
126: // Determine whether template location can be passed as parameter
127: _templatesPrefix = getXSLTLocation(runtimeProperties,
128: "parameter.prefix");
129: }
130:
131: /**
132: * Determines if the template cache should be enabled. If no value is
133: * passed, then by default the cache is enabled. An invalid value, however,
134: * will trigger an {@link InvalidPropertyValueException}.
135: *
136: * @param cacheEnabled
137: * the value of the runtime property that specifies whether the cache
138: * should be enabled, can be <code>null</code>.
139: *
140: * @throws InvalidPropertyValueException
141: * if the value is incorrect.
142: */
143: private void initCacheEnabled(String cacheEnabled)
144: throws InvalidPropertyValueException {
145:
146: // By default, the template cache is enabled
147: if (TextUtils.isEmpty(cacheEnabled)) {
148: _cacheTemplates = true;
149:
150: // Trim before comparing with 'true' and 'false'
151: } else {
152: cacheEnabled = cacheEnabled.trim();
153: if ("true".equals(cacheEnabled)) {
154: _cacheTemplates = true;
155: } else if ("false".equals(cacheEnabled)) {
156: _cacheTemplates = false;
157: } else {
158: throw new InvalidPropertyValueException(
159: TEMPLATES_CACHE_PROPERTY, cacheEnabled,
160: "Expected either \"true\" or \"false\".");
161: }
162: }
163:
164: // Log whether the cache is enabled or not
165: if (_cacheTemplates) {
166: Log.log_3440();
167: } else {
168: Log.log_3441();
169: }
170: }
171:
172: /**
173: * Initializes the location for the XSLT templates.
174: *
175: * The name of the runtime property that defines the location of the XSLT
176: * templates should indicate a directory, either locally or remotely.
177: * Local locations will be interpreted as relative to the user home
178: * directory. The value should be a URL or a relative directory.
179: *
180: * <p>Examples of valid locations include:
181: *
182: * <ul>
183: * <li><code>projects/dubey/xslt/</code></li>
184: * <li><code>file:///home/john.doe/projects/dubey/xslt/</code></li>
185: * <li><code>http://johndoe.com/projects/dubey/xslt/</code></li>
186: * <li><code>https://xslt.johndoe.com/</code></li>
187: * <li><code>http://xslt.mycompany.com/myapi/</code></li>
188: * <li><code>file:///c:/home/</code></li>
189: * </ul>
190: *
191: * <p>XSLT template files must match the names of the corresponding
192: * functions.
193: *
194: * @param runtimeProperties
195: * the runtime properties, cannot be <code>null</code>.
196: *
197: * @param propertySuffix
198: * the suffix of the runtime property we're looking for, cannot be <code>null</code>.
199: *
200: * @return
201: * the path location where to find the XSLT style sheet files or <code>null</code>
202: * if no location is specified.
203: */
204: private String getXSLTLocation(PropertyReader runtimeProperties,
205: String propertySuffix) {
206:
207: // Get the value of the property
208: String templatesProperty = "templates." + getAPI().getName()
209: + ".xins-xslt." + propertySuffix;
210: String location = runtimeProperties.get(templatesProperty);
211:
212: if (TextUtils.isEmpty(location)) {
213: return null;
214: }
215:
216: // If the value is not a URL, it's considered as a relative path.
217: // Relative URLs use the user directory as base dir.
218: if (location.indexOf("://") == -1) {
219:
220: // Attempt to convert the home directory to a URL
221: String home = System.getProperty("user.dir");
222: String homeURL = "";
223: try {
224: homeURL = new File(home).toURL().toString();
225:
226: // If the conversion to a URL failed, then just use the original
227: } catch (IOException exception) {
228: Utils.logIgnoredException(exception);
229: }
230:
231: // Prepend the home directory URL
232: location = homeURL + location;
233: }
234:
235: // Log the base directory for XSLT templates
236: Log.log_3442(getAPI().getName(), propertySuffix, location);
237: return location;
238: }
239:
240: protected void convertResultImpl(FunctionResult xinsResult,
241: HttpServletResponse httpResponse,
242: HttpServletRequest httpRequest) throws IOException {
243:
244: // If the request is to clear the cache, just clear the cache.
245: if ("true".equals(httpRequest
246: .getParameter(CLEAR_TEMPLATE_CACHE_PARAMETER))) {
247: _templateCache.clear();
248: PrintWriter out = httpResponse.getWriter();
249: out.write("Done.");
250: out.close();
251: return;
252: }
253:
254: // Get the XML output similar to the standard calling convention.
255: StringWriter xmlOutput = new StringWriter(1024);
256: CallResultOutputter.output(xmlOutput, xinsResult);
257: xmlOutput.close();
258:
259: // Get the location of the XSLT file.
260: String xsltLocation = null;
261: String templatesSuffix = httpRequest
262: .getParameter(TEMPLATE_PARAMETER);
263: if (_templatesPrefix != null && templatesSuffix != null) {
264: if (templatesSuffix.indexOf("..") != -1) {
265: throw new IOException("Incorrect " + TEMPLATE_PARAMETER
266: + " parameter: " + templatesSuffix);
267: }
268: xsltLocation = _templatesPrefix + templatesSuffix;
269: }
270: if (_templatesPrefix == null && templatesSuffix != null) {
271: throw new IOException(TEMPLATE_PARAMETER
272: + " parameter not allowed.");
273: }
274: if (xsltLocation == null) {
275: if (_location == null) {
276: throw new IOException(
277: "No location specified for the XSLT stylesheets.");
278: }
279: xsltLocation = _location
280: + httpRequest.getParameter("_function") + ".xslt";
281: }
282:
283: try {
284:
285: // Load the template or get it from the cache.
286: Templates templates;
287: if (_cacheTemplates
288: && _templateCache.containsKey(xsltLocation)) {
289: templates = (Templates) _templateCache
290: .get(xsltLocation);
291: } else {
292: Log.log_3443(xsltLocation);
293: templates = _factory.newTemplates(new StreamSource(
294: xsltLocation));
295: if (_cacheTemplates) {
296: _templateCache.put(xsltLocation, templates);
297: }
298: }
299:
300: // Proceed to the transformation.
301: Transformer xformer = templates.newTransformer();
302: Source source = new StreamSource(new StringReader(xmlOutput
303: .toString()));
304: Writer buffer = new StringWriter(4096);
305: Result result = new StreamResult(buffer);
306: xformer.transform(source, result);
307:
308: // Determine the MIME type for the output.
309: String mimeType = getContentType(templates
310: .getOutputProperties());
311: if (mimeType != null) {
312: httpResponse.setContentType(mimeType);
313: }
314:
315: httpResponse.setStatus(HttpServletResponse.SC_OK);
316: PrintWriter out = httpResponse.getWriter();
317: out.print(buffer.toString());
318: out.close();
319: } catch (Exception exception) {
320: if (exception instanceof IOException) {
321: throw (IOException) exception;
322: } else {
323: String message = "Cannot transform the result with the XSLT "
324: + "located at \"" + xsltLocation + "\".";
325: IOException ioe = new IOException(message);
326: ExceptionUtils.setCause(ioe, exception);
327: throw ioe;
328: }
329: }
330: }
331:
332: /**
333: * Gets the MIME type and the character encoding to return for the HTTP response.
334: *
335: * @param outputProperties
336: * the output properties defined in the XSLT, never <code>null</code>.
337: *
338: * @return
339: * the content type, never <code>null</code>.
340: */
341: private String getContentType(Properties outputProperties) {
342: String mimeType = outputProperties.getProperty("media-type");
343: if (mimeType == null) {
344: String method = outputProperties.getProperty("method");
345: if ("xml".equals(method)) {
346: mimeType = "text/xml";
347: } else if ("html".equals(method)) {
348: mimeType = "text/html";
349: } else if ("text".equals(method)) {
350: mimeType = "text/plain";
351: }
352: }
353: String encoding = outputProperties.getProperty("encoding");
354: if (mimeType != null && encoding != null) {
355: mimeType += "; charset=" + encoding;
356: }
357: return mimeType;
358: }
359: }
|