// // ERXFileUtilities.java // ERExtensions // // Created by Max Muller on Thu Jan 09 2003. // package er.extensions.foundation; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.StringReader; import java.io.Writer; import java.net.MalformedURLException; import java.net.URL; import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.Enumeration; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import org.apache.commons.lang3.CharEncoding; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOApplication; import com.webobjects.appserver.WOResourceManager; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSBundle; import com.webobjects.foundation.NSForwardException; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSPropertyListSerialization; import er.extensions.ERXExtensions; import er.extensions.foundation.ERXRuntimeUtilities.Result; import er.extensions.foundation.ERXRuntimeUtilities.TimeoutException; /** * Collection of handy {java.io.File} utilities. * * By default, will use UTF-8 for the character set, though one can set the static ivar to * override this choice. */ public class ERXFileUtilities { // =========================================================================== // Class Constants // --------------------------------------------------------------------------- private static final Logger log = LoggerFactory.getLogger(ERXFileUtilities.class); private static Charset charset = null; static { setDefaultCharset(CharEncoding.UTF_8); } // =========================================================================== // Static Methods // --------------------------------------------------------------------------- public static Charset charset() { return charset; } public static void setDefaultCharset(String name) { Charset original = charset; try { charset = Charset.forName(name); } catch (Exception e) { log.error("Unable to set default charset to '{}'", name); charset = original; } } // =========================================================================== // Class Methods // --------------------------------------------------------------------------- /** * Copies the contents of the given URL to a temporary file. * * @param url the URL to copy from * @param prefix the temporary file prefix * @param suffix the temporary file suffix (if null, the extension from the URL is used) * @return the temporary file * @throws IOException if the copy fails */ public static File writeUrlToTempFile(String url, String prefix, String suffix) throws IOException { return ERXFileUtilities.writeUrlToTempFile(new URL(url), prefix, suffix); } /** * Copies the contents of the given URL to a temporary file. * * @param url the URL to copy from * @param prefix the temporary file prefix * @param suffix the temporary file suffix (if null, the extension from the URL is used) * @return the temporary file * @throws IOException if the copy fails */ public static File writeUrlToTempFile(URL url, String prefix, String suffix) throws IOException { String extension; if (suffix == null) { String urlStr = url.toExternalForm(); int dotIndex = urlStr.lastIndexOf('.'); if (dotIndex >= 0) { int questionMarkIndex = urlStr.indexOf('?', dotIndex); if (questionMarkIndex == -1) { extension = urlStr.substring(dotIndex); } else { extension = urlStr.substring(dotIndex, questionMarkIndex); } } else { extension = ""; } } else { extension = suffix; } File tempFile = ERXFileUtilities.writeInputStreamToTempFile(url.openStream(), prefix, extension); return tempFile; } /** * Copies the contents of the given URL to a file. * * @param url the URL to copy from * @param file the File to write to * @throws IOException if the copy fails */ public static void writeUrlToTempFile(String url, File file) throws IOException { ERXFileUtilities.writeUrlToTempFile(new URL(url), file); } /** * Copies the contents of the given URL to a file. * * @param url the URL to copy from * @param file the File to write to * @throws IOException if the copy fails */ public static void writeUrlToTempFile(URL url, File file) throws IOException { ERXFileUtilities.writeInputStreamToFile(url.openStream(), file); } /** * Returns the byte array for a given stream. * @param in stream to get the bytes from * @throws IOException if things go wrong * @return byte array of the stream. */ public static byte[] bytesFromInputStream(InputStream in) throws IOException { if (in == null) throw new IllegalArgumentException("null input stream"); try (ByteArrayOutputStream bout = new ByteArrayOutputStream()) { int read; byte[] buf = new byte[1024 * 50]; while ((read = in.read(buf)) != -1) { bout.write(buf, 0, read); } return bout.toByteArray(); } } /** * Returns a string from the input stream using the specified * encoding. * @param in stream to read * @param encoding to be used, <code>null</code> will use the default * @return string representation of the stream * @throws IOException if things go wrong */ public static String stringFromInputStream(InputStream in, String encoding) throws IOException { return new String(bytesFromInputStream(in), encoding); } /** * Returns a string from the input stream using the default * encoding. * @param in stream to read * @return string representation of the stream. * @throws IOException if things go wrong */ public static String stringFromInputStream(InputStream in) throws IOException { return new String(bytesFromInputStream(in)); } /** * Returns the String from the contents of the given URL. * * @param url the URL to read from * @return the String contents of the URL * @throws IOException if an error occurs */ public static String stringFromURL(URL url) throws IOException { try (InputStream is = url.openStream()) { return ERXFileUtilities.stringFromInputStream(is); } } /** * Returns the byte array for a given gzipped file. * @param f file to get the bytes from * @throws IOException if things go wrong * @return byte array of the file. */ public static byte[] bytesFromGZippedFile(File f) throws IOException { if (f == null) throw new IllegalArgumentException("null file"); byte[] result = null; try (FileInputStream fis = new FileInputStream(f); GZIPInputStream gis = new GZIPInputStream(fis)) { result = bytesFromInputStream(gis); } return result; } /** * Returns the byte array for a given file. * @param f file to get the bytes from * @throws IOException if things go wrong * @return byte array of the file. */ public static byte[] bytesFromFile(File f) throws IOException { if (f == null) throw new IllegalArgumentException("null file"); return bytesFromFile(f, (int)f.length()); } /** * Returns an array of the first n bytes for a given file. * @param f file to get the bytes from * @param n number of bytes to read from input file * @throws IOException if things go wrong * @return byte array of the first n bytes from the file. */ public static byte[] bytesFromFile(File f, int n) throws IOException { if (f == null) throw new IllegalArgumentException("null file"); try (FileInputStream fis = new FileInputStream(f)) { byte[] result = bytesFromInputStream(fis, n); return result; } } /** * Returns an array of the first n bytes for a given input stream * @param fis inputstream to get the bytes from * @param n number of bytes to read from input stream * @throws IOException if things go wrong * @return byte array of the first n bytes from the file. */ public static byte[] bytesFromInputStream(InputStream fis, int n) throws IOException { byte[] data = new byte[n]; int bytesRead = 0; while (bytesRead < n) bytesRead += fis.read(data, bytesRead, n - bytesRead); return data; } /** * Writes the contents of an InputStream to a temporary file. * * @param stream * to pull data from * @return the temp file that was created * @throws IOException if things go wrong */ public static File writeInputStreamToTempFile(InputStream stream) throws IOException { return ERXFileUtilities.writeInputStreamToTempFile(stream, "_Wonder", ".tmp"); } /** * Writes the contents of an InputStream to a temporary file. * * @param stream * to pull data from * @param prefix the filename prefix of the temp file * @param suffix the filename suffix of the temp file * @return the temp file that was created * @throws IOException if things go wrong */ public static File writeInputStreamToTempFile(InputStream stream, String prefix, String suffix) throws IOException { File tempFile; try { tempFile = File.createTempFile(prefix, suffix); try { ERXFileUtilities.writeInputStreamToFile(stream, tempFile); } catch (RuntimeException e) { if (! tempFile.delete()) log.error("RuntimeException occured, but cannot delete tempFile '{}'", tempFile); throw e; } catch (IOException e) { if (! tempFile.delete()) log.error("IOException occured, but cannot delete tempFile '{}'", tempFile); throw e; } } finally { stream.close(); } return tempFile; } /** * Writes the contents of an InputStream to a specified file. * @param file to write to * @param stream to pull data from * @throws IOException if things go wrong */ public static void writeInputStreamToFile(InputStream stream, File file) throws IOException { try { if (file == null) throw new IllegalArgumentException("Attempting to write to a null file!"); File parent = file.getParentFile(); if(parent != null && !parent.exists()) { if (! parent.mkdirs()) throw new RuntimeException("Cannot create parent directory for file"); } try (FileOutputStream out = new FileOutputStream(file)) { ERXFileUtilities.writeInputStreamToOutputStream(stream, true, out, true); } } finally { stream.close(); } } public static void writeInputStreamToGZippedFile(InputStream stream, File file) throws IOException { if (file == null) throw new IllegalArgumentException("Attempting to write to a null file!"); try (GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(file))) { ERXFileUtilities.writeInputStreamToOutputStream(stream, false, out, true); } } /** * Copies the contents of the input stream to the given output stream. Both streams are * guaranteed to be closed by the end of this method. * * @param in the input stream to copy from * @param out the output stream to copy to * @throws IOException if there is any failure */ public static void writeInputStreamToOutputStream(InputStream in, OutputStream out) throws IOException { ERXFileUtilities.writeInputStreamToOutputStream(in, true, out, true); } /** * Copies the contents of the input stream to the given output stream. * * @param in the input stream to copy from * @param closeInputStream if true, the input stream will be closed * @param out the output stream to copy to * @param closeOutputStream if true, the output stream will be closed * @throws IOException if there is any failure */ public static void writeInputStreamToOutputStream(InputStream in, boolean closeInputStream, OutputStream out, boolean closeOutputStream) throws IOException { try { BufferedInputStream bis = new BufferedInputStream(in); try { byte buf[] = new byte[1024 * 50]; //64 KBytes buffer int read = -1; while ((read = bis.read(buf)) != -1) { out.write(buf, 0, read); } } finally { if (closeInputStream) { bis.close(); } } out.flush(); } finally { if (closeOutputStream) { out.close(); } } } public static void stringToGZippedFile(String s, File f) throws IOException { if (s == null) throw new NullPointerException("string argument cannot be null"); if (f == null) throw new NullPointerException("file argument cannot be null"); byte[] bytes = s.getBytes(charset().name()); ByteArrayInputStream bais = new ByteArrayInputStream(bytes); writeInputStreamToGZippedFile(bais, f); } /** * Writes the contents of <code>s</code> to <code>f</code> * using the platform's default encoding. * * @param s the string to be written to file * @param f the destination file * @throws IOException if things go wrong */ public static void stringToFile(String s, File f) throws IOException { stringToFile( s, f, System.getProperty("file.encoding") ); } /** * Writes the contents of <code>s</code> to <code>f</code> * using specified encoding. * * @param s the string to be written to file * @param f the destination file * @param encoding the desired encoding * @throws IOException if things go wrong */ public static void stringToFile(String s, File f, String encoding) throws IOException { if (s == null) throw new IllegalArgumentException("string argument cannot be null"); if (f == null) throw new IllegalArgumentException("file argument cannot be null"); if (encoding == null) throw new IllegalArgumentException("encoding argument cannot be null"); try (Reader reader = new BufferedReader(new StringReader(s)); FileOutputStream fos = new FileOutputStream(f); Writer out = new BufferedWriter(new OutputStreamWriter(fos, encoding))) { int read; char buf[] = new char[1024 * 50]; while ((read = reader.read(buf)) != -1) { out.write(buf, 0, read); } } } /** * Copy a file across hosts using scp. * @param srcHost host to send from (<code>null</code> if file is local) * @param srcPath path on srcHost to read from * @param dstHost host to send to (<code>null</code> if file is local) * @param dstPath path on srcHost to write to * @throws IOException if things go wrong */ public static void remoteCopyFile(String srcHost, String srcPath, String dstHost, String dstPath) throws IOException { if (srcPath == null) throw new IllegalArgumentException("null source path not allowed"); if (dstPath == null) throw new IllegalArgumentException("null source path not allowed"); NSMutableArray<String> args = new NSMutableArray<>(7); args.addObject("/usr/bin/scp"); args.addObject("-B"); args.addObject("-q"); args.addObject("-o"); args.addObject("StrictHostKeyChecking=no"); args.addObject(((srcHost != null) ? (srcHost + ":") : "") + srcPath); args.addObject(((dstHost != null) ? (dstHost + ":") : "") + dstPath); String[] cmd = ERXArrayUtilities.toStringArray(args); try { Result result = ERXRuntimeUtilities.execute(cmd, null, null, 0L); if(result.getExitValue() != 0) { throw new IOException("Unable to remote copy file: (exit status = " + result.getExitValue() + ") " + result.getErrorAsString() + "\n"); } } catch (TimeoutException e) { throw new IOException("Command timed out"); } } /** * Copy a file across hosts using scp. * @param srcFile local file to send * @param dstHost host to send to (<code>null</code> if file is local) * @param dstPath path on srcHost to write to * @throws IOException if things go wrong */ public static void remoteCopyFile(File srcFile, String dstHost, String dstPath) throws IOException { remoteCopyFile(null, srcFile.getPath(), dstHost, dstPath); } /** * Copy a file across hosts using scp. * @param srcHost host to send from (<code>null</code> if file is local) * @param srcPath path on srcHost to read from * @param dstFile local file to write to * @throws IOException if things go wrong */ public static void remoteCopyFile(String srcHost, String srcPath, File dstFile) throws IOException { remoteCopyFile(srcHost, srcPath, null, dstFile.getPath()); } /** * Returns a string from the gzipped file using the default * encoding. * @param f file to read * @return string representation of that file. * @throws IOException if things go wrong */ public static String stringFromGZippedFile(File f) throws IOException { return new String(bytesFromGZippedFile(f), charset().name()); } /** * Returns a string from the file using the default * encoding. * @param f file to read * @return string representation of that file. * @throws IOException if things go wrong */ public static String stringFromFile(File f) throws IOException { return new String(bytesFromFile(f), charset().name()); } /** * Returns a string from the file using the specified * encoding. * @param f file to read * @param encoding to be used, <code>null</code> will use the default * @return string representation of the file. * @throws IOException if things go wrong */ public static String stringFromFile(File f, String encoding) throws IOException { if (encoding == null) { return new String(bytesFromFile(f), charset().name()); } return new String(bytesFromFile(f), encoding); } /** * Determines the path of the specified Resource. This is done * to get a single entry point due to the deprecation of pathForResourceNamed * @param fileName name of the file * @param frameworkName name of the framework, <code>null</code> or "app" * for the application bundle * @param languages array of languages to get localized resource or <code>null</code> * @return the absolutePath method off of the * file object */ public static String pathForResourceNamed(String fileName, String frameworkName, NSArray<String> languages) { String path = null; NSBundle bundle = "app".equals(frameworkName) ? NSBundle.mainBundle() : NSBundle.bundleForName(frameworkName); if(bundle != null && bundle.isJar()) { log.warn("Can't get path when run as jar: {} - {}", frameworkName, fileName); } else { WOApplication application = WOApplication.application(); if (application != null) { URL url = application.resourceManager().pathURLForResourceNamed(fileName, frameworkName, languages); if(url != null) { path = url.getFile(); } } else if( bundle != null ) { URL url = bundle.pathURLForResourcePath(fileName); if(url != null) { path = url.getFile(); } } } return path; } /** * Determines if a given resource exists. This is done * to get a single entry point due to the deprecation of pathForResourceNamed * @param fileName name of the file * @param frameworkName name of the framework, <code>null</code> or "app" * for the application bundle * @param languages array of languages to get localized resource or <code>null</code> * @return the absolutePath method off of the * file object */ public static boolean resourceExists(String fileName, String frameworkName, NSArray<String> languages) { URL url = WOApplication.application().resourceManager().pathURLForResourceNamed(fileName, frameworkName, languages); return url != null; } /** * Get the input stream from the specified Resource. * @param fileName name of the file * @param frameworkName name of the framework, <code>null</code> or "app" * for the application bundle * @param languages array of languages to get localized resource or <code>null</code> * @return the absolutePath method off of the file object */ public static InputStream inputStreamForResourceNamed(String fileName, String frameworkName, NSArray<String> languages) { return WOApplication.application().resourceManager().inputStreamForResourceNamed(fileName, frameworkName, languages); } /** * Returns a path containing an optional root with a directory hierarchy based on the current time * @param rootPath Root of the path before the above the date directories * @return the path based on time. */ public static String datePathWithRoot(String rootPath){ Calendar defaultCalendar = Calendar.getInstance(); defaultCalendar.setTime(new Date()); int year = defaultCalendar.get(Calendar.YEAR); int month = defaultCalendar.get(Calendar.MONTH) + 1; int day = defaultCalendar.get(Calendar.DAY_OF_MONTH); int hour = defaultCalendar.get(Calendar.HOUR_OF_DAY); StringBuilder datePath = new StringBuilder(); datePath.append(rootPath); datePath.append("/y"); datePath.append(year); datePath.append((month > 9) ? "/m" : "/m0"); datePath.append(month); datePath.append((day > 9) ? "/d" : "/d0"); datePath.append(day); datePath.append((hour > 9) ? "/h" : "/h0"); datePath.append(hour); return datePath.toString(); } /** * Determines the path URL of the specified Resource. This is done * to get a single entry point due to the deprecation of pathForResourceNamed. * In a later version this will call out to the resource managers new methods directly. * @param fileName name of the file * @param frameworkName name of the framework, <code>null</code> or "app" * for the application bundle * @param languages array of languages to get localized resource or <code>null</code> * @return the absolutePath method off of the file object */ public static URL pathURLForResourceNamed(String fileName, String frameworkName, NSArray<String> languages) { URL url = null; WOApplication application = WOApplication.application(); if (application != null) { WOResourceManager resourceManager = application.resourceManager(); if (resourceManager != null) { url = resourceManager.pathURLForResourceNamed(fileName, frameworkName, languages); } } return url; } /** * Create an URL for a given file. * @param file name of the file * @return file:// URL for the given path */ public static URL URLFromFile(File file) { URL url = null; if(file != null) { try { url = URLFromPath(file.getCanonicalPath()); } catch (IOException ex) { throw new NSForwardException(ex); } } return url; } /** * Create an URL for a given path. * @param fileName path of the file * @return file:// URL for the given path */ public static URL URLFromPath(String fileName) { URL url = null; if(fileName != null) { try { url = new URL("file://" + fileName); } catch(MalformedURLException ex) { throw new NSForwardException(ex); } } return url; } /** * Determines the last modification date for a given file * in a framework. Note that this method will only test for * the global resource not the localized resources. * @param fileName name of the file * @param frameworkName name of the framework, <code>null</code> or "app" * for the application bundle * @return the <code>lastModified</code> method off of the * file object */ public static long lastModifiedDateForFileInFramework(String fileName, String frameworkName) { return lastModifiedDateForFileInFramework(fileName, frameworkName, null); } /** * Determines the last modification date for a given file * in a framework. Note that this method will only test for * the global resource not the localized resources. * @param fileName name of the file * @param frameworkName name of the framework, <code>null</code> or "app" * for the application bundle * @param languages array of languages to get localized resource or <code>null</code> * @return the <code>lastModified</code> method off of the file object */ public static long lastModifiedDateForFileInFramework(String fileName, String frameworkName, NSArray<String> languages) { long lastModified = 0; String filePath = pathForResourceNamed(fileName, frameworkName, languages); if (filePath != null) { lastModified = new File(filePath).lastModified(); } return lastModified; } /** * Reads a file in from the file system and then parses * it as if it were a property list, using the platform's default encoding. * @param fileName name of the file * @param aFrameWorkName name of the framework, <code>null</code> or * 'app' for the application bundle. * @return de-serialized object from the plist formatted file * specified. */ public static Object readPropertyListFromFileInFramework(String fileName, String aFrameWorkName) { return readPropertyListFromFileInFramework(fileName, aFrameWorkName, null, System.getProperty("file.encoding")); } /** * Reads a file in from the file system and then parses * it as if it were a property list, using the specified encoding. * * @param fileName name of the file * @param aFrameWorkName name of the framework, <code>null</code> or * 'app' for the application bundle. * @param encoding the encoding used with <code>fileName</code> * @return de-serialized object from the plist formatted file * specified. */ public static Object readPropertyListFromFileInFramework(String fileName, String aFrameWorkName, String encoding) { return readPropertyListFromFileInFramework(fileName, aFrameWorkName, null, encoding); } /** * Reads a file in from the file system for the given set * of languages and then parses the file as if it were a * property list, using the platform's default encoding. * @param fileName name of the file * @param aFrameWorkName name of the framework, <code>null</code> or * 'app' for the application bundle. * @param languageList language list search order * @return de-serialized object from the plist formatted file * specified. */ public static Object readPropertyListFromFileInFramework(String fileName, String aFrameWorkName, NSArray<String> languageList) { Object plist = null; try { plist = readPropertyListFromFileInFramework( fileName, aFrameWorkName, languageList, System.getProperty("file.encoding")); } catch (IllegalArgumentException e) { try { // BUGFIX: we didnt use an encoding before, so java tried to guess the encoding. Now some Localizable.strings plists // are encoded in MacRoman whereas others are UTF-16. plist = readPropertyListFromFileInFramework(fileName, aFrameWorkName, languageList, CharEncoding.UTF_16); } catch (IllegalArgumentException e1) { // OK, whatever it is, try to parse it! plist = readPropertyListFromFileInFramework(fileName, aFrameWorkName, languageList, CharEncoding.UTF_8); } } return plist; } /** * Reads a file in from the file system for the given set * of languages and then parses the file as if it were a * property list, using the specified encoding. * * @param fileName name of the file * @param aFrameWorkName name of the framework, <code>null</code> or * 'app' for the application bundle. * @param languageList language list search order * @param encoding the encoding used with <code>fileName</code> * @return de-serialized object from the plist formatted file * specified. */ public static Object readPropertyListFromFileInFramework(String fileName, String aFrameWorkName, NSArray<String> languageList, String encoding) { Object result = null; try (InputStream stream = inputStreamForResourceNamed(fileName, aFrameWorkName, languageList)) { if(stream != null) { String stringFromFile = stringFromInputStream(stream, encoding); result = NSPropertyListSerialization.propertyListFromString(stringFromFile); } } catch (IOException ioe) { log.error("ConfigurationManager: Error reading file <{}> from framework {}", fileName, aFrameWorkName); } return result; } /** * Deletes all of the files in a given directory with the option to * recursively delete all of the directories in the given directory. * @param directory to delete all of the files from * @param recurseIntoDirectories determines if the delete is recursive */ public static void deleteFilesInDirectory(File directory, boolean recurseIntoDirectories) { deleteFilesInDirectory(directory, null, recurseIntoDirectories, true); } /** * Deletes all of the files in a given directory with the option to * recursively delete all of the files in the given directory. * @param directory to delete all of the files from * @param filter optional FileFilter to restrict what gets deleted, <code>null</code> to delete everything * @param recurseIntoDirectories determines if the delete is recursive * @param removeDirectories <code>true</code> if directories should be removed as well as files, <code>false</code> to only remove files */ public static void deleteFilesInDirectory(File directory, FileFilter filter, boolean recurseIntoDirectories, boolean removeDirectories) { if (!directory.exists()) throw new RuntimeException("Attempting to delete files from a non-existent directory: " + directory); if (!directory.isDirectory()) throw new RuntimeException("Attmepting to delete files from a file that is not a directory: " + directory); File files[] = filter != null ? directory.listFiles(filter) : directory.listFiles() ; if (files != null && files.length > 0) { for (int i = 0; i < files.length; i++) { File aFile = files[i]; if (aFile.isDirectory() && recurseIntoDirectories) { deleteFilesInDirectory(aFile, filter, recurseIntoDirectories, removeDirectories); } if (aFile.isFile() || (aFile.isDirectory() && removeDirectories && (aFile.listFiles() == null || aFile.listFiles().length == 0))) { if (! aFile.delete()) throw new RuntimeException("Directory \""+directory+"\" not successfully deleted."); } } } } /** * Deletes a given directory in a recursive fashion. * @param directory to be deleted * @return if the directory deleted successfully */ public static boolean deleteDirectory(File directory) { if (! directory.isDirectory()) return directory.delete(); boolean deletedAllFiles = true; String[] fileNames = directory.list(); for (int i = 0; i < fileNames.length; i++) { File file = new File(directory, fileNames[i]); if (file.isDirectory()) { if (!deleteDirectory(file) && deletedAllFiles) deletedAllFiles = false; } else { if (!file.delete() && deletedAllFiles) deletedAllFiles = false; } } if (!directory.delete() && deletedAllFiles) deletedAllFiles = false; return deletedAllFiles; } /** * Java wrapper for call out to chmod. Only works if your OS supports the chmod command. * * @param file the File to run chmod on * @param mode see the chmod man page * @throws IOException if things go wrong */ public static void chmod(File file, String mode) throws IOException { Process process = Runtime.getRuntime().exec(new String[] {"chmod", mode, file.getAbsolutePath()}); ERXRuntimeUtilities.freeProcessResources(process); } /** * Java wrapper for call out to chmod with -R parameter for recursive processing. Only works if your OS supports the chmod command. * * @param dir the File to run chmod on * @param mode see the chmod man page * @throws IOException if things go wrong */ public static void chmodRecursively(File dir, String mode) throws IOException { Process process = Runtime.getRuntime().exec(new String[] {"chmod", "-R", mode, dir.getAbsolutePath()}); ERXRuntimeUtilities.freeProcessResources(process); } /** * Creates a symlink for a given file. Note this only works on * civilized OSs which support symbolic linking. * @param source to create the link to * @param destination file to create the link to * @param symbolic determines if a symlink should be created * @param allowUnlink determines if the symlink is a hardlink which allows unlinking * @param followSymbolicLinks If the destination is a symbolic link, follow it * @throws IOException if the link could not be created */ public static void linkFiles(File source, File destination, boolean symbolic, boolean allowUnlink, boolean followSymbolicLinks) throws IOException { if (destination == null || source == null) throw new IllegalArgumentException("null source or destination not allowed"); ArrayList<String> array = new ArrayList<>(); array.add("ln"); if (allowUnlink) array.add("-f"); if (symbolic) array.add("-s"); if (!followSymbolicLinks) array.add("-n"); array.add(source.getPath()); array.add(destination.getPath()); String[] cmd = new String[array.size()]; for (int i=0; i<array.size(); i++) cmd[i] = array.get(i); Process task = null; try { task = Runtime.getRuntime().exec(cmd); while (true) { try { task.waitFor(); break; } catch (InterruptedException e) {} } if (task.exitValue() != 0) { BufferedReader err = new BufferedReader(new InputStreamReader(task.getErrorStream(), charset())); throw new IOException("Unable to create link: " + err.readLine()); } } finally { ERXExtensions.freeProcessResources(task); } } /** * Copies all of the files in a given directory to another directory. Existing files are replaced. * @param srcDirectory source directory * @param dstDirectory destination directory * @param deleteOriginals tells if the original files, the file is deleted even if appuser has no write * rights. This is compareable to a <code>rm -f filename</code> instead of <code>rm filename</code> * @param recursiveCopy specifies if directories should be recursively copied * @param filter which restricts the files to be copied * @throws IOException if things go wrong */ public static void copyFilesFromDirectory(File srcDirectory, File dstDirectory, boolean deleteOriginals, boolean recursiveCopy, FileFilter filter) throws IOException { copyFilesFromDirectory(srcDirectory, dstDirectory, deleteOriginals, true, recursiveCopy, filter); } /** * Copies all of the files in a given directory to another directory. * @param srcDirectory source directory * @param dstDirectory destination directory * @param deleteOriginals tells if the original files, the file is deleted even if appuser has no write * rights. This is comparable to a <code>rm -f filename</code> instead of <code>rm filename</code> * @param replaceExistingFiles <code>true</code> if the destination should be overwritten if it already exists * @param recursiveCopy specifies if directories should be recursively copied * @param filter which restricts the files to be copied * @throws IOException if things go wrong */ public static void copyFilesFromDirectory(File srcDirectory, File dstDirectory, boolean deleteOriginals, boolean replaceExistingFiles, boolean recursiveCopy, FileFilter filter) throws IOException { if (!srcDirectory.exists() || !dstDirectory.exists()) throw new RuntimeException("Both the src and dst directories must exist! Src: " + srcDirectory + " Dst: " + dstDirectory); File srcFiles[] = filter!=null ? srcDirectory.listFiles(filter) : srcDirectory.listFiles(); if (srcFiles != null && srcFiles.length > 0) { for (int i = 0; i < srcFiles.length; i++) { File srcFile = srcFiles[i]; File dstFile = new File(dstDirectory, srcFile.getName()); if (srcFile.isDirectory() && recursiveCopy) { // Create the destination directory if (deleteOriginals) { renameTo(srcFile, dstFile); } else { if (dstFile.exists() || dstFile.mkdirs()) { copyFilesFromDirectory(srcFile, dstFile, deleteOriginals, replaceExistingFiles, recursiveCopy, filter); } else { log.error("Error creating directories for destination '{}'", dstDirectory); } } } else if (!srcFile.isDirectory()) { if (replaceExistingFiles || ! dstFile.exists()) { copyFileToFile(srcFile, dstFile, deleteOriginals, true); } else if (log.isDebugEnabled()) { log.debug("Destination file: {} skipped as it exists and replaceExistingFiles is set to false.", dstFile); } } else if (log.isDebugEnabled()) { log.debug("Source file: {} is a directory inside: {} and recursive copy is set to false.", srcFile, dstDirectory); } } } } /** * Copies the source file to the destination. * Automatically creates parent directory or directories of {@code dstFile} if they are missing. * * @param srcFile source file * @param dstFile destination file which may or may not exist already. If it exists, its contents will be overwritten. * @param deleteOriginals if {@code true} then {@code srcFile} will be deleted. Note that if the appuser has no write rights * on {@code srcFile} it is NOT deleted unless {@code forceDelete} is true * @param forceDelete if {@code true} then missing write rights are ignored and the file is deleted. * @throws IOException if things go wrong */ public static void copyFileToFile(File srcFile, File dstFile, boolean deleteOriginals, boolean forceDelete) throws IOException { if (srcFile.exists() && srcFile.isFile()) { boolean copied = false; if (deleteOriginals && (!forceDelete || srcFile.canWrite())) { copied = srcFile.renameTo(dstFile); } if (!copied) { File parent = dstFile.getParentFile(); if (! parent.exists() && ! parent.mkdirs()) { throw new IOException("Failed to create the directory " + parent + "."); } try (FileInputStream in = new FileInputStream(srcFile); FileChannel srcChannel = in.getChannel(); FileOutputStream out = new FileOutputStream(dstFile); FileChannel dstChannel = out.getChannel()) { // Copy file contents from source to destination dstChannel.transferFrom(srcChannel, 0, srcChannel.size()); } finally { if (deleteOriginals && (srcFile.canWrite() || forceDelete)) { if (!srcFile.delete()) { throw new IOException("Failed to delete " + srcFile + "."); } } } } } } /** * Creates a temporary directory. * * @return a temporary directory * * @exception IOException if something goes wrong */ public static final File createTempDir() throws IOException { File f = createTempDir("WonderTempDir", ""); if (f.delete() || f.delete()) log.debug("Could not delete temporary directory: '{}'", f); if (! f.mkdirs()) log.error("Could not create temporary directory: '{}'", f); return f; } /** * Creates a temporary directory. * * @param prefix prefix to use for the filename * @param suffix suffix to use for the filename * * @return a temporary directory * * @exception IOException if something goes wrong */ public static final File createTempDir(String prefix, String suffix) throws IOException { File f = File.createTempFile(prefix, suffix); if (f.delete() || f.delete()) log.debug("Could not delete temporary directory: '{}'", f); if (! f.mkdirs()) log.error("Could not create temporary directory: '{}'", f); return f; } /** * Creates a new NSArray which contains all files in the specified directory. * * @param directory the directory from which to add the files * @param recursive if <code>true</code> then files are added recursively meaning subdirectories are scanned, too. * * @return a NSArray containing the files in the directory. If the specified directory does not * exist then the array is empty. */ public static NSArray<File> arrayByAddingFilesInDirectory(File directory, boolean recursive) { if (!directory.exists()) { return NSArray.emptyArray(); } File[] fileList = directory.listFiles(); if (fileList == null || fileList.length == 0) { return NSArray.emptyArray(); } NSMutableArray<File> files = new NSMutableArray<>(); for (int i = 0; i < fileList.length; i++) { File f = fileList[i]; if (f.isDirectory() && recursive) { files.addObjectsFromArray(arrayByAddingFilesInDirectory(f, true)); } else { files.addObject(f); } } return files; } /** * Replaces the extension of the given file with the new extension. * * @param path the path of the file. * @param newExtension the new extension. * * @return the new path. */ public static String replaceFileExtension(String path, String newExtension) { String tmp = "." + newExtension; if (path.endsWith(tmp)) { return path; } int index = path.lastIndexOf("."); if (index > 0) { String p = path.substring(0, index); return p + tmp; } return path + tmp; } /** * Decompresses the specified zipfile. If the file is a compressed directory, the whole subdirectory * structure is created as a subdirectory with the name if the zip file minus the .zip extension * from destination. All intermittent directories are also created. If destination is <code>null</code> * then the <code>System Property</code> "java.io.tmpdir" is used as destination for the * uncompressed file(s). * * * @param f The file to unzip * @param destination the destination directory. If directory is <code>null</code> then the file will be unzipped in * java.io.tmpdir, if it does not exist, then a directory is created and if it exists but is a file * then the destination is set to the directory in which the file is located. * * * @return the file or directory in which the zipfile was unzipped * * @exception IOException if something goes wrong */ public static File unzipFile(File f, File destination) throws IOException { if (!f.exists()) { throw new FileNotFoundException("file "+f+" does not exist"); } String absolutePath; if (destination != null) { absolutePath = destination.getAbsolutePath(); if (!destination.exists()) { if (! destination.mkdirs()) throw new RuntimeException("Cannot create destination directory: \""+destination.getPath()+"\""); } else if (!destination.isDirectory()) { absolutePath = absolutePath.substring(0, absolutePath.lastIndexOf(File.separator)); } } else { absolutePath = System.getProperty("java.io.tmpdir"); } if (!absolutePath.endsWith(File.separator)) { absolutePath += File.separator; } ZipFile zipFile = new ZipFile(f); Enumeration en = zipFile.entries(); if (en.hasMoreElements()) { ZipEntry firstEntry = (ZipEntry)en.nextElement(); if (firstEntry.isDirectory() || en.hasMoreElements()) { String dir = absolutePath + f.getName(); if (dir.endsWith(".zip")) { dir = dir.substring(0, dir.length() - 4); } if (new File(dir).mkdirs()) absolutePath = dir + File.separator; else throw new IOException("Cannot create directory: \""+dir+"\""); } } else { return null; } for (Enumeration e = zipFile.entries(); e.hasMoreElements(); ) { ZipEntry ze = (ZipEntry)e.nextElement(); String name = ze.getName(); if (ze.isDirectory()) { File d = new File(absolutePath + name); if (! d.mkdirs()) throw new IOException("Cannot create directory: \""+d.getPath()+"\""); log.debug("created directory {}", d); } else { try (InputStream is = zipFile.getInputStream(ze)){ writeInputStreamToFile(is, new File(absolutePath, name)); if (log.isDebugEnabled()) { log.debug("unzipped file {} into {}", ze.getName(), absolutePath + name); } } } } return new File(absolutePath); } /** * Compresses a given File with zip. * @param f the file to zip, either a file or a directory * @param absolutePaths if <code>true</code> then the files are added with absolute paths * @param deleteOriginal if <code>true</code> then the original file is deleted * @param forceDelete if <code>true</code> then the original is deleted even if the file is read only * @return file pointer to the zip archive * @throws IOException if something goes wrong */ public static File zipFile(File f, boolean absolutePaths, boolean deleteOriginal, boolean forceDelete) throws IOException { return zipFile(f, absolutePaths, deleteOriginal, forceDelete, 9); } /** * Compresses a given File with zip. * @param f the file to zip, either a file or a directory * @param absolutePaths if <code>true</code> then the files are added with absolute paths * @param deleteOriginal if <code>true</code> then the original file is deleted * @param forceDelete if <code>true</code> then the original is deleted even if the file is read only * @param level the compression level (0-9) * @return file pointer to the zip archive * @throws IOException if something goes wrong */ public static File zipFile(File f, boolean absolutePaths, boolean deleteOriginal, boolean forceDelete, int level) throws IOException { if (!f.exists()) { throw new FileNotFoundException("file "+f+" does not exist"); } File destination = new File(f.getAbsolutePath() + ".zip"); if (destination.exists()) { throw new IOException("zipped file "+destination+" exists"); } NSArray<File> files = f.isDirectory() ? arrayByAddingFilesInDirectory(f, true) : new NSArray<>(f); try (ZipOutputStream zout = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destination)))) { zout.setLevel(level); byte data[] = new byte[2048]; // get a list of files from current directory for (int i = 0; i < files.count(); i++) { File currentFile = files.objectAtIndex(i); try (FileInputStream fi = new FileInputStream(currentFile); BufferedInputStream origin = new BufferedInputStream(fi, 2048)) { String entryName = currentFile.getAbsolutePath(); if (!absolutePaths) { if (f.isDirectory()) { entryName = entryName.substring(f.getAbsolutePath().length() + 1, entryName.length()); } else { entryName = entryName.substring(f.getParentFile().getAbsolutePath().length() + 1, entryName.length()); } } ZipEntry entry = new ZipEntry(entryName); zout.putNextEntry(entry); int count; while((count = origin.read(data, 0, 2048)) != -1) { zout.write(data, 0, count); } } } } catch(Exception e) { e.printStackTrace(); } if (deleteOriginal) { if (f.canWrite() || forceDelete) { if (!deleteDirectory(f)) { deleteDirectory(f); } } } return destination; } /** * Generate an MD5 hash from a file. * * @param file the file to sum * @return the MD5 sum of the bytes in file * @exception IOException if file could not be read */ public static byte[] md5(File file) throws IOException { try (FileInputStream fis = new FileInputStream(file)) { return md5(fis); } } /** * Generate an MD5 hash from an input stream. * * @param in the input stream to sum * @return the MD5 sum of the bytes in file * @exception IOException if the input stream could not be read */ public static byte[] md5(InputStream in) throws IOException { try { java.security.MessageDigest md5 = java.security.MessageDigest.getInstance("MD5"); byte[] buf = new byte[50 * 1024]; int numRead; while ((numRead = in.read(buf)) != -1) { md5.update(buf, 0, numRead); } return md5.digest(); } catch (java.security.NoSuchAlgorithmException e) { throw new NSForwardException(e); } } /** * Generate an MD5 hash from a file. * * @param file the file to sum * @return the hex encoded MD5 sum of the bytes in file * @exception IOException if the file could not be read */ public static String md5Hex(File file) throws IOException { return ERXStringUtilities.byteArrayToHexString(md5(file)); } /** * Generate an MD5 hash from an input stream. * * @param in the input stream to sum * @return the hex encoded MD5 sum of the bytes in file * @exception IOException if the input stream could not be read */ public static String md5Hex(InputStream in) throws IOException { return ERXStringUtilities.byteArrayToHexString(md5(in)); } /** * Returns the size of the given file. If <code>f</code> points * to a directory the size of all its children will be computed. * @param f file to get the size of * @return the file size */ public static long length(File f) { if (!f.isDirectory()) { return f.length(); } long length = 0; File[] files = f.listFiles(); for (int i = files.length; i-- > 0;) { length += length(files[i]); } return length; } /** shortens a filename, for example aVeryLongFileName.java -> aVer...Name.java * @param name the name to modify * @param maxLength the maximum length of the name. * <code>maxLength</code> values under 4 have no effect, the returned string is * always a....java * @return the shortened filename */ public static String shortenFilename(String name, int maxLength) { String ext = fileExtension( name ); String s = removeFileExtension( name ); // not sure but we could use \u2026, instead... String elips = "..."; int elipsLength = elips.length(); int stringLength = s.length(); if( stringLength == maxLength ) return name; if( maxLength <= elipsLength ) maxLength = elipsLength + 1; int noOfChars = maxLength - elipsLength; int mod = noOfChars%2; int firstHalf = noOfChars/2 + mod; int secondHalf = firstHalf - mod; StringBuilder sb = new StringBuilder(); sb.append( s.substring( 0, firstHalf ) ); sb.append( elips ); sb.append( s.substring( stringLength-secondHalf, stringLength ) ); sb.append('.'); sb.append( ext ); return sb.toString(); } /** returns the filename without its fileExtension * @param name the name of the file * @return the name of the file without the fileExtension */ public static String removeFileExtension(String name) { int index = name.lastIndexOf("."); if (index == -1) { return name; } return name.substring(0, index); } /** returns the fileExtension from the specified filename * @param name the name of the file * @return the fileExtension */ public static String fileExtension(String name) { int index = name.lastIndexOf("."); if (index == -1) { return ""; } return name.substring(index + 1); } /** * Deletes all files in array <code>filesToDelete</code> by * using the method deleteDirectory. * * @param filesToDelete array of files to delete * @return <code>true</code> if all file have been deleted, * <code>false</code> otherwise */ public static boolean deleteFiles(NSArray<File> filesToDelete) { boolean deletedAllFiles = true; for (int i = filesToDelete.count(); i-- > 0;) { File currentFile = filesToDelete.objectAtIndex(i); if (!deleteFile(currentFile) && deletedAllFiles) deletedAllFiles = false; } return deletedAllFiles; } /** * Deletes the given file by using the method deleteDirectory. * @param fileToDelete file to delete * @return <code>true</code> if file has been deleted, * <code>false</code> otherwise */ public static boolean deleteFile(File fileToDelete) { return deleteDirectory(fileToDelete); } /** Lists all directories in the specified directory, is desired recursive. * * @param baseDir the dir from which to list the child directories * @param recursive if <code>true</code> this methods works recursively * @return an array of files which are directories */ public static File[] listDirectories(File baseDir, boolean recursive) { File[] files = baseDir.listFiles(new FileFilter() { public boolean accept(File f) { return f.isDirectory(); } }); if (recursive) { NSMutableArray<File> a = new NSMutableArray<>(files); for (int i = files.length; i-- > 0;) { File currentDir = files [i]; File[] currentDirs = listDirectories(currentDir, true); a.addObjects(currentDirs); } Object[] objects = a.objects(); files = new File[objects.length]; System.arraycopy(objects, 0, files, 0, objects.length); } return files; } /** * Lists all files in the specified directory, if desired recursively. * * @param baseDir the dir from which to list the child files * @param recursive if <code>true</code> this method works recursively * @param filter filter to match the files against. If <code>null</code>, all files will be included. * @return an array of files */ public static File[] listFiles(File baseDir, boolean recursive, FileFilter filter) { File[] files = baseDir.listFiles(filter); if (files != null && recursive) { NSMutableArray<File> a = new NSMutableArray<>(); for (int i = files.length; i-- > 0;) { File currentFile = files[i]; a.addObject(currentFile); if(currentFile.isDirectory()) { File[] currentFiles = listFiles(currentFile, true, filter); a.addObjects(currentFiles); } } Object[] objects = a.objects(); files = new File[objects.length]; System.arraycopy(objects, 0, files, 0, objects.length); } return files; } /** * Moves a file from one location to another one. This works different * than java.io.File.renameTo as renameTo does not work across partitions * * @param source the file to move * @param destination the destination to move the source to * @throws IOException if things go wrong */ public static void renameTo(File source, File destination) throws IOException { if (!source.renameTo(destination)) { ERXFileUtilities.copyFileToFile(source, destination, true, true); } } /** * Returns the file name portion of a browser submitted path. * * @param path the full path from the browser * @return the file name portion */ public static String fileNameFromBrowserSubmittedPath(String path) { String fileName = path; if (path != null) { // Windows int separatorIndex = path.lastIndexOf("\\"); // Unix if (separatorIndex == -1) { separatorIndex = path.lastIndexOf("/"); } // MacOS 9 if (separatorIndex == -1) { separatorIndex = path.lastIndexOf(":"); } if (separatorIndex != -1) { fileName = path.substring(separatorIndex + 1); } // ... A tiny security check here ... Just in case. fileName = fileName.replaceAll("\\.\\.", "_"); } return fileName; } /** * Reserves a unique file on the filesystem based on the given file name. If the given * file cannot be reserved, then "-1", "-2", etc will be appended to the filename in front * of the extension until a unique file name is found. This will also ensure that the * parent folder is created. * * @param desiredFile the desired destination file to write * @param overwrite if true, this will immediately return desiredFile * @return a unique, reserved, filename * @throws IOException if the file cannot be created */ public static File reserveUniqueFile(File desiredFile, boolean overwrite) throws IOException { File destinationFile = desiredFile; // ... make sure the destination folder exists. This code runs twice here // in case there was a race condition. File destinationFolder = destinationFile.getParentFile(); if (!destinationFolder.exists()) { if (!destinationFolder.mkdirs()) { if (!destinationFolder.exists()) { throw new IOException("Unable to create the destination folder '" + destinationFolder + "'."); } } } if (!overwrite) { // try to reserve file name if (!desiredFile.createNewFile()) { File parentFolder = desiredFile.getParentFile(); String fileName = desiredFile.getName(); // didn't work, so try new name consisting of // prefix + number + suffix int dotIndex = fileName.lastIndexOf('.'); String prefix, suffix; if (dotIndex < 0) { prefix = fileName; suffix = ""; } else { prefix = fileName.substring(0, dotIndex); suffix = fileName.substring(dotIndex); } int counter = 1; // try until we can reserve a file do { destinationFile = new File(parentFolder, prefix + "-" + counter + suffix); counter ++; } while (!destinationFile.createNewFile()); } } return destinationFile; } }