/* * Copyright 1999-2004 The Apache Software Foundation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * $Id: Redirect.java,v 1.23 2004/08/13 21:08:27 minchau Exp $ */ package org.apache.xalan.lib; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; import java.util.Hashtable; import javax.xml.transform.Result; import javax.xml.transform.TransformerException; import javax.xml.transform.stream.StreamResult; import org.apache.xalan.extensions.XSLProcessorContext; import org.apache.xalan.res.XSLTErrorResources; import org.apache.xalan.templates.ElemExtensionCall; import org.apache.xalan.templates.OutputProperties; import org.apache.xalan.transformer.TransformerImpl; import org.apache.xpath.XPath; import org.apache.xpath.objects.XObject; import org.apache.xml.serializer.SerializationHandler; import org.xml.sax.ContentHandler; /** * Implements three extension elements to allow an XSLT transformation to * redirect its output to multiple output files. * * It is accessed by specifying a namespace URI as follows: * <pre> * xmlns:redirect="http://xml.apache.org/xalan/redirect" * </pre> * * <p>You can either just use redirect:write, in which case the file will be * opened and immediately closed after the write, or you can bracket the * write calls by redirect:open and redirect:close, in which case the * file will be kept open for multiple writes until the close call is * encountered. Calls can be nested. * * <p>Calls can take a 'file' attribute * and/or a 'select' attribute in order to get the filename. If a select * attribute is encountered, it will evaluate that expression for a string * that indicates the filename. If the string evaluates to empty, it will * attempt to use the 'file' attribute as a default. Filenames can be relative * or absolute. If they are relative, the base directory will be the same as * the base directory for the output document. This is obtained by calling * getOutputTarget() on the TransformerImpl. You can set this base directory * by calling TransformerImpl.setOutputTarget() or it is automatically set * when using the two argument form of transform() or transformNode(). * * <p>Calls to redirect:write and redirect:open also take an optional * attribute append="true|yes", which will attempt to simply append * to an existing file instead of always opening a new file. The * default behavior of always overwriting the file still happens * if you do not specify append. * <p><b>Note:</b> this may give unexpected results when using xml * or html output methods, since this is <b>not</b> coordinated * with the serializers - hence, you may get extra xml decls in * the middle of your file after appending to it. * * <p>Example:</p> * <PRE> * <?xml version="1.0"?> * <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" * version="1.0" * xmlns:redirect="http://xml.apache.org/xalan/redirect" * extension-element-prefixes="redirect"> * * <xsl:template match="/"> * <out> * default output. * </out> * <redirect:open file="doc3.out"/> * <redirect:write file="doc3.out"> * <out> * <redirect:write file="doc1.out"> * <out> * doc1 output. * <redirect:write file="doc3.out"> * Some text to doc3 * </redirect:write> * </out> * </redirect:write> * <redirect:write file="doc2.out"> * <out> * doc2 output. * <redirect:write file="doc3.out"> * Some more text to doc3 * <redirect:write select="doc/foo"> * text for doc4 * </redirect:write> * </redirect:write> * </out> * </redirect:write> * </out> * </redirect:write> * <redirect:close file="doc3.out"/> * </xsl:template> * * </xsl:stylesheet> * </PRE> * * @author Scott Boag * @version 1.0 * @see <a href="../../../../../../extensions.html#ex-redirect" target="_top">Example with Redirect extension</a> */ public class Redirect { /** * List of formatter listeners indexed by filename. */ protected Hashtable m_formatterListeners = new Hashtable (); /** * List of output streams indexed by filename. */ protected Hashtable m_outputStreams = new Hashtable (); /** * Default append mode for bare open calls. * False for backwards compatibility (I think). */ public static final boolean DEFAULT_APPEND_OPEN = false; /** * Default append mode for bare write calls. * False for backwards compatibility. */ public static final boolean DEFAULT_APPEND_WRITE = false; /** * Open the given file and put it in the XML, HTML, or Text formatter listener's table. */ public void open(XSLProcessorContext context, ElemExtensionCall elem) throws java.net.MalformedURLException, java.io.FileNotFoundException, java.io.IOException, TransformerException { String fileName = getFilename(context, elem); Object flistener = m_formatterListeners.get(fileName); if(null == flistener) { String mkdirsExpr = elem.getAttribute ("mkdirs", context.getContextNode(), context.getTransformer()); boolean mkdirs = (mkdirsExpr != null) ? (mkdirsExpr.equals("true") || mkdirsExpr.equals("yes")) : true; // Whether to append to existing files or not, <jpvdm@iafrica.com> String appendExpr = elem.getAttribute("append", context.getContextNode(), context.getTransformer()); boolean append = (appendExpr != null) ? (appendExpr.equals("true") || appendExpr.equals("yes")) : DEFAULT_APPEND_OPEN; Object ignored = makeFormatterListener(context, elem, fileName, true, mkdirs, append); } } /** * Write the evalutation of the element children to the given file. Then close the file * unless it was opened with the open extension element and is in the formatter listener's table. */ public void write(XSLProcessorContext context, ElemExtensionCall elem) throws java.net.MalformedURLException, java.io.FileNotFoundException, java.io.IOException, TransformerException { String fileName = getFilename(context, elem); Object flObject = m_formatterListeners.get(fileName); ContentHandler formatter; boolean inTable = false; if(null == flObject) { String mkdirsExpr = ((ElemExtensionCall)elem).getAttribute ("mkdirs", context.getContextNode(), context.getTransformer()); boolean mkdirs = (mkdirsExpr != null) ? (mkdirsExpr.equals("true") || mkdirsExpr.equals("yes")) : true; // Whether to append to existing files or not, <jpvdm@iafrica.com> String appendExpr = elem.getAttribute("append", context.getContextNode(), context.getTransformer()); boolean append = (appendExpr != null) ? (appendExpr.equals("true") || appendExpr.equals("yes")) : DEFAULT_APPEND_WRITE; formatter = makeFormatterListener(context, elem, fileName, true, mkdirs, append); } else { inTable = true; formatter = (ContentHandler)flObject; } TransformerImpl transf = context.getTransformer(); startRedirection(transf, formatter); // for tracing only transf.executeChildTemplates(elem, context.getContextNode(), context.getMode(), formatter); endRedirection(transf); // for tracing only if(!inTable) { OutputStream ostream = (OutputStream)m_outputStreams.get(fileName); if(null != ostream) { try { formatter.endDocument(); } catch(org.xml.sax.SAXException se) { throw new TransformerException(se); } ostream.close(); m_outputStreams.remove(fileName); m_formatterListeners.remove(fileName); } } } /** * Close the given file and remove it from the formatter listener's table. */ public void close(XSLProcessorContext context, ElemExtensionCall elem) throws java.net.MalformedURLException, java.io.FileNotFoundException, java.io.IOException, TransformerException { String fileName = getFilename(context, elem); Object formatterObj = m_formatterListeners.get(fileName); if(null != formatterObj) { ContentHandler fl = (ContentHandler)formatterObj; try { fl.endDocument(); } catch(org.xml.sax.SAXException se) { throw new TransformerException(se); } OutputStream ostream = (OutputStream)m_outputStreams.get(fileName); if(null != ostream) { ostream.close(); m_outputStreams.remove(fileName); } m_formatterListeners.remove(fileName); } } /** * Get the filename from the 'select' or the 'file' attribute. */ private String getFilename(XSLProcessorContext context, ElemExtensionCall elem) throws java.net.MalformedURLException, java.io.FileNotFoundException, java.io.IOException, TransformerException { String fileName; String fileNameExpr = ((ElemExtensionCall)elem).getAttribute ("select", context.getContextNode(), context.getTransformer()); if(null != fileNameExpr) { org.apache.xpath.XPathContext xctxt = context.getTransformer().getXPathContext(); XPath myxpath = new XPath(fileNameExpr, elem, xctxt.getNamespaceContext(), XPath.SELECT); XObject xobj = myxpath.execute(xctxt, context.getContextNode(), elem); fileName = xobj.str(); if((null == fileName) || (fileName.length() == 0)) { fileName = elem.getAttribute ("file", context.getContextNode(), context.getTransformer()); } } else { fileName = elem.getAttribute ("file", context.getContextNode(), context.getTransformer()); } if(null == fileName) { context.getTransformer().getMsgMgr().error(elem, elem, context.getContextNode(), XSLTErrorResources.ER_REDIRECT_COULDNT_GET_FILENAME); //"Redirect extension: Could not get filename - file or select attribute must return vald string."); } return fileName; } // yuck. // Note: this is not the best way to do this, and may not even // be fully correct! Patches (with test cases) welcomed. -sc private String urlToFileName(String base) { if(null != base) { if(base.startsWith("file:////")) { base = base.substring(7); } else if(base.startsWith("file:///")) { base = base.substring(6); } else if(base.startsWith("file://")) { base = base.substring(5); // absolute? } else if(base.startsWith("file:/")) { base = base.substring(5); } else if(base.startsWith("file:")) { base = base.substring(4); } } return base; } /** * Create a new ContentHandler, based on attributes of the current ContentHandler. */ private ContentHandler makeFormatterListener(XSLProcessorContext context, ElemExtensionCall elem, String fileName, boolean shouldPutInTable, boolean mkdirs, boolean append) throws java.net.MalformedURLException, java.io.FileNotFoundException, java.io.IOException, TransformerException { File file = new File(fileName); TransformerImpl transformer = context.getTransformer(); String base; // Base URI to use for relative paths if(!file.isAbsolute()) { // This code is attributed to Jon Grov <jon@linpro.no>. A relative file name // is relative to the Result used to kick off the transform. If no such // Result was supplied, the filename is relative to the source document. // When transforming with a SAXResult or DOMResult, call // TransformerImpl.setOutputTarget() to set the desired Result base. // String base = urlToFileName(elem.getStylesheet().getSystemId()); Result outputTarget = transformer.getOutputTarget(); if ( (null != outputTarget) && ((base = outputTarget.getSystemId()) != null) ) { base = urlToFileName(base); } else { base = urlToFileName(transformer.getBaseURLOfSource()); } if(null != base) { File baseFile = new File(base); file = new File(baseFile.getParent(), fileName); } // System.out.println("file is: "+file.toString()); } if(mkdirs) { String dirStr = file.getParent(); if((null != dirStr) && (dirStr.length() > 0)) { File dir = new File(dirStr); dir.mkdirs(); } } // This should be worked on so that the output format can be // defined by a first child of the redirect element. OutputProperties format = transformer.getOutputFormat(); // FileOutputStream ostream = new FileOutputStream(file); // Patch from above line to below by <jpvdm@iafrica.com> // Note that in JDK 1.2.2 at least, FileOutputStream(File) // is implemented as a call to // FileOutputStream(File.getPath, append), thus this should be // the equivalent instead of getAbsolutePath() FileOutputStream ostream = new FileOutputStream(file.getPath(), append); try { SerializationHandler flistener = createSerializationHandler(transformer, ostream, file, format); try { flistener.startDocument(); } catch(org.xml.sax.SAXException se) { throw new TransformerException(se); } if(shouldPutInTable) { m_outputStreams.put(fileName, ostream); m_formatterListeners.put(fileName, flistener); } return flistener; } catch(TransformerException te) { throw new TransformerException(te); } } /** * A class that extends this class can over-ride this public method and recieve * a callback that redirection is about to start * @param transf The transformer. * @param formatter The handler that receives the redirected output */ public void startRedirection(TransformerImpl transf, ContentHandler formatter) { // A class that extends this class could provide a method body } /** * A class that extends this class can over-ride this public method and receive * a callback that redirection to the ContentHandler specified in the startRedirection() * call has ended * @param transf The transformer. */ public void endRedirection(TransformerImpl transf) { // A class that extends this class could provide a method body } /** * A class that extends this one could over-ride this public method and receive * a callback for the creation of the serializer used in the redirection. * @param transformer The transformer * @param ostream The output stream that the serializer wraps * @param file The file associated with the ostream * @param format The format parameter used to create the serializer * @return the serializer that the redirection will go to. * * @throws java.io.IOException * @throws javax.xml.transform.TransformerException */ public SerializationHandler createSerializationHandler( TransformerImpl transformer, FileOutputStream ostream, File file, OutputProperties format) throws java.io.IOException, TransformerException { SerializationHandler serializer = transformer.createSerializationHandler( new StreamResult(ostream), format); return serializer; } }