/* * Copyright (C) 2011 Laurent Caillette * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.novelang.common; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.regex.Pattern; import com.google.common.collect.Lists; import org.apache.commons.io.DirectoryWalker; import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.IOFileFilter; import org.apache.commons.lang.SystemUtils; import org.novelang.logger.Logger; import org.novelang.logger.LoggerFactory; /** * Utility class for doing things with files. * * @author Laurent Caillette */ public class FileTools { private static final Logger LOGGER = LoggerFactory.getLogger( FileTools.class ); private FileTools() { } public static final Comparator< ? super File > ABSOLUTEPATH_COMPARATOR = new Comparator< File >() { @Override public int compare( final File file1, final File file2 ) { return file1.getAbsolutePath().compareTo( file2.getAbsolutePath() ) ; } } ; // ============ // File loading // ============ /** * Returns the first file in given directory, with one of given extensions. * Match is done on according to the order of given extensions. * * @param basedir * @param fileNameNoExtension * @param fileExtensions * @return a non-null object. * @throws java.io.FileNotFoundException with extensive message listing all names of files * that were looked for. */ public static File load( final File basedir, final String fileNameNoExtension, final String... fileExtensions ) throws IOException { final StringBuffer buffer = new StringBuffer( "Not found:" ) ; for( final String extension : fileExtensions ) { final File file = new File( basedir, fileNameNoExtension + "." + extension ) ; if( file.exists() ) { return file.getCanonicalFile() ; } else { buffer.append( "\n '" ) ; buffer.append( file.getAbsolutePath() ) ; buffer.append( "'" ) ; } } throw new FileNotFoundException( buffer.toString() ) ; } // ============= // File scanning // ============= private static final IOFileFilter VISIBLE_DIRECTORY_FILTER = new IOFileFilter() { @Override public boolean accept( final File file ) { return file.isDirectory() && ! file.isHidden() ; } @Override public boolean accept( final File dir, final String name ) { return ! dir.isHidden() /*&& ! name.startsWith( "." )*/ ; } } ; private static class MyDirectoryWalker extends DirectoryWalker { public MyDirectoryWalker() { super( VISIBLE_DIRECTORY_FILTER, -1 ); } @Override protected boolean handleDirectory( final File file, final int i, final Collection collection ) throws IOException { collection.add( file ) ; return true ; } public void walk( final File root, final List< File > results ) throws IOException { super.walk( root, results ) ; } } // ==================== // Relative directories // ==================== /** * Return files with given extensions in given directory. * * @param directory a non-null object. * @param extensions a non-null array containing no nulls. * @return a non-null object iterating on non-null objects. */ public static List< File > scanFiles( final File directory, final String[] extensions, final boolean recurse ) { final Collection fileCollection = FileUtils.listFiles( directory, extensions, recurse ) ; // Workaround: Commons Collection doesn't know about Generics. final List< File > files = Lists.newArrayList() ; for( final Object o : fileCollection ) { files.add( ( File ) o ) ; } return files ; } /** * Returns a list of visible directories under a root directory. * The root directory is included in the list. * * @param root a non-null object representing a directory. * @return a non-null object containing no nulls. */ public static List< File > scanDirectories( final File root ) { final List< File > directories = Lists.newArrayList() ; try { new MyDirectoryWalker().walk( root, directories ) ; } catch( IOException e ) { throw new RuntimeException( e ) ; } return directories ; } private static final Pattern PATTERN = Pattern.compile( "\\\\" ) ; /** * Returns a URL-friendly path where file separator is a solidus, not a reverse solidus. * Useful on Windows. */ public static String urlifyPath( final String path ) { return PATTERN.matcher( path ).replaceAll( "/" ); } /** * For a {@code File} object, returns its path relative to a given directory. * <p> * Given this code: * <pre> final File parent = ... final File child = new File( parent, "some relative path" ) ; final File relative = new File( parent, relativizePath( parent, child ) ) ; * </pre> * We should have {@code child} and {@code relative} referencing the same file. * * @param parent a non-null object representing a directory. * @param child a non-null {@code File} object that must be a child of {@code base}. * @return a non-null, non-empty {@code String} representing the name of {@code child} * relative to {@code parent}. * separator. * @throws IllegalArgumentException */ public static String relativizePath( final File parent, final File child ) throws IllegalArgumentException { final String baseAbsolutePath = parent.getAbsolutePath() ; if( ! parent.isDirectory() ) { throw new IllegalArgumentException( "Not a directory: " + baseAbsolutePath ) ; } final String baseAbsolutePathFixed = normalizePath( baseAbsolutePath ) ; final String childAbsolutePath = child.isDirectory() ? normalizePath( child.getAbsolutePath() ) : child.getAbsolutePath() ; if( childAbsolutePath.startsWith( baseAbsolutePathFixed ) ) { final String relativePath = childAbsolutePath.substring( baseAbsolutePathFixed.length() ) ; if( relativePath.startsWith( SystemUtils.FILE_SEPARATOR ) ) { return relativePath.substring( 1 ) ; } else { return relativePath ; } } else { throw new IllegalArgumentException( "No parent-child relationship: '" + baseAbsolutePathFixed + "' " + "not parent of '" + childAbsolutePath + "'" ) ; } } private static String normalizePath( final String path ) { return path.endsWith( SystemUtils.FILE_SEPARATOR ) ? path : path + SystemUtils.FILE_SEPARATOR ; } public static boolean isParentOf( final File maybeParent, final File maybeChild ) { final String maybeParentPathName = normalizePath( maybeParent.getAbsolutePath() ) ; final String maybeChildPathName = normalizePath( maybeChild.getAbsolutePath() ) ; return ( maybeParentPathName.length() < maybeChildPathName.length() ) && maybeChildPathName.startsWith( maybeParentPathName ) ; } public static boolean isParentOfOrSameAs( final File maybeParent, final File maybeChild ) { final String maybeParentPathName = normalizePath( maybeParent.getAbsolutePath() ) ; final String maybeChildPathName = normalizePath( maybeChild.getAbsolutePath() ) ; return ( maybeParentPathName.length() <= maybeChildPathName.length() ) && maybeChildPathName.startsWith( maybeParentPathName ) ; } // =================== // Temporary directory // =================== private static final List< File > DIRECTORIES_TO_CLEAN_ON_EXIT = Collections.synchronizedList( new ArrayList< File >() ) ; private static final Thread DIRECTORIES_CLEANER = new Thread( new Runnable() { @Override public void run() { LOGGER.debug( "Cleaning up directories scheduled for deletion..." ); // Defensive copy even if no directory should be added at the time this runs. final List< File > files = Lists.newArrayList( DIRECTORIES_TO_CLEAN_ON_EXIT ) ; for( final File file : files ) { try { LOGGER.info( "Deleting temporary directory '", file.getAbsolutePath(), "'" ) ; FileUtils.deleteDirectory( file ) ; } catch( IOException e ) { LOGGER.error( e, "Failed to clean directory" ) ; } } } }, "Cleaner of temporary directories" ) ; static { Runtime.getRuntime().addShutdownHook( DIRECTORIES_CLEANER ) ; } private static final String TEMPORARY_DIRECTORY_SUFFIX = ".temp" ; public static File createTemporaryDirectory( final String prefix, final File parent, final boolean deleteOnExit ) throws IOException { File temporaryDirectory = null ; temporaryDirectory = File.createTempFile( prefix, TEMPORARY_DIRECTORY_SUFFIX, parent ) ; if ( ! temporaryDirectory.delete() ) { throw new IOException( "Created temporary file to get a name from, its deletion failed for an unknown reason") ; } if ( ! temporaryDirectory.mkdir() ) { throw new IOException( "Creation of temporary directory failed for an unknown reason" ) ; } if( deleteOnExit && null != temporaryDirectory ) { DIRECTORIES_TO_CLEAN_ON_EXIT.add( temporaryDirectory ) ; LOGGER.info( "Scheduled for deletion on exit: '", temporaryDirectory.getAbsolutePath(), "'" ) ; } return temporaryDirectory ; } // ================== // Directory creation // ================== public static File createDirectoryForSure( final File directory ) { if( ! directory.exists() ) { if( directory.mkdirs() ) { LOGGER.debug( "Created directory '", directory.getAbsolutePath(), "'" ) ; } } if( ! directory.exists() ) { throw new IllegalStateException( "Couldn't create '" +directory.getAbsolutePath() + "'" ) ; } return directory ; } public static File createFreshDirectory( final File parentDirectory, final String name ) throws IOException { return createFreshDirectory( new File( parentDirectory, name ) ) ; } public static File createFreshDirectory( final String fileName ) throws IOException { return createFreshDirectory( new File( fileName ) ) ; } public static File createFreshDirectory( final File directory ) throws IOException { FileUtils.deleteDirectory( directory ) ; createDirectoryForSure( directory ) ; return directory ; } // ===== // Names // ===== private static final Pattern SANITIZATION_PATTERN = Pattern.compile( "[^0-9a-zA-Z]" ) ; public static String sanitizeFileName( final String name ) { return SANITIZATION_PATTERN.matcher( name ).replaceAll( "" ); } }