| package org.apache.maven.plugins.war.packaging; |
| |
| /* |
| * 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.IOException; |
| |
| import org.apache.commons.io.input.XmlStreamReader; |
| import org.apache.maven.artifact.Artifact; |
| import org.apache.maven.plugin.MojoExecutionException; |
| import org.apache.maven.plugins.war.util.PathSet; |
| import org.apache.maven.plugins.war.util.WebappStructure; |
| import org.apache.maven.shared.filtering.MavenFilteringException; |
| import org.apache.maven.shared.mapping.MappingUtils; |
| 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.NoSuchArchiverException; |
| import org.codehaus.plexus.interpolation.InterpolationException; |
| import org.codehaus.plexus.util.DirectoryScanner; |
| import org.codehaus.plexus.util.FileUtils; |
| import org.codehaus.plexus.util.IOUtil; |
| |
| /** |
| * @author Stephane Nicoll |
| * @version $Id$ |
| */ |
| public abstract class AbstractWarPackagingTask |
| implements WarPackagingTask |
| { |
| /** |
| * The default list of includes. |
| */ |
| public static final String[] DEFAULT_INCLUDES = { "**/**" }; |
| |
| /** |
| * The {@code WEB-INF} path. |
| */ |
| public static final String WEB_INF_PATH = "WEB-INF"; |
| |
| /** |
| * The {@code META-INF} path. |
| */ |
| public static final String META_INF_PATH = "META-INF"; |
| |
| /** |
| * The {@code classes} path. |
| */ |
| public static final String CLASSES_PATH = "WEB-INF/classes/"; |
| |
| /** |
| * The {@code lib} path. |
| */ |
| public static final String LIB_PATH = "WEB-INF/lib/"; |
| |
| /** |
| * Copies the files if possible with an optional target prefix. |
| * <p/> |
| * Copy uses a first-win strategy: files that have already been copied by previous tasks are ignored. This method |
| * makes sure to update the list of protected files which gives the list of files that have already been copied. |
| * <p/> |
| * If the structure of the source directory is not the same as the root of the webapp, use the <tt>targetPrefix</tt> |
| * parameter to specify in which particular directory the files should be copied. Use <tt>null</tt> to copy the |
| * files with the same structure |
| * |
| * @param sourceId the source id |
| * @param context the context to use |
| * @param sourceBaseDir the base directory from which the <tt>sourceFilesSet</tt> will be copied |
| * @param sourceFilesSet the files to be copied |
| * @param targetPrefix the prefix to add to the target file name |
| * @param filtered filter or not. |
| * @throws IOException if an error occurred while copying the files |
| * @throws MojoExecutionException if an error occurs. |
| */ |
| protected void copyFiles( String sourceId, WarPackagingContext context, File sourceBaseDir, PathSet sourceFilesSet, |
| String targetPrefix, boolean filtered ) |
| throws IOException, MojoExecutionException |
| { |
| for ( String fileToCopyName : sourceFilesSet.paths() ) |
| { |
| final File sourceFile = new File( sourceBaseDir, fileToCopyName ); |
| |
| String destinationFileName; |
| if ( targetPrefix == null ) |
| { |
| destinationFileName = fileToCopyName; |
| } |
| else |
| { |
| destinationFileName = targetPrefix + fileToCopyName; |
| } |
| |
| if ( filtered && !context.isNonFilteredExtension( sourceFile.getName() ) ) |
| { |
| copyFilteredFile( sourceId, context, sourceFile, destinationFileName ); |
| } |
| else |
| { |
| copyFile( sourceId, context, sourceFile, destinationFileName ); |
| } |
| } |
| } |
| |
| /** |
| * Copies the files if possible as is. |
| * <p/> |
| * Copy uses a first-win strategy: files that have already been copied by previous tasks are ignored. This method |
| * makes sure to update the list of protected files which gives the list of files that have already been copied. |
| * |
| * @param sourceId the source id |
| * @param context the context to use |
| * @param sourceBaseDir the base directory from which the <tt>sourceFilesSet</tt> will be copied |
| * @param sourceFilesSet the files to be copied |
| * @param filtered filter or not. |
| * @throws IOException if an error occurred while copying the files |
| * @throws MojoExecutionException break the build. |
| */ |
| protected void copyFiles( String sourceId, WarPackagingContext context, File sourceBaseDir, PathSet sourceFilesSet, |
| boolean filtered ) |
| throws IOException, MojoExecutionException |
| { |
| copyFiles( sourceId, context, sourceBaseDir, sourceFilesSet, null, filtered ); |
| } |
| |
| /** |
| * Copy the specified file if the target location has not yet already been used. |
| * <p/> |
| * The <tt>targetFileName</tt> is the relative path according to the root of the generated web application. |
| * |
| * @param sourceId the source id |
| * @param context the context to use |
| * @param file the file to copy |
| * @param targetFilename the relative path according to the root of the webapp |
| * @throws IOException if an error occurred while copying |
| */ |
| // CHECKSTYLE_OFF: LineLength |
| protected void copyFile( String sourceId, final WarPackagingContext context, final File file, String targetFilename ) |
| throws IOException |
| // CHECKSTYLE_ON: LineLength |
| { |
| final File targetFile = new File( context.getWebappDirectory(), targetFilename ); |
| |
| if ( file.isFile() ) |
| { |
| context.getWebappStructure().registerFile( sourceId, targetFilename, |
| new WebappStructure.RegistrationCallback() |
| { |
| public void registered( String ownerId, String targetFilename ) |
| throws IOException |
| { |
| copyFile( context, file, targetFile, targetFilename, |
| false ); |
| } |
| |
| public void alreadyRegistered( String ownerId, |
| String targetFilename ) |
| throws IOException |
| { |
| copyFile( context, file, targetFile, targetFilename, |
| true ); |
| } |
| |
| public void refused( String ownerId, String targetFilename, |
| String actualOwnerId ) |
| throws IOException |
| { |
| context.getLog().debug( " - " |
| + targetFilename |
| + " wasn't copied because it has " |
| + "already been packaged for overlay [" |
| + actualOwnerId + "]." ); |
| } |
| |
| public void superseded( String ownerId, |
| String targetFilename, |
| String deprecatedOwnerId ) |
| throws IOException |
| { |
| context.getLog().info( "File [" |
| + targetFilename |
| + "] belonged to overlay [" |
| + deprecatedOwnerId |
| + "] so it will be overwritten." ); |
| copyFile( context, file, targetFile, targetFilename, |
| false ); |
| } |
| |
| public void supersededUnknownOwner( String ownerId, |
| String targetFilename, |
| String unknownOwnerId ) |
| throws IOException |
| { |
| // CHECKSTYLE_OFF: LineLength |
| context.getLog().warn( "File [" |
| + targetFilename |
| + "] belonged to overlay [" |
| + unknownOwnerId |
| + "] which does not exist anymore in the current project. It is recommended to invoke " |
| + "clean if the dependencies of the project changed." ); |
| // CHECKSTYLE_ON: LineLength |
| copyFile( context, file, targetFile, targetFilename, |
| false ); |
| } |
| } ); |
| } |
| else if ( !targetFile.exists() && !targetFile.mkdirs() ) |
| { |
| context.getLog().info( "Failed to create directory " + targetFile.getAbsolutePath() ); |
| } |
| } |
| |
| /** |
| * Copy the specified file if the target location has not yet already been used and filter its content with the |
| * configured filter properties. |
| * <p/> |
| * The <tt>targetFileName</tt> is the relative path according to the root of the generated web application. |
| * |
| * @param sourceId the source id |
| * @param context the context to use |
| * @param file the file to copy |
| * @param targetFilename the relative path according to the root of the webapp |
| * @return true if the file has been copied, false otherwise |
| * @throws IOException if an error occurred while copying |
| * @throws MojoExecutionException if an error occurred while retrieving the filter properties |
| */ |
| protected boolean copyFilteredFile( String sourceId, final WarPackagingContext context, File file, |
| String targetFilename ) |
| throws IOException, MojoExecutionException |
| { |
| |
| if ( context.getWebappStructure().registerFile( sourceId, targetFilename ) ) |
| { |
| final File targetFile = new File( context.getWebappDirectory(), targetFilename ); |
| final String encoding; |
| try |
| { |
| if ( isXmlFile( file ) ) |
| { |
| // For xml-files we extract the encoding from the files |
| encoding = getEncoding( file ); |
| } |
| else |
| { |
| // For all others we use the configured encoding |
| encoding = context.getResourceEncoding(); |
| } |
| // fix for MWAR-36, ensures that the parent dir are created first |
| targetFile.getParentFile().mkdirs(); |
| |
| context.getMavenFileFilter().copyFile( file, targetFile, true, context.getFilterWrappers(), encoding ); |
| } |
| catch ( MavenFilteringException e ) |
| { |
| throw new MojoExecutionException( e.getMessage(), e ); |
| } |
| // CHECKSTYLE_OFF: LineLength |
| // Add the file to the protected list |
| context.getLog().debug( " + " + targetFilename + " has been copied (filtered encoding='" + encoding + "')." ); |
| // CHECKSTYLE_ON: LineLength |
| return true; |
| } |
| else |
| { |
| context.getLog().debug( " - " + targetFilename |
| + " wasn't copied because it has already been packaged (filtered)." ); |
| return false; |
| } |
| } |
| |
| /** |
| * Unpacks the specified file to the specified directory. |
| * |
| * @param context the packaging context |
| * @param file the file to unpack |
| * @param unpackDirectory the directory to use for th unpacked file |
| * @throws MojoExecutionException if an error occurred while unpacking the file |
| */ |
| protected void doUnpack( WarPackagingContext context, File file, File unpackDirectory ) |
| throws MojoExecutionException |
| { |
| String archiveExt = FileUtils.getExtension( file.getAbsolutePath() ).toLowerCase(); |
| |
| try |
| { |
| UnArchiver unArchiver = context.getArchiverManager().getUnArchiver( archiveExt ); |
| unArchiver.setSourceFile( file ); |
| unArchiver.setUseJvmChmod( context.isUseJvmChmod() ); |
| unArchiver.setDestDirectory( unpackDirectory ); |
| unArchiver.setOverwrite( true ); |
| unArchiver.extract(); |
| } |
| catch ( ArchiverException e ) |
| { |
| throw new MojoExecutionException( "Error unpacking file [" + file.getAbsolutePath() + "]" + "to [" |
| + unpackDirectory.getAbsolutePath() + "]", e ); |
| } |
| catch ( NoSuchArchiverException e ) |
| { |
| context.getLog().warn( "Skip unpacking dependency file [" + file.getAbsolutePath() |
| + " with unknown extension [" + archiveExt + "]" ); |
| } |
| } |
| |
| /** |
| * Copy file from source to destination. The directories up to <code>destination</code> will be created if they |
| * don't already exist. if the <code>onlyIfModified</code> flag is <tt>false</tt>, <code>destination</code> will be |
| * overwritten if it already exists. If the flag is <tt>true</tt> destination will be overwritten if it's not up to |
| * date. |
| * <p/> |
| * |
| * @param context the packaging context |
| * @param source an existing non-directory <code>File</code> to copy bytes from |
| * @param destination a non-directory <code>File</code> to write bytes to (possibly overwriting). |
| * @param targetFilename the relative path of the file from the webapp root directory |
| * @param onlyIfModified if true, copy the file only if the source has changed, always copy otherwise |
| * @return true if the file has been copied/updated, false otherwise |
| * @throws IOException if <code>source</code> does not exist, <code>destination</code> cannot be written to, or an |
| * IO error occurs during copying |
| */ |
| protected boolean copyFile( WarPackagingContext context, File source, File destination, String targetFilename, |
| boolean onlyIfModified ) |
| throws IOException |
| { |
| if ( onlyIfModified && destination.lastModified() >= source.lastModified() ) |
| { |
| context.getLog().debug( " * " + targetFilename + " is up to date." ); |
| return false; |
| } |
| else |
| { |
| if ( source.isDirectory() ) |
| { |
| context.getLog().warn( " + " + targetFilename + " is packaged from the source folder" ); |
| |
| try |
| { |
| JarArchiver archiver = context.getJarArchiver(); |
| archiver.addDirectory( source ); |
| archiver.setDestFile( destination ); |
| archiver.createArchive(); |
| } |
| catch ( ArchiverException e ) |
| { |
| String msg = "Failed to create " + targetFilename; |
| context.getLog().error( msg, e ); |
| IOException ioe = new IOException( msg ); |
| ioe.initCause( e ); |
| throw ioe; |
| } |
| } |
| else |
| { |
| FileUtils.copyFile( source.getCanonicalFile(), destination ); |
| // preserve timestamp |
| destination.setLastModified( source.lastModified() ); |
| context.getLog().debug( " + " + targetFilename + " has been copied." ); |
| } |
| return true; |
| } |
| } |
| |
| /** |
| * Get the encoding from an XML-file. |
| * |
| * @param webXml the XML-file |
| * @return The encoding of the XML-file, or UTF-8 if it's not specified in the file |
| * @throws java.io.IOException if an error occurred while reading the file |
| */ |
| protected String getEncoding( File webXml ) |
| throws IOException |
| { |
| XmlStreamReader xmlReader = new XmlStreamReader( webXml ); |
| try |
| { |
| return xmlReader.getEncoding(); |
| } |
| finally |
| { |
| IOUtil.close( xmlReader ); |
| } |
| } |
| |
| /** |
| * Returns the file to copy. If the includes are <tt>null</tt> or empty, the default includes are used. |
| * |
| * @param baseDir the base directory to start from |
| * @param includes the includes |
| * @param excludes the excludes |
| * @return the files to copy |
| */ |
| protected PathSet getFilesToIncludes( File baseDir, String[] includes, String[] excludes ) |
| { |
| return getFilesToIncludes( baseDir, includes, excludes, false ); |
| } |
| |
| /** |
| * Returns the file to copy. If the includes are <tt>null</tt> or empty, the default includes are used. |
| * |
| * @param baseDir the base directory to start from |
| * @param includes the includes |
| * @param excludes the excludes |
| * @param includeDirectories include directories yes or not. |
| * @return the files to copy |
| */ |
| // CHECKSTYLE_OFF: LineLength |
| protected PathSet getFilesToIncludes( File baseDir, String[] includes, String[] excludes, boolean includeDirectories ) |
| // CHECKSTYLE_ON: LineLength |
| { |
| final DirectoryScanner scanner = new DirectoryScanner(); |
| scanner.setBasedir( baseDir ); |
| |
| if ( excludes != null ) |
| { |
| scanner.setExcludes( excludes ); |
| } |
| scanner.addDefaultExcludes(); |
| |
| if ( includes != null && includes.length > 0 ) |
| { |
| scanner.setIncludes( includes ); |
| } |
| else |
| { |
| scanner.setIncludes( DEFAULT_INCLUDES ); |
| } |
| |
| scanner.scan(); |
| |
| PathSet pathSet = new PathSet( scanner.getIncludedFiles() ); |
| |
| if ( includeDirectories ) |
| { |
| pathSet.addAll( scanner.getIncludedDirectories() ); |
| } |
| |
| return pathSet; |
| } |
| |
| /** |
| * Returns the final name of the specified artifact. |
| * <p/> |
| * If the <tt>outputFileNameMapping</tt> is set, it is used, otherwise the standard naming scheme is used. |
| * |
| * @param context the packaging context |
| * @param artifact the artifact |
| * @return the converted filename of the artifact |
| * @throws InterpolationException in case of interpolation problem. |
| */ |
| protected String getArtifactFinalName( WarPackagingContext context, Artifact artifact ) |
| throws InterpolationException |
| { |
| if ( context.getOutputFileNameMapping() != null ) |
| { |
| return MappingUtils.evaluateFileNameMapping( context.getOutputFileNameMapping(), artifact ); |
| } |
| |
| String classifier = artifact.getClassifier(); |
| if ( ( classifier != null ) && !( "".equals( classifier.trim() ) ) ) |
| { |
| return MappingUtils.evaluateFileNameMapping( MappingUtils.DEFAULT_FILE_NAME_MAPPING_CLASSIFIER, artifact ); |
| } |
| else |
| { |
| return MappingUtils.evaluateFileNameMapping( MappingUtils.DEFAULT_FILE_NAME_MAPPING, artifact ); |
| } |
| |
| } |
| |
| /** |
| * Returns <code>true</code> if the <code>File</code>-object is a file (not a directory) that is not |
| * <code>null</code> and has a file name that ends in ".xml". |
| * |
| * @param file The file to check |
| * @return <code>true</code> if the file is an xml-file, otherwise <code>false</code> |
| * @since 2.3 |
| */ |
| private boolean isXmlFile( File file ) |
| { |
| return file != null && file.isFile() && file.getName().endsWith( ".xml" ); |
| } |
| } |