1. /*
  2. * The Apache Software License, Version 1.1
  3. *
  4. *
  5. * Copyright (c) 1999 The Apache Software Foundation. All rights
  6. * reserved.
  7. *
  8. * Redistribution and use in source and binary forms, with or without
  9. * modification, are permitted provided that the following conditions
  10. * are met:
  11. *
  12. * 1. Redistributions of source code must retain the above copyright
  13. * notice, this list of conditions and the following disclaimer.
  14. *
  15. * 2. Redistributions in binary form must reproduce the above copyright
  16. * notice, this list of conditions and the following disclaimer in
  17. * the documentation and/or other materials provided with the
  18. * distribution.
  19. *
  20. * 3. The end-user documentation included with the redistribution,
  21. * if any, must include the following acknowledgment:
  22. * "This product includes software developed by the
  23. * Apache Software Foundation (http://www.apache.org/)."
  24. * Alternately, this acknowledgment may appear in the software itself,
  25. * if and wherever such third-party acknowledgments normally appear.
  26. *
  27. * 4. The names "Xalan" and "Apache Software Foundation" must
  28. * not be used to endorse or promote products derived from this
  29. * software without prior written permission. For written
  30. * permission, please contact apache@apache.org.
  31. *
  32. * 5. Products derived from this software may not be called "Apache",
  33. * nor may "Apache" appear in their name, without prior written
  34. * permission of the Apache Software Foundation.
  35. *
  36. * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
  37. * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  38. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  39. * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
  40. * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  41. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  42. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
  43. * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  44. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  45. * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
  46. * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  47. * SUCH DAMAGE.
  48. * ====================================================================
  49. *
  50. * This software consists of voluntary contributions made by many
  51. * individuals on behalf of the Apache Software Foundation and was
  52. * originally based on software copyright (c) 1999, Lotus
  53. * Development Corporation., http://www.lotus.com. For more
  54. * information on the Apache Software Foundation, please see
  55. * <http://www.apache.org/>.
  56. */
  57. package org.apache.xalan.lib;
  58. import java.util.*;
  59. import java.io.*;
  60. import java.net.URL;
  61. import org.xml.sax.ContentHandler;
  62. import org.apache.xalan.extensions.XSLProcessorContext;
  63. import org.apache.xalan.transformer.TransformerImpl;
  64. import org.apache.xalan.templates.StylesheetRoot;
  65. import org.apache.xalan.templates.ElemExtensionCall;
  66. import org.apache.xalan.templates.OutputProperties;
  67. import org.apache.xalan.res.XSLTErrorResources;
  68. import org.apache.xpath.objects.XObject;
  69. import org.apache.xpath.XPath;
  70. import javax.xml.transform.stream.StreamResult;
  71. import javax.xml.transform.Result;
  72. import javax.xml.transform.TransformerException;
  73. import org.w3c.dom.*;
  74. /**
  75. * Implements three extension elements to allow an XSLT transformation to
  76. * redirect its output to multiple output files.
  77. *
  78. * It is accessed by specifying a namespace URI as follows:
  79. * <pre>
  80. * xmlns:redirect="http://xml.apache.org/xalan/redirect"
  81. * </pre>
  82. *
  83. * <p>You can either just use redirect:write, in which case the file will be
  84. * opened and immediately closed after the write, or you can bracket the
  85. * write calls by redirect:open and redirect:close, in which case the
  86. * file will be kept open for multiple writes until the close call is
  87. * encountered. Calls can be nested.
  88. *
  89. * <p>Calls can take a 'file' attribute
  90. * and/or a 'select' attribute in order to get the filename. If a select
  91. * attribute is encountered, it will evaluate that expression for a string
  92. * that indicates the filename. If the string evaluates to empty, it will
  93. * attempt to use the 'file' attribute as a default. Filenames can be relative
  94. * or absolute. If they are relative, the base directory will be the same as
  95. * the base directory for the output document. This is obtained by calling
  96. * getOutputTarget() on the TransformerImpl. You can set this base directory
  97. * by calling TransformerImpl.setOutputTarget() or it is automatically set
  98. * when using the two argument form of transform() or transformNode().
  99. *
  100. * <p>Calls to redirect:write and redirect:open also take an optional
  101. * attribute append="true|yes", which will attempt to simply append
  102. * to an existing file instead of always opening a new file. The
  103. * default behavior of always overwriting the file still happens
  104. * if you do not specify append.
  105. * <p><b>Note:</b> this may give unexpected results when using xml
  106. * or html output methods, since this is <b>not</b> coordinated
  107. * with the serializers - hence, you may get extra xml decls in
  108. * the middle of your file after appending to it.
  109. *
  110. * <p>Example:</p>
  111. * <PRE>
  112. * <?xml version="1.0"?>
  113. * <xsl:stylesheet xmlns:xsl="http://www.w3.org/XSL/Transform/1.0"
  114. * xmlns:redirect="http://xml.apache.org/xalan/redirect"
  115. * extension-element-prefixes="redirect">
  116. *
  117. * <xsl:template match="/">
  118. * <out>
  119. * default output.
  120. * </out>
  121. * <redirect:open file="doc3.out"/>
  122. * <redirect:write file="doc3.out">
  123. * <out>
  124. * <redirect:write file="doc1.out">
  125. * <out>
  126. * doc1 output.
  127. * <redirect:write file="doc3.out">
  128. * Some text to doc3
  129. * </redirect:write>
  130. * </out>
  131. * </redirect:write>
  132. * <redirect:write file="doc2.out">
  133. * <out>
  134. * doc2 output.
  135. * <redirect:write file="doc3.out">
  136. * Some more text to doc3
  137. * <redirect:write select="doc/foo">
  138. * text for doc4
  139. * </redirect:write>
  140. * </redirect:write>
  141. * </out>
  142. * </redirect:write>
  143. * </out>
  144. * </redirect:write>
  145. * <redirect:close file="doc3.out"/>
  146. * </xsl:template>
  147. *
  148. * </xsl:stylesheet>
  149. * </PRE>
  150. *
  151. * @author Scott Boag
  152. * @version 1.0
  153. * @see <a href="../../../../../../extensions.html#ex-redirect" target="_top">Example with Redirect extension</a>
  154. */
  155. public class Redirect
  156. {
  157. /**
  158. * List of formatter listeners indexed by filename.
  159. */
  160. protected Hashtable m_formatterListeners = new Hashtable ();
  161. /**
  162. * List of output streams indexed by filename.
  163. */
  164. protected Hashtable m_outputStreams = new Hashtable ();
  165. /**
  166. * Default append mode for bare open calls.
  167. * False for backwards compatibility (I think).
  168. */
  169. public static final boolean DEFAULT_APPEND_OPEN = false;
  170. /**
  171. * Default append mode for bare write calls.
  172. * False for backwards compatibility.
  173. */
  174. public static final boolean DEFAULT_APPEND_WRITE = false;
  175. /**
  176. * Open the given file and put it in the XML, HTML, or Text formatter listener's table.
  177. */
  178. public void open(XSLProcessorContext context, ElemExtensionCall elem)
  179. throws java.net.MalformedURLException,
  180. java.io.FileNotFoundException,
  181. java.io.IOException,
  182. javax.xml.transform.TransformerException
  183. {
  184. String fileName = getFilename(context, elem);
  185. Object flistener = m_formatterListeners.get(fileName);
  186. if(null == flistener)
  187. {
  188. String mkdirsExpr
  189. = elem.getAttribute ("mkdirs", context.getContextNode(),
  190. context.getTransformer());
  191. boolean mkdirs = (mkdirsExpr != null)
  192. ? (mkdirsExpr.equals("true") || mkdirsExpr.equals("yes")) : true;
  193. // Whether to append to existing files or not, <jpvdm@iafrica.com>
  194. String appendExpr = elem.getAttribute("append", context.getContextNode(), context.getTransformer());
  195. boolean append = (appendExpr != null)
  196. ? (appendExpr.equals("true") || appendExpr.equals("yes")) : DEFAULT_APPEND_OPEN;
  197. Object ignored = makeFormatterListener(context, elem, fileName, true, mkdirs, append);
  198. }
  199. }
  200. /**
  201. * Write the evalutation of the element children to the given file. Then close the file
  202. * unless it was opened with the open extension element and is in the formatter listener's table.
  203. */
  204. public void write(XSLProcessorContext context, ElemExtensionCall elem)
  205. throws java.net.MalformedURLException,
  206. java.io.FileNotFoundException,
  207. java.io.IOException,
  208. javax.xml.transform.TransformerException
  209. {
  210. String fileName = getFilename(context, elem);
  211. Object flObject = m_formatterListeners.get(fileName);
  212. ContentHandler formatter;
  213. boolean inTable = false;
  214. if(null == flObject)
  215. {
  216. String mkdirsExpr
  217. = ((ElemExtensionCall)elem).getAttribute ("mkdirs",
  218. context.getContextNode(),
  219. context.getTransformer());
  220. boolean mkdirs = (mkdirsExpr != null)
  221. ? (mkdirsExpr.equals("true") || mkdirsExpr.equals("yes")) : true;
  222. // Whether to append to existing files or not, <jpvdm@iafrica.com>
  223. String appendExpr = elem.getAttribute("append", context.getContextNode(), context.getTransformer());
  224. boolean append = (appendExpr != null)
  225. ? (appendExpr.equals("true") || appendExpr.equals("yes")) : DEFAULT_APPEND_WRITE;
  226. formatter = makeFormatterListener(context, elem, fileName, true, mkdirs, append);
  227. }
  228. else
  229. {
  230. inTable = true;
  231. formatter = (ContentHandler)flObject;
  232. }
  233. TransformerImpl transf = context.getTransformer();
  234. transf.executeChildTemplates(elem,
  235. context.getContextNode(),
  236. context.getMode(), formatter);
  237. if(!inTable)
  238. {
  239. OutputStream ostream = (OutputStream)m_outputStreams.get(fileName);
  240. if(null != ostream)
  241. {
  242. try
  243. {
  244. formatter.endDocument();
  245. }
  246. catch(org.xml.sax.SAXException se)
  247. {
  248. throw new TransformerException(se);
  249. }
  250. ostream.close();
  251. m_outputStreams.remove(fileName);
  252. m_formatterListeners.remove(fileName);
  253. }
  254. }
  255. }
  256. /**
  257. * Close the given file and remove it from the formatter listener's table.
  258. */
  259. public void close(XSLProcessorContext context, ElemExtensionCall elem)
  260. throws java.net.MalformedURLException,
  261. java.io.FileNotFoundException,
  262. java.io.IOException,
  263. javax.xml.transform.TransformerException
  264. {
  265. String fileName = getFilename(context, elem);
  266. Object formatterObj = m_formatterListeners.get(fileName);
  267. if(null != formatterObj)
  268. {
  269. ContentHandler fl = (ContentHandler)formatterObj;
  270. try
  271. {
  272. fl.endDocument();
  273. }
  274. catch(org.xml.sax.SAXException se)
  275. {
  276. throw new TransformerException(se);
  277. }
  278. OutputStream ostream = (OutputStream)m_outputStreams.get(fileName);
  279. if(null != ostream)
  280. {
  281. ostream.close();
  282. m_outputStreams.remove(fileName);
  283. }
  284. m_formatterListeners.remove(fileName);
  285. }
  286. }
  287. /**
  288. * Get the filename from the 'select' or the 'file' attribute.
  289. */
  290. private String getFilename(XSLProcessorContext context, ElemExtensionCall elem)
  291. throws java.net.MalformedURLException,
  292. java.io.FileNotFoundException,
  293. java.io.IOException,
  294. javax.xml.transform.TransformerException
  295. {
  296. String fileName;
  297. String fileNameExpr
  298. = ((ElemExtensionCall)elem).getAttribute ("select",
  299. context.getContextNode(),
  300. context.getTransformer());
  301. if(null != fileNameExpr)
  302. {
  303. org.apache.xpath.XPathContext xctxt
  304. = context.getTransformer().getXPathContext();
  305. XPath myxpath = new XPath(fileNameExpr, elem, xctxt.getNamespaceContext(), XPath.SELECT);
  306. XObject xobj = myxpath.execute(xctxt, context.getContextNode(), elem);
  307. fileName = xobj.str();
  308. if((null == fileName) || (fileName.length() == 0))
  309. {
  310. fileName = elem.getAttribute ("file",
  311. context.getContextNode(),
  312. context.getTransformer());
  313. }
  314. }
  315. else
  316. {
  317. fileName = elem.getAttribute ("file", context.getContextNode(),
  318. context.getTransformer());
  319. }
  320. if(null == fileName)
  321. {
  322. context.getTransformer().getMsgMgr().error(elem, elem,
  323. context.getContextNode(),
  324. XSLTErrorResources.ER_REDIRECT_COULDNT_GET_FILENAME);
  325. //"Redirect extension: Could not get filename - file or select attribute must return vald string.");
  326. }
  327. return fileName;
  328. }
  329. // yuck.
  330. // Note: this is not the best way to do this, and may not even
  331. // be fully correct! Patches (with test cases) welcomed. -sc
  332. private String urlToFileName(String base)
  333. {
  334. if(null != base)
  335. {
  336. if(base.startsWith("file:////"))
  337. {
  338. base = base.substring(7);
  339. }
  340. else if(base.startsWith("file:///"))
  341. {
  342. base = base.substring(6);
  343. }
  344. else if(base.startsWith("file://"))
  345. {
  346. base = base.substring(5); // absolute?
  347. }
  348. else if(base.startsWith("file:/"))
  349. {
  350. base = base.substring(5);
  351. }
  352. else if(base.startsWith("file:"))
  353. {
  354. base = base.substring(4);
  355. }
  356. }
  357. return base;
  358. }
  359. /**
  360. * Create a new ContentHandler, based on attributes of the current ContentHandler.
  361. */
  362. private ContentHandler makeFormatterListener(XSLProcessorContext context,
  363. ElemExtensionCall elem,
  364. String fileName,
  365. boolean shouldPutInTable,
  366. boolean mkdirs,
  367. boolean append)
  368. throws java.net.MalformedURLException,
  369. java.io.FileNotFoundException,
  370. java.io.IOException,
  371. javax.xml.transform.TransformerException
  372. {
  373. File file = new File(fileName);
  374. TransformerImpl transformer = context.getTransformer();
  375. String base; // Base URI to use for relative paths
  376. if(!file.isAbsolute())
  377. {
  378. // This code is attributed to Jon Grov <jon@linpro.no>. A relative file name
  379. // is relative to the Result used to kick off the transform. If no such
  380. // Result was supplied, the filename is relative to the source document.
  381. // When transforming with a SAXResult or DOMResult, call
  382. // TransformerImpl.setOutputTarget() to set the desired Result base.
  383. // String base = urlToFileName(elem.getStylesheet().getSystemId());
  384. Result outputTarget = transformer.getOutputTarget();
  385. if ( (null != outputTarget) && ((base = outputTarget.getSystemId()) != null) ) {
  386. base = urlToFileName(base);
  387. }
  388. else
  389. {
  390. base = urlToFileName(transformer.getBaseURLOfSource());
  391. }
  392. if(null != base)
  393. {
  394. File baseFile = new File(base);
  395. file = new File(baseFile.getParent(), fileName);
  396. }
  397. // System.out.println("file is: "+file.toString());
  398. }
  399. if(mkdirs)
  400. {
  401. String dirStr = file.getParent();
  402. if((null != dirStr) && (dirStr.length() > 0))
  403. {
  404. File dir = new File(dirStr);
  405. dir.mkdirs();
  406. }
  407. }
  408. // This should be worked on so that the output format can be
  409. // defined by a first child of the redirect element.
  410. OutputProperties format = transformer.getOutputFormat();
  411. // FileOutputStream ostream = new FileOutputStream(file);
  412. // Patch from above line to below by <jpvdm@iafrica.com>
  413. // Note that in JDK 1.2.2 at least, FileOutputStream(File)
  414. // is implemented as a call to
  415. // FileOutputStream(File.getPath, append), thus this should be
  416. // the equivalent instead of getAbsolutePath()
  417. FileOutputStream ostream = new FileOutputStream(file.getPath(), append);
  418. try
  419. {
  420. ContentHandler flistener
  421. = transformer.createResultContentHandler(new StreamResult(ostream), format);
  422. try
  423. {
  424. flistener.startDocument();
  425. }
  426. catch(org.xml.sax.SAXException se)
  427. {
  428. throw new TransformerException(se);
  429. }
  430. if(shouldPutInTable)
  431. {
  432. m_outputStreams.put(fileName, ostream);
  433. m_formatterListeners.put(fileName, flistener);
  434. }
  435. return flistener;
  436. }
  437. catch(TransformerException te)
  438. {
  439. throw new javax.xml.transform.TransformerException(te);
  440. }
  441. }
  442. }