/** * Copyright 2013-2017 Linagora, Université Joseph Fourier, Floralis * * The present code is developed in the scope of the joint LINAGORA - * Université Joseph Fourier - Floralis research program and is designated * as a "Result" pursuant to the terms and conditions of the LINAGORA * - Université Joseph Fourier - Floralis research program. Each copyright * holder of Results enumerated here above fully & independently holds complete * ownership of the complete Intellectual Property rights applicable to the whole * of said Results, and may freely exploit it in any manner which does not infringe * the moral rights of the other copyright holders. * * Licensed 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. */ package net.roboconf.core.utils; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Reader; import java.io.Serializable; import java.io.StringWriter; import java.io.Writer; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** * Various utilities. * @author Vincent Zurczak - Linagora * @author Amadou Diarra - UJF */ public final class Utils { /** * Private empty constructor. */ private Utils() { // nothing } /** * @param s a string (can be null) * @return true if the string is null or only made up of white spaces */ public static boolean isEmptyOrWhitespaces( String s ) { return s == null || s.trim().length() == 0; } /** * Capitalizes a string. * @param s a string * @return the capitalized string */ public static String capitalize( String s ) { String result = s; if( ! Utils.isEmptyOrWhitespaces( s )) result = Character.toUpperCase( s.charAt( 0 )) + s.substring( 1 ).toLowerCase(); return result; } /** * Removes the extension from a file name. * @param filename a non-null file name * @return a non-null string */ public static String removeFileExtension( String filename ) { String result = filename; int index = filename.lastIndexOf( '.' ); if( index != -1 ) result= filename.substring( 0, index ); return result; } /** * Splits a string and formats the result. * @param toSplit the string to split (can be null) * @param separator the separator (cannot be null or the empty string) * @return a list of items (never null), with every item being trimmed */ public static List<String> splitNicely( String toSplit, String separator ) { if( separator == null || separator.isEmpty()) throw new IllegalArgumentException( "The separator cannot be null or the empty string." ); return splitNicelyWithPattern( toSplit, Pattern.quote( separator )); } /** * Splits a string and formats the result. * @param toSplit the string to split (can be null) * @param patternSeparator the separator pattern (cannot be null or the empty string) * @return a list of items (never null), with every item being trimmed */ public static List<String> splitNicelyWithPattern( String toSplit, String patternSeparator ) { if( patternSeparator == null || patternSeparator.isEmpty()) throw new IllegalArgumentException( "The separator cannot be null or the empty string." ); List<String> result = new ArrayList<> (); if( ! Utils.isEmptyOrWhitespaces( toSplit )) { for( String s : toSplit.split( patternSeparator )) result.add( s .trim()); } return result; } /** * Creates a new list and only keeps values that are not null or made up of white characters. * @param values a non-null list of items (can contain null and "empty" values) * @return a list of items (never null), with no null or "empty" values */ public static List<String> filterEmptyValues( List<String> values ) { List<String> result = new ArrayList<> (); for( String s : values ) { if( ! Utils.isEmptyOrWhitespaces( s )) result.add( s ); } return result; } /** * Formats a collection of elements as a string. * @param items a non-null list of items * @param separator a string to separate items * @return a non-null string */ public static String format( Collection<String> items, String separator ) { StringBuilder sb = new StringBuilder(); for( Iterator<String> it = items.iterator(); it.hasNext(); ) { sb.append( it.next()); if( it.hasNext()) sb.append( separator ); } return sb.toString(); } /** * Expands a template, replacing each {{ param }} by the corresponding value. * <p> * Eg. "My name is {{ name }}" will result in "My name is Bond", provided that "params" contains "name=Bond". * </p> * * @param s the template to expand * @param params the parameters to be expanded in the template * @return the expanded template */ public static String expandTemplate(String s, Properties params) { String result; if( params == null || params.size() < 1 ) { result = s; } else { StringBuffer sb = new StringBuffer(); Pattern pattern = Pattern.compile( "\\{\\{\\s*\\S+\\s*\\}\\}" ); Matcher m = pattern.matcher( s ); while( m.find()) { String raw = m.group(); String varName = m.group().replace('{', ' ').replace('}', ' ').trim(); String val = params.getProperty(varName); val = (val == null ? raw : val.trim()); m.appendReplacement(sb, val); } m.appendTail( sb ); result = sb.toString(); } return result; } /** * Closes a stream quietly. * @param in an input stream (can be null) */ public static void closeQuietly( InputStream in ) { if( in != null ) { try { in.close(); } catch( IOException e ) { // nothing } } } /** * Closes a stream quietly. * @param out an output stream (can be null) */ public static void closeQuietly( OutputStream out ) { if( out != null ) { try { out.close(); } catch( IOException e ) { // nothing } } } /** * Closes a reader quietly. * @param reader a reader (can be null) */ public static void closeQuietly( Reader reader ) { if( reader != null ) { try { reader.close(); } catch( IOException e ) { // nothing } } } /** * Closes a writer quietly. * @param writer a writer (can be null) */ public static void closeQuietly( Writer writer ) { if( writer != null ) { try { writer.close(); } catch( IOException e ) { // nothing } } } /** * Copies the content from in into os. * <p> * Neither <i>in</i> nor <i>os</i> are closed by this method.<br> * They must be explicitly closed after this method is called. * </p> * <p> * Be careful, this method should be avoided when possible. * It was responsible for memory leaks. See #489. * </p> * * @param in an input stream (not null) * @param os an output stream (not null) * @throws IOException if an error occurred */ public static void copyStreamUnsafelyUseWithCaution( InputStream in, OutputStream os ) throws IOException { byte[] buf = new byte[ 1024 ]; int len; while((len = in.read( buf )) > 0) { os.write( buf, 0, len ); } } /** * Copies the content from in into os. * <p> * This method closes the input stream. * <i>os</i> does not need to be closed. * </p> * * @param in an input stream (not null) * @param os an output stream (not null) * @throws IOException if an error occurred */ public static void copyStreamSafely( InputStream in, ByteArrayOutputStream os ) throws IOException { try { copyStreamUnsafelyUseWithCaution( in, os ); } finally { in.close(); } } /** * Copies the content from in into outputFile. * <p> * <i>in</i> is not closed by this method.<br> * It must be explicitly closed after this method is called. * </p> * * @param in an input stream (not null) * @param outputFile will be created if it does not exist * @throws IOException if the file could not be created */ public static void copyStream( InputStream in, File outputFile ) throws IOException { OutputStream os = new FileOutputStream( outputFile ); try { copyStreamUnsafelyUseWithCaution( in, os ); } finally { os.close (); } } /** * Copies the content from inputFile into outputFile. * * @param inputFile an input file (must be a file and exist) * @param outputFile will be created if it does not exist * @throws IOException if something went wrong */ public static void copyStream( File inputFile, File outputFile ) throws IOException { InputStream is = new FileInputStream( inputFile ); try { copyStream( is, outputFile ); } finally { is.close(); } } /** * Copies the content from inputFile into an output stream. * * @param inputFile an input file (must be a file and exist) * @param os the output stream * @throws IOException if something went wrong */ public static void copyStream( File inputFile, OutputStream os ) throws IOException { InputStream is = new FileInputStream( inputFile ); try { copyStreamUnsafelyUseWithCaution( is, os ); } finally { is.close(); } } /** * Writes a string into a file. * * @param s the string to write (not null) * @param outputFile the file to write into * @throws IOException if something went wrong */ public static void writeStringInto( String s, File outputFile ) throws IOException { InputStream in = new ByteArrayInputStream( s.getBytes( "UTF-8" )); copyStream( in, outputFile ); } /** * Appends a string into a file. * * @param s the string to write (not null) * @param outputFile the file to write into * @throws IOException if something went wrong */ public static void appendStringInto( String s, File outputFile ) throws IOException { OutputStreamWriter fw = null; try { fw = new OutputStreamWriter( new FileOutputStream( outputFile, true ), "UTF-8" ); fw.append( s ); } finally { Utils.closeQuietly( fw ); } } /** * Reads a text file content and returns it as a string. * <p> * The file is tried to be read with UTF-8 encoding. * If it fails, the default system encoding is used. * </p> * * @param file the file whose content must be loaded * @return the file content * @throws IOException if the file content could not be read */ public static String readFileContent( File file ) throws IOException { String result = null; ByteArrayOutputStream os = new ByteArrayOutputStream(); Utils.copyStream( file, os ); result = os.toString( "UTF-8" ); return result; } /** * Reads properties from a file. * @param file a properties file * @return a {@link Properties} instance * @throws IOException if reading failed */ public static Properties readPropertiesFile( File file ) throws IOException { Properties result = new Properties(); InputStream in = null; try { in = new FileInputStream( file ); result.load( in ); } finally { closeQuietly( in ); } return result; } /** * Reads properties from a file but does not throw any error in case of problem. * @param file a properties file * @param logger a logger (not null) * @return a {@link Properties} instance (never null) */ public static Properties readPropertiesFileQuietly( File file, Logger logger ) { Properties result = new Properties(); try { if( file.exists() ) result = readPropertiesFile( file ); } catch( Exception e ) { logger.severe( "Properties file " + file + " could not be read." ); logException( logger, e ); } return result; } /** * Writes Java properties into a file. * @param properties non-null properties * @param file a properties file * @throws IOException if writing failed */ public static void writePropertiesFile( Properties properties, File file ) throws IOException { OutputStream out = null; try { out = new FileOutputStream( file ); properties.store( out, "" ); } finally { closeQuietly( out ); } } /** * Creates a directory if it does not exist. * @param directory the directory to create * @throws IOException if it did not exist and that it could not be created */ public static void createDirectory( File directory ) throws IOException { if( ! directory.exists() && ! directory.mkdirs()) throw new IOException( "The directory " + directory + " could not be created." ); } /** * Equivalent to <code>listAllFiles( directory, false )</code>. * @param directory an existing directory * @return a non-null list of files */ public static List<File> listAllFiles( File directory ) { return listAllFiles( directory, false ); } /** * Finds all the files (directly and indirectly) contained in a directory and with a given extension. * <p> * Search is case-insensitive. * It means searching for properties or PROPERTIES extensions will give * the same result. * </p> * * @param directory an existing directory * @param fileExtension a file extension (null will not filter extensions) * <p> * If it does not start with a dot, then one will be inserted at the first position. * </p> * * @return a non-null list of files */ public static List<File> listAllFiles( File directory, String fileExtension ) { String ext = fileExtension; if( ext != null ) { ext = ext.toLowerCase(); if( ! ext.startsWith( "." )) ext = "." + ext; } List<File> result = new ArrayList<> (); for( File f : listAllFiles( directory )) { if( ext == null || f.getName().toLowerCase().endsWith( ext )) result.add( f ); } return result; } /** * Finds all the files (direct and indirect) from a directory. * <p> * This method skips hidden files and files whose name starts * with a dot. * </p> * * @param directory an existing directory * @param includeDirectories true to include directories, false to exclude them from the result * @return a non-null list of files, sorted alphabetically by file names */ public static List<File> listAllFiles( File directory, boolean includeDirectories ) { if( ! directory.isDirectory()) throw new IllegalArgumentException( directory.getAbsolutePath() + " does not exist or is not a directory." ); List<File> result = new ArrayList<> (); List<File> directoriesToInspect = new ArrayList<> (); directoriesToInspect.add( directory ); while( ! directoriesToInspect.isEmpty()) { File currentDirectory = directoriesToInspect.remove( 0 ); if( includeDirectories ) result.add( currentDirectory ); File[] subFiles = currentDirectory.listFiles(); if( subFiles == null ) continue; for( File subFile : subFiles ) { if( subFile.isHidden() || subFile.getName().startsWith( "." )) continue; if( subFile.isFile()) result.add( subFile ); else directoriesToInspect.add( subFile ); } } Collections.sort( result, new FileNameComparator()); return result; } /** * Finds all the files directly contained in a directory and with a given extension. * <p> * Search is case-insensitive. * It means searching for properties or PROPERTIES extensions will give * the same result. * </p> * * @param directory an existing directory * @param fileExtension a file extension (null will not filter extensions) * <p> * If it does not start with a dot, then one will be inserted at the first position. * </p> * * @return a non-null list of files */ public static List<File> listDirectFiles( File directory, String fileExtension ) { String ext = fileExtension; if( ext != null ) { ext = ext.toLowerCase(); if( ! ext.startsWith( "." )) ext = "." + ext; } List<File> result = new ArrayList<> (); File[] files = directory.listFiles(); if( files != null ) { for( File f : files ) { if( f.isFile() && (ext == null || f.getName().toLowerCase().endsWith( ext ))) { result.add( f ); } } } return result; } /** * Lists directories located under a given file. * @param root a file * @return a non-null list of directories, sorted alphabetically by file names */ public static List<File> listDirectories( File root ) { List<File> result = new ArrayList<> (); File[] files = root.listFiles( new DirectoryFileFilter()); if( files != null ) result.addAll( Arrays.asList( files )); Collections.sort( result, new FileNameComparator()); return result; } /** * @author Vincent Zurczak - Linagora */ static final class FileNameComparator implements Serializable, Comparator<File> { private static final long serialVersionUID = -4671366958457961589L; @Override public int compare( File o1, File o2 ) { return o1.getName().compareTo( o2.getName()); } } /** * @author Vincent Zurczak - Linagora */ static class DirectoryFileFilter implements FileFilter { @Override public boolean accept( File f ) { return f.isDirectory(); } } /** * Stores the resources from a directory into a map. * @param directory an existing directory * @return a non-null map (key = the file location, relative to the directory, value = file content) * @throws IOException if something went wrong while reading a file */ public static Map<String,byte[]> storeDirectoryResourcesAsBytes( File directory ) throws IOException { return storeDirectoryResourcesAsBytes( directory, new ArrayList<String>( 0 )); } /** * Stores the resources from a directory into a map. * @param directory an existing directory * @param exclusionPatteners a non-null list of exclusion patterns for file names (e.g. ".*\\.properties") * @return a non-null map (key = the file location, relative to the directory, value = file content) * @throws IOException if something went wrong while reading a file */ public static Map<String,byte[]> storeDirectoryResourcesAsBytes( File directory, List<String> exclusionPatteners ) throws IOException { if( ! directory.exists()) throw new IllegalArgumentException( "The resource directory was not found. " + directory.getAbsolutePath()); if( ! directory.isDirectory()) throw new IllegalArgumentException( "The resource directory is not a valid directory. " + directory.getAbsolutePath()); Map<String,byte[]> result = new HashMap<> (); List<File> resourceFiles = listAllFiles( directory, false ); fileLoop: for( File file : resourceFiles ) { for( String exclusionPattern : exclusionPatteners ) { if( file.getName().matches( exclusionPattern )) continue fileLoop; } String key = computeFileRelativeLocation( directory, file ); ByteArrayOutputStream os = new ByteArrayOutputStream(); Utils.copyStream( file, os ); result.put( key, os.toByteArray()); } return result; } /** * Stores the resources from a directory into a map. * @param directory an existing directory * @return a non-null map (key = the file location, relative to the directory, value = file content) * @throws IOException if something went wrong while reading a file */ public static Map<String,String> storeDirectoryResourcesAsString( File directory ) throws IOException { Map<String,byte[]> map = storeDirectoryResourcesAsBytes( directory ); Map<String,String> result = new HashMap<>( map.size()); for( Map.Entry<String,byte[]> entry : map.entrySet()) result.put( entry.getKey(), new String( entry.getValue(), "UTF-8" )); return result; } /** * Computes the relative location of a file with respect to a root directory. * @param rootDirectory a directory * @param subFile a file contained (directly or indirectly) in the directory * @return a non-null string */ public static String computeFileRelativeLocation( File rootDirectory, File subFile ) { String rootPath = rootDirectory.getAbsolutePath(); String subPath = subFile.getAbsolutePath(); if( ! subPath.startsWith( rootPath )) throw new IllegalArgumentException( "The sub-file must be contained in the directory." ); if( rootDirectory.equals( subFile )) throw new IllegalArgumentException( "The sub-file must be different than the directory." ); return subPath.substring( rootPath.length() + 1 ).replace( '\\', '/' ); } /** * Extracts a ZIP archive in a directory. * @param zipFile a ZIP file (not null, must exist) * @param targetDirectory the target directory (may not exist but must be a directory) * @throws IOException if something went wrong */ public static void extractZipArchive( File zipFile, File targetDirectory ) throws IOException { extractZipArchive( zipFile, targetDirectory, null, null ); } /** * Extracts a ZIP archive in a directory (with advanced options). * <p> * Imagine you have an archive that contains pom.xml, graph/main.graph and graph/vm/init.pp. * Let's now suppose you want to extract only the files located under "graph". And let's suppose * you do not want to create a "graph" sub-directory in your target. Then... * </p> * <code>extractZipArchive( your.zip, your.target.dir, "graph/.*\\.graph, "graph/" );</code> * <p> * ... will only create main.graph in your target directory (and not graph/main.graph). * </p> * * @param zipFile a ZIP file (not null, must exist) * @param targetDirectory the target directory (may not exist but must be a directory) * @param entryPattern a pattern to only extract some entries (null for all entries) * @param removedEntryPrefix an entry prefix to remove (null to ignore) * @throws IOException if something went wrong */ public static void extractZipArchive( File zipFile, File targetDirectory, String entryPattern, String removedEntryPrefix ) throws IOException { // Make some checks if( zipFile == null || targetDirectory == null ) throw new IllegalArgumentException( "The ZIP file and the target directory cannot be null." ); if( ! zipFile.isFile()) throw new IllegalArgumentException( "ZIP file " + targetDirectory.getName() + " does not exist." ); if( targetDirectory.exists() && ! targetDirectory.isDirectory()) throw new IllegalArgumentException( "Target directory " + targetDirectory.getName() + " is not a directory." ); Utils.createDirectory( targetDirectory ); // Load the ZIP file ZipFile theZipFile = new ZipFile( zipFile ); Enumeration<? extends ZipEntry> entries = theZipFile.entries(); // And start the copy try { while( entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); String suffix = entry.getName(); // Deal with extract options if( entryPattern != null && ! suffix.matches( entryPattern )) continue; if( removedEntryPrefix != null && suffix.startsWith( removedEntryPrefix )) suffix = suffix.substring( removedEntryPrefix.length()); if( isEmptyOrWhitespaces( suffix )) continue; // Extract... File f = new File( targetDirectory, suffix ); // Case 'directory': create it. // Case 'file': create its parents and copy the content. if( entry.isDirectory()) { Utils.createDirectory( f ); } else { Utils.createDirectory( f.getParentFile()); copyStream( theZipFile.getInputStream( entry ), f ); } } } finally { // Close the stream theZipFile.close(); } } /** * Determines whether a directory contains a given file. * @param ancestorCandidate the directory * @param file the file * @return true if the directory directly or indirectly contains the file */ public static boolean isAncestor( File ancestorCandidate, File file ) { String path = ancestorCandidate.getAbsolutePath(); if( ! path.endsWith( "/" )) path += "/"; return file.getAbsolutePath().startsWith( path ); } /** * Deletes files recursively. * @param files the files to delete * @throws IOException if a file could not be deleted */ public static void deleteFilesRecursively( File... files ) throws IOException { if( files == null ) return; List<File> filesToDelete = new ArrayList<> (); filesToDelete.addAll( Arrays.asList( files )); while( ! filesToDelete.isEmpty()) { File currentFile = filesToDelete.remove( 0 ); if( currentFile == null || ! currentFile.exists()) continue; // Non-empty directory: add sub-files and reinsert the current directory after File[] subFiles = currentFile.listFiles(); if( subFiles != null && subFiles.length > 0 ) { filesToDelete.add( 0, currentFile ); filesToDelete.addAll( 0, Arrays.asList( subFiles )); } // Existing file or empty directory => delete it else if( ! currentFile.delete()) throw new IOException( currentFile.getAbsolutePath() + " could not be deleted." ); } } /** * Deletes files recursively and remains quiet even if an exception is thrown. * @param files the files to delete */ public static void deleteFilesRecursivelyAndQuietly( File... files ) { try { deleteFilesRecursively( files ); } catch( IOException e ) { Logger logger = Logger.getLogger( Utils.class.getName()); logException( logger, e ); } } /** * Writes an exception's stack trace into a string. * <p> * This method used to be public.<br> * Its visibility was reduced to promote {@link #logException(Logger, Exception)}, * which has better performances. * </p> * * @param t an exception or a throwable (not null) * @return a string */ static String writeException( Throwable t ) { StringWriter sw = new StringWriter(); t.printStackTrace( new PrintWriter( sw )); return sw.toString(); } /** * Logs an exception with the given logger and the given level. * <p> * Writing a stack trace may be time-consuming in some environments. * To prevent useless computing, this method checks the current log level * before trying to log anything. * </p> * * @param logger the logger * @param t an exception or a throwable * @param logLevel the log level (see {@link Level}) */ public static void logException( Logger logger, Level logLevel, Throwable t ) { if( logger.isLoggable( logLevel )) logger.log( logLevel, writeException( t )); } /** * Logs an exception with the given logger and the FINEST level. * @param logger the logger * @param t an exception or a throwable */ public static void logException( Logger logger, Throwable t ) { logException( logger, Level.FINEST, t ); } /** * Determines whether a file is a parent of another file. * <p> * This method handles intermediate '.' and '..' segments. * </p> * * @param potentialAncestor a file that may contain the other one * @param file a file * @return true if the path of 'file' starts with the path of 'potentialAncestor', false otherwise * @throws IOException if the file location cannot be made canonical */ public static boolean isAncestorFile( File potentialAncestor, File file ) throws IOException { String ancestorPath = potentialAncestor.getCanonicalPath(); String path = file.getCanonicalPath(); boolean result = false; if( path.startsWith( ancestorPath )) { String s = path.substring( ancestorPath.length()); result = s.isEmpty() || s.startsWith( System.getProperty( "file.separator" )); } return result; } /** * Copies a directory. * <p> * This method copies the content of the source directory * into the a target directory. This latter is created if necessary. * </p> * * @param source the directory to copy * @param target the target directory * @throws IOException if a problem occurred during the copy */ public static void copyDirectory( File source, File target ) throws IOException { Utils.createDirectory( target ); for( File sourceFile : listAllFiles( source, false )) { String path = computeFileRelativeLocation( source, sourceFile ); File targetFile = new File( target, path ); Utils.createDirectory( targetFile.getParentFile()); copyStream( sourceFile, targetFile ); } } /** * Parses a raw URL and extracts the host and port. * @param url a raw URL (not null) * @return a non-null map entry (key = host URL without the port, value = the port, -1 if not specified) */ public static Map.Entry<String,Integer> findUrlAndPort( String url ) { Matcher m = Pattern.compile( ".*(:\\d+).*" ).matcher( url ); String portAsString = m.find() ? m.group( 1 ).substring( 1 ) : null; Integer port = portAsString == null ? - 1 : Integer.parseInt( portAsString ); String address = portAsString == null ? url : url.replace( m.group( 1 ), "" ); return new AbstractMap.SimpleEntry<>( address, port ); } /** * Returns the value contained in a map of string if it exists using the key. * @param map a map of string * @param key a string * @param defaultValue the default value */ public static String getValue(Map<String,String> map, String key, String defaultValue) { return map.containsKey( key ) ? map.get( key ) : defaultValue; } }