package org.codehaus.mojo.xml; /* * 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. */ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; import javax.xml.transform.Source; 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.sax.SAXSource; import javax.xml.transform.stream.StreamResult; import org.apache.maven.model.Resource; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.project.MavenProject; import org.codehaus.mojo.xml.transformer.NameValuePair; import org.codehaus.mojo.xml.transformer.TransformationSet; import org.codehaus.plexus.components.io.filemappers.FileMapper; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.IOUtil; import org.xml.sax.InputSource; /** * The TransformMojo is used for transforming a set of files using a common stylesheet. * * @goal transform * @phase generate-resources */ public class TransformMojo extends AbstractXmlMojo { /** * Specifies one or more sets of files, which are being * transformed. * @parameter */ private TransformationSet[] transformationSets; /** * Whether creating the transformed files should be forced. * @parameter expression="${xml.forceCreation}" default-value="false" */ private boolean forceCreation; /** * Transformer factory use. By default, the systems default transformer * factory is used. * <b>If you use this feature you must use at least jdk 1.6</b> * @parameter expression="${xml.transformerFactory}" */ private String transformerFactory; private void setFeature( TransformerFactory pTransformerFactory, String name, Boolean value ) throws MojoExecutionException { // Try to use the method setFeature, which isn't available until JAXP 1.3 Method m; try { m = pTransformerFactory.getClass().getMethod( "setFeature", new Class[]{ String.class, boolean.class } ); } catch ( NoSuchMethodException e ) { m = null; } if ( m == null ) { // Not available, try to use setAttribute pTransformerFactory.setAttribute( name, value ); } else { try { m.invoke( pTransformerFactory, new Object[]{ name, value } ); } catch ( IllegalAccessException e ) { throw new MojoExecutionException( e.getMessage(), e ); } catch ( InvocationTargetException e ) { Throwable t = e.getTargetException(); throw new MojoExecutionException( t.getMessage(), t ); } } } private Templates getTemplate( Resolver pResolver, Source stylesheet, TransformationSet transformationSet ) throws MojoExecutionException, MojoFailureException { TransformerFactory tf = getTransformerFactory(); if ( pResolver != null ) { tf.setURIResolver( pResolver ); } NameValuePair[] features = transformationSet.getFeatures(); if ( features != null ) { for ( int i = 0; i < features.length; i++ ) { final NameValuePair feature = features[i]; final String name = feature.getName(); if ( name == null || name.length() == 0 ) { throw new MojoFailureException( "A features name is missing or empty." ); } final String value = feature.getValue(); if ( value == null ) { throw new MojoFailureException( "No value specified for feature " + name ); } setFeature( tf, name, Boolean.valueOf( value ) ); } } NameValuePair[] attributes = transformationSet.getAttributes(); if ( attributes != null ) { for ( int i = 0; i < attributes.length; i++ ) { final NameValuePair attribute = attributes[i]; final String name = attribute.getName(); if ( name == null || name.length() == 0 ) { throw new MojoFailureException( "An attributes name is missing or empty." ); } final String value = attribute.getValue(); if ( value == null ) { throw new MojoFailureException( "No value specified for attribute " + name ); } tf.setAttribute( name, value ); } } try { return tf.newTemplates( stylesheet ); } catch ( TransformerConfigurationException e ) { throw new MojoExecutionException( "Failed to parse stylesheet " + stylesheet + ": " + e.getMessage(), e ); } } /** * Creates a new instance of {@link TransformerFactory}. */ private TransformerFactory getTransformerFactory( ) throws MojoFailureException, MojoExecutionException { if ( transformerFactory == null ) { return TransformerFactory.newInstance(); } try { return newTransformerFactory( transformerFactory, Thread.currentThread().getContextClassLoader() ); } catch ( NoSuchMethodException exception ) { throw new MojoFailureException( "JDK6 required when using transformerFactory parameter" ); } catch ( IllegalAccessException exception ) { throw new MojoExecutionException( "Cannot instantiate transformer factory", exception ); } catch ( InvocationTargetException exception ) { throw new MojoExecutionException( "Cannot instantiate transformer factory", exception ); } } // public for use by unit test public static TransformerFactory newTransformerFactory( String factoryClassName, ClassLoader classLoader ) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { // use reflection to avoid JAXP 1.4 (and hence JDK6) requirement Class[] methodTypes = new Class[] { String.class, ClassLoader.class }; Method method = TransformerFactory.class.getDeclaredMethod( "newInstance", methodTypes ); Object[] methodArgs = new Object[] { factoryClassName, classLoader }; return (TransformerFactory) method.invoke( null, methodArgs ); } private File getFile( File pDir, String pFile ) { if ( new File( pFile ).isAbsolute() ) { throw new IllegalStateException( "Output/Input file names must not be absolute." ); } return new File( pDir, pFile ); } private File getDir( File pDir ) { if ( pDir == null ) { return getBasedir(); } return asAbsoluteFile( pDir ); } private void addToClasspath( File pOutputDir ) { MavenProject project = getProject(); for ( Iterator iter = project.getResources().iterator(); iter.hasNext(); ) { Resource resource = (Resource) iter.next(); if ( resource.getDirectory().equals( pOutputDir ) ) { return; } } Resource resource = new Resource(); resource.setDirectory( pOutputDir.getPath() ); resource.setFiltering( false ); project.addResource( resource ); } private File getOutputDir( File pOutputDir ) { if ( pOutputDir == null ) { MavenProject project = getProject(); String dir = project.getBuild().getDirectory(); if ( dir == null ) { throw new IllegalStateException( "The projects build directory is null." ); } dir += "/generated-resources/xml/xslt"; return asAbsoluteFile( new File( dir ) ); } return asAbsoluteFile( pOutputDir ); } private static String getAllExMsgs( Throwable ex, boolean includeExName ) { StringBuffer sb = new StringBuffer( ( includeExName ? ex.toString() : ex.getLocalizedMessage() ) ); while ( ( ex = ex.getCause() ) != null ) { sb.append( "\nCaused by: " + ex.toString() ); } return sb.toString(); } /** * * @param files the fileNames or URLs to scan their lastModified timestamp. * @param oldest if true, returns the latest modificationDate of all files, * otherwise returns the earliest. * @return the older or younger last modification timestamp of all files. */ protected long findLastModified( List/*<Object>*/files, boolean oldest ) { long timeStamp = ( oldest ? Long.MIN_VALUE : Long.MAX_VALUE ); for ( Iterator it = files.iterator(); it.hasNext(); ) { Object no = it.next(); if ( no != null ) { long fileModifTime; if ( no instanceof File ) { fileModifTime = ( (File) no ).lastModified(); } else // either URL or filePath { String sdep = no.toString(); try { URL url = new URL( sdep ); URLConnection uCon = url.openConnection(); uCon.setUseCaches( false ); fileModifTime = uCon.getLastModified(); } catch ( MalformedURLException e ) { fileModifTime = new File( sdep ).lastModified(); } catch ( IOException ex ) { fileModifTime = ( oldest ? Long.MIN_VALUE : Long.MAX_VALUE ); getLog().warn( "Skipping URL '" + no + "' from up-to-date check due to error while opening connection: " + getAllExMsgs( ex, true ) ); } } getLog().debug( ( oldest ? "Depends " : "Produces " ) + no + ": " + new Date( fileModifTime ) ); if ( ( fileModifTime > timeStamp ) ^ !oldest ) { timeStamp = fileModifTime; } } // end if file null. } // end filesloop if ( timeStamp == Long.MIN_VALUE ) { // no older file found return Long.MAX_VALUE; // assume re-execution required. } else if ( timeStamp == Long.MAX_VALUE ) { // no younger file found return Long.MIN_VALUE; // assume re-execution required. } return timeStamp; } /** * @return true to indicate results are up-to-date, that is, when the latest * from input files is earlier than the younger from the output * files (meaning no re-execution required). */ protected boolean isUpdToDate( List dependsFiles, List producesFiles ) { // The older timeStamp of all input files; long inputTimeStamp = findLastModified( dependsFiles, true ); // The younger of all destination files. long destTimeStamp = producesFiles == null ? Long.MIN_VALUE : findLastModified( producesFiles, false ); getLog().debug( "Depends timeStamp: " + inputTimeStamp + ", produces timestamp: " + destTimeStamp ); return inputTimeStamp < destTimeStamp; } private void transform( Transformer pTransformer, File input, File output, Resolver pResolver ) throws MojoExecutionException { File dir = output.getParentFile(); dir.mkdirs(); getLog().info( "Transforming file: " + input.getPath() ); FileOutputStream fos = null; try { final boolean transformInPlace = output.equals( input ); File tmpOutput = null; if ( transformInPlace ) { tmpOutput = File.createTempFile( "xml-maven-plugin", "xml" ); tmpOutput.deleteOnExit(); fos = new FileOutputStream( tmpOutput ); } else { fos = new FileOutputStream( output ); } final String parentFile = input.getParent() == null ? null : input.getParentFile().toURI().toURL().toExternalForm(); pTransformer.transform( pResolver.resolve( input.toURI().toURL().toExternalForm(), parentFile ), new StreamResult( fos ) ); fos.close(); fos = null; if ( transformInPlace ) { FileUtils.copyFile( tmpOutput, output ); /* tmpOutput is a temporary file */ tmpOutput.delete(); } } catch ( IOException e ) { throw new MojoExecutionException( "Failed to create output file " + output.getPath() + ": " + e.getMessage(), e ); } catch ( TransformerException e ) { throw new MojoExecutionException( "Failed to transform input file " + input.getPath() + ": " + e.getMessage(), e ); } finally { if ( fos != null ) { try { fos.close(); } catch ( Throwable t ) { /* Ignore me */ } } } } private File getOutputFile( File targetDir, String pName, FileMapper[] pFileMappers ) { String name = pName; if ( pFileMappers != null ) { for ( int i = 0; i < pFileMappers.length; i++ ) { name = pFileMappers[i].getMappedFileName( name ); } } return getFile( targetDir, name ); } private void transform( Resolver pResolver, TransformationSet pTransformationSet ) throws MojoExecutionException, MojoFailureException { String[] fileNames = getFileNames( pTransformationSet.getDir(), pTransformationSet.getIncludes(), getExcludes( pTransformationSet.getExcludes(), pTransformationSet.isSkipDefaultExcludes() ) ); if ( fileNames == null || fileNames.length == 0 ) { getLog().warn( "No files found for transformation by stylesheet " + pTransformationSet.getStylesheet() ); return; } String stylesheetName = pTransformationSet.getStylesheet(); if ( stylesheetName == null ) { getLog().warn( "No stylesheet configured." ); return; } final URL stylesheetUrl = getResource( stylesheetName ); Templates template; InputStream stream = null; try { stream = stylesheetUrl.openStream(); InputSource isource = new InputSource( stream ); isource.setSystemId( stylesheetUrl.toExternalForm() ); template = getTemplate( pResolver, new SAXSource( isource ), pTransformationSet ); stream.close(); stream = null; } catch ( IOException e ) { throw new MojoExecutionException( e.getMessage(), e ); } finally { IOUtil.close( stream ); } int filesTransformed = 0; File inputDir = getDir( pTransformationSet.getDir() ); File outputDir = getOutputDir( pTransformationSet.getOutputDir() ); for ( int i = 0; i < fileNames.length; i++ ) { final Transformer t; File input = getFile( inputDir, fileNames[i] ); File output = getOutputFile( outputDir, fileNames[i], pTransformationSet.getFileMappers() ); // Perform up-to-date-check. boolean needsTransform = forceCreation; if ( !needsTransform ) { List dependsFiles = new ArrayList(); List producesFiles = new ArrayList(); // Depends from pom.xml file for when project configuration changes. dependsFiles.add( getProject().getFile() ); if ( "file".equals( stylesheetUrl.getProtocol() ) ) { dependsFiles.add( new File( stylesheetUrl.getFile() ) ); } List catalogFiles = new ArrayList(); List catalogUrls = new ArrayList(); setCatalogs( catalogFiles, catalogUrls ); dependsFiles.addAll( catalogFiles ); dependsFiles.add( input ); File[] files = asFiles( getBasedir(), pTransformationSet.getOtherDepends() ); for ( int j = 0; j < files.length; j++ ) { dependsFiles.add( files[j] ); } producesFiles.add( output ); needsTransform = !isUpdToDate( dependsFiles, producesFiles ); } if ( !needsTransform ) { getLog().debug( "Skipping XSL transformation. File " + fileNames[i] + " is up-to-date." ); } else { filesTransformed++; // Perform transformation. try { t = newTransformer( template, pTransformationSet ); t.setURIResolver( pResolver ); NameValuePair[] parameters = pTransformationSet.getParameters(); if ( parameters != null ) { for ( int j = 0; j < parameters.length; j++ ) { NameValuePair key = parameters[j]; t.setParameter( key.getName(), key.getValue() ); } } transform( t, input, output, pResolver ); } catch ( TransformerConfigurationException e ) { throw new MojoExecutionException( "Failed to create Transformer: " + e.getMessage(), e ); } } } // end file loop if ( filesTransformed > 0 ) { getLog().info( "Transformed " + filesTransformed + " file(s)." ); } if ( pTransformationSet.isAddedToClasspath() ) { addToClasspath( pTransformationSet.getOutputDir() ); } } private Transformer newTransformer( Templates template, TransformationSet pTransformationSet ) throws TransformerConfigurationException, MojoExecutionException, MojoFailureException { Transformer t = template.newTransformer(); NameValuePair[] properties = pTransformationSet.getOutputProperties(); if ( properties != null ) { for ( int i = 0; i < properties.length; i++ ) { final String name = properties[i].getName(); if ( name == null || "".equals( name ) ) { throw new MojoFailureException( "Missing or empty output property name" ); } final String value = properties[i].getValue(); if ( value == null ) { throw new MojoFailureException( "Missing value for output property " + name ); } try { t.setOutputProperty( name, value ); } catch ( IllegalArgumentException e ) { throw new MojoExecutionException( "Unsupported property name or value: " + name + " => " + value + e.getMessage(), e ); } } } return t; } /** * Called by Maven to run the plugin. */ public void execute() throws MojoExecutionException, MojoFailureException { if ( transformationSets == null || transformationSets.length == 0 ) { throw new MojoFailureException( "No TransformationSets configured." ); } Object oldProxySettings = activateProxy(); try { Resolver resolver = getResolver( ); for ( int i = 0; i < transformationSets.length; i++ ) { TransformationSet transformationSet = transformationSets[i]; resolver.setValidating( transformationSet.isValidating() ); transform( resolver, transformationSet ); } } finally { passivateProxy( oldProxySettings ); } } }