| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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$ |
| */ |
| 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, |
| javax.xml.transform.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, |
| javax.xml.transform.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, |
| javax.xml.transform.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, |
| javax.xml.transform.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, |
| javax.xml.transform.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 javax.xml.transform.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 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; |
| } |
| } |