All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.unkrig.commons.file.filetransformation.FileTransformations Maven / Gradle / Ivy

There is a newer version: 1.2.19
Show newest version

/*
 * de.unkrig.commons - A general-purpose Java class library
 *
 * Copyright (c) 2011, Arno Unkrig
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *       following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *       following disclaimer in the documentation and/or other materials provided with the distribution.
 *    3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
 *       products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package de.unkrig.commons.file.filetransformation;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Comparator;

import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.ArchiveOutputStream;
import org.apache.commons.compress.compressors.CompressorException;
import org.apache.commons.compress.compressors.CompressorInputStream;

import de.unkrig.commons.file.CompressUtil;
import de.unkrig.commons.file.CompressUtil.ArchiveHandler;
import de.unkrig.commons.file.CompressUtil.CompressorHandler;
import de.unkrig.commons.file.CompressUtil.NormalContentsHandler;
import de.unkrig.commons.file.ExceptionHandler;
import de.unkrig.commons.file.FileUtil;
import de.unkrig.commons.file.contentstransformation.ContentsTransformations;
import de.unkrig.commons.file.contentstransformation.ContentsTransformer;
import de.unkrig.commons.file.org.apache.commons.compress.archivers.ArchiveFormat;
import de.unkrig.commons.file.org.apache.commons.compress.compressors.CompressionFormat;
import de.unkrig.commons.io.IoUtil;
import de.unkrig.commons.lang.AssertionUtil;
import de.unkrig.commons.lang.protocol.ConsumerWhichThrows;
import de.unkrig.commons.lang.protocol.Predicate;
import de.unkrig.commons.nullanalysis.Nullable;
import de.unkrig.commons.text.pattern.Glob;

/**
 * Various {@link FileTransformer}-related utility methods.
 * 

* A typical application is to wrap file transformers and content transformers as follows: *

*
 *              |
 *              v
 *     directoryTreeTransformer()
 *              |
 *              v
 *     recursiveCompressedAndArchiveFileTransformer()
 *              |
 *              v
 *     (some contents fransformer, e.g. 'ContentsTransformerUtil.NOP')
 * 
*

* The {@link #directoryTreeTransformer(Comparator, Predicate, Glob, DirectoryCombiner, FileTransformer, boolean, * boolean, ExceptionHandler) directoryTreeTransformer())} can be left out if you don't need recursive directory * traversal. *

*

* The {@link #recursiveCompressedAndArchiveFileTransformer(Predicate, Predicate, Glob, ArchiveCombiner, * ContentsTransformer, boolean, ExceptionHandler) recursiveCompressedAndArchiveFileTransformer()} can be left out if * you don't want to look into archives nor compressed files. *

*/ public final class FileTransformations { static { AssertionUtil.enableAssertionsForThisClass(); } /** * If in equals out ("in-place transformation"), then file in is deleted. Otherwise * ("out-of-place transformation") nothing is done, i.e. a file out is not created. */ public static FileTransformer remove() { return new FileTransformer() { @Override public void transform(String path, File in, File out, Mode mode) throws IOException { switch (mode) { case CHECK: throw FileTransformer.NOT_IDENTICAL; case CHECK_AND_TRANSFORM: throw AssertionUtil.fail("Must not invoke 'remove()' in mode CHECK_AND_TRANSFORM"); case TRANSFORM: if (in.equals(out)) { if (!in.delete()) throw new IOException("Could not delete '" + in + "'"); } return; default: throw AssertionUtil.fail("Unexpected mode '" + mode + "'"); } } }; } /** * If in equals out ("in-place transformation"), then file is left untouched. Otherwise * ("out-of-place transformation") in is copied byte-by-byte to out. */ public static final FileTransformer UNCHANGED = new FileTransformer() { @Override public void transform(String path, File in, File out, Mode mode) throws IOException { switch (mode) { case CHECK: return; case CHECK_AND_TRANSFORM: case TRANSFORM: if (!in.equals(out)) IoUtil.copy(in, out); return; default: throw AssertionUtil.fail("Unexpected mode '" + mode + "'"); } } }; private FileTransformations() {} /** * @see DirectoryCombiner#combineDirectory(String, ConsumerWhichThrows) * @see ArchiveCombiner#combineArchive(String, ConsumerWhichThrows) */ public interface NameAndContents { /** @return A simple name (must not contain separators) */ String getName(); /** * @return A stream producing the contents * @throws FileNotFoundException */ InputStream open() throws FileNotFoundException; } /** @see #combineDirectory(String, ConsumerWhichThrows) */ public interface DirectoryCombiner { /** * Is invoked after the members of the directory have been transformed. *

* The name of the interface and this method is historical; actually nothing is "combined". *

* * @param directoryPath The path designating the directory being transformed * @param memberAdder Can be called to add members to the output directory */ void combineDirectory( String directoryPath, ConsumerWhichThrows memberAdder ) throws IOException; /** * An {@link DirectoryCombiner} that does nothing. */ DirectoryCombiner NOP = new DirectoryCombiner() { @Override public void combineDirectory( String directoryPath, ConsumerWhichThrows memberAdder ) {} }; } /** @see #combineArchive(String, ConsumerWhichThrows) */ public interface ArchiveCombiner { /** * Combines the results of {@link ContentsTransformer#transform(String, InputStream, java.io.OutputStream)} * for all archive entries into one object. * * @param archivePath The path designating the archive being transformed * @param entryAdder Can be called to add entries to the archive */ void combineArchive( String archivePath, ConsumerWhichThrows entryAdder ) throws IOException; /** * An {@link ArchiveCombiner} that does nothing. */ ArchiveCombiner NOP = new ArchiveCombiner() { @Override public void combineArchive( String archivePath, ConsumerWhichThrows entryAdder ) {} }; } /** * Creates and returns a {@link DirectoryTransformer} which implements the following features: *
    *
  • Recursive processing of directories
  • *
  • Removal, renaming and addition of directory members
  • *
* * @param directoryMemberNameComparator The comparator used to sort a directory's members; a {@code null} value * means to NOT sort the members, i.e. leave them in their 'natural' order as * {@link File#list()} returns them * @param directoryMemberRemoval Whether to remove (i.e. not copy) a member; the subject is the path of the * member * @param directoryMemberRenaming {@link Glob#replace(String) Transforms the name} of a member * @param saveSpace If {@code true}, then the method attempts to save file system space for * in-place transformations by transforming each file, otherwise it * creates a copy of the entire directory tree before deleting the original */ public static DirectoryTransformer directoryTreeTransformer( @Nullable Comparator directoryMemberNameComparator, Predicate directoryMemberRemoval, Glob directoryMemberRenaming, DirectoryCombiner directoryCombiner, FileTransformer regularFileTransformer, boolean saveSpace, boolean keepOriginals, ExceptionHandler exceptionHandler ) { ProxyFileTransformer loopback = new ProxyFileTransformer(); DirectoryTransformer result = FileTransformations.directoryTransformer( regularFileTransformer, // regularFileTransformer directoryMemberNameComparator, // directoryMemberNameComparator directoryMemberRemoval, // directoryMemberRemoval directoryMemberRenaming, // directoryMemberRenaming loopback, // directoryMemberTransformer directoryCombiner, // directoryCombiner saveSpace, // saveSpace keepOriginals, // keepOriginals exceptionHandler // exceptionHandler ); loopback.set(result); return result; } /** * @param fileRemovedResult Is called when a file was removed to produce a {@code T} */ private static DirectoryTransformer directoryTransformer( FileTransformer regularFileTransformer, @Nullable Comparator directoryMemberNameComparator, final Predicate directoryMemberRemoval, final Glob directoryMemberRenaming, final FileTransformer directoryMemberTransformer, final DirectoryCombiner directoryCombiner, final boolean saveSpace, final boolean keepOriginals, ExceptionHandler exceptionHandler ) { return new DirectoryTransformer( regularFileTransformer, // regularFileTransformer directoryMemberNameComparator, // directoryMemberNameComparator FileTransformations.renameRemoveFileTransformer( // directoryMemberTransformer directoryMemberRemoval, directoryMemberRenaming, directoryMemberTransformer, keepOriginals ), directoryCombiner, // directoryCombiner saveSpace, // saveSpace keepOriginals, // keepOriginals exceptionHandler // exceptionHandler ); } /** * Wraps the given delegate in a {@link FileTransformer} which handles removal and renaming of files. * * @return A file transformer which implements removal and renaming of files. */ public static FileTransformer renameRemoveFileTransformer( final Predicate removal, final Glob renaming, final FileTransformer delegate, final boolean keepOriginals ) { return new FileTransformer() { @Override public void transform(String path, File inputFile, File outputFile, Mode mode) throws IOException { boolean inPlace = inputFile.equals(outputFile); // Remove file? if (removal.evaluate(path)) { if (mode == Mode.CHECK) throw FileTransformer.NOT_IDENTICAL; if (inPlace) { if (keepOriginals) { File origFile = FileTransformations.origFile(inputFile); if (origFile.exists()) FileUtil.deleteRecursively(origFile); FileUtil.rename(inputFile, origFile); } else { inputFile.delete(); } } return; } // Rename file? String newPath = renaming.replace(path); if (newPath != null) { File newOutputFile = new File(newPath); if (!new File(newPath).getParentFile().equals(inputFile.getParentFile())) { throw new IOException( "Cannot rename '" + path + "' across directory boundaries" ); } newOutputFile = new File(outputFile.getParentFile(), newOutputFile.getName()); if (!newOutputFile.equals(outputFile)) { if (mode == FileTransformer.Mode.CHECK) throw FileTransformer.NOT_IDENTICAL; delegate.transform(path, inputFile, newOutputFile, mode); if (inPlace) { if (keepOriginals) { File origFile = FileTransformations.origFile(inputFile); if (origFile.exists()) FileUtil.deleteRecursively(origFile); FileUtil.rename(inputFile, origFile); } else { inputFile.delete(); } } return; } } delegate.transform(path, inputFile, outputFile, mode); } }; } /** * Returns a {@link FileTransformer} which transforms files by feeding their contents through the {@code * normalContentsTransformer} (just like the {@link FileContentsTransformer}), or, iff the file is an archive * file, by feeding the archive file's entries through the archiveEntryContentsTransformer, or, iff the * file is a compressed file, be feeding the compressed file's decompressed contents through the {@code * compressedContentsTransformer}. *

* Archive files and compressed files are introspected iff lookIntoFormat evaluates to {@code true} * for "format:path". *

*/ public static FileTransformer compressedAndArchiveFileTransformer( final Predicate lookIntoFormat, final Predicate archiveEntryRemoval, final Glob archiveEntryRenaming, final ArchiveCombiner archiveCombiner, final ContentsTransformer archiveEntryContentsTransformer, final ContentsTransformer compressedContentsTransformer, final ContentsTransformer normalContentsTransformer, final boolean keepOriginals, final ExceptionHandler exceptionHandler ) { return new FileTransformer() { @Override public void transform(final String path, File in, final File out, Mode mode) throws IOException { switch (mode) { case CHECK: FileContentsTransformer.checkIdentity( path, new FileInputStream(in), ContentsTransformations.compressedAndArchiveContentsTransformer( lookIntoFormat, archiveEntryRemoval, archiveEntryRenaming, archiveEntryContentsTransformer, archiveCombiner, compressedContentsTransformer, normalContentsTransformer, exceptionHandler ), true // closeInputStream ); return; case CHECK_AND_TRANSFORM: try { // Transform in 'check identity' mode. this.transform(path, in, out, Mode.CHECK); if (!in.equals(out)) { IoUtil.copy(in, out); } return; } catch (RuntimeException re) { if (re != FileTransformer.NOT_IDENTICAL) throw re; // Non-identical transformation. this.transform(path, in, out, Mode.TRANSFORM); return; } case TRANSFORM: if (!in.equals(out)) { this.transform2(path, in, out); return; } File newFile = FileTransformations.newFile(out); if (newFile.exists()) FileUtil.deleteRecursively(newFile); this.transform2(path, in, newFile); if (keepOriginals) { File origFile = FileTransformations.origFile(in); if (origFile.exists()) FileUtil.deleteRecursively(origFile); FileUtil.rename(in, origFile); } else { if (!in.delete()) throw new IOException("Could not delete '" + in + "'"); } FileUtil.rename(newFile, out); return; default: throw AssertionUtil.fail("Unexpected mode '" + mode + "'"); } } /** * Same as {@link #transform}, but does not care about in-place transformations and {@link Mode}. */ private void transform2(final String path, File in, final File out) throws IOException { CompressUtil.processFile( path, // path in, // file lookIntoFormat, // lookIntoFormat FileTransformations.archiveHandler( // archiveHandler path, out, archiveEntryRemoval, archiveEntryRenaming, archiveEntryContentsTransformer, archiveCombiner, exceptionHandler ), FileTransformations.compressorHandler( // compressorHandler path, out, compressedContentsTransformer ), FileTransformations.normalContentsHandler( // normalContentsHandler path, out, normalContentsTransformer ) ); } @Override public String toString() { return "compressedAndArchiveFileTransformer"; } }; } /** * Returns a {@link FileTransformer} which transforms files by feeding their contents through the * delegate (just like the {@link FileContentsTransformer}), but automagically detects various archive * and compression formats (also nested) and processes the entries of the archive and the uncompressed * contents instead of the "raw" contents. * * @param lookIntoFormat Whether the file/entry should be introspected; the subject is * "format:container-path" * @param archiveEntryRemoval Whether to remove (i.e. not copy) a subject file or an archive entry; the subject is * the path of the subject file resp. the archive entry * @param archiveEntryRenaming {@link Glob#replace(String) Transforms the name} of a subject file or an archive * entry * @param archiveCombiner Is invoked after the processing of each archive */ public static FileTransformer recursiveCompressedAndArchiveFileTransformer( final Predicate lookIntoFormat, final Predicate archiveEntryRemoval, final Glob archiveEntryRenaming, final ArchiveCombiner archiveCombiner, final ContentsTransformer delegate, boolean keepOriginals, ExceptionHandler exceptionHandler ) { ContentsTransformer tmp = ContentsTransformations.recursiveCompressedAndArchiveContentsTransformer( lookIntoFormat, // lookIntoFormat archiveEntryRemoval, // archiveEntryRemoval archiveEntryRenaming, // archiveEntryRenaming archiveCombiner, // archiveCombiner delegate, // delegate exceptionHandler // exceptionHandler ); return FileTransformations.compressedAndArchiveFileTransformer( lookIntoFormat, // lookIntoFormat archiveEntryRemoval, // archiveEntryRemoval archiveEntryRenaming, // archiveEntryRenaming archiveCombiner, // archiveCombiner tmp, // archiveEntryContentsTransformer tmp, // compressedContentsTransformer delegate, // normalContentsTransformer keepOriginals, // keepOriginals exceptionHandler // exceptionHandler ); } private static ArchiveHandler archiveHandler( final String path, final File out, final Predicate archiveEntryRemoval, final Glob archiveEntryRenaming, final ContentsTransformer contentsTransformer, final ArchiveCombiner archiveCombiner, final ExceptionHandler exceptionHandler ) { return new ArchiveHandler() { @Override @Nullable public Void handleArchive(final ArchiveInputStream archiveInputStream, final ArchiveFormat archiveFormat) throws IOException { ArchiveOutputStream aos; try { aos = archiveFormat.create(out); } catch (ArchiveException ae) { throw new IOException(ae); } try { ContentsTransformations.transformArchive( path, archiveInputStream, aos, archiveEntryRemoval, archiveEntryRenaming, contentsTransformer, archiveCombiner, exceptionHandler ); aos.close(); return null; } finally { try { aos.close(); } catch (Exception e) {} } } }; } private static CompressorHandler compressorHandler( final String path, final File out, final ContentsTransformer contentsTransformer ) { return new CompressorHandler() { @Override @Nullable public Void handleCompressor(CompressorInputStream compressorInputStream, CompressionFormat compressorFormat) throws IOException { FileOutputStream os = new FileOutputStream(out); try { contentsTransformer.transform( path + '!', compressorInputStream, compressorFormat.compressorOutputStream(os) ); os.close(); return null; } catch (CompressorException ce) { throw new IOException(ce); } finally { try { os.close(); } catch (Exception e) {} } } }; } private static NormalContentsHandler normalContentsHandler(final String path, final File out, final ContentsTransformer contentsTransformer) { return new NormalContentsHandler() { @Override @Nullable public Void handleNormalContents(InputStream inputStream) throws IOException { FileOutputStream os = new FileOutputStream(out); try { contentsTransformer.transform(path, inputStream, os); os.close(); return null; } finally { try { os.close(); } catch (Exception e) {} } } }; } /** * Transforms a set of files based on an array of strings, similar to many UNIX™ command line tools. *

* If {@code args.length == 1}, then the file or directory {@code args[0]} is transformed in-place. *

*

* If {@code args.length == 2} and {@code args[1]} is not an existing directory, then the file {@code args[0]} is * transformed into the file {@code args[1]}. *

*

* If {@code args.length >= 2} and {@code args[args.length - 1]} is an existing directory, then the files {@code * args[0]} ... {@code args[args.length - 2]} are transformed into files with the same names in the directory * {@code args[args.length - 1]}. *

* * @param exceptionHandler If the transformation of one of the inputFiles throws an {@link IOException} * or a {@link RuntimeException}, then {@link ExceptionHandler#handle(String, Exception)} * resp. {@link ExceptionHandler#handle(String, RuntimeException)} is called. Iff that call * completes normally, then processing continues with the next inputFile. * @throws IOException {@code args.length == 0} * @throws IOException {@code args.length > 2} and {@code args[args.length - 1]} is not an existing directory */ public static void transform( String[] args, FileTransformer fileTransformer, FileTransformer.Mode mode, ExceptionHandler exceptionHandler ) throws IOException { if (args.length == 0) { throw new IOException("Input file name missing"); } else if (args.length == 1) { // In-place transformation. File file = new File(args[0]); FileTransformations.transformOneFile( file, // in file, // out fileTransformer, mode, exceptionHandler ); } else if (args.length == 2) { File in = new File(args[0]); File out = new File(args[1]); if (out.isDirectory()) { // Transform file into directory. FileTransformations.transformOneFile( in, new File(out, in.getName()), fileTransformer, mode, exceptionHandler ); } else { // Transform one file to into another file. FileTransformations.transformOneFile( in, out, fileTransformer, mode, exceptionHandler ); } } else { // Transform files into output directory. File outputDirectory = new File(args[args.length - 1]); if (!outputDirectory.isDirectory()) { throw new IOException("Output directory '" + outputDirectory + "' does not exist"); } for (int i = 0; i < args.length - 1; i++) { File in = new File(args[i]); FileTransformations.transformOneFile( in, new File(outputDirectory, in.getName()), fileTransformer, mode, exceptionHandler ); } } } private static void transformOneFile( File in, File out, FileTransformer fileTransformer, FileTransformer.Mode mode, ExceptionHandler exceptionHandler ) throws IOException { String path = in.getPath(); try { fileTransformer.transform(path, in, out, mode); } catch (IOException ioe) { exceptionHandler.handle(path, ioe); } catch (RuntimeException re) { exceptionHandler.handle(path, re); } } /** * @return A file derived from the file which is typically used to create a temporary file which is * later renamed to replace some "original" file */ public static File newFile(File file) { return new File(file.getParentFile(), "." + file.getName() + ".new"); } /** * @return A file derived from the file which is typically used as a container to keep an "original" * file */ public static File origFile(File file) { return new File(file.getParentFile(), "." + file.getName() + ".orig"); } }