001: /*
002: * This file is part of PFIXCORE.
003: *
004: * PFIXCORE is free software; you can redistribute it and/or modify
005: * it under the terms of the GNU Lesser General Public License as published by
006: * the Free Software Foundation; either version 2 of the License, or
007: * (at your option) any later version.
008: *
009: * PFIXCORE is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
012: * GNU Lesser General Public License for more details.
013: *
014: * You should have received a copy of the GNU Lesser General Public License
015: * along with PFIXCORE; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017: *
018: */
019: package de.schlund.pfixcore.util;
020:
021: import java.io.File;
022: import java.util.HashMap;
023: import java.util.Iterator;
024:
025: import javax.xml.transform.Result;
026: import javax.xml.transform.Source;
027: import javax.xml.transform.Templates;
028: import javax.xml.transform.Transformer;
029: import javax.xml.transform.TransformerConfigurationException;
030: import javax.xml.transform.TransformerException;
031: import javax.xml.transform.TransformerFactory;
032: import javax.xml.transform.sax.SAXSource;
033: import javax.xml.transform.stream.StreamResult;
034: import javax.xml.transform.stream.StreamSource;
035:
036: import org.apache.tools.ant.BuildException;
037: import org.apache.tools.ant.Project;
038: import org.xml.sax.EntityResolver;
039: import org.xml.sax.InputSource;
040: import org.xml.sax.XMLReader;
041:
042: import com.icl.saxon.TransformerFactoryImpl;
043: import com.sun.org.apache.xerces.internal.parsers.SAXParser;
044:
045: /**
046: * @author adam
047: *
048: * To change the template for this generated type comment go to
049: * Window - Preferences - Java - Code Generation - Code and Comments
050: */
051: public class XsltTransformer {
052:
053: // sax feature ids, see http://xml.apache.org/xerces2-j/features.html
054:
055: /** Namespaces feature id (http://xml.org/sax/features/namespaces). See http://xml.apache.org/xerces2-j/features.html
056: * Xerces-Default: true */
057: protected static final String NAMESPACES_FEATURE_ID = "http://xml.org/sax/features/namespaces";
058:
059: /** Namespace prefixes feature id (http://xml.org/sax/features/namespace-prefixes). See http://xml.apache.org/xerces2-j/features.html
060: * Xerces-Default: false */
061: protected static final String NAMESPACE_PREFIXES_FEATURE_ID = "http://xml.org/sax/features/namespace-prefixes";
062:
063: /** Validation feature id (http://xml.org/sax/features/validation). See http://xml.apache.org/xerces2-j/features.html
064: * Xerces-Default: false */
065: protected static final String VALIDATION_FEATURE_ID = "http://xml.org/sax/features/validation";
066:
067: /** Schema validation feature id (http://apache.org/xml/features/validation/schema). See http://xml.apache.org/xerces2-j/features.html
068: * Xerces-Default: false */
069: protected static final String SCHEMA_VALIDATION_FEATURE_ID = "http://apache.org/xml/features/validation/schema";
070:
071: /** Schema full checking feature id (http://apache.org/xml/features/validation/schema-full-checking). See http://xml.apache.org/xerces2-j/features.html
072: * Xerces-Default: false */
073: protected static final String SCHEMA_FULL_CHECKING_FEATURE_ID = "http://apache.org/xml/features/validation/schema-full-checking";
074:
075: /** Dynamic validation feature id (http://apache.org/xml/features/validation/dynamic). See http://xml.apache.org/xerces2-j/features.html
076: * Xerces-Default: false */
077: protected static final String DYNAMIC_VALIDATION_FEATURE_ID = "http://apache.org/xml/features/validation/dynamic";
078:
079: /** Load external DTD feature id (http://apache.org/xml/features/nonvalidating/load-external-dtd). Does not seem to control loading of external xml schema as of xerces 2.6.2. See http://xml.apache.org/xerces2-j/features.html
080: * Xerces-Default: true */
081: protected static final String LOAD_EXTERNAL_DTD_FEATURE_ID = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
082:
083: protected static final String[] FEATURES = { NAMESPACES_FEATURE_ID,
084: NAMESPACE_PREFIXES_FEATURE_ID, VALIDATION_FEATURE_ID,
085: SCHEMA_VALIDATION_FEATURE_ID,
086: SCHEMA_FULL_CHECKING_FEATURE_ID,
087: DYNAMIC_VALIDATION_FEATURE_ID, LOAD_EXTERNAL_DTD_FEATURE_ID };
088:
089: protected TransformerFactory factory;
090: protected Transformer transformer;
091: protected File stylesheet;
092: protected boolean isValidStylesheet = false;
093: /** used by caching mechanism @see #validate() */
094: protected File stylesheetOld;
095: /** used by caching mechanism @see #validate() */
096: protected long stylesheetOldLastModified;
097: protected boolean cacheStylesheet = true;
098: /** The In memory version of the stylesheet */
099: protected Templates templates;
100: /** holds additional {@link XsltParam} objects to be passed to the stylesheets */
101: protected HashMap<String, XsltParam> params = new HashMap<String, XsltParam>(
102: 20);
103: protected boolean isValidParams = false;
104: protected boolean isValidParser = false;
105: /** controls {@link #VALIDATION_FEATURE_ID} and {@link #SCHEMA_VALIDATION_FEATURE_ID}; defaults to false */
106: protected boolean isValidate = false;
107: /** controls {@link #DYNAMIC_VALIDATION_FEATURE_ID}; defaults to false */
108: protected boolean isValidateDynamic = false;
109: /** controls {@link #NAMESPACES_FEATURE_ID}; defaults to true */
110: protected boolean isNamespaceAware = true;
111: protected EntityResolver entitiyResolver = null;
112: protected XMLReader xmlReader = null;
113: protected XsltErrorListener errorListener = null;
114: /** never null */
115: protected Project project = null;
116:
117: /**
118: * The Stylesheet needs to be set with {@link #setStylesheet(File)} when using the
119: * default constructor.
120: * @param project may not be null
121: */
122: public XsltTransformer(Project project) {
123: this (project, null);
124: }
125:
126: /**
127: * @param project must not be null
128: * @param stylesheet may be null
129: */
130: public XsltTransformer(Project project, File stylesheet) {
131: if (project == null) {
132: throw new NullPointerException("project=" + project
133: + "; stylesheet=" + stylesheet);
134: }
135: setProject(project);
136: setStylesheet(stylesheet);
137: // force Saxon (com.icl.saxon.TransformerFactoryImpl) because we have extension functions
138: factory = new TransformerFactoryImpl();
139: errorListener = new XsltErrorListener(getProject());
140: }
141:
142: public void transform(File baseDir, String infilename,
143: File destDir, String outfilename) {
144: transform(new File(baseDir, infilename), new File(destDir,
145: outfilename));
146: }
147:
148: public void transform(File infile, File outfile) {
149: // We have to validate here for XML Reader to be correctly initialized
150: if (!isValid()) {
151: validate();
152: }
153: StreamResult result = new StreamResult(outfile);
154: // TODO_AH check whether or not to prepend the systemid with file://
155: InputSource is = new InputSource("file://"
156: + infile.getAbsolutePath());
157: SAXSource source = new SAXSource(xmlReader, is);
158: transform(source, result);
159: }
160:
161: public void transform(Source source, Result result) {
162: if (!isValid()) {
163: validate();
164: }
165: try {
166: transformer.transform(source, result);
167: } catch (TransformerException tex) {
168: // TODO_AH delete this misguided attempt to be nice. Seems like transformer never leaves a half-finished destination file.
169: // Trying to set the last modified time of the target file
170: // to 1970, so it get's transformed again on the next build.
171: // (Allowing the developer to still be able to
172: // take a look at the eventually half-finished result file).
173: // try {
174: // String systemId = result.getSystemId();
175: // String filename = null;
176: // if ( systemId != null && systemId.startsWith("file:") ) {
177: // filename = systemId.substring(5);
178: // if ( filename.startsWith("///") ) {
179: // filename = filename.substring(2);
180: // }
181: // File file = new File(filename);
182: // if ( file.exists() && file.canWrite() ) {
183: // file.setLastModified(0);
184: // }
185: // }
186: // } catch(Exception e) {
187: // // this should not happen, anyways - better dump than hide it
188: // e.printStackTrace();
189: // }
190: throw new BuildException(tex.getMessageAndLocation()
191: + " - Transformation from source="
192: + source.getSystemId() + " to result="
193: + result.getSystemId() + " failed.", tex);
194: }
195: }
196:
197: protected boolean isValid() {
198: return (isValidParams == true && isValidStylesheet == true && isValidParser == true);
199: }
200:
201: /**
202: * @throws BuildException on {@link #stylesheet} == null, {@link TransformerConfigurationException}, {@link XMLReader#setFeature(java.lang.String, boolean)}
203: */
204: protected void validate() {
205: if (isValidStylesheet == false) {
206: if (stylesheet == null) {
207: throw new BuildException("No stylesheet specified");
208: } else {
209: try {
210: // check whether we have to reload the stylesheet or not
211: boolean reload = false;
212: long lastModified = stylesheet.lastModified();
213: reload = reload || (isCacheStylesheet() == false);
214: reload = reload
215: || (stylesheet.equals(stylesheetOld) == false);
216: reload = reload
217: || (lastModified > stylesheetOldLastModified);
218: if (reload) {
219: transformer = factory
220: .newTransformer(new StreamSource(
221: stylesheet));
222: // TODO_AH comment in custom ErrorListener
223: // transformer.setErrorListener(errorListener);
224: stylesheetOld = stylesheet;
225: stylesheetOldLastModified = lastModified;
226: isValidStylesheet = true;
227: isValidParams = false; // parameters have to be reapplied
228: }
229: } catch (TransformerConfigurationException e) {
230: stylesheetOld = null;
231: stylesheetOldLastModified = 0;
232: throw new BuildException(
233: "Could not initialize XSLT Transformer (stylesheet=\""
234: + stylesheet + "\")", e);
235: }
236: }
237: }
238: if (isValidParser == false) {
239: // force Xerces (org.apache.xerces.parsers.SAXParser) because xml schema validation features
240: this .xmlReader = new SAXParser();
241: try {
242: this .xmlReader.setFeature(VALIDATION_FEATURE_ID,
243: isValidate());
244: this .xmlReader.setFeature(SCHEMA_VALIDATION_FEATURE_ID,
245: isValidate());
246: this .xmlReader.setFeature(
247: DYNAMIC_VALIDATION_FEATURE_ID,
248: isValidateDynamic());
249: this .xmlReader.setFeature(NAMESPACES_FEATURE_ID,
250: isNamespaceAware());
251: } catch (Exception e) {
252: throw new BuildException(
253: "There was a problem configuring "
254: + this .xmlReader + ". this="
255: + this .toString(), e);
256: }
257: if (getEntitiyResolver() != null) {
258: this .xmlReader.setEntityResolver(getEntitiyResolver());
259: }
260: isValidParser = true;
261: }
262: if (isValidParams == false) {
263: // assert transformer != null : "Exception should have been thrown
264: // beforehand";
265: transformer.clearParameters();
266: for (Iterator<XsltParam> iter = params.values().iterator(); iter
267: .hasNext();) {
268: XsltParam param = iter.next();
269: transformer.setParameter(param.getName(), param
270: .getExpression());
271: }
272: }
273: }
274:
275: /**
276: * @return stylesheet or null
277: */
278: public File getStylesheet() {
279: return stylesheet;
280: }
281:
282: /**
283: * Sets a new Stylesheet File, which in turn creates a new {@link Transformer}.<br/>
284: * Note: you have to re-apply your parameters, as they get lost with the old
285: * transformer.
286: *
287: * @param stylesheet or null
288: * @see #setParameter(String, Object)
289: */
290: public void setStylesheet(File stylesheet) {
291: this .stylesheet = stylesheet;
292: isValidStylesheet = false;
293: }
294:
295: /**
296: * @throw IllegalArgumentException if param or param.getName() are null
297: */
298: public void setParameter(XsltParam param) {
299: isValidParams = false;
300: if (param == null || param.getName() == null) {
301: throw new IllegalArgumentException(
302: "param and param.getName() must not be null: "
303: + String.valueOf(param));
304: }
305: params.put(param.getName(), param);
306: }
307:
308: public EntityResolver getEntitiyResolver() {
309: return entitiyResolver;
310: }
311:
312: public void setEntitiyResolver(EntityResolver entitiyResolver) {
313: if (entitiyResolver != this .entitiyResolver) {
314: this .isValidParser = false;
315: }
316: this .entitiyResolver = entitiyResolver;
317: }
318:
319: /**
320: * Configures parser for input documents.
321: * Affects following features:
322: * <ul>
323: * <li>{@link #VALIDATION_FEATURE_ID }
324: * <li>{@link #SCHEMA_VALIDATION_FEATURE_ID }
325: * </ul>
326: * Default: false
327: * @see #validate()
328: */
329: public boolean isValidate() {
330: return isValidate;
331: }
332:
333: /**
334: * Configures parser for input documents.
335: * Affects following features:
336: * <ul>
337: * <li>{@link #VALIDATION_FEATURE_ID }
338: * <li>{@link #SCHEMA_VALIDATION_FEATURE_ID }
339: * </ul><br>
340: * Default: false
341: * @see #validate()
342: */
343: public void setValidate(boolean validate) {
344: if (validate != this .isValidate) {
345: this .isValidParser = false;
346: }
347: this .isValidate = validate;
348: }
349:
350: /**
351: * Configures parser for input documents.
352: * Affects following features:
353: * <ul>
354: * <li>{@link #DYNAMIC_VALIDATION_FEATURE_ID }
355: * </ul><br>
356: * Default: false
357: * @see #validate()
358: */
359: public boolean isValidateDynamic() {
360: return isValidateDynamic;
361: }
362:
363: /**
364: * Configures parser for input documents.
365: * Affects following features:
366: * <ul>
367: * <li>{@link #DYNAMIC_VALIDATION_FEATURE_ID }
368: * </ul><br>
369: * Default: false
370: * @see #validate()
371: */
372: public void setValidateDynamic(boolean validateDynamic) {
373: if (validateDynamic != this .isValidateDynamic
374: && this .isValidate == true) {
375: this .isValidParser = false;
376: }
377: this .isValidateDynamic = validateDynamic;
378: }
379:
380: /**
381: * Configures parser for input documents.
382: * Affects following features:
383: * <ul>
384: * <li>{@link #NAMESPACES_FEATURE_ID }
385: * </ul><br>
386: * Default: true
387: * @see #validate()
388: */
389: public boolean isNamespaceAware() {
390: return isNamespaceAware;
391: }
392:
393: /**
394: * Configures parser for input documents.
395: * Affects following features:
396: * <ul>
397: * <li>{@link #NAMESPACES_FEATURE_ID }
398: * </ul><br>
399: * Default: true
400: * @see #validate()
401: */
402: public void setNamespaceAware(boolean isNamespaceAware) {
403: if (isNamespaceAware != this .isNamespaceAware) {
404: this .isValidParser = false;
405: }
406: this .isNamespaceAware = isNamespaceAware;
407: }
408:
409: public void clearParameters() {
410: isValidParams = false;
411: params.clear();
412: }
413:
414: public Project getProject() {
415: return project;
416: }
417:
418: protected void setProject(Project project) {
419: this .project = project;
420: }
421:
422: public String toString() {
423: return shortClassname(getClass().getName()) + "[transformer="
424: + transformer + "; isValidate=" + isValidate
425: + " isValidateDynamic=" + isValidateDynamic
426: + "; isNamespaceAware=" + isNamespaceAware
427: + "; params=" + params + "]";
428: }
429:
430: //
431: // Helper methods
432: //
433:
434: /**
435: * @return classname without package prefix
436: */
437: public static String shortClassname(String classname) {
438: try {
439: int idx = classname.lastIndexOf('.');
440: if (idx >= 0) {
441: classname = classname.substring(idx + 1, classname
442: .length());
443: }
444: } catch (IndexOutOfBoundsException e) {
445: // This should never happen
446: e.printStackTrace();
447: }
448: return classname;
449: }
450:
451: public boolean isCacheStylesheet() {
452: return cacheStylesheet;
453: }
454:
455: public void setCacheStylesheet(boolean cacheStylesheet) {
456: if (cacheStylesheet == false) {
457: isValidStylesheet = false;
458: isValidParams = false;
459: }
460: this .cacheStylesheet = cacheStylesheet;
461: }
462:
463: //-- xslt extensions
464: // TODO: move into separate class?
465:
466: public static boolean exists(String file) {
467: return new File(file).exists();
468: }
469: }
|