1 /*
   2  * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
   3  */
   4 /*
   5  * Licensed to the Apache Software Foundation (ASF) under one or more
   6  * contributor license agreements.  See the NOTICE file distributed with
   7  * this work for additional information regarding copyright ownership.
   8  * The ASF licenses this file to You under the Apache License, Version 2.0
   9  * (the "License"); you may not use this file except in compliance with
  10  * 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, software
  15  * distributed under the License is distributed on an "AS IS" BASIS,
  16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17  * See the License for the specific language governing permissions and
  18  * limitations under the License.
  19  */
  20 package org.apache.qetest.xslwrapper;
  21 
  22 import java.io.ByteArrayInputStream;
  23 import java.io.ByteArrayOutputStream;
  24 import java.io.File;
  25 import java.io.FileInputStream;
  26 import java.io.FileOutputStream;
  27 import java.util.Properties;
  28 import javax.xml.transform.Source;
  29 import javax.xml.transform.Templates;
  30 import javax.xml.transform.Transformer;
  31 import javax.xml.transform.TransformerConfigurationException;
  32 import javax.xml.transform.TransformerFactory;
  33 import javax.xml.transform.stream.StreamResult;
  34 import javax.xml.transform.stream.StreamSource;
  35 import static jaxp.library.JAXPTestUtilities.filenameToURL;
  36 
  37 /**
  38  * Implementation of TransformWrapper that uses the TrAX API and uses in-memory
  39  * streams for it's sources.
  40  *
  41  * <p>
  42  * This implementation separates the process of reading xml and xsl files from
  43  * disk into byte arrays out from the time processing of a new
  44  * StreamSource(byte[]) takes to build a stylesheet. It also separates the time
  45  * of performing the transformation to a StreamResult(byte[]) from the time
  46  * spent simply sending the byte[] through a FileOutputStream to disk.</p>
  47  *
  48  * <p>
  49  * <b>Important!</b> The underlying System property of
  50  * javax.xml.transform.TransformerFactory will determine the actual TrAX
  51  * implementation used. This value will be reported out in our
  52  * getProcessorInfo() method.</p>
  53  *
  54  */
  55 public class TraxStreamWrapper extends TransformWrapperHelper {
  56 
  57     /**
  58      * TransformerFactory to use; constructed in newProcessor().
  59      */
  60     private TransformerFactory factory;
  61 
  62     /**
  63      * Templates to use for buildStylesheet().
  64      */
  65     private Templates builtTemplates;
  66 
  67     /**
  68      * Cached copy of newProcessor() properties.
  69      */
  70     private Properties newProcessorOpts;
  71 
  72     /**
  73      * Get a general description of this wrapper itself.
  74      *
  75      * @return Uses TrAX to perform transforms from StreamSource(systemId)
  76      */
  77     public String getDescription() {
  78         return "Uses TrAX to perform transforms from StreamSource(stream)";
  79     }
  80 
  81     /**
  82      * Get a specific description of the wrappered processor.
  83      *
  84      * @return specific description of the underlying processor or transformer
  85      * implementation: this should include both the general product name, as
  86      * well as specific version info. If possible, should be implemented without
  87      * actively creating an underlying processor.
  88      */
  89     @Override
  90     public Properties getProcessorInfo() {
  91         Properties p = TraxWrapperUtils.getTraxInfo();
  92         p.put("traxwrapper.method", "streams");
  93         p.put("traxwrapper.desc", getDescription());
  94         return p;
  95     }
  96 
  97     /**
  98      * Actually create/initialize an underlying processor or factory.
  99      *
 100      * For TrAX/javax.xml.transform implementations, this creates a new
 101      * TransformerFactory.
 102      *
 103      * @param options Properties of options, unused.
 104      *
 105      * @return (Object)getProcessor() as a side-effect, this will be null if
 106      * there was any problem creating the processor OR if the underlying
 107      * implementation doesn't use this.
 108      * @throws javax.xml.transform.TransformerConfigurationException when 
 109      * actual implementation doesn't support StreamSource or StreamResult.
 110      */
 111     @Override
 112     public TransformerFactory newProcessor(Properties options)
 113             throws TransformerConfigurationException {
 114         newProcessorOpts = options;
 115         reset(false);
 116         factory = TransformerFactory.newInstance();
 117         // Verify the factory supports Streams!
 118         if (!(factory.getFeature(StreamSource.FEATURE)
 119                 && factory.getFeature(StreamResult.FEATURE))) {
 120             throw new TransformerConfigurationException("TraxStreamWrapper.newProcessor: factory does not support Streams!");
 121         }
 122         // Set any of our options as Attributes on the factory
 123         TraxWrapperUtils.setAttributes(factory, options);
 124         return factory;
 125     }
 126 
 127     /**
 128      * Transform supplied xmlName file with the stylesheet in the xslName file
 129      * into a resultName file.
 130      *
 131      * Names are assumed to be local path\filename references, and will be read
 132      * as byte streams before being passed to underlying StreamSources, etc.
 133      *
 134      * @param xmlName local path\filename of XML file to transform
 135      * @param xslName local path\filename of XSL stylesheet to use
 136      * @param resultName local path\filename to put result in
 137      *
 138      * @throws Exception any underlying exceptions from the wrappered processor
 139      * are simply allowed to propagate; throws a RuntimeException if any other
 140      * problems prevent us from actually completing the operation
 141      */
 142     @Override
 143     public void transform(String xmlName, String xslName, String resultName)
 144             throws Exception {
 145         preventFootShooting();
 146 
 147         File xslFile = new File(xslName);
 148         int xslLength = new Long(xslFile.length()).intValue();
 149         byte[] xslBytes = new byte[xslLength];
 150         File xmlFile = new File(xmlName);
 151         int xmlLength = new Long(xmlFile.length()).intValue();
 152         byte[] xmlBytes = new byte[xmlLength];
 153         try (FileInputStream xslStream = new FileInputStream(xslFile);
 154                 FileInputStream xmlStream = new FileInputStream(xmlFile)) {
 155             // Read xsl into a byte array
 156             xslStream.read(xslBytes);
 157             // Create StreamSource and setSystemId
 158             StreamSource xslSource = new StreamSource(new ByteArrayInputStream(xslBytes));
 159             // Note that systemIds must be a legal URI
 160             xslSource.setSystemId(filenameToURL(xslName));
 161 
 162             // Build Transformer from StreamSource
 163             Transformer transformer = factory.newTransformer(xslSource);
 164 
 165             // Read xml into a byte array
 166             xmlStream.read(xmlBytes);
 167 
 168             // Create StreamSource and setSystemId
 169             StreamSource xmlSource = new StreamSource(new ByteArrayInputStream(xmlBytes));
 170             xmlSource.setSystemId(filenameToURL(xmlName));
 171 
 172             // Create StreamResult
 173             ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
 174             StreamResult byteResult = new StreamResult(outBytes);
 175 
 176             // Set any of our options as Attributes on the transformer
 177             TraxWrapperUtils.setAttributes(transformer, newProcessorOpts);
 178 
 179             // Apply any parameters needed
 180             applyParameters(transformer);
 181 
 182             // Build xml (so to speak) and transform
 183             transformer.transform(xmlSource, byteResult);
 184 
 185             // writeResults from the byte array
 186             byte[] writeBytes = outBytes.toByteArray(); // Should this be timed too or not?
 187             try (FileOutputStream writeStream = new FileOutputStream(resultName)) {
 188                 writeStream.write(writeBytes);
 189             }
 190         }
 191     }
 192 
 193     /**
 194      * Pre-build/pre-compile a stylesheet.
 195      *
 196      * Although the actual mechanics are implementation-dependent, most
 197      * processors have some method of pre-setting up the data needed by the
 198      * stylesheet itself for later use in transforms. In
 199      * TrAX/javax.xml.transform, this equates to creating a Templates object.
 200      *
 201      * Sets isStylesheetReady() to true if it succeeds. Users can then call
 202      * transformWithStylesheet(xmlName, resultName) to actually perform a
 203      * transformation with this pre-built stylesheet.
 204      *
 205      * @param xslName local path\filename of XSL stylesheet to use
 206      *
 207      * @throws Exception any underlying exceptions from the wrappered processor
 208      * are simply allowed to propagate; throws a RuntimeException if any other
 209      * problems prevent us from actually completing the operation
 210      *
 211      * @see #transformWithStylesheet(String xmlName, String resultName)
 212      */
 213     @Override
 214     public void buildStylesheet(String xslName) throws Exception {
 215         preventFootShooting();
 216 
 217         File xslFile = new File(xslName);
 218         int xslLength = new Long(xslFile.length()).intValue();
 219         byte[] xslBytes = new byte[xslLength];
 220         
 221         try(FileInputStream xslStream = new FileInputStream(xslFile)) {
 222             // Read xsl into a byte array
 223             xslStream.read(xslBytes);
 224 
 225             // Create StreamSource and setSystemId
 226             StreamSource xslSource = new StreamSource(new ByteArrayInputStream(xslBytes));
 227             // Note that systemIds must be a legal URI
 228             xslSource.setSystemId(filenameToURL(xslName));
 229 
 230             // Build Transformer from StreamSource
 231             builtTemplates = factory.newTemplates(xslSource);
 232 
 233             // Set internal state that we have a templates ready
 234             //  Note: in theory, there's no need to check builtTemplates
 235             //  since the newTemplates should never return null
 236             //  (it might have thrown an exception, but we don't care)
 237             m_stylesheetReady = true;
 238         }
 239     }
 240 
 241     /**
 242      * Transform supplied xmlName file with a pre-built/pre-compiled stylesheet
 243      * into a resultName file.
 244      *
 245      * User must have called buildStylesheet(xslName) beforehand, obviously.
 246      * Names are assumed to be local path\filename references, and will be
 247      * converted to URLs as needed.
 248      *
 249      * @param xmlName local path\filename of XML file to transform
 250      * @param resultName local path\filename to put result in
 251      *
 252      * @throws Exception any underlying exceptions from the wrapped processor
 253      * are simply allowed to propagate; throws a RuntimeException if any other
 254      * problems prevent us from actually completing the operation; throws an
 255      * IllegalStateException if isStylesheetReady() == false.
 256      *
 257      * @see #buildStylesheet(String xslName)
 258      */
 259     @Override
 260     public void transformWithStylesheet(String xmlName, String resultName)
 261             throws Exception {
 262         if (!isStylesheetReady()) {
 263             throw new IllegalStateException("transformWithStylesheet() when isStylesheetReady() == false");
 264         }
 265 
 266         preventFootShooting();
 267 
 268         try (FileOutputStream writeStream = new FileOutputStream(resultName)) {
 269             // Get Transformer from Templates
 270             Transformer transformer = builtTemplates.newTransformer();
 271 
 272             File xmlFile = new File(xmlName);
 273             int xmlLength = new Long(xmlFile.length()).intValue();
 274             byte[] xmlBytes = new byte[xmlLength];
 275             FileInputStream xmlStream = new FileInputStream(xmlFile);
 276             // Read xml into a byte array
 277             xmlStream.read(xmlBytes);
 278 
 279             // Create StreamSource and setSystemId
 280             StreamSource xmlSource = new StreamSource(new ByteArrayInputStream(xmlBytes));
 281             xmlSource.setSystemId(filenameToURL(xmlName));
 282 
 283             // Create StreamResult
 284             ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
 285             StreamResult byteResult = new StreamResult(outBytes);
 286 
 287             // Set any of our options as Attributes on the transformer
 288             TraxWrapperUtils.setAttributes(transformer, newProcessorOpts);
 289 
 290             // Untimed: Apply any parameters needed
 291             applyParameters(transformer);
 292 
 293             // Build xml (so to speak) and transform
 294             transformer.transform(xmlSource, byteResult);
 295 
 296             // writeResults from the byte array
 297             byte[] writeBytes = outBytes.toByteArray();
 298             writeStream.write(writeBytes);
 299         }
 300     }
 301 
 302     /**
 303      * Transform supplied xmlName file with a stylesheet found in an
 304      * xml-stylesheet PI into a resultName file.
 305      *
 306      * Names are assumed to be local path\filename references, and will be
 307      * converted to URLs as needed. Implementations will use whatever facilities
 308      * exist in their wrappered processor to fetch and build the stylesheet to
 309      * use for the transform.
 310      *
 311      * @param xmlName local path\filename of XML file to transform
 312      * @param resultName local path\filename to put result in
 313      *
 314      * @throws Exception any underlying exceptions from the wrapped processor
 315      * are simply allowed to propagate; throws a RuntimeException if any other
 316      * problems prevent us from actually completing the operation
 317      */
 318     @Override
 319     public void transformEmbedded(String xmlName, String resultName)
 320             throws Exception {
 321         preventFootShooting();
 322 
 323         File xmlFile = new File(xmlName);
 324         try (FileOutputStream writeStream = new FileOutputStream(resultName);) {
 325             int xmlLength = new Long(xmlFile.length()).intValue();
 326             byte[] xmlBytes = new byte[xmlLength];
 327             FileInputStream xmlStream = new FileInputStream(xmlFile);
 328             // Read xml into a byte array
 329             xmlStream.read(xmlBytes);
 330 
 331             // Create StreamSource and setSystemId
 332             StreamSource xmlSource = new StreamSource(new ByteArrayInputStream(xmlBytes));
 333             xmlSource.setSystemId(filenameToURL(xmlName));
 334 
 335             // Read xsl from the xml document
 336             Source xslSource = factory.getAssociatedStylesheet(xmlSource, null, null, null);
 337 
 338             // Build xsl from a URL
 339             Transformer transformer = factory.newTransformer(xslSource);
 340 
 341             // Re-read the XML file for use in transform; not timed
 342             xmlFile = new File(xmlName);
 343             xmlLength = new Long(xmlFile.length()).intValue(); 
 344             xmlBytes = new byte[xmlLength];
 345             xmlStream = new FileInputStream(xmlFile);
 346             xmlStream.read(xmlBytes);
 347 
 348             // Create StreamSource and setSystemId
 349             xmlSource = new StreamSource(new ByteArrayInputStream(xmlBytes));
 350             xmlSource.setSystemId(filenameToURL(xmlName));
 351 
 352             // Create StreamResult
 353             ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
 354             StreamResult byteResult = new StreamResult(outBytes);
 355 
 356             // Set any of our options as Attributes on the transformer
 357             TraxWrapperUtils.setAttributes(transformer, newProcessorOpts);
 358 
 359             // Apply any parameters needed
 360             applyParameters(transformer);
 361 
 362             // Build xml (so to speak) and transform
 363             transformer.transform(xmlSource, byteResult);
 364 
 365             // writeResults from the byte array
 366             byte[] writeBytes = outBytes.toByteArray();
 367 
 368             writeStream.write(writeBytes);
 369         }
 370 
 371     }
 372 
 373     /**
 374      * Reset our parameters and wrapper state, and optionally force creation of
 375      * a new underlying processor implementation.
 376      *
 377      * This always clears our built stylesheet and any parameters that have been
 378      * set. If newProcessor is true, also forces a re-creation of our underlying
 379      * processor as if by calling newProcessor().
 380      *
 381      * @param newProcessor if we should reset our underlying processor
 382      * implementation as well
 383      */
 384     @Override
 385     public void reset(boolean newProcessor) {
 386         super.reset(newProcessor); // clears indent and parameters
 387         m_stylesheetReady = false;
 388         builtTemplates = null;
 389         if (newProcessor) {
 390             try {
 391                 newProcessor(newProcessorOpts);
 392             } catch (TransformerConfigurationException ignore) {}
 393         }
 394     }
 395 
 396     /**
 397      * Apply a single parameter to a Transformer.
 398      *
 399      * Overridden to take a Transformer and call setParameter().
 400      *
 401      * @param transformer a Transformer object.
 402      * @param namespace for the parameter, may be null
 403      * @param name for the parameter, should not be null
 404      * @param value for the parameter, may be null
 405      */
 406     @Override
 407     protected void applyParameter(Transformer transformer, String namespace,
 408             String name, String value) {
 409         try {
 410             // Munge the namespace into the name per
 411             //  javax.xml.transform.Transformer.setParameter()
 412             if (null != namespace) {
 413                 name = "{" + namespace + "}" + name;
 414             }
 415             transformer.setParameter(name, value);
 416         } catch (Exception e) {
 417             throw new IllegalArgumentException("applyParameter threw: " + e.toString());
 418         }
 419     }
 420 
 421     /**
 422      * Ensure newProcessor has been called when needed.
 423      *
 424      * Prevent users from shooting themselves in the foot by calling a
 425      * transform* API before newProcessor().
 426      *
 427      * @throws java.lang.Exception
 428      */
 429     public void preventFootShooting() throws Exception {
 430         if (null == factory) {
 431             newProcessor(newProcessorOpts);
 432         }
 433     }
 434 }