blob: 5b71ae5cf48a7670408f7a57f666f7ae89d677f2 [file] [log] [blame]
/*
* 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
*
* https://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.
*
*/
package org.apache.tools.ant.taskdefs.optional;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.ErrorListener;
import javax.xml.transform.Source;
import javax.xml.transform.SourceLocator;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.XSLTLiaison4;
import org.apache.tools.ant.taskdefs.XSLTLogger;
import org.apache.tools.ant.taskdefs.XSLTLoggerAware;
import org.apache.tools.ant.taskdefs.XSLTProcess;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.XMLCatalog;
import org.apache.tools.ant.types.resources.FileProvider;
import org.apache.tools.ant.types.resources.FileResource;
import org.apache.tools.ant.types.resources.URLProvider;
import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.JAXPUtils;
import org.apache.tools.ant.util.JavaEnvUtils;
import org.apache.tools.ant.util.StreamUtils;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
/**
* Concrete liaison for XSLT processor implementing TraX. (ie JAXP 1.1)
*
* @since Ant 1.3
*/
public class TraXLiaison implements XSLTLiaison4, ErrorListener, XSLTLoggerAware {
/**
* Helper for transforming filenames to URIs.
*
* @since Ant 1.7
*/
private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();
/**
* The current <code>Project</code>
*/
private Project project;
/**
* the name of the factory implementation class to use
* or null for default JAXP lookup.
*/
private String factoryName = null;
/** The trax TransformerFactory */
private TransformerFactory tfactory = null;
/** stylesheet to use for transformation */
private Resource stylesheet;
private XSLTLogger logger;
/** possible resolver for publicIds */
private EntityResolver entityResolver;
/** transformer to use for processing files */
private Transformer transformer;
/** The In memory version of the stylesheet */
private Templates templates;
/**
* The modification time of the stylesheet from which the templates
* are read
*/
private long templatesModTime;
/** possible resolver for URIs */
private URIResolver uriResolver;
/** transformer output properties */
private final Vector<String[]> outputProperties = new Vector<>();
/** stylesheet parameters */
private final Hashtable<String, Object> params = new Hashtable<>();
/** factory attributes */
private final List<Object[]> attributes = new ArrayList<>();
/** factory features */
private final Map<String, Boolean> features = new HashMap<>();
/** whether to suppress warnings */
private boolean suppressWarnings = false;
/** optional trace configuration. */
private XSLTProcess.TraceConfiguration traceConfiguration = null;
/**
* Constructor for TraXLiaison.
* @throws Exception never
*/
public TraXLiaison() throws Exception { //NOSONAR
}
/**
* Set the stylesheet file.
* @param stylesheet a <code>File</code> value
* @throws Exception on error
*/
public void setStylesheet(final File stylesheet) throws Exception {
final FileResource fr = new FileResource();
fr.setProject(project);
fr.setFile(stylesheet);
setStylesheet(fr);
}
/**
* Set the stylesheet file.
* @param stylesheet a {@link org.apache.tools.ant.types.Resource} value
* @throws Exception on error
*/
public void setStylesheet(final Resource stylesheet) throws Exception {
if (this.stylesheet != null) {
// resetting the stylesheet - reset transformer
transformer = null;
// do we need to reset templates as well
if (!this.stylesheet.equals(stylesheet)
|| (stylesheet.getLastModified() != templatesModTime)) {
templates = null;
}
}
this.stylesheet = stylesheet;
}
/**
* Transform an input file.
* @param infile the file to transform
* @param outfile the result file
* @throws Exception on error
*/
public void transform(final File infile, final File outfile) throws Exception {
if (transformer == null) {
createTransformer();
}
// autoclose all handles, otherwise the garbage collector will close them...
// and Windows may complain about not being able to delete files.
try (InputStream fis = new BufferedInputStream(Files.newInputStream(infile.toPath()));
OutputStream fos = new BufferedOutputStream(Files.newOutputStream(outfile.toPath()))) {
final StreamResult res = new StreamResult(fos);
// not sure what could be the need of this...
res.setSystemId(JAXPUtils.getSystemId(outfile));
// set parameters on each transformation, maybe something has changed
//(e.g. value of file name parameter)
setTransformationParameters();
transformer.transform(getSource(fis, infile), res);
}
}
/**
* Get the source instance from the stream and id of the file.
* @param is the stream containing the stylesheet data.
* @param infile the file that will be used for the systemid.
* @return the configured source instance matching the stylesheet.
* @throws ParserConfigurationException if a parser cannot be created which
* satisfies the requested configuration.
* @throws SAXException in case of problem detected by the SAX parser.
*/
private Source getSource(final InputStream is, final File infile)
throws ParserConfigurationException, SAXException {
// todo: is this comment still relevant ??
// FIXME: need to use a SAXSource as the source for the transform
// so we can plug in our own entity resolver
Source src = null;
if (entityResolver != null) {
if (getFactory().getFeature(SAXSource.FEATURE)) {
final SAXParserFactory spFactory = SAXParserFactory.newInstance();
spFactory.setNamespaceAware(true);
final XMLReader reader = spFactory.newSAXParser().getXMLReader();
reader.setEntityResolver(entityResolver);
src = new SAXSource(reader, new InputSource(is));
} else {
throw new IllegalStateException("xcatalog specified, but "
+ "parser doesn't support SAX");
}
} else {
// WARN: Don't use the StreamSource(File) ctor. It won't work with
// xalan prior to 2.2 because of systemid bugs.
src = new StreamSource(is);
}
src.setSystemId(JAXPUtils.getSystemId(infile));
return src;
}
private Source getSource(final InputStream is, final Resource resource)
throws ParserConfigurationException, SAXException {
// todo: is this comment still relevant ??
// FIXME: need to use a SAXSource as the source for the transform
// so we can plug in our own entity resolver
Source src = null;
if (entityResolver != null) {
if (getFactory().getFeature(SAXSource.FEATURE)) {
final SAXParserFactory spFactory = SAXParserFactory.newInstance();
spFactory.setNamespaceAware(true);
final XMLReader reader = spFactory.newSAXParser().getXMLReader();
reader.setEntityResolver(entityResolver);
src = new SAXSource(reader, new InputSource(is));
} else {
throw new IllegalStateException("xcatalog specified, but "
+ "parser doesn't support SAX");
}
} else {
// WARN: Don't use the StreamSource(File) ctor. It won't work with
// xalan prior to 2.2 because of systemid bugs.
src = new StreamSource(is);
}
// The line below is a hack: the system id must an URI, but it is not
// cleat to get the URI of an resource, so just set the name of the
// resource as a system id
src.setSystemId(resourceToURI(resource));
return src;
}
private String resourceToURI(final Resource resource) {
final FileProvider fp = resource.as(FileProvider.class);
if (fp != null) {
return FILE_UTILS.toURI(fp.getFile().getAbsolutePath());
}
final URLProvider up = resource.as(URLProvider.class);
if (up != null) {
final URL u = up.getURL();
return String.valueOf(u);
} else {
return resource.getName();
}
}
/**
* Read in templates from the stylesheet
*/
private void readTemplates()
throws IOException, TransformerConfigurationException,
ParserConfigurationException, SAXException {
// Use a stream so that you can close it yourself quickly
// and avoid keeping the handle until the object is garbaged.
// (always keep control), otherwise you won't be able to delete
// the file quickly on windows.
try (InputStream xslStream =
new BufferedInputStream(stylesheet.getInputStream())) {
templatesModTime = stylesheet.getLastModified();
final Source src = getSource(xslStream, stylesheet);
templates = getFactory().newTemplates(src);
}
}
/**
* Create a new transformer based on the liaison settings
* @see #setStylesheet(java.io.File)
* @see #addParam(java.lang.String, java.lang.String)
* @see #setOutputProperty(java.lang.String, java.lang.String)
*/
private void createTransformer()
throws IOException, ParserConfigurationException, SAXException, TransformerException {
if (templates == null) {
readTemplates();
}
transformer = templates.newTransformer();
// configure the transformer...
transformer.setErrorListener(this);
if (uriResolver != null) {
transformer.setURIResolver(uriResolver);
}
for (final String[] pair : outputProperties) {
transformer.setOutputProperty(pair[0], pair[1]);
}
if (traceConfiguration != null) {
if ("org.apache.xalan.transformer.TransformerImpl" //NOSONAR
.equals(transformer.getClass().getName())) {
try {
final Class<?> traceSupport =
Class.forName("org.apache.tools.ant.taskdefs.optional."
+ "Xalan2TraceSupport", true,
Thread.currentThread()
.getContextClassLoader());
final XSLTTraceSupport ts =
(XSLTTraceSupport) traceSupport.getDeclaredConstructor().newInstance();
ts.configureTrace(transformer, traceConfiguration);
} catch (final Exception e) {
final String msg = "Failed to enable tracing because of " + e;
if (project != null) {
project.log(msg, Project.MSG_WARN);
} else {
System.err.println(msg);
}
}
} else {
final String msg = "Not enabling trace support for transformer"
+ " implementation" + transformer.getClass().getName();
if (project != null) {
project.log(msg, Project.MSG_WARN);
} else {
System.err.println(msg);
}
}
}
}
/**
* Sets the parameters for the transformer.
*/
private void setTransformationParameters() {
params.forEach((key, value) -> transformer.setParameter(key, value));
}
/**
* return the Transformer factory associated to this liaison.
* @return the Transformer factory associated to this liaison.
* @throws BuildException thrown if there is a problem creating
* the factory.
* @see #setFactory(String)
* @since Ant 1.5.2
*/
private TransformerFactory getFactory() throws BuildException {
if (tfactory != null) {
return tfactory;
}
// not initialized yet, so create the factory
if (factoryName == null) {
tfactory = TransformerFactory.newInstance();
} else {
try {
Class<?> clazz = null;
try {
clazz =
Class.forName(factoryName, true,
Thread.currentThread()
.getContextClassLoader());
} catch (final ClassNotFoundException cnfe) {
final String msg = "Failed to load " + factoryName
+ " via the configured classpath, will try"
+ " Ant's classpath instead.";
if (logger != null) {
logger.log(msg);
} else if (project != null) {
project.log(msg, Project.MSG_WARN);
} else {
System.err.println(msg);
}
}
if (clazz == null) {
clazz = Class.forName(factoryName);
}
tfactory = (TransformerFactory) clazz.getDeclaredConstructor().newInstance();
} catch (final Exception e) {
throw new BuildException(e);
}
}
applyReflectionHackForExtensionMethods();
tfactory.setErrorListener(this);
// specific attributes for the transformer
for (final Object[] pair : attributes) {
tfactory.setAttribute((String) pair[0], pair[1]);
}
for (Map.Entry<String, Boolean> feature : features.entrySet()) {
try {
tfactory.setFeature(feature.getKey(), feature.getValue());
} catch (TransformerConfigurationException ex) {
throw new BuildException(ex);
}
}
if (uriResolver != null) {
tfactory.setURIResolver(uriResolver);
}
return tfactory;
}
/**
* Set the factory name to use instead of JAXP default lookup.
* @param name the fully qualified class name of the factory to use
* or null for the default JAXP look up mechanism.
* @since Ant 1.6
*/
public void setFactory(final String name) {
factoryName = name;
}
/**
* Set a custom attribute for the JAXP factory implementation.
* @param name the attribute name.
* @param value the value of the attribute, usually a boolean
* string or object.
* @since Ant 1.6
*/
public void setAttribute(final String name, final Object value) {
final Object[] pair = new Object[]{name, value};
attributes.add(pair);
}
/**
* Set a custom feature for the JAXP factory implementation.
* @param name the feature name.
* @param value the value of the feature
* @since Ant 1.9.8
*/
public void setFeature(final String name, final boolean value) {
features.put(name, value);
}
/**
* Set the output property for the current transformer.
* Note that the stylesheet must be set prior to calling
* this method.
* @param name the output property name.
* @param value the output property value.
* @since Ant 1.5
* @since Ant 1.5
*/
public void setOutputProperty(final String name, final String value) {
final String[] pair = new String[]{name, value};
outputProperties.addElement(pair);
}
/**
* Set the class to resolve entities during the transformation.
* @param aResolver the resolver class.
*/
public void setEntityResolver(final EntityResolver aResolver) {
entityResolver = aResolver;
}
/**
* Set the class to resolve URIs during the transformation
* @param aResolver a <code>EntityResolver</code> value
*/
public void setURIResolver(final URIResolver aResolver) {
uriResolver = aResolver;
}
/**
* Add a parameter.
* @param name the name of the parameter
* @param value the value of the parameter
*/
public void addParam(final String name, final String value) {
params.put(name, value);
}
/**
* Add a parameter.
* @param name the name of the parameter
* @param value the value of the parameter
* @since Ant 1.9.3
*/
public void addParam(final String name, final Object value) {
params.put(name, value);
}
/**
* Set a logger.
* @param l a logger.
*/
public void setLogger(final XSLTLogger l) {
logger = l;
}
/**
* Log an error.
* @param e the exception to log.
*/
public void error(final TransformerException e) {
logError(e, "Error");
}
/**
* Log a fatal error.
* @param e the exception to log.
*/
public void fatalError(final TransformerException e) {
logError(e, "Fatal Error");
throw new BuildException("Fatal error during transformation using " + stylesheet + ": " + e.getMessageAndLocation(), e);
}
/**
* Log a warning.
* @param e the exception to log.
*/
public void warning(final TransformerException e) {
if (!suppressWarnings) {
logError(e, "Warning");
}
}
private void logError(final TransformerException e, final String type) {
if (logger == null) {
return;
}
final StringBuffer msg = new StringBuffer();
final SourceLocator locator = e.getLocator();
if (locator != null) {
final String systemid = locator.getSystemId();
if (systemid != null) {
String url = systemid;
if (url.startsWith("file:")) {
url = FileUtils.getFileUtils().fromURI(url);
}
msg.append(url);
} else {
msg.append("Unknown file");
}
final int line = locator.getLineNumber();
if (line != -1) {
msg.append(":");
msg.append(line);
final int column = locator.getColumnNumber();
if (column != -1) {
msg.append(":");
msg.append(column);
}
}
}
msg.append(": ");
msg.append(type);
msg.append("! ");
msg.append(e.getMessage());
if (e.getCause() != null) {
msg.append(" Cause: ");
msg.append(e.getCause());
}
logger.log(msg.toString());
}
// kept for backwards compatibility
/**
* @param file the filename to use for the systemid
* @return the systemid
* @deprecated since 1.5.x.
* Use org.apache.tools.ant.util.JAXPUtils#getSystemId instead.
*/
@Deprecated
protected String getSystemId(final File file) {
return JAXPUtils.getSystemId(file);
}
/**
* Specific configuration for the TRaX liaison.
* @param xsltTask the XSLTProcess task instance from which this liaison
* is to be configured.
*/
public void configure(final XSLTProcess xsltTask) {
project = xsltTask.getProject();
final XSLTProcess.Factory factory = xsltTask.getFactory();
if (factory != null) {
setFactory(factory.getName());
// configure factory attributes
StreamUtils.enumerationAsStream(factory.getAttributes())
.forEach(attr -> setAttribute(attr.getName(), attr.getValue()));
factory.getFeatures()
.forEach(feature -> setFeature(feature.getName(), feature.getValue()));
}
final XMLCatalog xmlCatalog = xsltTask.getXMLCatalog();
// use XMLCatalog as the entity resolver and URI resolver
if (xmlCatalog != null) {
setEntityResolver(xmlCatalog);
setURIResolver(xmlCatalog);
}
// configure output properties
StreamUtils.enumerationAsStream(xsltTask.getOutputProperties())
.forEach(prop -> setOutputProperty(prop.getName(), prop.getValue()));
suppressWarnings = xsltTask.getSuppressWarnings();
traceConfiguration = xsltTask.getTraceConfiguration();
}
private void applyReflectionHackForExtensionMethods() {
// Jigsaw doesn't allow reflection to work, so we can stop trying
if (!JavaEnvUtils.isAtLeastJavaVersion(JavaEnvUtils.JAVA_9)) {
try { // #51668, #52382
final Field _isNotSecureProcessing = tfactory.getClass().getDeclaredField("_isNotSecureProcessing");
_isNotSecureProcessing.setAccessible(true);
_isNotSecureProcessing.set(tfactory, Boolean.TRUE);
} catch (final Exception x) {
if (project != null) {
project.log(x.toString(), Project.MSG_DEBUG);
}
}
}
}
}