de.unkrig.commons.file.contentstransformation.ContentsTransformations Maven / Gradle / Ivy
/*
* 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.contentstransformation;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.apache.commons.compress.archivers.ArchiveEntry;
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 org.apache.commons.compress.compressors.CompressorOutputStream;
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.filetransformation.FileTransformations;
import de.unkrig.commons.file.filetransformation.FileTransformations.ArchiveCombiner;
import de.unkrig.commons.file.filetransformation.FileTransformations.NameAndContents;
import de.unkrig.commons.file.org.apache.commons.compress.archivers.ArchiveFormat;
import de.unkrig.commons.file.org.apache.commons.compress.archivers.ArchiveFormatFactory;
import de.unkrig.commons.file.org.apache.commons.compress.compressors.CompressionFormat;
import de.unkrig.commons.io.ByteFilterInputStream;
import de.unkrig.commons.io.ByteFilterOutputStream;
import de.unkrig.commons.io.IoUtil;
import de.unkrig.commons.lang.AssertionUtil;
import de.unkrig.commons.lang.ExceptionUtil;
import de.unkrig.commons.lang.protocol.ConsumerWhichThrows;
import de.unkrig.commons.lang.protocol.HardReference;
import de.unkrig.commons.lang.protocol.Predicate;
import de.unkrig.commons.nullanalysis.Nullable;
import de.unkrig.commons.text.pattern.Glob;
/**
* Utility class for {@link ContentsTransformer}.
*/
public final
class ContentsTransformations {
static { AssertionUtil.enableAssertionsForThisClass(); }
private
ContentsTransformations() {}
/**
* A {@link ContentsTransformer} which {@link IoUtil#copy(InputStream, OutputStream)}s all data unmodified and
* returns the number of copied bytes.
*/
public static final ContentsTransformer
COPY = new ContentsTransformer() {
@Override public void
transform(String name, InputStream is, OutputStream os) throws IOException { IoUtil.copy(is, os); }
@Override public String
toString() { return "COPY"; }
};
/**
* Returns a {@link ContentsTransformer} which transforms contents by feeding it through the {@code
* normalContentsTransformer}, but automagically detects various archive and compression formats and processes the
* entries of the archive and the uncompressed contents instead of the "raw" contents.
*
* Archive files and compressed files are introspected iff {@code lookIntoFormat} evaluates {@code true} for
* "format:path".
*
*/
public static ContentsTransformer
compressedAndArchiveContentsTransformer(
final Predicate super String> lookIntoFormat,
final Predicate super String> archiveEntryRemoval,
final Glob archiveEntryRenaming,
final ContentsTransformer archiveEntryContentsTransformer,
final ArchiveCombiner archiveCombiner,
final ContentsTransformer compressedContentsTransformer,
final ContentsTransformer normalContentsTransformer,
final ExceptionHandler exceptionHandler
) {
return new ContentsTransformer() {
@Override public void
transform(final String path, InputStream is, OutputStream os) throws IOException {
CompressUtil.processStream(
is, // is
new Predicate() { // lookIntoArchive
@Override public boolean
evaluate(ArchiveFormat af) { return lookIntoFormat.evaluate(af.getName() + ':' + path); }
@Override public String
toString() { return "path =* " + lookIntoFormat; }
},
ContentsTransformations.archiveHandler( // archiveHandler
path,
os,
archiveEntryRemoval,
archiveEntryRenaming,
archiveEntryContentsTransformer,
archiveCombiner,
exceptionHandler
),
new Predicate() { // lookIntoCompressed
@Override public boolean
evaluate(CompressionFormat cf) { return lookIntoFormat.evaluate(cf.getName() + ':' + path); }
@Override public String
toString() { return "compression-format + ':' + path =* " + lookIntoFormat; }
},
ContentsTransformations.compressorHandler( // compressorHandler
path,
os,
compressedContentsTransformer
),
ContentsTransformations.normalContentsHandler( // normalContentsHandler
path,
os,
normalContentsTransformer
)
);
}
@Override public String
toString() { return "compressedAndArchiveContentsTransformer"; }
};
}
/**
* Returns a {@link ContentsTransformer} which transforms a stream by feeding it through the {@code delegate}, but
* automagically detects various archive formats and compression formats (also nested) and processes the
* entries of the archive and the uncompressed contents instead of the "raw" contents.
*
* Archive streams/entries and compressed streams/entries are introspected iff {@code lookIntoFormat} evaluates
* {@code true} for "format:path".
*
*/
public static ContentsTransformer
recursiveCompressedAndArchiveContentsTransformer(
final Predicate super String> lookIntoFormat,
final Predicate super String> archiveEntryRemoval,
final Glob archiveEntryRenaming,
final ArchiveCombiner archiveCombiner,
final ContentsTransformer delegate,
ExceptionHandler exceptionHandler
) {
final HardReference loopback = new HardReference();
// To implement the "feedback look" it is necessary to create a temporary contents transformer.
ContentsTransformer tmp = new ContentsTransformer() {
@Override public void
transform(String name, InputStream is, OutputStream os) throws IOException {
ContentsTransformer l = loopback.get();
assert l != null;
l.transform(name, is, os);
}
};
ContentsTransformer result = ContentsTransformations.compressedAndArchiveContentsTransformer(
lookIntoFormat, // lookIntoFormat
archiveEntryRemoval, // archiveEntryRemoval
archiveEntryRenaming, // archiveEntryRenaming
tmp, // archiveEntryContentsTransformer
archiveCombiner, // archiveCombiner
tmp, // compressedContentsTransformer
delegate, // normalContentsTransformer
exceptionHandler // exceptionHandler
);
loopback.set(result);
return result;
}
/**
* Transforms the given {@code archiveInputStream} into the given {@code archiveOutputStream}, honoring the given
* {@code archiveEntryRemoval}, {@code archiveEntryRenaming} and {@code archiveEntryAddition}, and using the given
* {@code contentsTransformer}.
*/
public static void
transformArchive(
final String path,
final ArchiveInputStream archiveInputStream,
final ArchiveOutputStream archiveOutputStream,
final Predicate super String> archiveEntryRemoval,
final Glob archiveEntryRenaming,
final ContentsTransformer contentsTransformer,
final ArchiveCombiner archiveCombiner,
ExceptionHandler exceptionHandler
) throws IOException {
@SuppressWarnings("deprecation") final ArchiveFormat
outputFormat = ArchiveFormatFactory.forArchiveOutputStream(archiveOutputStream);
for (
ArchiveEntry ae = archiveInputStream.getNextEntry();
ae != null;
ae = archiveInputStream.getNextEntry()
) {
final String entryPath = path + '!' + ArchiveFormatFactory.normalizeEntryName(ae.getName());
// Is the entry to be removed?
if (archiveEntryRemoval.evaluate(entryPath)) continue;
// Is the entry to be renamed?
String newName = archiveEntryRenaming.replace(entryPath);
if (newName != null) {
{
int idx = newName.lastIndexOf('!');
if (idx != -1) {
if (!path.equals(newName.substring(0, idx))) {
throw new IOException(
"Cannot rename '"
+ entryPath
+ "' across archive boundaries"
);
}
newName = newName.substring(idx + 1);
assert newName != null;
}
}
}
// Now append the entry to the output.
try {
outputFormat.writeEntry(
archiveOutputStream,
ae,
newName,
new ConsumerWhichThrows() {
@Override public void
consume(OutputStream os) throws IOException {
contentsTransformer.transform(entryPath, archiveInputStream, os);
}
@Override public String
toString() { return "WRITE CONTENTS OF ARCHIVE ENTRY '" + entryPath + "'"; }
}
);
} catch (IOException ioe) {
exceptionHandler.handle(path, ExceptionUtil.wrap("Transforming entry '" + ae + "'", ioe));
} catch (RuntimeException re) {
exceptionHandler.handle(path, ExceptionUtil.wrap("Transforming entry '" + ae + "'", re));
}
}
// Optionally append new entries to the output.
archiveCombiner.combineArchive(
path,
new ConsumerWhichThrows() { // entryAdder
@Override public void
consume(final NameAndContents nac) throws IOException {
outputFormat.writeEntry(
archiveOutputStream,
nac.getName(),
new ConsumerWhichThrows() {
@Override public void
consume(OutputStream os) throws IOException { IoUtil.copy(nac.open(), true, os, false); }
}
);
}
@Override public String
toString() { return "ADD TO ARCHIVE"; }
}
);
}
/**
* Creates and returns an {@link ArchiveHandler} which transforms {@link ArchiveInputStream}s into {@link
* ArchiveOutputStream}s, honoring the given {@code archiveEntryRemoval}, {@code archiveEntryRenaming} and {@code
* archiveEntryAddition}, and using the given {@code contentsTransformer}.
*/
public static ArchiveHandler
archiveHandler(
final String path,
final OutputStream os,
final Predicate super String> archiveEntryRemoval,
final Glob archiveEntryRenaming,
final ContentsTransformer contentsTransformer,
final ArchiveCombiner archiveFileCombiner,
final ExceptionHandler exceptionHandler
) {
return new ArchiveHandler() {
@Override @Nullable public Void
handleArchive(final ArchiveInputStream archiveInputStream, final ArchiveFormat archiveFormat)
throws IOException {
final ArchiveOutputStream aos;
try {
aos = archiveFormat.archiveOutputStream(os);
} catch (ArchiveException ae) {
throw new IOException(path, ae);
}
ContentsTransformations.transformArchive(
path,
archiveInputStream,
aos,
archiveEntryRemoval,
archiveEntryRenaming,
contentsTransformer,
archiveFileCombiner,
exceptionHandler
);
aos.finish();
return null;
}
};
}
/**
* Creates and returns a {@link CompressorHandler} which transforms an {@link ArchiveInputStream} into an {@link
* OutputStream}, using the given {@code contentsTransformer}.
*/
public static CompressorHandler
compressorHandler(
final String path,
final OutputStream os,
final ContentsTransformer contentsTransformer
) {
return new CompressorHandler() {
@Override @Nullable public Void
handleCompressor(CompressorInputStream compressorInputStream, CompressionFormat compressionFormat)
throws IOException {
CompressorOutputStream cos;
try {
cos = compressionFormat.compressorOutputStream(os);
} catch (CompressorException ce) {
throw new IOException(ce);
}
contentsTransformer.transform(path + '!', compressorInputStream, cos);
cos.flush();
return null;
}
};
}
/**
* Creates and returns a handler which transforms an {@link InputStream} into an {@link OutputStream} using the
* given {@code contentsTransformer}.
*/
public static NormalContentsHandler
normalContentsHandler(final String path, final OutputStream os, final ContentsTransformer contentsTransformer) {
return new NormalContentsHandler() {
@Override @Nullable public Void
handleNormalContents(InputStream inputStream) throws IOException {
contentsTransformer.transform(path, inputStream, os);
return null;
}
};
}
/**
* Creates and returns a {@link ContentsTransformer}s that "chains" the two delegates.
*
* If neither of the two delegate transformers is {@link ContentsTransformations#COPY}, then the returned
* transfomer, when executed, will create and later joind one background thread.
*
*/
public static ContentsTransformer
chain(final ContentsTransformer transformer1, final ContentsTransformer transformer2) {
if (transformer1 == ContentsTransformations.COPY) return transformer2;
if (transformer2 == ContentsTransformations.COPY) return transformer1;
return new ContentsTransformer() {
@Override public void
transform(final String name, InputStream is, OutputStream os) throws IOException {
transformer2.transform(
name,
new ByteFilterInputStream(is, new ContentsTransformerByteFilter(transformer1, name)),
os
);
}
};
}
/**
* Creates and returns an {@link InputStream} that reads from the delegate and through the
* transformer
*
* @param name Designates the contents that is transformed; may be used by the transformer e.g. to
* decide how to transform the contents
*/
public static InputStream
asInputStream(InputStream delegate, ContentsTransformer transformer, String name) {
if (transformer == ContentsTransformations.COPY) return delegate;
return new ByteFilterInputStream(delegate, new ContentsTransformerByteFilter(transformer, name));
}
/**
* Creates and returns an {@link OutputStream} that writes through the transformer and to the
* delegate.
*
* @param name Designates the contents that is transformed; may be used by the transformer e.g. to
* decide how to transform the contents
*/
public static OutputStream
asOutputStream(ContentsTransformer transformer, OutputStream delegate, String name) {
if (transformer == ContentsTransformations.COPY) return delegate;
return new ByteFilterOutputStream(new ContentsTransformerByteFilter(transformer, name), delegate);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy