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

de.unkrig.zz.find.Find Maven / Gradle / Ivy

There is a newer version: 1.3.10
Show newest version

/*
 * 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 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 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 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 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 predicate; private final Class propertyType; private final String propertyName; PredicateTest(String propertyName, Class propertyType, Predicate 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 predicate; private final String propertyName; StringPredicateTest(String propertyName, Predicate 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 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 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); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy