package org.codehaus.mojo.truezip.util; /* * 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.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.maven.plugin.logging.Log; import org.apache.maven.shared.io.logging.DefaultMessageHolder; import org.apache.maven.shared.io.logging.MessageHolder; import org.apache.maven.shared.io.logging.MessageLevels; import org.apache.maven.shared.io.logging.MojoLogSink; import org.apache.maven.shared.io.logging.PlexusLoggerSink; import org.apache.maven.shared.model.fileset.FileSet; import org.apache.maven.shared.model.fileset.mappers.FileNameMapper; import org.apache.maven.shared.model.fileset.mappers.MapperException; import org.apache.maven.shared.model.fileset.mappers.MapperUtil; import org.codehaus.plexus.logging.Logger; import org.codehaus.plexus.util.FileUtils; import de.schlichtherle.io.File; /** * Provides operations for use with FileSet instances, such as retrieving the included/excluded files, deleting all * matching entries, etc. * * This is a fork of maven's shared FileSetManager with the following changes * - Use TrueZipDirectoryScanner instead DirectoryScanner * - use java.io.File in isSymLink(); * * Note: symbolic support is unsupported * * @author jdcasey * @version $Id$ */ public class TrueZipFileSetManager { private static final String[] EMPTY_STRING_ARRAY = new String[0]; private final boolean verbose; private MessageHolder messages; // ---------------------------------------------------------------------- // Constructors // ---------------------------------------------------------------------- /** * Create a new manager instance with the supplied log instance and flag for whether to output verbose messages. * * @param log The mojo log instance * @param verbose Whether to output verbose messages */ public TrueZipFileSetManager( Log log, boolean verbose ) { if ( verbose ) { this.messages = new DefaultMessageHolder( MessageLevels.LEVEL_DEBUG, MessageLevels.LEVEL_INFO, new MojoLogSink( log ) ); } else { this.messages = new DefaultMessageHolder( MessageLevels.LEVEL_INFO, MessageLevels.LEVEL_INFO, new MojoLogSink( log ) ); } this.verbose = verbose; } /** * Create a new manager instance with the supplied log instance. Verbose flag is set to false. * * @param log The mojo log instance */ public TrueZipFileSetManager( Log log ) { this.messages = new DefaultMessageHolder( MessageLevels.LEVEL_INFO, MessageLevels.LEVEL_INFO, new MojoLogSink( log ) ); this.verbose = false; } /** * Create a new manager instance with the supplied log instance and flag for whether to output verbose messages. * * @param log The mojo log instance * @param verbose Whether to output verbose messages */ public TrueZipFileSetManager( Logger log, boolean verbose ) { if ( verbose ) { this.messages = new DefaultMessageHolder( MessageLevels.LEVEL_DEBUG, MessageLevels.LEVEL_INFO, new PlexusLoggerSink( log ) ); } else { this.messages = new DefaultMessageHolder( MessageLevels.LEVEL_INFO, MessageLevels.LEVEL_INFO, new PlexusLoggerSink( log ) ); } this.verbose = verbose; } /** * Create a new manager instance with the supplied log instance. Verbose flag is set to false. * * @param log The mojo log instance */ public TrueZipFileSetManager( Logger log ) { this.messages = new DefaultMessageHolder( MessageLevels.LEVEL_INFO, MessageLevels.LEVEL_INFO, new PlexusLoggerSink( log ) ); this.verbose = false; } /** * Create a new manager instance with an empty messages. Verbose flag is set to false. */ public TrueZipFileSetManager() { this.verbose = false; } // ---------------------------------------------------------------------- // Public methods // ---------------------------------------------------------------------- /** * * @param fileSet * @return the included files as map * @throws MapperException if any * @see #getIncludedFiles(FileSet) */ public Map mapIncludedFiles( TrueZipFileSet fileSet ) throws MapperException { String[] sourcePaths = getIncludedFiles( fileSet ); Map mappedPaths = new LinkedHashMap(); FileNameMapper fileMapper = MapperUtil.getFileNameMapper( fileSet.getMapper() ); for ( int i = 0; i < sourcePaths.length; i++ ) { String sourcePath = sourcePaths[i]; String destPath; if ( fileMapper != null ) { destPath = fileMapper.mapFileName( sourcePath ); } else { destPath = sourcePath; } mappedPaths.put( sourcePath, destPath ); } return mappedPaths; } /** * Get all the filenames which have been included by the rules in this fileset. * * @param fileSet * The fileset defining rules for inclusion/exclusion, and base directory. * @return the array of matching filenames, relative to the basedir of the file-set. */ public String[] getIncludedFiles( TrueZipFileSet fileSet ) { TrueZipDirectoryScanner scanner = scan( fileSet ); if ( scanner != null ) { return scanner.getIncludedFiles(); } return EMPTY_STRING_ARRAY; } /** * Get all the directory names which have been included by the rules in this fileset. * * @param fileSet * The fileset defining rules for inclusion/exclusion, and base directory. * @return the array of matching dirnames, relative to the basedir of the file-set. */ public String[] getIncludedDirectories( TrueZipFileSet fileSet ) { TrueZipDirectoryScanner scanner = scan( fileSet ); if ( scanner != null ) { return scanner.getIncludedDirectories(); } return EMPTY_STRING_ARRAY; } /** * Get all the filenames which have been excluded by the rules in this fileset. * * @param fileSet * The fileset defining rules for inclusion/exclusion, and base directory. * @return the array of non-matching filenames, relative to the basedir of the file-set. */ public String[] getExcludedFiles( TrueZipFileSet fileSet ) { TrueZipDirectoryScanner scanner = scan( fileSet ); if ( scanner != null ) { return scanner.getExcludedFiles(); } return EMPTY_STRING_ARRAY; } /** * Get all the directory names which have been excluded by the rules in this fileset. * * @param fileSet * The fileset defining rules for inclusion/exclusion, and base directory. * @return the array of non-matching dirnames, relative to the basedir of the file-set. */ public String[] getExcludedDirectories( TrueZipFileSet fileSet ) { TrueZipDirectoryScanner scanner = scan( fileSet ); if ( scanner != null ) { return scanner.getExcludedDirectories(); } return EMPTY_STRING_ARRAY; } /** * Delete the matching files and directories for the given file-set definition. * * @param fileSet The file-set matching rules, along with search base directory * @throws IOException If a matching file cannot be deleted */ public void delete( TrueZipFileSet fileSet ) throws IOException { delete( fileSet, true ); } /** * Delete the matching files and directories for the given file-set definition. * * @param fileSet The file-set matching rules, along with search base directory. * @param throwsError Throw IOException when errors have occurred by deleting files or directories. * @throws IOException If a matching file cannot be deleted and <code>throwsError=true</code>, otherwise * print warning messages. */ public void delete( TrueZipFileSet fileSet, boolean throwsError ) throws IOException { Set deletablePaths = findDeletablePaths( fileSet ); if ( messages != null && messages.isDebugEnabled() ) { messages .addDebugMessage( "Found deletable paths: " + String.valueOf( deletablePaths ).replace( ',', '\n' ) ).flush(); } List warnMessages = new LinkedList(); for ( Iterator it = deletablePaths.iterator(); it.hasNext(); ) { String path = (String) it.next(); File file = new File( fileSet.getDirectory(), path ); if ( file.exists() ) { if ( file.isDirectory() ) { if ( fileSet.isFollowSymlinks() || !isSymlink( file ) ) { if ( verbose && messages != null ) { messages.addInfoMessage( "Deleting directory: " + file ).flush(); } removeDir( file, fileSet.isFollowSymlinks(), throwsError, warnMessages ); } else { // delete a symlink to a directory without follow if ( verbose && messages != null ) { messages.addInfoMessage( "Deleting symlink to directory: " + file ).flush(); } if ( !file.delete() ) { String message = "Unable to delete symlink " + file.getAbsolutePath(); if ( throwsError ) { throw new IOException( message ); } if ( !warnMessages.contains( message ) ) { warnMessages.add( message ); } } } } else { if ( verbose && messages != null ) { messages.addInfoMessage( "Deleting file: " + file ).flush(); } if ( !delete( file ) ) { String message = "Failed to delete file " + file.getAbsolutePath() + ". Reason is unknown."; if ( throwsError ) { throw new IOException( message ); } warnMessages.add( message ); } } } } if ( messages != null && messages.isWarningEnabled() && !throwsError && ( warnMessages.size() > 0 ) ) { for ( Iterator it = warnMessages.iterator(); it.hasNext(); ) { String msg = (String) it.next(); messages.addWarningMessage( msg ).flush(); } } } // ---------------------------------------------------------------------- // Private methods // ---------------------------------------------------------------------- private boolean isSymlink( File file ) throws IOException { File fileInCanonicalParent = null; java.io.File parentDir = file.getParentFile();//truezip-plugin specific change if ( parentDir == null ) { fileInCanonicalParent = file; } else { fileInCanonicalParent = new File( parentDir.getCanonicalPath(), file.getName() ); } if ( messages != null && messages.isDebugEnabled() ) { messages.addDebugMessage( "Checking for symlink:\nFile's canonical path: " + fileInCanonicalParent.getCanonicalPath() + "\nFile's absolute path with canonical parent: " + fileInCanonicalParent.getPath() ).flush(); } return !fileInCanonicalParent.getCanonicalFile().equals( fileInCanonicalParent.getAbsoluteFile() ); } private Set findDeletablePaths( TrueZipFileSet fileSet ) { Set includes = findDeletableDirectories( fileSet ); includes.addAll( findDeletableFiles( fileSet, includes ) ); return includes; } private Set findDeletableDirectories( TrueZipFileSet fileSet ) { if ( verbose && messages != null ) { messages.addInfoMessage( "Scanning for deletable directories." ).flush(); } TrueZipDirectoryScanner scanner = scan( fileSet ); if ( scanner == null ) { return Collections.EMPTY_SET; } Set includes = new HashSet( Arrays.asList( scanner.getIncludedDirectories() ) ); Collection excludes = new ArrayList( Arrays.asList( scanner.getExcludedDirectories() ) ); Collection linksForDeletion = new ArrayList(); if ( !fileSet.isFollowSymlinks() ) { if ( verbose && messages != null ) { messages .addInfoMessage( "Adding symbolic link dirs which were previously excluded to the list being deleted." ).flush(); } // we need to see which entries were only excluded because they're symlinks... scanner.setFollowSymlinks( true ); scanner.scan(); if ( messages != null && messages.isDebugEnabled() ) { messages.addDebugMessage( "Originally marked for delete: " + includes ).flush(); messages.addDebugMessage( "Marked for preserve (with followSymlinks == false): " + excludes ).flush(); } List includedDirsAndSymlinks = Arrays.asList( scanner.getIncludedDirectories() ); linksForDeletion.addAll( excludes ); linksForDeletion.retainAll( includedDirsAndSymlinks ); if ( messages != null && messages.isDebugEnabled() ) { messages.addDebugMessage( "Symlinks marked for deletion (originally mismarked): " + linksForDeletion ).flush(); } excludes.removeAll( includedDirsAndSymlinks ); } excludeParentDirectoriesOfExcludedPaths( excludes, includes ); includes.addAll( linksForDeletion ); return includes; } private Set findDeletableFiles( TrueZipFileSet fileSet, Set deletableDirectories ) { if ( verbose && messages != null ) { messages.addInfoMessage( "Re-scanning for deletable files." ).flush(); } TrueZipDirectoryScanner scanner = scan( fileSet ); if ( scanner == null ) { return deletableDirectories; } Set includes = deletableDirectories; includes.addAll( Arrays.asList( scanner.getIncludedFiles() ) ); Collection excludes = new ArrayList( Arrays.asList( scanner.getExcludedFiles() ) ); Collection linksForDeletion = new ArrayList(); if ( !fileSet.isFollowSymlinks() ) { if ( verbose && messages != null ) { messages .addInfoMessage( "Adding symbolic link files which were previously excluded to the list being deleted." ).flush(); } // we need to see which entries were only excluded because they're symlinks... scanner.setFollowSymlinks( true ); scanner.scan(); if ( messages != null && messages.isDebugEnabled() ) { messages.addDebugMessage( "Originally marked for delete: " + includes ).flush(); messages.addDebugMessage( "Marked for preserve (with followSymlinks == false): " + excludes ).flush(); } List includedFilesAndSymlinks = Arrays.asList( scanner.getIncludedFiles() ); linksForDeletion.addAll( excludes ); linksForDeletion.retainAll( includedFilesAndSymlinks ); if ( messages != null && messages.isDebugEnabled() ) { messages.addDebugMessage( "Symlinks marked for deletion (originally mismarked): " + linksForDeletion ).flush(); } excludes.removeAll( includedFilesAndSymlinks ); } excludeParentDirectoriesOfExcludedPaths( excludes, includes ); includes.addAll( linksForDeletion ); return includes; } /** * Removes all parent directories of the already excluded files/directories from the given set of deletable * directories. I.e. if "subdir/excluded.txt" should not be deleted, "subdir" should be excluded from deletion, too. * * @param excludedPaths The relative paths of the files/directories which are excluded from deletion, must not be * <code>null</code>. * @param deletablePaths The relative paths to files/directories which are scheduled for deletion, must not be * <code>null</code>. */ private void excludeParentDirectoriesOfExcludedPaths( Collection excludedPaths, Set deletablePaths ) { for ( Iterator it = excludedPaths.iterator(); it.hasNext(); ) { String path = (String) it.next(); String parentPath = new File( path ).getParent(); while ( parentPath != null ) { if ( messages != null && messages.isDebugEnabled() ) { messages.addDebugMessage( "Verifying path " + parentPath + " is not present; contains file which is excluded." ).flush(); } boolean removed = deletablePaths.remove( parentPath ); if ( removed && messages != null && messages.isDebugEnabled() ) { messages.addDebugMessage( "Path " + parentPath + " was removed from delete list." ).flush(); } parentPath = new File( parentPath ).getParent(); } } if ( !excludedPaths.isEmpty() ) { if ( messages != null && messages.isDebugEnabled() ) { messages.addDebugMessage( "Verifying path " + "." + " is not present; contains file which is excluded." ).flush(); } boolean removed = deletablePaths.remove( "" ); if ( removed && messages != null && messages.isDebugEnabled() ) { messages.addDebugMessage( "Path " + "." + " was removed from delete list." ).flush(); } } } /** * Delete a directory * * @param dir the directory to delete * @param followSymlinks whether to follow symbolic links, or simply delete the link * @param throwsError Throw IOException when errors have occurred by deleting files or directories. * @param warnMessages A list of warning messages used when <code>throwsError=false</code>. * @throws IOException If a matching file cannot be deleted and <code>throwsError=true</code>. */ private void removeDir( File dir, boolean followSymlinks, boolean throwsError, List warnMessages ) throws IOException { String[] list = dir.list(); if ( list == null ) { list = new String[0]; } for ( int i = 0; i < list.length; i++ ) { String s = list[i]; File f = new File( dir, s ); if ( f.isDirectory() && ( followSymlinks || !isSymlink( f ) ) ) { removeDir( f, followSymlinks, throwsError, warnMessages ); } else { if ( !delete( f ) ) { String message = "Unable to delete file " + f.getAbsolutePath(); if ( throwsError ) { throw new IOException( message ); } if ( !warnMessages.contains( message ) ) { warnMessages.add( message ); } } } } if ( !delete( dir ) ) { String message = "Unable to delete directory " + dir.getAbsolutePath(); if ( throwsError ) { throw new IOException( message ); } if ( !warnMessages.contains( message ) ) { warnMessages.add( message ); } } } /** * Delete a file * * @param f a file */ private boolean delete( File f ) { try { FileUtils.forceDelete( f ); } catch ( IOException e ) { return false; } return true; } private TrueZipDirectoryScanner scan( TrueZipFileSet fileSet ) { File basedir = new File( fileSet.getDirectory() ); if ( !basedir.exists() || !basedir.isDirectory() ) { return null; } TrueZipDirectoryScanner scanner = new TrueZipDirectoryScanner(); String[] includesArray = fileSet.getIncludesArray(); String[] excludesArray = fileSet.getExcludesArray(); if ( includesArray.length > 0 ) { scanner.setIncludes( includesArray ); } if ( excludesArray.length > 0 ) { scanner.setExcludes( excludesArray ); } if ( fileSet.isUseDefaultExcludes() ) { scanner.addDefaultExcludes(); } scanner.setBasedir( basedir ); scanner.setFollowSymlinks( fileSet.isFollowSymlinks() ); scanner.setFollowArchive( fileSet.isFollowArchive() ); scanner.scan(); return scanner; } }