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 * <?xml version="1.0"?> 081 * <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 * <xsl:template match="/"> 087 * <out> 088 * default output. 089 * </out> 090 * <redirect:open file="doc3.out"/> 091 * <redirect:write file="doc3.out"> 092 * <out> 093 * <redirect:write file="doc1.out"> 094 * <out> 095 * doc1 output. 096 * <redirect:write file="doc3.out"> 097 * Some text to doc3 098 * </redirect:write> 099 * </out> 100 * </redirect:write> 101 * <redirect:write file="doc2.out"> 102 * <out> 103 * doc2 output. 104 * <redirect:write file="doc3.out"> 105 * Some more text to doc3 106 * <redirect:write select="doc/foo"> 107 * text for doc4 108 * </redirect:write> 109 * </redirect:write> 110 * </out> 111 * </redirect:write> 112 * </out> 113 * </redirect:write> 114 * <redirect:close file="doc3.out"/> 115 * </xsl:template> 116 * 117 * </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 }