de.unkrig.zz.find.Find Maven / Gradle / Ivy
/*
* de.unkrig.find - An advanced version of the UNIX FIND utility
*
* 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. The name of the author may not be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.zz.find;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Formatter;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.Checksum;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
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.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.file.org.apache.commons.compress.compressors.CompressionFormatFactory;
import de.unkrig.commons.io.IoUtil;
import de.unkrig.commons.lang.AssertionUtil;
import de.unkrig.commons.lang.ExceptionUtil;
import de.unkrig.commons.lang.ProcessUtil;
import de.unkrig.commons.lang.protocol.ConsumerUtil;
import de.unkrig.commons.lang.protocol.ConsumerWhichThrows;
import de.unkrig.commons.lang.protocol.Mapping;
import de.unkrig.commons.lang.protocol.Mappings;
import de.unkrig.commons.lang.protocol.Predicate;
import de.unkrig.commons.lang.protocol.PredicateUtil;
import de.unkrig.commons.lang.protocol.Producer;
import de.unkrig.commons.lang.protocol.RunnableUtil;
import de.unkrig.commons.lang.protocol.RunnableWhichThrows;
import de.unkrig.commons.nullanalysis.Nullable;
import de.unkrig.commons.text.Printers;
import de.unkrig.commons.text.pattern.Glob;
import de.unkrig.commons.text.pattern.Pattern2;
import de.unkrig.jdisasm.Disassembler;
/**
* The central API for the ZZFIND functionality.
*/
public
class Find {
static { AssertionUtil.enableAssertionsForThisClass(); }
private static final Logger LOGGER = Logger.getLogger(Find.class.getName());
// BEGIN CONFIGURATION VARIABLES
private Predicate super String> lookIntoFormat = PredicateUtil.always();
private boolean depth;
private int minDepth;
private int maxDepth = Integer.MAX_VALUE;
/**
* The expression to match files/entries against.
*/
private Expression expression = Test.TRUE;
private ConsumerWhichThrows super IOException, IOException> exceptionHandler = ConsumerUtil.throwsSubject();
// END CONFIGURATION VARIABLES
// BEGIN CONFIGURATION SETTERS
/**
* @param value Is evaluated against "format:path"
* @see ArchiveFormatFactory#allFormats()
* @see ArchiveFormat#getName()
* @see CompressionFormatFactory#allFormats()
* @see CompressionFormat#getName()
*/
public void
setLookIntoFormat(Predicate super String> value) {
Find.LOGGER.log(Level.FINE, "setLookIntoFormat({0})", value);
this.lookIntoFormat = value;
}
/**
* Whether to process each directory's contents before the directory itself, and each archive's entries before
* the archive itself, and each compressed contents before the enclosing file or archive entry.
*/
public void
setDepth(boolean value) { this.depth = value; }
/**
* Do not apply any tests or actions at levels less than levels. E.g. "1" means "process all files
* except the top level files".
*/
public void
setMinDepth(int levels) {
// Negative values have exactly the same effect as zero and are thus not forbidden.
this.minDepth = levels;
}
/**
* Descend at most levels of directories below the top level files and directories. "0" means "only
* apply the tests and actions to the top level files and directories".
*/
public void
setMaxDepth(int levels) {
// Negative values cause "find()" to return immediately. This is only logical, and thus negative values are not
// forbidden.
this.maxDepth = levels;
}
/**
* Sets the "find expression", i.e. the construct specified with the "-name", "-print", etc. command line options.
*/
public void
setExpression(Expression value) {
Find.LOGGER.log(Level.FINE, "setExpression({0})", value);
this.expression = value;
}
/**
* Under some conditions recovery from exceptions within {@link #findInFile(File)} and {@link
* #findInStream(InputStream)} makes sense, e.g. by continuing with the "next file". For this purpose a custom
* exception handler can be configured.
*
* The default behavior is to not attempt exception recovery, i.e. {@link #findInFile(File)} resp. {@link
* #findInStream(InputStream)} complete abnormally on the first {@link IOException} that occurs.
*
*/
public Find
setExceptionHandler(ConsumerWhichThrows super IOException, IOException> value) {
this.exceptionHandler = value;
return this;
}
/** Getter for the ZZFIND expression. */
public Expression
getExpression() { return this.expression; }
// END CONFIGURATION SETTERS
/**
* Representation of the "FIND expression".
*/
public
interface Expression extends Predicate> {
/**
* Evaluates to {@code true} or {@code false}, depending on the properties:
*
* - String name
* -
* The name of the file, directory or archive entry. For archive entries, the name is relative to the
* archive, i.e. it may contain slashes ("{@code /}").
*
* - String path
* -
* The path of the file, directory or archive entry.
*
* - String type
* -
* The type of the file, directory or archive entry:
*
* - "{@code directory}"
- A directory on the file system
* - "{@code file}"
- A file on the file system
* - "{@code archive-file}"
- An archive file on the file system
* - "{@code compressed-file}"
- A compressed file on the file system
* - "{@code directory-entry}"
- An archive entry which denotes a directory
* - "{@code archive}"
- An archive nested inside an archive or compressed file
* - "{@code compressed-contents}"
- Nested compressed contents
* - "{@code normal-contents}"
* - (Non-compressed, non-archive) contents in an archive or in a compressed file
*
*
*
*
* The following properties apply to the current file or directory, or, iff the current
* document is the contents of a compressed file, or the contents of an archive entry, to the enclosing
* compressed resp. archive file:
*
*
* - String absolutePath
* -
* The absolute path of the file or directory; see {@link File#getAbsolutePath()}.
*
* - String canonicalPath
* -
* The "canonical path" of the file or directory; see {@link File#getCanonicalPath()}.
*
* - Date lastModifiedDate
* -
* The "modification time" of the file or directory.
* Notice that the comparison operators (e.g. "{@code ==}") silently convert dates into strings with
* format "{@code EEE MMM dd HH:mm:ss zzz yyyy}" (see {@link SimpleDateFormat}).
*
* - long size
* -
* The size of the file (zero for directories).
*
* - long freeSpace
* -
* The number of unallocated bytes in the partition (see {@link File#getFreeSpace()}.
*
* - long totalSpace
* -
* The size of the partition (see {@link File#getTotalSpace()}.
*
* - long usableSpace
* -
* The number of bytes available to this virtual machine on the partition (see {@link
* File#getUsableSpace()}.
*
* - boolean isDirectory
* -
* {@code true} for a directory, otherwise {@code false}.
*
* - boolean isFile
* -
* {@code false} for a directory, otherwise {@code true}.
*
* - boolean isHidden
* -
* Whether the file or directory is a hidden file (see {@link File#isHidden()}).
*
* - boolean isReadable
* -
* Whether the application can read the file or directory (see {@link File#canRead()}.
*
* - boolean isWritable
* -
* Whether the application can modify the file or directory (see {@link File#canWrite()}.
*
-
*
* - boolean isExecutable
* -
* Whether the application can execute the file (see {@link File#canExecute()}).
*
*
*
* The following properties are available iff the current file is an archive file, or the current document
* exists within the contents of an archive:
*
*
* - String archiveFormat
* -
* The format of the immediately enclosing archive.
*
*
*
* The following properties are available iff the current file is a compressed file, or the current
* document exists within compressed contents:
*
*
* - String compressionFormat
* -
* The format of the immediately enclosing compressed document.
*
*
*/
@Override boolean evaluate(Mapping properties);
}
/**
* An {@link Expression} that has no side effects.
*/
interface Test extends Expression {
@Override boolean evaluate(Mapping properties);
/** A {@link Find.Test} which unconditionally evaluates to {@code true}. */
Test TRUE = new ConstantTest(true);
/** A {@link Find.Test} which unconditionally evaluates to {@code false}. */
Test FALSE = new ConstantTest(false);
}
static
class ConstantTest implements Test {
private final boolean value;
public
ConstantTest(boolean value) { this.value = value; }
@Override public boolean
evaluate(Mapping properties) { return this.value; }
@Override public String
toString() { return String.valueOf(this.value); }
}
abstract static
class UnaryTest implements Test {
/** The single operand of this test. */
protected final Expression operand;
UnaryTest(Expression operand) { this.operand = operand; }
}
abstract static
class BinaryTest implements Test {
/** The two operands of this test. */
protected final Expression lhs, rhs;
BinaryTest(Expression lhs, Expression rhs) { this.lhs = lhs; this.rhs = rhs; }
}
/**
* Evaluates {@code lhs}, then {@code rhs}, and reutrns the result of the latter evaluation.
*/
static
class CommaTest extends BinaryTest {
CommaTest(Expression lhs, Expression rhs) { super(lhs, rhs); }
@Override public boolean
evaluate(Mapping properties) {
this.lhs.evaluate(properties);
return this.rhs.evaluate(properties);
}
@Override public String
toString() { return "(" + this.lhs + ", " + this.rhs + ")"; }
}
/**
* Iff {@code lhs} evaluates to FALSE, then {@code rhs} is evaluated and its result is returned. Otherwise, TRUE
* is returned.
*/
static
class OrTest extends BinaryTest {
OrTest(Expression lhs, Expression rhs) { super(lhs, rhs); }
@Override public boolean
evaluate(Mapping properties) {
return this.lhs.evaluate(properties) || this.rhs.evaluate(properties);
}
@Override public String
toString() { return "(" + this.lhs + " || " + this.rhs + ")"; }
}
/**
* Iff {@code lhs} evaluates to TRUE, then {@code rhs} is evaluated and its result is returned. Otherwise, FALSE
* is returned.
*/
static
class AndTest extends BinaryTest {
AndTest(Expression lhs, Expression rhs) { super(lhs, rhs); }
@Override public boolean
evaluate(Mapping properties) {
return this.lhs.evaluate(properties) && this.rhs.evaluate(properties);
}
@Override public String
toString() { return "(" + this.lhs + " && " + this.rhs + ")"; }
}
static
class NotExpression extends UnaryTest {
NotExpression(Expression operand) { super(operand); }
@Override public boolean
evaluate(Mapping properties) { return !this.operand.evaluate(properties); }
@Override public String
toString() { return "(not " + this.operand + ")"; }
}
/** @see #evaluate(Mapping) */
private static
class BooleanTest implements Test {
private final String propertyName;
/** @see #evaluate(Mapping) */
BooleanTest(String propertyName) { this.propertyName = propertyName; }
/**
* @return The value of the {@link EqualsTest#EqualsTest(String, Object) named} boolean property of the {@code
* subject}
*/
@Override public boolean
evaluate(Mapping properties) {
Boolean value = Mappings.get(properties, this.propertyName, Boolean.class);
return value != null && value.booleanValue();
}
@Override public final String
toString() { return this.propertyName; }
}
private static
class PredicateTest implements Test {
private final Predicate super T> predicate;
private final Class propertyType;
private final String propertyName;
PredicateTest(String propertyName, Class propertyType, Predicate super T> predicate) {
this.propertyName = propertyName;
this.propertyType = propertyType;
this.predicate = predicate;
}
@Override public boolean
evaluate(Mapping properties) {
T propertyValue = Mappings.get(properties, this.propertyName, this.propertyType);
return propertyValue != null && this.predicate.evaluate(propertyValue);
}
@Override public final String
toString() { return "( " + this.propertyName + " =* '" + this.predicate + "')"; }
}
private static
class StringPredicateTest implements Test {
private final Predicate super String> predicate;
private final String propertyName;
StringPredicateTest(String propertyName, Predicate super String> predicate) {
this.propertyName = propertyName;
this.predicate = predicate;
}
@Override public boolean
evaluate(Mapping properties) {
Object propertyValue = Mappings.get(properties, this.propertyName, Object.class);
return propertyValue != null && this.predicate.evaluate(propertyValue.toString());
}
@Override public final String
toString() { return "( " + this.propertyName + " =* '" + this.predicate + "')"; }
}
private static
class GlobTest extends StringPredicateTest {
GlobTest(String propertyName, String pattern) {
super(propertyName, Glob.compile(pattern, Pattern2.WILDCARD | Glob.INCLUDES_EXCLUDES));
}
}
/** Tests the value of property "name" against a given pattern. */
public static
class NameTest extends GlobTest {
public NameTest(String nameGlob) { super("name", nameGlob); }
}
/** Tests the value of property "path" against a given pattern. */
public static
class PathTest extends GlobTest {
public PathTest(String pathGlob) { super("path", pathGlob); }
}
/** Tests the value of property "type" against a given pattern. */
public static
class TypeTest extends GlobTest {
public TypeTest(String typeGlob) { super("type", typeGlob); }
}
/** Tests the value of the boolean property "canRead". */
public static
class ReadabilityTest extends BooleanTest {
public ReadabilityTest() { super("canRead"); }
}
/** Tests the value of the boolean property "canWrite". */
public static
class WritabilityTest extends BooleanTest {
public WritabilityTest() { super("canWrite"); }
}
/** Tests the value of the boolean property "canExecute". */
public static
class ExecutabilityTest extends BooleanTest {
public ExecutabilityTest() { super("canExecute"); }
}
/** Tests the value of the LONG property "size". */
public static
class SizeTest extends PredicateTest {
public
SizeTest(Predicate super Long> predicate) { super("size", Long.class, predicate); }
}
/**
* Representation of a {@link Find.Test} which checks the node's modification time against the current time and a
* (days-based) predicate.
*
*
* - ==0
- 0...23:59:59.000
*
- ==1
- 24...47:59:59.000
*
- ==2
- 48...71:59:59.000
*
*
*
* - >0
- >= 24h
*
- >1
- >= 48h
*
- >2
- >= 72h
*
*
*
* - <1
- < 24h
*
- <2
- < 48h
*
- -3
- < 72h
*
*/
public static
class ModificationTimeTest extends PredicateTest {
/**
* @see ModificationTimeTest
*/
public static final long DAYS = 24L * 3600L * 1000L;
/**
* @see ModificationTimeTest
*/
public static final long MINUTES = 60L * 1000L;
/**
* Iff factor is {@link #DAYS}:
*
* - Age 0:00:00.000 ... 23:59:59.999:
- Value 0
* - Age 24:00:00.000 ... 47:59:59.999:
- Value 1
* - Age 48:00:00.000 ... 71:59:59.999:
- Value 2
* - etc.
*
*
* Iff factor is {@link #MINUTES}:
*
*
* - Age 0:00:00.000 ... 0:00:59.999:
- Value 0
* - Age 0:01:00.000 ... 00:01:59.999:
- Value 1
* - Age 0:02:00.000 ... 00:02:59.999:
- Value 2
* - etc.
*
*/
public
ModificationTimeTest(final Predicate super Long> predicate, final long factor) {
super("lastModifiedDate", Date.class, new Predicate() {
@Override public boolean
evaluate(Date lastModifiedDate) {
long milliseconds = System.currentTimeMillis() - lastModifiedDate.getTime();
long days = milliseconds / factor;
return predicate.evaluate(days);
}
@Override public String
toString() { return "(" + predicate + " days)"; }
});
}
}
/**
* An {@link Expression} that has side effects, e.g. text being printed.
*/
interface Action extends Expression {
}
/**
* Prints the path of the current file and returns {@code true}.
*/
static
class PrintAction implements Action {
@Override public boolean
evaluate(Mapping properties) {
Printers.info(Mappings.getNonNull(properties, "path", String.class));
return true;
}
@Override public String
toString() { return "(print)"; }
}
/**
* Prints the path of the current file and returns {@code true}.
*/
static
class EchoAction implements Action {
private final String message;
EchoAction(String message) {
this.message = message;
}
@Override public boolean
evaluate(Mapping properties) {
String message = Find.expandVariables(this.message, properties);
Printers.info(message);
return true;
}
@Override public String
toString() { return "(echo '" + this.message + "')"; }
}
/**
* Prints the file type ('d' or '-'), readability ('r' or '-'), writability ('w' or '-'), size, modification time
* and path to the given {@link Writer} and evaluates to {@code true}.
*/
static
class LsAction implements Action {
@Override public boolean
evaluate(Mapping properties) {
Printers.info(String.format(
"%c%c%c%c %10d %tF % command;
ExecAction(List command) { this.command = command; }
@Override public boolean
evaluate(Mapping properties) {
List command2;
{
String path = null;
command2 = new ArrayList();
for (String word : this.command) {
if (word.contains("{}")) {
if (path == null) path = Mappings.getNonNull(properties, "path", String.class);
word = word.replace("{}", path);
}
command2.add(Find.expandVariables(word, properties));
}
}
try {
return ProcessUtil.execute(
command2, // command
null, // workingDirectory
System.in, // stdin
false, // closeStdin
System.out, // stdout
false, // closeStdout
System.err, // stderr
false // closeStderr
);
} catch (Exception e) {
throw ExceptionUtil.wrap("Executing '" + command2 + "'", e, RuntimeException.class);
}
}
@Override public String
toString() { return "(exec '" + this.command + "')"; }
}
/**
* Copies the contents of the current file to a given {@link OutputStream} and evaluates to {@code true}.
*/
static
class CatAction implements Action {
private final OutputStream out;
CatAction(OutputStream out) { this.out = out; }
@Override public boolean
evaluate(Mapping properties) {
try {
InputStream is = Mappings.getNonNull(properties, "inputStream", InputStream.class);
IoUtil.copy(is, this.out);
} catch (IOException ioe) {
throw ExceptionUtil.wrap("Running '-cat' on '" + properties + "'", ioe, RuntimeException.class);
}
return true;
}
@Override public String
toString() { return "(cat " + this.out + ")"; }
}
/**
* Copies the contents of the current file to a given file and evaluates to TRUE.
*/
static
class CopyAction implements Action {
private final File tofile;
private final boolean mkdirs;
CopyAction(File tofile, boolean mkdirs) {
this.tofile = tofile;
this.mkdirs = mkdirs;
}
@Override public boolean
evaluate(Mapping properties) {
try {
File tofile = new File(Find.expandVariables(this.tofile.getPath(), properties));
if (this.mkdirs) IoUtil.createMissingParentDirectoriesFor(tofile);
InputStream in = Mappings.getNonNull(properties, "inputStream", InputStream.class);
OutputStream out = new FileOutputStream(tofile);
try {
IoUtil.copy(in, out);
out.close();
} finally {
try { out.close(); } catch (IOException e) {}
}
} catch (IOException ioe) {
throw ExceptionUtil.wrap("Running 'copy' on '" + properties + "'", ioe, RuntimeException.class);
}
return true;
}
@Override public String
toString() { return "(copy to '" + this.tofile + "')"; }
}
/**
* Copies the contents of the current file to the STDIN of a given command and returns whether the command exited
* with status 0.
*/
static
class PipeAction implements Action {
private final List command;
@Nullable private final File workingDirectory;
PipeAction(List command, @Nullable File workingDirectory) {
this.command = command;
this.workingDirectory = workingDirectory;
}
@Override public boolean
evaluate(Mapping properties) {
final InputStream in = Mappings.getNonNull(properties, "inputStream", InputStream.class);
List command2 = new ArrayList();
for (String word : this.command) {
command2.add(Find.expandVariables(word, properties));
}
try {
return ProcessUtil.execute(
command2, // command
this.workingDirectory, // workingDirectory
in, // stdin
false, // closeStdin
System.out, // stdout
false, // closeStdout
System.err, // stderr
false // closeStderr
);
} catch (Exception e) {
throw ExceptionUtil.wrap("Running 'pipe' on '" + properties + "'", e, RuntimeException.class);
}
}
@Override public String
toString() { return "(pipe contents to command " + this.command + ")"; }
}
/**
* Disassembles a Java class file
*/
static
class DisassembleAction implements Action {
private final boolean hideLines;
private final boolean hideVars;
@Nullable private final File toFile;
DisassembleAction(boolean hideLines, boolean hideVars, @Nullable File toFile) {
this.hideLines = hideLines;
this.hideVars = hideVars;
this.toFile = toFile;
}
@Override public boolean
evaluate(Mapping properties) {
final Disassembler disassembler = new Disassembler();
disassembler.setHideLines(this.hideLines);
disassembler.setHideVars(this.hideVars);
final InputStream in = Mappings.getNonNull(properties, "inputStream", InputStream.class);
File toFile = this.toFile;
try {
if (toFile == null) {
disassembler.disasm(in);
} else {
IoUtil.outputFileOutputStream(
toFile, // file
new ConsumerWhichThrows() { // delegate
@Override public void
consume(OutputStream os) throws IOException {
disassembler.setOut(os);
disassembler.disasm(in);
}
},
true // createMissingParentDirectories
);
}
} catch (IOException ioe) {
return false;
}
return true;
}
@Override public String
toString() { return "(Disassemble .class file)"; }
}
/**
* Calculates a "message digest" of an input stream's content and prints it to {@link Printers#info(String)}.
*/
static
class DigestAction implements Action {
private final String algorithm;
DigestAction(String algorithm) {
this.algorithm = algorithm;
}
@Override public boolean
evaluate(Mapping properties) {
MessageDigest md;
try {
md = MessageDigest.getInstance(this.algorithm);
} catch (NoSuchAlgorithmException nsae) {
throw ExceptionUtil.wrap(
"Running '-digest' on '" + properties + "'",
nsae,
IllegalArgumentException.class
);
}
InputStream is = Mappings.getNonNull(properties, "inputStream", InputStream.class);
try {
DigestAction.updateAll(md, is);
} catch (IOException ioe) {
throw ExceptionUtil.wrap("Running '-digest' on '" + properties + "'", ioe, RuntimeException.class);
}
byte[] digest = md.digest();
Formatter f = new Formatter();
for (byte b : digest) {
f.format("%02x", b & 0xff);
}
Printers.info(f.toString());
return true;
}
/**
* Updates the messageDigest from the remaining content of the inputStream.
*/
private static void
updateAll(MessageDigest messageDigest, InputStream inputStream) throws IOException {
byte[] buffer = new byte[8192];
for (;;) {
int n = inputStream.read(buffer);
if (n == -1) return;
messageDigest.update(buffer, 0, n);
}
}
@Override public String
toString() { return "(digest " + this.algorithm + ")"; }
}
/**
* Calculates a "checksum" of an input stream's content and prints it to {@link Printers#info(String)}.
*/
static
class ChecksumAction implements Action {
enum ChecksumType {
/**
* @see java.util.zip.CRC32
*/
CRC32 { @Override Checksum newChecksum() { return new java.util.zip.CRC32(); } },
/**
* @see java.util.zip.Adler32
*/
ADLER32 { @Override Checksum newChecksum() { return new java.util.zip.Adler32(); } },
;
/**
* @return A new {@link Checksum} of this type
*/
abstract Checksum newChecksum();
}
private final ChecksumType checksumType;
ChecksumAction(ChecksumType checksumType) {
this.checksumType = checksumType;
}
@Override public boolean
evaluate(Mapping properties) {
Checksum cs = this.checksumType.newChecksum();
InputStream is = Mappings.getNonNull(properties, "inputStream", InputStream.class);
try {
ChecksumAction.updateAll(cs, is);
} catch (IOException ioe) {
throw ExceptionUtil.wrap("Running '-checksum' on '" + properties + "'", ioe, RuntimeException.class);
}
Printers.info(Long.toHexString(cs.getValue()));
return true;
}
/**
* Updates the checksum from the remaining content of the inputStream.
*/
private static void
updateAll(Checksum checksum, InputStream inputStream) throws IOException {
byte[] buffer = new byte[8192];
for (;;) {
int n = inputStream.read(buffer);
if (n == -1) return;
checksum.update(buffer, 0, n);
}
}
@Override public String
toString() { return "(checksum " + this.checksumType + ")"; }
}
/**
* Replaces all occurrences of "@variableName
" or
* "@{variable-name}
" in {@code s} with the value to which variables maps the
* variable-name
, or with "" iff the named variable is not mapped.
*
* Notice that in the first notation the variableName
must follow the rules of a Java
* identifier, while in the second notation variable-name
can contain any any
* character except "}
".
*
*
* "@
" characters are left untouched under any of the following conditions:
*
*
* - It is the last character of the subject string
* - The closing "
}
" for a "@{
" is missing
* - It is followed neither by "
{
" nor Java-identifier-start-letter
*
*
* @param s The subject string
* @param variables The {@link Mapping} that is used for variable expansion
* @return The subject string with the variables expanded
*/
public static String
expandVariables(String s, Mapping variables) {
for (int idx = s.indexOf('@'); idx != -1; idx = s.indexOf('@', idx)) {
if (idx == s.length() - 1) {
// The '@' is the LAST character of the string; terminate.
break;
}
int from = idx, to; // The region to replace.
String variableName;
char c = s.charAt(from + 1);
if (c == '{') {
to = s.indexOf('}', from + 2);
if (to == -1) {
// Closing '} missing: Terminate.
break;
}
variableName = s.substring(from + 2, to++);
} else
if (Character.isJavaIdentifierStart(c)) {
to = from + 2;
for (; to < s.length() && Character.isJavaIdentifierPart(s.charAt(to)); to++);
variableName = s.substring(from + 1, to);
} else
{
// '@' is followed neither by a JavaIdentifierStart letter nor by '{'; leave it as a literal '@'.
idx++;
continue;
}
Object value = variables.get(variableName);
String replacement = value == null ? "" : value.toString();
// Substitute the match with the replacement string.
s = s.substring(0, from) + replacement + s.substring(to);
// Continue the search BEHIND the replacement.
idx += replacement.length();
}
return s;
}
/**
* Executes the search in STDIN, with path "-".
*
* This method is thread-safe.
*
*/
public void
findInStream(InputStream is) throws IOException {
if (this.maxDepth < 0) return;
this.findInStream("-", System.in, Mappings.mapping(
"isDirectory", false,
"isExecutable", false,
"isReadable", true,
"isWritable", false,
"lastModifiedDate", new Date(),
"path", "-",
"size", -1L
), 0);
}
/**
* Executes the search in the file (which may be a normal file or a directory).
*
* This method is thread-safe.
*
*/
public void
findInFile(File file) throws IOException {
this.findInDirectoryTree(file.getPath(), file, 0);
}
private void
findInDirectoryTree(final String path, File fileOrDirectory, int depth)
throws IOException {
if (fileOrDirectory.isDirectory()) {
this.findInDirectory(path, fileOrDirectory, depth);
} else {
this.findInFile(path, fileOrDirectory, depth);
}
}
private void
findInDirectory(final String directoryPath, final File directory, final int depth)
throws IOException {
Find.LOGGER.log(
Level.FINER,
"Processing directory \"{0}\" (path is \"{1}\")",
new Object[] { directory, directoryPath }
);
RunnableUtil.swapIf(
this.depth,
new RunnableWhichThrows() {
@Override public void
run() {
// Evaluate the FIND expression for the directory.
Find.this.evaluateExpression(Mappings.augment(
Find.fileProperties(directoryPath, directory),
"type", "directory",
"depth", depth
));
}
},
new RunnableWhichThrows() {
@Override public void
run() throws IOException {
// Process the directory's members.
if (depth < Find.this.maxDepth) {
for (String memberName : directory.list()) {
try {
Find.this.findInDirectoryTree(
directoryPath + File.separatorChar + memberName,
new File(directory, memberName),
depth + 1
);
} catch (IOException ioe) {
Find.this.exceptionHandler.consume(ioe);
}
}
}
}
}
);
}
/**
* Returns a mapping of all relevant properties of the given {@code file}.
*
* - {@code "absolutePath"}:
- {@code String}
* - {@code "canonicalPath"}:
- {@code String}
* - {@code "lastModifiedDate"}:
- {@link Date}
* - {@code "name"}:
- {@code String}
* - {@code "path"}:
- {@code String}
* - {@code "size"}:
- {@code long}
* - {@code "isDirectory"}:
- {@code boolean}
* - {@code "isFile"}:
- {@code boolean}
* - {@code "isHidden"}:
- {@code boolean}
* - {@code "isReadable"}:
- {@code boolean}
* - {@code "isWritable"}:
- {@code boolean}
* - {@code "isExecutable"}:
- {@code boolean}
*
*/
@SuppressWarnings("unused") public static Mapping
fileProperties(final String path, final File file) {
return Mappings.propertiesOf(new Object() {
public String getAbsolutePath() { return file.getAbsolutePath(); }
public String getCanonicalPath() throws IOException { return file.getCanonicalPath(); }
public Date getLastModifiedDate() { return new Date(file.lastModified()); }
public String getName() { return file.getName(); }
public String getPath() { return path; }
public long getSize() { return file.length(); }
public boolean isDirectory() { return file.isDirectory(); }
public boolean isFile() { return file.isFile(); }
public boolean isHidden() { return file.isHidden(); }
public boolean isReadable() { return file.canRead(); }
public boolean isWritable() { return file.canWrite(); }
public boolean isExecutable() { return file.canExecute(); }
@Override public String toString() { return "File \"" + path + "\""; }
});
}
private void
findInFile(final String path, final File file, final int depth)
throws IOException {
Find.LOGGER.log(Level.FINER, "Processing file \"{0}\" (path is \"{1}\")", new Object[] { file, path });
CompressUtil.processFile(
path, // file
file, // file
this.lookIntoFormat, // lookIntoFormat
new ArchiveHandler() { // archiveHandler
@Override @Nullable public Void
handleArchive(final ArchiveInputStream archiveInputStream, final ArchiveFormat archiveFormat)
throws IOException {
RunnableUtil.swapIf(
Find.this.depth,
new RunnableWhichThrows() {
@Override public void
run() {
// Evaluate the FIND expression for the archive file.
Find.this.evaluateExpression(Mappings.augment(
Find.fileProperties(path, file),
"type", "archive-file",
"archiveFormat", archiveFormat,
"depth", depth
));
}
},
new RunnableWhichThrows() {
@Override public void
run() throws IOException {
// Process the archive's entries.
if (depth < Find.this.maxDepth) {
for (;;) {
final ArchiveEntry ae = archiveInputStream.getNextEntry();
if (ae == null) break;
String entryPath = (
path
+ '!'
+ ArchiveFormatFactory.normalizeEntryName(ae.getName())
);
if (ae.isDirectory()) {
// Evaluate the FIND expression for the directory entry.
Find.this.evaluateExpression(Mappings.override(
Mappings.union(
Mappings.propertiesOf(ae),
Find.fileProperties(path, file)
),
"path", entryPath,
"name", ArchiveFormatFactory.normalizeEntryName(ae.getName()),
"archiveFormat", archiveFormat,
"type", "directory-entry",
"depth", depth + 1
));
} else {
try {
Find.this.findInStream(
entryPath,
archiveInputStream,
Mappings.override(
Mappings.union(
Mappings.propertiesOf(ae),
Find.fileProperties(path, file)
),
"archiveFormat", archiveFormat
),
depth + 1
);
} catch (IOException ioe) {
Find.this.exceptionHandler.consume(ioe);
}
}
}
}
}
}
);
return null;
}
},
new CompressorHandler() { // compressorHandler
@Override @Nullable public Void
handleCompressor(
final CompressorInputStream compressorInputStream,
final CompressionFormat compressionFormat
) throws IOException {
RunnableUtil.swapIf(
Find.this.depth,
new RunnableWhichThrows() {
@Override public void
run() {
// Evaluate the FIND expression for the compressed file.
// Notice that we don't define an "inputStream" property, because otherwise we couldn't
// process the CONTENTS of the compressed file.
Find.this.evaluateExpression(Mappings.augment(
Find.fileProperties(path, file),
"type", "compressed-file",
"compressionFormat", compressionFormat,
"depth", depth
));
}
},
new RunnableWhichThrows() {
@Override public void
run() throws IOException {
// Process compressed file's contents.
if (depth < Find.this.maxDepth) {
Find.this.findInStream(path + '%', compressorInputStream, Mappings.override(
Find.fileProperties(path, file),
"compressionFormat", compressionFormat,
"name", file.getName() + '%',
"size", -1L
), depth + 1);
}
}
}
);
return null;
}
},
new NormalContentsHandler() { // normalContentsHandler
@Override @Nullable public Void
handleNormalContents(final InputStream inputStream) {
// Evaluate the FIND expression for the normal file.
Find.this.evaluateExpression(Mappings.augment(
Find.fileProperties(path, file),
"type", "file",
"inputStream", inputStream,
"depth", depth
));
return null;
}
}
);
}
private void
findInStream(
final String path,
InputStream inputStream,
final Mapping streamProperties,
final int depth
) throws IOException {
try {
CompressUtil.processStream(
path, // path
inputStream, // inputStream
Find.this.lookIntoFormat, // lookIntoArchive
new ArchiveHandler() { // archiveHandler
@Override @Nullable public Void
handleArchive(final ArchiveInputStream archiveInputStream, final ArchiveFormat archiveFormat)
throws IOException {
RunnableUtil.swapIf(
Find.this.depth,
new RunnableWhichThrows() {
@Override public void
run() {
// Evaluate the FIND expression for the nested archive.
Find.this.evaluateExpression(Mappings.override(
streamProperties,
"path", path,
"type", "archive",
"archiveFormat", archiveFormat,
"depth", depth
));
}
},
new RunnableWhichThrows() {
@Override public void
run() throws IOException {
// Process the nested archive's entries.
if (depth < Find.this.maxDepth) {
for (
ArchiveEntry ae = archiveInputStream.getNextEntry();
ae != null;
ae = archiveInputStream.getNextEntry()
) {
String entryName = ArchiveFormatFactory.normalizeEntryName(ae.getName());
final String entryPath = path + '!' + entryName;
if (ae.isDirectory()) {
// Evaluate the FIND expression for the "directory entry".
Find.this.evaluateExpression(Mappings.override(
Mappings.union(Mappings.propertiesOf(ae), streamProperties),
"archiveFormat", archiveFormat,
"path", entryPath,
"name", entryName,
"type", "directory-entry",
"depth", depth + 1
));
} else {
// Process the contents of the nested archive's entry.
try {
Find.this.findInStream(
entryPath,
archiveInputStream,
Mappings.override(
Mappings.union(Mappings.propertiesOf(ae), streamProperties),
"archiveFormat", archiveFormat
),
depth + 1
);
} catch (IOException ioe) {
Find.this.exceptionHandler.consume(ioe);
}
}
}
}
}
}
);
return null;
}
},
new CompressorHandler() { // compressorHandler
@Override @Nullable public Void
handleCompressor(
final CompressorInputStream compressorInputStream,
final CompressionFormat compressionFormat
) throws IOException {
RunnableUtil.swapIf(
Find.this.depth,
new RunnableWhichThrows() {
@Override public void
run() {
// Evaluate the FIND expression for the compressed entry.
// Notice that we don't define an "inputStream" property, because otherwise we
// couldn't process the CONTENTS of the compressed entry.
Find.this.evaluateExpression(Mappings.override(
streamProperties,
"type", "compressed-contents",
"path", path,
"compressionFormat", compressionFormat,
"depth", depth
));
}
},
new RunnableWhichThrows() {
@Override public void
run() throws IOException {
// Process the compressed entry's contents.
if (depth < Find.this.maxDepth) {
String name = (String) streamProperties.get("name");
assert name != null;
Find.this.findInStream(
path + '%',
compressorInputStream,
Mappings.override(
streamProperties,
"compressionFormat", compressionFormat,
"name", name + '%',
"size", -1L
),
depth + 1
);
}
}
}
);
return null;
}
},
new NormalContentsHandler() { // normalContentsHandler
@Override @Nullable public Void
handleNormalContents(final InputStream inputStream) {
// Evaluate the FIND expression for the nested normal contents.
Find.this.evaluateExpression(Mappings.override(
streamProperties,
"path", path,
"type", "normal-contents",
"inputStream", inputStream,
"depth", depth,
"size", new Producer() {
@Override @Nullable public Long
produce() {
// Check if the "size" property inherited from the ArchiveEntry has a reasonable
// value (ZipArchiveEntries have size -1 iff the archive was created in "streaming
// mode").
Long size = (Long) streamProperties.get("size");
assert size != null;
if (size != -1) return size;
// Compute the value of the "size" property only IF it is needed, and WHEN it is
// needed, because it consumes the contents.
try {
return IoUtil.skipAll(inputStream);
} catch (IOException ioe) {
throw ExceptionUtil.wrap(
"Measuring size of \"" + path + "\"",
ioe,
RuntimeException.class
);
}
}
}
));
return null;
}
}
);
} catch (IOException ioe) {
throw ExceptionUtil.wrap(path, ioe);
} catch (RuntimeException re) {
throw ExceptionUtil.wrap(path, re);
}
}
private void
evaluateExpression(Mapping properties) {
if (this.minDepth > 0) {
Object depthValue = properties.get("depth");
assert depthValue instanceof Integer;
int depth = (Integer) depthValue;
if (depth < this.minDepth) return;
}
this.expression.evaluate(properties);
}
}