001/*
002 * Copyright 2002-2017 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.servlet.view.xslt;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.Reader;
022import java.util.Enumeration;
023import java.util.Map;
024import java.util.Properties;
025
026import javax.servlet.http.HttpServletRequest;
027import javax.servlet.http.HttpServletResponse;
028import javax.xml.transform.ErrorListener;
029import javax.xml.transform.OutputKeys;
030import javax.xml.transform.Result;
031import javax.xml.transform.Source;
032import javax.xml.transform.Templates;
033import javax.xml.transform.Transformer;
034import javax.xml.transform.TransformerConfigurationException;
035import javax.xml.transform.TransformerFactory;
036import javax.xml.transform.TransformerFactoryConfigurationError;
037import javax.xml.transform.URIResolver;
038import javax.xml.transform.dom.DOMSource;
039import javax.xml.transform.stream.StreamResult;
040import javax.xml.transform.stream.StreamSource;
041
042import org.w3c.dom.Document;
043import org.w3c.dom.Node;
044
045import org.springframework.beans.BeansException;
046import org.springframework.context.ApplicationContextException;
047import org.springframework.core.io.Resource;
048import org.springframework.lang.Nullable;
049import org.springframework.util.Assert;
050import org.springframework.util.CollectionUtils;
051import org.springframework.util.ReflectionUtils;
052import org.springframework.util.StringUtils;
053import org.springframework.util.xml.SimpleTransformErrorListener;
054import org.springframework.util.xml.TransformerUtils;
055import org.springframework.web.servlet.view.AbstractUrlBasedView;
056import org.springframework.web.util.WebUtils;
057
058/**
059 * XSLT-driven View that allows for response context to be rendered as the
060 * result of an XSLT transformation.
061 *
062 * <p>The XSLT Source object is supplied as a parameter in the model and then
063 * {@link #locateSource detected} during response rendering. Users can either specify
064 * a specific entry in the model via the {@link #setSourceKey sourceKey} property or
065 * have Spring locate the Source object. This class also provides basic conversion
066 * of objects into Source implementations. See {@link #getSourceTypes() here}
067 * for more details.
068 *
069 * <p>All model parameters are passed to the XSLT Transformer as parameters.
070 * In addition the user can configure {@link #setOutputProperties output properties}
071 * to be passed to the Transformer.
072 *
073 * @author Rob Harrop
074 * @author Juergen Hoeller
075 * @since 2.0
076 */
077public class XsltView extends AbstractUrlBasedView {
078
079        @Nullable
080        private Class<? extends TransformerFactory> transformerFactoryClass;
081
082        @Nullable
083        private String sourceKey;
084
085        @Nullable
086        private URIResolver uriResolver;
087
088        private ErrorListener errorListener = new SimpleTransformErrorListener(logger);
089
090        private boolean indent = true;
091
092        @Nullable
093        private Properties outputProperties;
094
095        private boolean cacheTemplates = true;
096
097        @Nullable
098        private TransformerFactory transformerFactory;
099
100        @Nullable
101        private Templates cachedTemplates;
102
103
104        /**
105         * Specify the XSLT TransformerFactory class to use.
106         * <p>The default constructor of the specified class will be called
107         * to build the TransformerFactory for this view.
108         */
109        public void setTransformerFactoryClass(Class<? extends TransformerFactory> transformerFactoryClass) {
110                this.transformerFactoryClass = transformerFactoryClass;
111        }
112
113        /**
114         * Set the name of the model attribute that represents the XSLT Source.
115         * If not specified, the model map will be searched for a matching value type.
116         * <p>The following source types are supported out of the box:
117         * {@link Source}, {@link Document}, {@link Node}, {@link Reader},
118         * {@link InputStream} and {@link Resource}.
119         * @see #getSourceTypes
120         * @see #convertSource
121         */
122        public void setSourceKey(String sourceKey) {
123                this.sourceKey = sourceKey;
124        }
125
126        /**
127         * Set the URIResolver used in the transform.
128         * <p>The URIResolver handles calls to the XSLT {@code document()} function.
129         */
130        public void setUriResolver(URIResolver uriResolver) {
131                this.uriResolver = uriResolver;
132        }
133
134        /**
135         * Set an implementation of the {@link javax.xml.transform.ErrorListener}
136         * interface for custom handling of transformation errors and warnings.
137         * <p>If not set, a default
138         * {@link org.springframework.util.xml.SimpleTransformErrorListener} is
139         * used that simply logs warnings using the logger instance of the view class,
140         * and rethrows errors to discontinue the XML transformation.
141         * @see org.springframework.util.xml.SimpleTransformErrorListener
142         */
143        public void setErrorListener(@Nullable ErrorListener errorListener) {
144                this.errorListener = (errorListener != null ? errorListener : new SimpleTransformErrorListener(logger));
145        }
146
147        /**
148         * Set whether the XSLT transformer may add additional whitespace when
149         * outputting the result tree.
150         * <p>Default is {@code true} (on); set this to {@code false} (off)
151         * to not specify an "indent" key, leaving the choice up to the stylesheet.
152         * @see javax.xml.transform.OutputKeys#INDENT
153         */
154        public void setIndent(boolean indent) {
155                this.indent = indent;
156        }
157
158        /**
159         * Set arbitrary transformer output properties to be applied to the stylesheet.
160         * <p>Any values specified here will override defaults that this view sets
161         * programmatically.
162         * @see javax.xml.transform.Transformer#setOutputProperty
163         */
164        public void setOutputProperties(Properties outputProperties) {
165                this.outputProperties = outputProperties;
166        }
167
168        /**
169         * Turn on/off the caching of the XSLT {@link Templates} instance.
170         * <p>The default value is "true". Only set this to "false" in development,
171         * where caching does not seriously impact performance.
172         */
173        public void setCacheTemplates(boolean cacheTemplates) {
174                this.cacheTemplates = cacheTemplates;
175        }
176
177
178        /**
179         * Initialize this XsltView's TransformerFactory.
180         */
181        @Override
182        protected void initApplicationContext() throws BeansException {
183                this.transformerFactory = newTransformerFactory(this.transformerFactoryClass);
184                this.transformerFactory.setErrorListener(this.errorListener);
185                if (this.uriResolver != null) {
186                        this.transformerFactory.setURIResolver(this.uriResolver);
187                }
188                if (this.cacheTemplates) {
189                        this.cachedTemplates = loadTemplates();
190                }
191        }
192
193        /**
194         * Instantiate a new TransformerFactory for this view.
195         * <p>The default implementation simply calls
196         * {@link javax.xml.transform.TransformerFactory#newInstance()}.
197         * If a {@link #setTransformerFactoryClass "transformerFactoryClass"}
198         * has been specified explicitly, the default constructor of the
199         * specified class will be called instead.
200         * <p>Can be overridden in subclasses.
201         * @param transformerFactoryClass the specified factory class (if any)
202         * @return the new TransactionFactory instance
203         * @see #setTransformerFactoryClass
204         * @see #getTransformerFactory()
205         */
206        protected TransformerFactory newTransformerFactory(
207                        @Nullable Class<? extends TransformerFactory> transformerFactoryClass) {
208
209                if (transformerFactoryClass != null) {
210                        try {
211                                return ReflectionUtils.accessibleConstructor(transformerFactoryClass).newInstance();
212                        }
213                        catch (Exception ex) {
214                                throw new TransformerFactoryConfigurationError(ex, "Could not instantiate TransformerFactory");
215                        }
216                }
217                else {
218                        return TransformerFactory.newInstance();
219                }
220        }
221
222        /**
223         * Return the TransformerFactory that this XsltView uses.
224         * @return the TransformerFactory (never {@code null})
225         */
226        protected final TransformerFactory getTransformerFactory() {
227                Assert.state(this.transformerFactory != null, "No TransformerFactory available");
228                return this.transformerFactory;
229        }
230
231
232        @Override
233        protected void renderMergedOutputModel(
234                        Map<String, Object> model, HttpServletRequest request, HttpServletResponse response)
235                        throws Exception {
236
237                Templates templates = this.cachedTemplates;
238                if (templates == null) {
239                        templates = loadTemplates();
240                }
241
242                Transformer transformer = createTransformer(templates);
243                configureTransformer(model, response, transformer);
244                configureResponse(model, response, transformer);
245                Source source = null;
246                try {
247                        source = locateSource(model);
248                        if (source == null) {
249                                throw new IllegalArgumentException("Unable to locate Source object in model: " + model);
250                        }
251                        transformer.transform(source, createResult(response));
252                }
253                finally {
254                        closeSourceIfNecessary(source);
255                }
256        }
257
258        /**
259         * Create the XSLT {@link Result} used to render the result of the transformation.
260         * <p>The default implementation creates a {@link StreamResult} wrapping the supplied
261         * HttpServletResponse's {@link HttpServletResponse#getOutputStream() OutputStream}.
262         * @param response current HTTP response
263         * @return the XSLT Result to use
264         * @throws Exception if the Result cannot be built
265         */
266        protected Result createResult(HttpServletResponse response) throws Exception {
267                return new StreamResult(response.getOutputStream());
268        }
269
270        /**
271         * <p>Locate the {@link Source} object in the supplied model,
272         * converting objects as required.
273         * The default implementation first attempts to look under the configured
274         * {@link #setSourceKey source key}, if any, before attempting to locate
275         * an object of {@link #getSourceTypes() supported type}.
276         * @param model the merged model Map
277         * @return the XSLT Source object (or {@code null} if none found)
278         * @throws Exception if an error occurred during locating the source
279         * @see #setSourceKey
280         * @see #convertSource
281         */
282        @Nullable
283        protected Source locateSource(Map<String, Object> model) throws Exception {
284                if (this.sourceKey != null) {
285                        return convertSource(model.get(this.sourceKey));
286                }
287                Object source = CollectionUtils.findValueOfType(model.values(), getSourceTypes());
288                return (source != null ? convertSource(source) : null);
289        }
290
291        /**
292         * Return the array of {@link Class Classes} that are supported when converting to an
293         * XSLT {@link Source}.
294         * <p>Currently supports {@link Source}, {@link Document}, {@link Node},
295         * {@link Reader}, {@link InputStream} and {@link Resource}.
296         * @return the supported source types
297         */
298        protected Class<?>[] getSourceTypes() {
299                return new Class<?>[] {Source.class, Document.class, Node.class, Reader.class, InputStream.class, Resource.class};
300        }
301
302        /**
303         * Convert the supplied {@link Object} into an XSLT {@link Source} if the
304         * {@link Object} type is {@link #getSourceTypes() supported}.
305         * @param source the original source object
306         * @return the adapted XSLT Source
307         * @throws IllegalArgumentException if the given Object is not of a supported type
308         */
309        protected Source convertSource(Object source) throws Exception {
310                if (source instanceof Source) {
311                        return (Source) source;
312                }
313                else if (source instanceof Document) {
314                        return new DOMSource(((Document) source).getDocumentElement());
315                }
316                else if (source instanceof Node) {
317                        return new DOMSource((Node) source);
318                }
319                else if (source instanceof Reader) {
320                        return new StreamSource((Reader) source);
321                }
322                else if (source instanceof InputStream) {
323                        return new StreamSource((InputStream) source);
324                }
325                else if (source instanceof Resource) {
326                        Resource resource = (Resource) source;
327                        return new StreamSource(resource.getInputStream(), resource.getURI().toASCIIString());
328                }
329                else {
330                        throw new IllegalArgumentException("Value '" + source + "' cannot be converted to XSLT Source");
331                }
332        }
333
334        /**
335         * Configure the supplied {@link Transformer} instance.
336         * <p>The default implementation copies parameters from the model into the
337         * Transformer's {@link Transformer#setParameter parameter set}.
338         * This implementation also copies the {@link #setOutputProperties output properties}
339         * into the {@link Transformer} {@link Transformer#setOutputProperty output properties}.
340         * Indentation properties are set as well.
341         * @param model merged output Map (never {@code null})
342         * @param response current HTTP response
343         * @param transformer the target transformer
344         * @see #copyModelParameters(Map, Transformer)
345         * @see #copyOutputProperties(Transformer)
346         * @see #configureIndentation(Transformer)
347         */
348        protected void configureTransformer(Map<String, Object> model, HttpServletResponse response,
349                        Transformer transformer) {
350
351                copyModelParameters(model, transformer);
352                copyOutputProperties(transformer);
353                configureIndentation(transformer);
354        }
355
356        /**
357         * Configure the indentation settings for the supplied {@link Transformer}.
358         * @param transformer the target transformer
359         * @see org.springframework.util.xml.TransformerUtils#enableIndenting(javax.xml.transform.Transformer)
360         * @see org.springframework.util.xml.TransformerUtils#disableIndenting(javax.xml.transform.Transformer)
361         */
362        protected final void configureIndentation(Transformer transformer) {
363                if (this.indent) {
364                        TransformerUtils.enableIndenting(transformer);
365                }
366                else {
367                        TransformerUtils.disableIndenting(transformer);
368                }
369        }
370
371        /**
372         * Copy the configured output {@link Properties}, if any, into the
373         * {@link Transformer#setOutputProperty output property set} of the supplied
374         * {@link Transformer}.
375         * @param transformer the target transformer
376         */
377        protected final void copyOutputProperties(Transformer transformer) {
378                if (this.outputProperties != null) {
379                        Enumeration<?> en = this.outputProperties.propertyNames();
380                        while (en.hasMoreElements()) {
381                                String name = (String) en.nextElement();
382                                transformer.setOutputProperty(name, this.outputProperties.getProperty(name));
383                        }
384                }
385        }
386
387        /**
388         * Copy all entries from the supplied Map into the
389         * {@link Transformer#setParameter(String, Object) parameter set}
390         * of the supplied {@link Transformer}.
391         * @param model merged output Map (never {@code null})
392         * @param transformer the target transformer
393         */
394        protected final void copyModelParameters(Map<String, Object> model, Transformer transformer) {
395                model.forEach(transformer::setParameter);
396        }
397
398        /**
399         * Configure the supplied {@link HttpServletResponse}.
400         * <p>The default implementation of this method sets the
401         * {@link HttpServletResponse#setContentType content type} and
402         * {@link HttpServletResponse#setCharacterEncoding encoding}
403         * from the "media-type" and "encoding" output properties
404         * specified in the {@link Transformer}.
405         * @param model merged output Map (never {@code null})
406         * @param response current HTTP response
407         * @param transformer the target transformer
408         */
409        protected void configureResponse(Map<String, Object> model, HttpServletResponse response, Transformer transformer) {
410                String contentType = getContentType();
411                String mediaType = transformer.getOutputProperty(OutputKeys.MEDIA_TYPE);
412                String encoding = transformer.getOutputProperty(OutputKeys.ENCODING);
413                if (StringUtils.hasText(mediaType)) {
414                        contentType = mediaType;
415                }
416                if (StringUtils.hasText(encoding)) {
417                        // Only apply encoding if content type is specified but does not contain charset clause already.
418                        if (contentType != null && !contentType.toLowerCase().contains(WebUtils.CONTENT_TYPE_CHARSET_PREFIX)) {
419                                contentType = contentType + WebUtils.CONTENT_TYPE_CHARSET_PREFIX + encoding;
420                        }
421                }
422                response.setContentType(contentType);
423        }
424
425        /**
426         * Load the {@link Templates} instance for the stylesheet at the configured location.
427         */
428        private Templates loadTemplates() throws ApplicationContextException {
429                Source stylesheetSource = getStylesheetSource();
430                try {
431                        Templates templates = getTransformerFactory().newTemplates(stylesheetSource);
432                        return templates;
433                }
434                catch (TransformerConfigurationException ex) {
435                        throw new ApplicationContextException("Can't load stylesheet from '" + getUrl() + "'", ex);
436                }
437                finally {
438                        closeSourceIfNecessary(stylesheetSource);
439                }
440        }
441
442        /**
443         * Create the {@link Transformer} instance used to prefer the XSLT transformation.
444         * <p>The default implementation simply calls {@link Templates#newTransformer()}, and
445         * configures the {@link Transformer} with the custom {@link URIResolver} if specified.
446         * @param templates the XSLT Templates instance to create a Transformer for
447         * @return the Transformer object
448         * @throws TransformerConfigurationException in case of creation failure
449         */
450        protected Transformer createTransformer(Templates templates) throws TransformerConfigurationException {
451                Transformer transformer = templates.newTransformer();
452                if (this.uriResolver != null) {
453                        transformer.setURIResolver(this.uriResolver);
454                }
455                return transformer;
456        }
457
458        /**
459         * Get the XSLT {@link Source} for the XSLT template under the {@link #setUrl configured URL}.
460         * @return the Source object
461         */
462        protected Source getStylesheetSource() {
463                String url = getUrl();
464                Assert.state(url != null, "'url' not set");
465
466                if (logger.isDebugEnabled()) {
467                        logger.debug("Applying stylesheet [" + url + "]");
468                }
469                try {
470                        Resource resource = obtainApplicationContext().getResource(url);
471                        return new StreamSource(resource.getInputStream(), resource.getURI().toASCIIString());
472                }
473                catch (IOException ex) {
474                        throw new ApplicationContextException("Can't load XSLT stylesheet from '" + url + "'", ex);
475                }
476        }
477
478        /**
479         * Close the underlying resource managed by the supplied {@link Source} if applicable.
480         * <p>Only works for {@link StreamSource StreamSources}.
481         * @param source the XSLT Source to close (may be {@code null})
482         */
483        private void closeSourceIfNecessary(@Nullable Source source) {
484                if (source instanceof StreamSource) {
485                        StreamSource streamSource = (StreamSource) source;
486                        if (streamSource.getReader() != null) {
487                                try {
488                                        streamSource.getReader().close();
489                                }
490                                catch (IOException ex) {
491                                        // ignore
492                                }
493                        }
494                        if (streamSource.getInputStream() != null) {
495                                try {
496                                        streamSource.getInputStream().close();
497                                }
498                                catch (IOException ex) {
499                                        // ignore
500                                }
501                        }
502                }
503        }
504
505}