View Javadoc

    1   /*
    2    * $Id: XSLTResult.java 484626 2006-12-08 15:02:24Z ddewolf $
    3    *
    4    * Licensed to the Apache Software Foundation (ASF) under one
    5    * or more contributor license agreements.  See the NOTICE file
    6    * distributed with this work for additional information
    7    * regarding copyright ownership.  The ASF licenses this file
    8    * to you under the Apache License, Version 2.0 (the
    9    * "License"); you may not use this file except in compliance
    10   * with the License.  You may obtain a copy of the License at
    11   *
    12   *  http://www.apache.org/licenses/LICENSE-2.0
    13   *
    14   * Unless required by applicable law or agreed to in writing,
    15   * software distributed under the License is distributed on an
    16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    17   * KIND, either express or implied.  See the License for the
    18   * specific language governing permissions and limitations
    19   * under the License.
    20   */
    21  package org.apache.struts2.views.xslt;
    22
    23  import java.io.IOException;
    24  import java.io.PrintWriter;
    25  import java.io.Writer;
    26  import java.net.URL;
    27  import java.util.HashMap;
    28  import java.util.Map;
    29
    30  import javax.servlet.http.HttpServletResponse;
    31  import javax.xml.transform.OutputKeys;
    32  import javax.xml.transform.Source;
    33  import javax.xml.transform.Templates;
    34  import javax.xml.transform.Transformer;
    35  import javax.xml.transform.TransformerException;
    36  import javax.xml.transform.TransformerFactory;
    37  import javax.xml.transform.URIResolver;
    38  import javax.xml.transform.dom.DOMSource;
    39  import javax.xml.transform.stream.StreamResult;
    40  import javax.xml.transform.stream.StreamSource;
    41
    42  import org.apache.commons.logging.Log;
    43  import org.apache.commons.logging.LogFactory;
    44  import org.apache.struts2.ServletActionContext;
    45  import org.apache.struts2.StrutsConstants;
    46
    47  import com.opensymphony.xwork2.ActionContext;
    48  import com.opensymphony.xwork2.ActionInvocation;
    49  import com.opensymphony.xwork2.Result;
    50  import com.opensymphony.xwork2.inject.Inject;
    51  import com.opensymphony.xwork2.util.TextParseUtil;
    52  import com.opensymphony.xwork2.util.ValueStack;
    53
    54
    55  /***
    56   * <!-- START SNIPPET: description -->
    57   *
    58   * XSLTResult uses XSLT to transform action object to XML. Recent version has
    59   * been specifically modified to deal with Xalan flaws. When using Xalan you may
    60   * notice that even though you have very minimal stylesheet like this one
    61   * <pre>
    62   * &lt;xsl:template match="/result"&gt;
    63   *   &lt;result /&gt;
    64   * &lt;/xsl:template&gt;</pre>
    65   *
    66   * <p>
    67   * then Xalan would still iterate through every property of your action and it's
    68   * all descendants.
    69   * </p>
    70   *
    71   * <p>
    72   * If you had double-linked objects then Xalan would work forever analysing
    73   * infinite object tree. Even if your stylesheet was not constructed to process
    74   * them all. It's becouse current Xalan eagerly and extensively converts
    75   * everything to it's internal DTM model before further processing.
    76   * </p>
    77   *
    78   * <p>
    79   * Thet's why there's a loop eliminator added that works by indexing every
    80   * object-property combination during processing. If it notices that some
    81   * object's property were already walked through, it doesn't get any deeper.
    82   * Say, you have two objects x and y with the following properties set
    83   * (pseudocode):
    84   * </p>
    85   * <pre>
    86   * x.y = y;
    87   * and
    88   * y.x = x;
    89   * action.x=x;</pre>
    90   *
    91   * <p>
    92   * Due to that modification the resulting XML document based on x would be:
    93   * </p>
    94   *
    95   * <pre>
    96   * &lt;result&gt;
    97   *   &lt;x&gt;
    98   *     &lt;y/&gt;
    99   *   &lt;/x&gt;
    100  * &lt;/result&gt;</pre>
    101  *
    102  * <p>
    103  * Without it there would be an endless x/y/x/y/x/y/... elements.
    104  * </p>
    105  *
    106  * <p>
    107  * The XSLTResult code tries also to deal with the fact that DTM model is built
    108  * in a manner that childs are processed before siblings. The result is that if
    109  * there is object x that is both set in action's x property, and very deeply
    110  * under action's a property then it would only appear under a, not under x.
    111  * That's not what we expect, and that's why XSLTResult allows objects to repeat
    112  * in various places to some extent.
    113  * </p>
    114  *
    115  * <p>
    116  * Sometimes the object mesh is still very dense and you may notice that even
    117  * though you have relatively simple stylesheet execution takes a tremendous
    118  * amount of time. To help you to deal with that obstacle of Xalan you may
    119  * attach regexp filters to elements paths (xpath).
    120  * </p>
    121  *
    122  * <p>
    123  * <b>Note:</b> In your .xsl file the root match must be named <tt>result</tt>.
    124  * <br/>This example will output the username by using <tt>getUsername</tt> on your
    125  * action class:
    126  * <pre>
    127  * &lt;xsl:template match="result"&gt;
    128  *   &lt;html&gt;
    129  *   &lt;body&gt;
    130  *   Hello &lt;xsl:value-of select="username"/&gt; how are you?
    131  *   &lt;/body&gt;
    132  *   &lt;html&gt;
    133  * &lt;xsl:template/&gt;
    134  * </pre>
    135  *
    136  * <p>
    137  * In the following example the XSLT result would only walk through action's
    138  * properties without their childs. It would also skip every property that has
    139  * "hugeCollection" in their name. Element's path is first compared to
    140  * excludingPattern - if it matches it's no longer processed. Then it is
    141  * compared to matchingPattern and processed only if there's a match.
    142  * </p>
    143  *
    144  * <!-- END SNIPPET: description -->
    145  *
    146  * <pre><!-- START SNIPPET: description.example -->
    147  * &lt;result name="success" type="xslt"&gt;
    148  *   &lt;param name="location"&gt;foo.xslt&lt;/param&gt;
    149  *   &lt;param name="matchingPattern"&gt;^/result/[^/*]$&lt;/param&gt;
    150  *   &lt;param name="excludingPattern"&gt;.*(hugeCollection).*&lt;/param&gt;
    151  * &lt;/result&gt;
    152  * <!-- END SNIPPET: description.example --></pre>
    153  *
    154  * <p>
    155  * In the following example the XSLT result would use the action's user property
    156  * instead of the action as it's base document and walk through it's properties.
    157  * The exposedValue uses an ognl expression to derive it's value.
    158  * </p>
    159  *
    160  * <pre>
    161  * &lt;result name="success" type="xslt"&gt;
    162  *   &lt;param name="location"&gt;foo.xslt&lt;/param&gt;
    163  *   &lt;param name="exposedValue"&gt;user$&lt;/param&gt;
    164  * &lt;/result&gt;
    165  * </pre>
    166  * *
    167  * <b>This result type takes the following parameters:</b>
    168  *
    169  * <!-- START SNIPPET: params -->
    170  *
    171  * <ul>
    172  *
    173  * <li><b>location (default)</b> - the location to go to after execution.</li>
    174  *
    175  * <li><b>parse</b> - true by default. If set to false, the location param will
    176  * not be parsed for Ognl expressions.</li>
    177  *
    178  * <li><b>matchingPattern</b> - Pattern that matches only desired elements, by
    179  * default it matches everything.</li>
    180  *
    181  * <li><b>excludingPattern</b> - Pattern that eliminates unwanted elements, by
    182  * default it matches none.</li>
    183  *
    184  * </ul>
    185  *
    186  * <p>
    187  * <code>struts.properties</code> related configuration:
    188  * </p>
    189  * <ul>
    190  *
    191  * <li><b>struts.xslt.nocache</b> - Defaults to false. If set to true, disables
    192  * stylesheet caching. Good for development, bad for production.</li>
    193  *
    194  * </ul>
    195  *
    196  * <!-- END SNIPPET: params -->
    197  *
    198  * <b>Example:</b>
    199  *
    200  * <pre><!-- START SNIPPET: example -->
    201  * &lt;result name="success" type="xslt"&gt;foo.xslt&lt;/result&gt;
    202  * <!-- END SNIPPET: example --></pre>
    203  *
    204  */
    205 public class XSLTResult implements Result {
    206
    207     private static final long serialVersionUID = 6424691441777176763L;
    208
    209     /*** Log instance for this result. */
    210     private static final Log LOG = LogFactory.getLog(XSLTResult.class);
    211
    212     /*** 'stylesheetLocation' parameter.  Points to the xsl. */
    213     public static final String DEFAULT_PARAM = "stylesheetLocation";
    214
    215     /*** Cache of all tempaltes. */
    216     private static final Map<String, Templates> templatesCache;
    217
    218     static {
    219         templatesCache = new HashMap<String, Templates>();
    220     }
    221
    222     // Configurable Parameters
    223
    224     /*** Determines whether or not the result should allow caching. */
    225     protected boolean noCache;
    226
    227     /*** Indicates the location of the xsl template. */
    228     private String stylesheetLocation;
    229
    230     /*** Indicates the property name patterns which should be exposed to the xml. */
    231     private String matchingPattern;
    232
    233     /*** Indicates the property name patterns which should be excluded from the xml. */
    234     private String exludingPattern;
    235
    236     /*** Indicates the ognl expression respresenting the bean which is to be exposed as xml. */
    237     private String exposedValue;
    238
    239     private boolean parse;
    240     private AdapterFactory adapterFactory;
    241
    242     public XSLTResult() {
    243     }
    244
    245     public XSLTResult(String stylesheetLocation) {
    246         this();
    247         setStylesheetLocation(stylesheetLocation);
    248     }
    249
    250     @Inject(StrutsConstants.STRUTS_XSLT_NOCACHE)
    251     public void setNoCache(String val) {
    252         noCache = "true".equals(val);
    253     }
    254
    255     /***
    256      * @deprecated Use #setStylesheetLocation(String)
    257      */
    258     public void setLocation(String location) {
    259         setStylesheetLocation(location);
    260     }
    261
    262     public void setStylesheetLocation(String location) {
    263         if (location == null)
    264             throw new IllegalArgumentException("Null location");
    265         this.stylesheetLocation = location;
    266     }
    267
    268     public String getStylesheetLocation() {
    269         return stylesheetLocation;
    270     }
    271
    272     public String getExposedValue() {
    273         return exposedValue;
    274     }
    275
    276     public void setExposedValue(String exposedValue) {
    277         this.exposedValue = exposedValue;
    278     }
    279
    280     public String getMatchingPattern() {
    281         return matchingPattern;
    282     }
    283
    284     public void setMatchingPattern(String matchingPattern) {
    285         this.matchingPattern = matchingPattern;
    286     }
    287
    288     public String getExludingPattern() {
    289         return exludingPattern;
    290     }
    291
    292     public void setExludingPattern(String exludingPattern) {
    293         this.exludingPattern = exludingPattern;
    294     }
    295
    296     /***
    297      * If true, parse the stylesheet location for OGNL expressions.
    298      *
    299      * @param parse
    300      */
    301     public void setParse(boolean parse) {
    302         this.parse = parse;
    303     }
    304
    305     public void execute(ActionInvocation invocation) throws Exception {
    306         long startTime = System.currentTimeMillis();
    307         String location = getStylesheetLocation();
    308
    309         if (parse) {
    310             ValueStack stack = ActionContext.getContext().getValueStack();
    311             location = TextParseUtil.translateVariables(location, stack);
    312         }
    313
    314
    315         try {
    316             HttpServletResponse response = ServletActionContext.getResponse();
    317
    318             Writer writer = response.getWriter();
    319
    320             // Create a transformer for the stylesheet.
    321             Templates templates = null;
    322             Transformer transformer;
    323             if (location != null) {
    324                 templates = getTemplates(location);
    325                 transformer = templates.newTransformer();
    326             } else
    327                 transformer = TransformerFactory.newInstance().newTransformer();
    328
    329             transformer.setURIResolver(getURIResolver());
    330
    331             String mimeType;
    332             if (templates == null)
    333                 mimeType = "text/xml"; // no stylesheet, raw xml
    334             else
    335                 mimeType = templates.getOutputProperties().getProperty(OutputKeys.MEDIA_TYPE);
    336             if (mimeType == null) {
    337                 // guess (this is a servlet, so text/html might be the best guess)
    338                 mimeType = "text/html";
    339             }
    340
    341             response.setContentType(mimeType);
    342
    343             Object result = invocation.getAction();
    344             if (exposedValue != null) {
    345                 ValueStack stack = invocation.getStack();
    346                 result = stack.findValue(exposedValue);
    347             }
    348
    349             Source xmlSource = getDOMSourceForStack(result);
    350
    351             // Transform the source XML to System.out.
    352             PrintWriter out = response.getWriter();
    353
    354             LOG.debug("xmlSource = " + xmlSource);
    355             transformer.transform(xmlSource, new StreamResult(out));
    356
    357             out.close(); // ...and flush...
    358
    359             if (LOG.isDebugEnabled()) {
    360                 LOG.debug("Time:" + (System.currentTimeMillis() - startTime) + "ms");
    361             }
    362
    363             writer.flush();
    364         } catch (Exception e) {
    365             LOG.error("Unable to render XSLT Template, '" + location + "'", e);
    366             throw e;
    367         }
    368     }
    369
    370     protected AdapterFactory getAdapterFactory() {
    371         if (adapterFactory == null)
    372             adapterFactory = new AdapterFactory();
    373         return adapterFactory;
    374     }
    375
    376     protected void setAdapterFactory(AdapterFactory adapterFactory) {
    377         this.adapterFactory = adapterFactory;
    378     }
    379
    380     /***
    381      * Get the URI Resolver to be called by the processor when it encounters an xsl:include, xsl:import, or document()
    382      * function. The default is an instance of ServletURIResolver, which operates relative to the servlet context.
    383      */
    384     protected URIResolver getURIResolver() {
    385         return new ServletURIResolver(
    386                 ServletActionContext.getServletContext());
    387     }
    388
    389     protected Templates getTemplates(String path) throws TransformerException, IOException {
    390         String pathFromRequest = ServletActionContext.getRequest().getParameter("xslt.location");
    391
    392         if (pathFromRequest != null)
    393             path = pathFromRequest;
    394
    395         if (path == null)
    396             throw new TransformerException("Stylesheet path is null");
    397
    398         Templates templates = templatesCache.get(path);
    399
    400         if (noCache || (templates == null)) {
    401             synchronized (templatesCache) {
    402                 URL resource = ServletActionContext.getServletContext().getResource(path);
    403
    404                 if (resource == null) {
    405                     throw new TransformerException("Stylesheet " + path + " not found in resources.");
    406                 }
    407
    408                 LOG.debug("Preparing XSLT stylesheet templates: " + path);
    409
    410                 TransformerFactory factory = TransformerFactory.newInstance();
    411                 templates = factory.newTemplates(new StreamSource(resource.openStream()));
    412                 templatesCache.put(path, templates);
    413             }
    414         }
    415
    416         return templates;
    417     }
    418
    419     protected Source getDOMSourceForStack(Object value)
    420             throws IllegalAccessException, InstantiationException {
    421         return new DOMSource(getAdapterFactory().adaptDocument("result", value) );
    422     }
    423 }