001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements. See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership. The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the  "License");
007     * you may not use this file except in compliance with the License.
008     * You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    /*
019     * $Id: Redirect.java 468639 2006-10-28 06:52:33Z minchau $
020     */
021    package org.apache.xalan.lib;
022    
023    import java.io.File;
024    import java.io.FileOutputStream;
025    import java.io.OutputStream;
026    import java.util.Hashtable;
027    
028    import javax.xml.transform.Result;
029    import javax.xml.transform.TransformerException;
030    import javax.xml.transform.stream.StreamResult;
031    
032    import org.apache.xalan.extensions.XSLProcessorContext;
033    import org.apache.xalan.res.XSLTErrorResources;
034    import org.apache.xalan.templates.ElemExtensionCall;
035    import org.apache.xalan.templates.OutputProperties;
036    import org.apache.xalan.transformer.TransformerImpl;
037    import org.apache.xpath.XPath;
038    import org.apache.xpath.objects.XObject;
039    import org.apache.xml.serializer.SerializationHandler;
040    import org.xml.sax.ContentHandler;
041    
042    /**
043     * Implements three extension elements to allow an XSLT transformation to
044     * redirect its output to multiple output files.
045     *
046     * It is accessed by specifying a namespace URI as follows:
047     * <pre>
048     *    xmlns:redirect="http://xml.apache.org/xalan/redirect"
049     * </pre>
050     *
051     * <p>You can either just use redirect:write, in which case the file will be
052     * opened and immediately closed after the write, or you can bracket the
053     * write calls by redirect:open and redirect:close, in which case the
054     * file will be kept open for multiple writes until the close call is
055     * encountered.  Calls can be nested.  
056     *
057     * <p>Calls can take a 'file' attribute
058     * and/or a 'select' attribute in order to get the filename.  If a select
059     * attribute is encountered, it will evaluate that expression for a string
060     * that indicates the filename.  If the string evaluates to empty, it will
061     * attempt to use the 'file' attribute as a default.  Filenames can be relative
062     * or absolute.  If they are relative, the base directory will be the same as
063     * the base directory for the output document.  This is obtained by calling
064     * getOutputTarget() on the TransformerImpl.  You can set this base directory
065     * by calling TransformerImpl.setOutputTarget() or it is automatically set
066     * when using the two argument form of transform() or transformNode().
067     *
068     * <p>Calls to redirect:write and redirect:open also take an optional 
069     * attribute append="true|yes", which will attempt to simply append 
070     * to an existing file instead of always opening a new file.  The 
071     * default behavior of always overwriting the file still happens 
072     * if you do not specify append.
073     * <p><b>Note:</b> this may give unexpected results when using xml 
074     * or html output methods, since this is <b>not</b> coordinated 
075     * with the serializers - hence, you may get extra xml decls in 
076     * the middle of your file after appending to it.
077     *
078     * <p>Example:</p>
079     * <PRE>
080     * &lt;?xml version="1.0"?>
081     * &lt;xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
082     *                 version="1.0"
083     *                 xmlns:redirect="http://xml.apache.org/xalan/redirect"
084     *                 extension-element-prefixes="redirect">
085     *
086     *   &lt;xsl:template match="/">
087     *     &lt;out>
088     *       default output.
089     *     &lt;/out>
090     *     &lt;redirect:open file="doc3.out"/>
091     *     &lt;redirect:write file="doc3.out">
092     *       &lt;out>
093     *         &lt;redirect:write file="doc1.out">
094     *           &lt;out>
095     *             doc1 output.
096     *             &lt;redirect:write file="doc3.out">
097     *               Some text to doc3
098     *             &lt;/redirect:write>
099     *           &lt;/out>
100     *         &lt;/redirect:write>
101     *         &lt;redirect:write file="doc2.out">
102     *           &lt;out>
103     *             doc2 output.
104     *             &lt;redirect:write file="doc3.out">
105     *               Some more text to doc3
106     *               &lt;redirect:write select="doc/foo">
107     *                 text for doc4
108     *               &lt;/redirect:write>
109     *             &lt;/redirect:write>
110     *           &lt;/out>
111     *         &lt;/redirect:write>
112     *       &lt;/out>
113     *     &lt;/redirect:write>
114     *     &lt;redirect:close file="doc3.out"/>
115     *   &lt;/xsl:template>
116     *
117     * &lt;/xsl:stylesheet>
118     * </PRE>
119     *
120     * @author Scott Boag
121     * @version 1.0
122     * @see <a href="../../../../../../extensions.html#ex-redirect" target="_top">Example with Redirect extension</a>
123     */
124    public class Redirect
125    {
126      /**
127       * List of formatter listeners indexed by filename.
128       */
129      protected Hashtable m_formatterListeners = new Hashtable ();
130    
131      /**
132       * List of output streams indexed by filename.
133       */
134      protected Hashtable m_outputStreams = new Hashtable ();
135    
136      /** 
137       * Default append mode for bare open calls.  
138       * False for backwards compatibility (I think). 
139       */
140      public static final boolean DEFAULT_APPEND_OPEN = false;
141    
142      /** 
143       * Default append mode for bare write calls.  
144       * False for backwards compatibility. 
145       */
146      public static final boolean DEFAULT_APPEND_WRITE = false;
147    
148      /**
149       * Open the given file and put it in the XML, HTML, or Text formatter listener's table.
150       */
151      public void open(XSLProcessorContext context, ElemExtensionCall elem)
152        throws java.net.MalformedURLException,
153               java.io.FileNotFoundException,
154               java.io.IOException,
155               javax.xml.transform.TransformerException
156      {
157        String fileName = getFilename(context, elem);
158        Object flistener = m_formatterListeners.get(fileName);
159        if(null == flistener)
160        {
161          String mkdirsExpr 
162            = elem.getAttribute ("mkdirs", context.getContextNode(), 
163                                                      context.getTransformer());
164          boolean mkdirs = (mkdirsExpr != null)
165                           ? (mkdirsExpr.equals("true") || mkdirsExpr.equals("yes")) : true;
166    
167          // Whether to append to existing files or not, <jpvdm@iafrica.com>
168          String appendExpr = elem.getAttribute("append", context.getContextNode(), context.getTransformer());
169              boolean append = (appendExpr != null)
170                           ? (appendExpr.equals("true") || appendExpr.equals("yes")) : DEFAULT_APPEND_OPEN;
171    
172          Object ignored = makeFormatterListener(context, elem, fileName, true, mkdirs, append);
173        }
174      }
175      
176      /**
177       * Write the evalutation of the element children to the given file. Then close the file
178       * unless it was opened with the open extension element and is in the formatter listener's table.
179       */
180      public void write(XSLProcessorContext context, ElemExtensionCall elem)
181        throws java.net.MalformedURLException,
182               java.io.FileNotFoundException,
183               java.io.IOException,
184               javax.xml.transform.TransformerException
185      {
186        String fileName = getFilename(context, elem);
187        Object flObject = m_formatterListeners.get(fileName);
188        ContentHandler formatter;
189        boolean inTable = false;
190        if(null == flObject)
191        {
192          String mkdirsExpr 
193            = ((ElemExtensionCall)elem).getAttribute ("mkdirs", 
194                                                      context.getContextNode(), 
195                                                      context.getTransformer());
196          boolean mkdirs = (mkdirsExpr != null)
197                           ? (mkdirsExpr.equals("true") || mkdirsExpr.equals("yes")) : true;
198    
199          // Whether to append to existing files or not, <jpvdm@iafrica.com>
200          String appendExpr = elem.getAttribute("append", context.getContextNode(), context.getTransformer());
201              boolean append = (appendExpr != null)
202                           ? (appendExpr.equals("true") || appendExpr.equals("yes")) : DEFAULT_APPEND_WRITE;
203    
204          formatter = makeFormatterListener(context, elem, fileName, true, mkdirs, append);
205        }
206        else
207        {
208          inTable = true;
209          formatter = (ContentHandler)flObject;
210        }
211        
212        TransformerImpl transf = context.getTransformer();
213        
214        startRedirection(transf, formatter);  // for tracing only
215        
216        transf.executeChildTemplates(elem,
217                                     context.getContextNode(),
218                                     context.getMode(), formatter);
219                                     
220        endRedirection(transf); // for tracing only
221        
222        if(!inTable)
223        {
224          OutputStream ostream = (OutputStream)m_outputStreams.get(fileName);
225          if(null != ostream)
226          {
227            try
228            {
229              formatter.endDocument();
230            }
231            catch(org.xml.sax.SAXException se)
232            {
233              throw new TransformerException(se);
234            }
235            ostream.close();
236            m_outputStreams.remove(fileName);
237            m_formatterListeners.remove(fileName);
238          }
239        }
240      }
241    
242    
243      /**
244       * Close the given file and remove it from the formatter listener's table.
245       */
246      public void close(XSLProcessorContext context, ElemExtensionCall elem)
247        throws java.net.MalformedURLException,
248        java.io.FileNotFoundException,
249        java.io.IOException,
250        javax.xml.transform.TransformerException
251      {
252        String fileName = getFilename(context, elem);
253        Object formatterObj = m_formatterListeners.get(fileName);
254        if(null != formatterObj)
255        {
256          ContentHandler fl = (ContentHandler)formatterObj;
257          try
258          {
259            fl.endDocument();
260          }
261          catch(org.xml.sax.SAXException se)
262          {
263            throw new TransformerException(se);
264          }
265          OutputStream ostream = (OutputStream)m_outputStreams.get(fileName);
266          if(null != ostream)
267          {
268            ostream.close();
269            m_outputStreams.remove(fileName);
270          }
271          m_formatterListeners.remove(fileName);
272        }
273      }
274    
275      /**
276       * Get the filename from the 'select' or the 'file' attribute.
277       */
278      private String getFilename(XSLProcessorContext context, ElemExtensionCall elem)
279        throws java.net.MalformedURLException,
280        java.io.FileNotFoundException,
281        java.io.IOException,
282        javax.xml.transform.TransformerException
283      {
284        String fileName;
285        String fileNameExpr 
286          = ((ElemExtensionCall)elem).getAttribute ("select", 
287                                                    context.getContextNode(), 
288                                                    context.getTransformer());
289        if(null != fileNameExpr)
290        {
291          org.apache.xpath.XPathContext xctxt 
292            = context.getTransformer().getXPathContext();
293          XPath myxpath = new XPath(fileNameExpr, elem, xctxt.getNamespaceContext(), XPath.SELECT);
294          XObject xobj = myxpath.execute(xctxt, context.getContextNode(), elem);
295          fileName = xobj.str();
296          if((null == fileName) || (fileName.length() == 0))
297          {
298            fileName = elem.getAttribute ("file", 
299                                          context.getContextNode(), 
300                                          context.getTransformer());
301          }
302        }
303        else
304        {
305          fileName = elem.getAttribute ("file", context.getContextNode(), 
306                                                                   context.getTransformer());
307        }
308        if(null == fileName)
309        {
310          context.getTransformer().getMsgMgr().error(elem, elem, 
311                                         context.getContextNode(), 
312                                         XSLTErrorResources.ER_REDIRECT_COULDNT_GET_FILENAME);
313                                  //"Redirect extension: Could not get filename - file or select attribute must return vald string.");
314        }
315        return fileName;
316      }
317      
318      // yuck.
319      // Note: this is not the best way to do this, and may not even 
320      //    be fully correct! Patches (with test cases) welcomed. -sc
321      private String urlToFileName(String base)
322      {
323        if(null != base)
324        {
325          if(base.startsWith("file:////"))
326          {
327            base = base.substring(7);
328          }
329          else if(base.startsWith("file:///"))
330          {
331            base = base.substring(6);
332          }
333          else if(base.startsWith("file://"))
334          {
335            base = base.substring(5); // absolute?
336          }
337          else if(base.startsWith("file:/"))
338          {
339            base = base.substring(5);
340          }
341          else if(base.startsWith("file:"))
342          {
343            base = base.substring(4);
344          }
345        }
346        return base;
347      }
348    
349      /**
350       * Create a new ContentHandler, based on attributes of the current ContentHandler.
351       */
352      private ContentHandler makeFormatterListener(XSLProcessorContext context,
353                                                   ElemExtensionCall elem,
354                                                   String fileName,
355                                                   boolean shouldPutInTable,
356                                                   boolean mkdirs, 
357                                                   boolean append)
358        throws java.net.MalformedURLException,
359        java.io.FileNotFoundException,
360        java.io.IOException,
361        javax.xml.transform.TransformerException
362      {
363        File file = new File(fileName);
364        TransformerImpl transformer = context.getTransformer();
365        String base;          // Base URI to use for relative paths
366    
367        if(!file.isAbsolute())
368        {
369          // This code is attributed to Jon Grov <jon@linpro.no>.  A relative file name
370          // is relative to the Result used to kick off the transform.  If no such
371          // Result was supplied, the filename is relative to the source document.
372          // When transforming with a SAXResult or DOMResult, call
373          // TransformerImpl.setOutputTarget() to set the desired Result base.
374      //      String base = urlToFileName(elem.getStylesheet().getSystemId());
375    
376          Result outputTarget = transformer.getOutputTarget();
377          if ( (null != outputTarget) && ((base = outputTarget.getSystemId()) != null) ) {
378            base = urlToFileName(base);
379          }
380          else
381          {
382            base = urlToFileName(transformer.getBaseURLOfSource());
383          }
384    
385          if(null != base)
386          {
387            File baseFile = new File(base);
388            file = new File(baseFile.getParent(), fileName);
389          }
390          // System.out.println("file is: "+file.toString());
391        }
392    
393        if(mkdirs)
394        {
395          String dirStr = file.getParent();
396          if((null != dirStr) && (dirStr.length() > 0))
397          {
398            File dir = new File(dirStr);
399            dir.mkdirs();
400          }
401        }
402    
403        // This should be worked on so that the output format can be 
404        // defined by a first child of the redirect element.
405        OutputProperties format = transformer.getOutputFormat();
406    
407        // FileOutputStream ostream = new FileOutputStream(file);
408        // Patch from above line to below by <jpvdm@iafrica.com>
409        //  Note that in JDK 1.2.2 at least, FileOutputStream(File)
410        //  is implemented as a call to 
411        //  FileOutputStream(File.getPath, append), thus this should be 
412        //  the equivalent instead of getAbsolutePath()
413        FileOutputStream ostream = new FileOutputStream(file.getPath(), append);
414        
415        try
416        {
417          SerializationHandler flistener = 
418            createSerializationHandler(transformer, ostream, file, format);
419            
420          try
421          {
422            flistener.startDocument();
423          }
424          catch(org.xml.sax.SAXException se)
425          {
426            throw new TransformerException(se);
427          }
428          if(shouldPutInTable)
429          {
430            m_outputStreams.put(fileName, ostream);
431            m_formatterListeners.put(fileName, flistener);
432          }
433          return flistener;
434        }
435        catch(TransformerException te)
436        {
437          throw new javax.xml.transform.TransformerException(te);
438        }
439        
440      }
441    
442      /**
443       * A class that extends this class can over-ride this public method and recieve
444       * a callback that redirection is about to start
445       * @param transf The transformer.
446       * @param formatter The handler that receives the redirected output
447       */
448      public void startRedirection(TransformerImpl transf, ContentHandler formatter)
449      {
450          // A class that extends this class could provide a method body        
451      }
452        
453      /**
454       * A class that extends this class can over-ride this public method and receive
455       * a callback that redirection to the ContentHandler specified in the startRedirection()
456       * call has ended
457       * @param transf The transformer.
458       */
459      public void endRedirection(TransformerImpl transf)
460      {
461          // A class that extends this class could provide a method body        
462      }
463        
464      /**
465       * A class that extends this one could over-ride this public method and receive
466       * a callback for the creation of the serializer used in the redirection.
467       * @param transformer The transformer
468       * @param ostream The output stream that the serializer wraps
469       * @param file The file associated with the ostream
470       * @param format The format parameter used to create the serializer
471       * @return the serializer that the redirection will go to.
472       * 
473       * @throws java.io.IOException
474       * @throws TransformerException
475       */
476      public SerializationHandler createSerializationHandler(
477            TransformerImpl transformer,
478            FileOutputStream ostream,
479            File file,
480            OutputProperties format) 
481            throws java.io.IOException, TransformerException
482      {
483    
484          SerializationHandler serializer =
485              transformer.createSerializationHandler(
486                  new StreamResult(ostream),
487                  format);
488          return serializer;
489      }
490    }