package org.apache.tuscany.maven.zip; /* * 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 org.apache.maven.archiver.MavenArchiveConfiguration; import org.apache.maven.archiver.MavenArchiver; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter; import org.apache.maven.model.Resource; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.archiver.ArchiverException; import org.codehaus.plexus.archiver.UnArchiver; import org.codehaus.plexus.archiver.jar.JarArchiver; import org.codehaus.plexus.archiver.manager.ArchiverManager; import org.codehaus.plexus.archiver.manager.NoSuchArchiverException; import org.codehaus.plexus.util.DirectoryScanner; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.IOUtil; import org.codehaus.plexus.util.InterpolationFilterReader; import org.codehaus.plexus.util.StringUtils; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; /** * Based on code from the Maven WAR plugin 2.0.2 AbstractWarMojo */ public abstract class AbstractZipMojo extends AbstractMojo { /** * The maven project. * * @parameter expression="${project}" * @required * @readonly */ private MavenProject project; /** * The directory containing generated classes. * * @parameter expression="${project.build.outputDirectory}" * @required * @readonly */ private File classesDirectory; /** * Whether a JAR file will be created for the classes in the webapp. Using this optional configuration * parameter will make the generated classes to be archived into a jar file * and the classes directory will then be excluded from the webapp. * * @parameter expression="${archiveClasses}" default-value="false" */ private boolean archiveClasses; /** * The Jar archiver needed for archiving classes directory into jar file. * * @parameter expression="${component.org.codehaus.plexus.archiver.Archiver#jar}" * @required */ private JarArchiver jarArchiver; /** * The directory where the SCA ZIP contribution is built. * * @parameter expression="${project.build.directory}/${project.build.finalName}" * @required */ private File zipDirectory; /** * Filters (property files) to include during the interpolation of the pom.xml. * * @parameter expression="${project.build.filters}" */ private List filters; /** * Directory to unpack dependent WARs into if needed * * @parameter expression="${project.build.directory}/war/work" * @required */ private File workDirectory; /** * To look up Archiver/UnArchiver implementations * * @parameter expression="${component.org.codehaus.plexus.archiver.manager.ArchiverManager}" * @required */ protected ArchiverManager archiverManager; private static final String META_INF = "META-INF"; private static final String[] DEFAULT_INCLUDES = {"**/**"}; /** * The comma separated list of tokens to include in the SCA ZIP. * Default is '**'. * * @parameter alias="includes" */ private String warSourceIncludes = "**"; /** * The comma separated list of tokens to exclude from the WAR. * * @parameter alias="excludes" */ private String warSourceExcludes; /** * The comma separated list of tokens to include when doing * a war overlay. * Default is '**' * * @parameter */ private String dependentWarIncludes = "**"; /** * The comma separated list of tokens to exclude when doing * a war overlay. * * @parameter */ private String dependentWarExcludes; /** * The maven archive configuration to use. * * @parameter */ protected MavenArchiveConfiguration archive = new MavenArchiveConfiguration(); private static final String[] EMPTY_STRING_ARRAY = {}; public MavenProject getProject() { return project; } public void setProject( MavenProject project ) { this.project = project; } public File getClassesDirectory() { return classesDirectory; } public void setClassesDirectory( File classesDirectory ) { this.classesDirectory = classesDirectory; } public File getZipDirectory() { return zipDirectory; } public void setScaZipDirectory( File scaZipDirectory ) { this.zipDirectory = scaZipDirectory; } /** * Returns a string array of the excludes to be used * when assembling/copying the war. * * @return an array of tokens to exclude */ protected String[] getExcludes() { List excludeList = new ArrayList(); if ( StringUtils.isNotEmpty( warSourceExcludes ) ) { excludeList.addAll( Arrays.asList( StringUtils.split( warSourceExcludes, "," ) ) ); } return (String[]) excludeList.toArray( EMPTY_STRING_ARRAY ); } /** * Returns a string array of the includes to be used * when assembling/copying the war. * * @return an array of tokens to include */ protected String[] getIncludes() { return StringUtils.split( StringUtils.defaultString( warSourceIncludes ), "," ); } /** * Returns a string array of the excludes to be used * when adding dependent wars as an overlay onto this war. * * @return an array of tokens to exclude */ protected String[] getDependentWarExcludes() { String[] excludes; if ( StringUtils.isNotEmpty( dependentWarExcludes ) ) { excludes = StringUtils.split( dependentWarExcludes, "," ); } else { excludes = EMPTY_STRING_ARRAY; } return excludes; } /** * Returns a string array of the includes to be used * when adding dependent wars as an overlay onto this war. * * @return an array of tokens to include */ protected String[] getDependentWarIncludes() { return StringUtils.split( StringUtils.defaultString( dependentWarIncludes ), "," ); } public void buildExplodedWebapp( File webappDirectory ) throws MojoExecutionException, MojoFailureException { getLog().info( "Exploding webapp..." ); webappDirectory.mkdirs(); try { buildWebapp( project, webappDirectory ); } catch ( IOException e ) { throw new MojoExecutionException( "Could not explode webapp...", e ); } } private Map getBuildFilterProperties() throws MojoExecutionException { Map filterProperties = new Properties(); // System properties filterProperties.putAll( System.getProperties() ); // Project properties filterProperties.putAll( project.getProperties() ); for ( Iterator i = filters.iterator(); i.hasNext(); ) { String filtersfile = (String) i.next(); try { Properties properties = PropertyUtils.loadPropertyFile( new File( filtersfile ), true, true ); filterProperties.putAll( properties ); } catch ( IOException e ) { throw new MojoExecutionException( "Error loading property file '" + filtersfile + "'", e ); } } // can't putAll, as ReflectionProperties doesn't enumerate - so we make a composite map with the project variables as dominant return new CompositeMap( new ReflectionProperties( project ), filterProperties ); } /** * Copies webapp webResources from the specified directory. *

* Note that the webXml parameter could be null and may * specify a file which is not named web.xml. If the file * exists, it will be copied to the META-INF directory and * renamed accordingly. * * @param resource the resource to copy * @param webappDirectory the target directory * @param filterProperties * @throws java.io.IOException if an error occured while copying webResources */ public void copyResources( Resource resource, File webappDirectory, Map filterProperties ) throws IOException { if ( !resource.getDirectory().equals( webappDirectory.getPath() ) ) { getLog().info( "Copy webapp webResources to " + webappDirectory.getAbsolutePath() ); if ( webappDirectory.exists() ) { String[] fileNames = getWarFiles( resource ); String targetPath = (resource.getTargetPath() == null) ? "" : resource.getTargetPath(); File destination = new File(webappDirectory,targetPath); for ( int i = 0; i < fileNames.length; i++ ) { if ( resource.isFiltering() ) { copyFilteredFile( new File( resource.getDirectory(), fileNames[i] ), new File( destination, fileNames[i] ), null, getFilterWrappers(), filterProperties ); } else { copyFileIfModified( new File( resource.getDirectory(), fileNames[i] ), new File( destination, fileNames[i] ) ); } } } } } /** * Generates the JAR. * * @todo Add license files in META-INF directory. */ public void createJarArchive( File libDirectory ) throws MojoExecutionException { String archiveName = project.getBuild().getFinalName() + ".jar"; File jarFile = new File( libDirectory, archiveName ); MavenArchiver archiver = new MavenArchiver(); archiver.setArchiver( jarArchiver ); archiver.setOutputFile( jarFile ); try { archiver.getArchiver().addDirectory( classesDirectory, getIncludes(), getExcludes() ); archiver.createArchive( project, archive ); } catch ( Exception e ) { // TODO: improve error handling throw new MojoExecutionException( "Error assembling JAR", e ); } } /** * Builds the webapp for the specified project. *

* Classes, libraries and tld files are copied to * the webappDirectory during this phase. * * @param project the maven project * @param webappDirectory * @throws java.io.IOException if an error occured while building the webapp */ public void buildWebapp( MavenProject project, File webappDirectory ) throws MojoExecutionException, IOException, MojoFailureException { getLog().info( "Assembling webapp " + project.getArtifactId() + " in " + webappDirectory ); File metainfDir = new File( webappDirectory, META_INF ); metainfDir.mkdirs(); File libDirectory = new File( webappDirectory, "/lib" ); File webappClassesDirectory = new File( webappDirectory, "/" ); if ( classesDirectory.exists() && !classesDirectory.equals( webappClassesDirectory ) ) { if ( archiveClasses ) { createJarArchive( libDirectory ); } else { copyDirectoryStructureIfModified( classesDirectory, webappClassesDirectory ); } } Set artifacts = project.getArtifacts(); List duplicates = findDuplicates( artifacts ); List dependentWarDirectories = new ArrayList(); for ( Iterator iter = artifacts.iterator(); iter.hasNext(); ) { Artifact artifact = (Artifact) iter.next(); String targetFileName = getDefaultFinalName( artifact ); getLog().debug( "Processing: " + targetFileName ); if ( duplicates.contains( targetFileName ) ) { getLog().debug( "Duplicate found: " + targetFileName ); targetFileName = artifact.getGroupId() + "-" + targetFileName; getLog().debug( "Renamed to: " + targetFileName ); } // TODO: utilise appropriate methods from project builder ScopeArtifactFilter filter = new ScopeArtifactFilter( Artifact.SCOPE_RUNTIME ); if ( !artifact.isOptional() && filter.include( artifact ) ) { String type = artifact.getType(); if ( "jar".equals( type ) || "ejb".equals( type ) || "ejb-client".equals( type ) ) { copyFileIfModified( artifact.getFile(), new File( libDirectory, targetFileName ) ); } else { if ( "par".equals( type ) ) { targetFileName = targetFileName.substring( 0, targetFileName.lastIndexOf( '.' ) ) + ".jar"; getLog().debug( "Copying " + artifact.getFile() + " to " + new File( libDirectory, targetFileName ) ); copyFileIfModified( artifact.getFile(), new File( libDirectory, targetFileName ) ); } else { if ( "war".equals( type ) ) { dependentWarDirectories.add( unpackWarToTempDirectory( artifact ) ); } else { getLog().debug( "Skipping artifact of type " + type + " for WEB-INF/lib" ); } } } } } if ( dependentWarDirectories.size() > 0 ) { getLog().info( "Overlaying " + dependentWarDirectories.size() + " war(s)." ); // overlay dependent wars for ( Iterator iter = dependentWarDirectories.iterator(); iter.hasNext(); ) { copyDependentWarContents( (File) iter.next(), webappDirectory ); } } } /** * Searches a set of artifacts for duplicate filenames and returns a list of duplicates. * * @param artifacts set of artifacts * @return List of duplicated artifacts */ private List findDuplicates( Set artifacts ) { List duplicates = new ArrayList(); List identifiers = new ArrayList(); for ( Iterator iter = artifacts.iterator(); iter.hasNext(); ) { Artifact artifact = (Artifact) iter.next(); String candidate = getDefaultFinalName( artifact ); if ( identifiers.contains( candidate ) ) { duplicates.add( candidate ); } else { identifiers.add( candidate ); } } return duplicates; } /** * Unpacks war artifacts into a temporary directory inside workDirectory * named with the name of the war. * * @param artifact War artifact to unpack. * @return Directory containing the unpacked war. * @throws MojoExecutionException */ private File unpackWarToTempDirectory( Artifact artifact ) throws MojoExecutionException { String name = artifact.getFile().getName(); File tempLocation = new File( workDirectory, name.substring( 0, name.length() - 4 ) ); boolean process = false; if ( !tempLocation.exists() ) { tempLocation.mkdirs(); process = true; } else if ( artifact.getFile().lastModified() > tempLocation.lastModified() ) { process = true; } if ( process ) { File file = artifact.getFile(); try { unpack( file, tempLocation ); } catch ( NoSuchArchiverException e ) { this.getLog().info( "Skip unpacking dependency file with unknown extension: " + file.getPath() ); } } return tempLocation; } /** * Unpacks the archive file. * * @param file File to be unpacked. * @param location Location where to put the unpacked files. */ private void unpack( File file, File location ) throws MojoExecutionException, NoSuchArchiverException { String archiveExt = FileUtils.getExtension( file.getAbsolutePath() ).toLowerCase(); try { UnArchiver unArchiver = archiverManager.getUnArchiver( archiveExt ); unArchiver.setSourceFile( file ); unArchiver.setDestDirectory( location ); unArchiver.setOverwrite( true ); unArchiver.extract(); } catch ( IOException e ) { throw new MojoExecutionException( "Error unpacking file: " + file + "to: " + location, e ); } catch ( ArchiverException e ) { throw new MojoExecutionException( "Error unpacking file: " + file + "to: " + location, e ); } } /** * Recursively copies contents of srcDir into targetDir. * This will not overwrite any existing files. * * @param srcDir Directory containing unpacked dependent war contents * @param targetDir Directory to overlay srcDir into */ private void copyDependentWarContents( File srcDir, File targetDir ) throws MojoExecutionException { DirectoryScanner scanner = new DirectoryScanner(); scanner.setBasedir( srcDir ); scanner.setExcludes( getDependentWarExcludes() ); scanner.addDefaultExcludes(); scanner.setIncludes( getDependentWarIncludes() ); scanner.scan(); String[] dirs = scanner.getIncludedDirectories(); for ( int j = 0; j < dirs.length; j++ ) { new File( targetDir, dirs[j] ).mkdirs(); } String[] files = scanner.getIncludedFiles(); for ( int j = 0; j < files.length; j++ ) { File targetFile = new File( targetDir, files[j] ); try { targetFile.getParentFile().mkdirs(); copyFileIfModified( new File( srcDir, files[j] ), targetFile ); } catch ( IOException e ) { throw new MojoExecutionException( "Error copying file '" + files[j] + "' to '" + targetFile + "'", e ); } } } /** * Returns a list of filenames that should be copied * over to the destination directory. * * @param sourceDir the directory to be scanned * @return the array of filenames, relative to the sourceDir */ private String[] getWarFiles( File sourceDir ) { DirectoryScanner scanner = new DirectoryScanner(); scanner.setBasedir( sourceDir ); scanner.setExcludes( getExcludes() ); scanner.addDefaultExcludes(); scanner.setIncludes( getIncludes() ); scanner.scan(); return scanner.getIncludedFiles(); } /** * Returns a list of filenames that should be copied * over to the destination directory. * * @param resource the resource to be scanned * @return the array of filenames, relative to the sourceDir */ private String[] getWarFiles( Resource resource ) { DirectoryScanner scanner = new DirectoryScanner(); scanner.setBasedir( resource.getDirectory() ); if ( resource.getIncludes() != null && !resource.getIncludes().isEmpty() ) { scanner.setIncludes( (String[]) resource.getIncludes().toArray( EMPTY_STRING_ARRAY ) ); } else { scanner.setIncludes( DEFAULT_INCLUDES ); } if ( resource.getExcludes() != null && !resource.getExcludes().isEmpty() ) { scanner.setExcludes( (String[]) resource.getExcludes().toArray( EMPTY_STRING_ARRAY ) ); } scanner.addDefaultExcludes(); scanner.scan(); return scanner.getIncludedFiles(); } /** * Copy file from source to destination only if source is newer than the target file. * If destinationDirectory does not exist, it * (and any parent directories) will be created. If a file source in * destinationDirectory exists, it will be overwritten. * * @param source An existing File to copy. * @param destinationDirectory A directory to copy source into. * @throws java.io.FileNotFoundException if source isn't a normal file. * @throws IllegalArgumentException if destinationDirectory isn't a directory. * @throws java.io.IOException if source does not exist, the file in * destinationDirectory cannot be written to, or an IO error occurs during copying. *

* TO DO: Remove this method when Maven moves to plexus-utils version 1.4 */ private static void copyFileToDirectoryIfModified( File source, File destinationDirectory ) throws IOException { // TO DO: Remove this method and use the method in WarFileUtils when Maven 2 changes // to plexus-utils 1.2. if ( destinationDirectory.exists() && !destinationDirectory.isDirectory() ) { throw new IllegalArgumentException( "Destination is not a directory" ); } copyFileIfModified( source, new File( destinationDirectory, source.getName() ) ); } private FilterWrapper[] getFilterWrappers() { return new FilterWrapper[]{ // support ${token} new FilterWrapper() { public Reader getReader( Reader fileReader, Map filterProperties ) { return new InterpolationFilterReader( fileReader, filterProperties, "${", "}" ); } }, // support @token@ new FilterWrapper() { public Reader getReader( Reader fileReader, Map filterProperties ) { return new InterpolationFilterReader( fileReader, filterProperties, "@", "@" ); } }}; } /** * @param from * @param to * @param encoding * @param wrappers * @param filterProperties * @throws IOException TO DO: Remove this method when Maven moves to plexus-utils version 1.4 */ private static void copyFilteredFile( File from, File to, String encoding, FilterWrapper[] wrappers, Map filterProperties ) throws IOException { // buffer so it isn't reading a byte at a time! Reader fileReader = null; Writer fileWriter = null; try { // fix for MWAR-36, ensures that the parent dir are created first to.getParentFile().mkdirs(); if ( encoding == null || encoding.length() < 1 ) { fileReader = new BufferedReader( new FileReader( from ) ); fileWriter = new FileWriter( to ); } else { FileInputStream instream = new FileInputStream( from ); FileOutputStream outstream = new FileOutputStream( to ); fileReader = new BufferedReader( new InputStreamReader( instream, encoding ) ); fileWriter = new OutputStreamWriter( outstream, encoding ); } Reader reader = fileReader; for ( int i = 0; i < wrappers.length; i++ ) { FilterWrapper wrapper = wrappers[i]; reader = wrapper.getReader( reader, filterProperties ); } IOUtil.copy( reader, fileWriter ); } finally { IOUtil.close( fileReader ); IOUtil.close( fileWriter ); } } /** * Copy file from source to destination only if source timestamp is later than the destination timestamp. * The directories up to destination will be created if they don't already exist. * destination will be overwritten if it already exists. * * @param source An existing non-directory File to copy bytes from. * @param destination A non-directory File to write bytes to (possibly * overwriting). * @throws IOException if source does not exist, destination cannot be * written to, or an IO error occurs during copying. * @throws java.io.FileNotFoundException if destination is a directory *

* TO DO: Remove this method when Maven moves to plexus-utils version 1.4 */ private static void copyFileIfModified( File source, File destination ) throws IOException { // TO DO: Remove this method and use the method in WarFileUtils when Maven 2 changes // to plexus-utils 1.2. if ( destination.lastModified() < source.lastModified() ) { FileUtils.copyFile( source.getCanonicalFile(), destination ); // preserve timestamp destination.setLastModified( source.lastModified() ); } } /** * Copies a entire directory structure but only source files with timestamp later than the destinations'. *

* Note: *

* * @param sourceDirectory * @param destinationDirectory * @throws IOException TO DO: Remove this method when Maven moves to plexus-utils version 1.4 */ private static void copyDirectoryStructureIfModified( File sourceDirectory, File destinationDirectory ) throws IOException { if ( !sourceDirectory.exists() ) { throw new IOException( "Source directory doesn't exists (" + sourceDirectory.getAbsolutePath() + ")." ); } File[] files = sourceDirectory.listFiles(); String sourcePath = sourceDirectory.getAbsolutePath(); for ( int i = 0; i < files.length; i++ ) { File file = files[i]; String dest = file.getAbsolutePath(); dest = dest.substring( sourcePath.length() + 1 ); File destination = new File( destinationDirectory, dest ); if ( file.isFile() ) { destination = destination.getParentFile(); copyFileToDirectoryIfModified( file, destination ); } else if ( file.isDirectory() ) { if ( !destination.exists() && !destination.mkdirs() ) { throw new IOException( "Could not create destination directory '" + destination.getAbsolutePath() + "'." ); } copyDirectoryStructureIfModified( file, destination ); } else { throw new IOException( "Unknown file type: " + file.getAbsolutePath() ); } } } /** * TO DO: Remove this interface when Maven moves to plexus-utils version 1.4 */ private interface FilterWrapper { Reader getReader( Reader fileReader, Map filterProperties ); } /** * Converts the filename of an artifact to artifactId-version.type format. * * @param artifact * @return converted filename of the artifact */ private String getDefaultFinalName( Artifact artifact ) { String finalName = artifact.getArtifactId() + "-" + artifact.getVersion(); String classifier = artifact.getClassifier(); if ( ( classifier != null ) && ! ( "".equals( classifier.trim() ) ) ) { finalName += "-" + classifier; } finalName = finalName + "." + artifact.getArtifactHandler().getExtension(); return finalName; } }