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

de.unkrig.zz.find.AntTask 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.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectComponent;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.MacroDef;
import org.apache.tools.ant.taskdefs.MacroDef.NestedSequential;
import org.apache.tools.ant.taskdefs.MacroInstance;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.FileProvider;
import org.apache.tools.ant.types.resources.FileResource;

import de.unkrig.commons.lang.protocol.Consumer;
import de.unkrig.commons.lang.protocol.ConsumerWhichThrows;
import de.unkrig.commons.lang.protocol.Mapping;
import de.unkrig.commons.lang.protocol.Predicate;
import de.unkrig.commons.lang.protocol.RunnableWhichThrows;
import de.unkrig.commons.nullanalysis.Nullable;
import de.unkrig.commons.text.AbstractPrinter;
import de.unkrig.commons.text.Printer;
import de.unkrig.commons.text.Printers;
import de.unkrig.commons.text.ProxyPrinter;
import de.unkrig.commons.text.pattern.Glob;
import de.unkrig.commons.text.pattern.Pattern2;
import de.unkrig.zz.find.Find.Action;
import de.unkrig.zz.find.Find.AndTest;
import de.unkrig.zz.find.Find.CatAction;
import de.unkrig.zz.find.Find.ChecksumAction;
import de.unkrig.zz.find.Find.ChecksumAction.ChecksumType;
import de.unkrig.zz.find.Find.CommaTest;
import de.unkrig.zz.find.Find.CopyAction;
import de.unkrig.zz.find.Find.DigestAction;
import de.unkrig.zz.find.Find.DisassembleAction;
import de.unkrig.zz.find.Find.EchoAction;
import de.unkrig.zz.find.Find.ExecAction;
import de.unkrig.zz.find.Find.ExecutabilityTest;
import de.unkrig.zz.find.Find.Expression;
import de.unkrig.zz.find.Find.LsAction;
import de.unkrig.zz.find.Find.ModificationTimeTest;
import de.unkrig.zz.find.Find.NameTest;
import de.unkrig.zz.find.Find.NotExpression;
import de.unkrig.zz.find.Find.OrTest;
import de.unkrig.zz.find.Find.PathTest;
import de.unkrig.zz.find.Find.PipeAction;
import de.unkrig.zz.find.Find.PrintAction;
import de.unkrig.zz.find.Find.ReadabilityTest;
import de.unkrig.zz.find.Find.SizeTest;
import de.unkrig.zz.find.Find.Test;
import de.unkrig.zz.find.Find.TypeTest;
import de.unkrig.zz.find.Find.WritabilityTest;

/**
 * Recurses through a set of directories, files, archive files and nested archives and executes a set of tests and
 * actions for each file and archive entry.
 * 

* The execution of tests and actions stops when one of them evaluates to {@code false}, i.e. they are implicitly * AND-related. *

* * @ant.subelementOrder inheritedFirst */ public class AntTask extends AbstractElementWithOperands { private final Find find = new Find(); private final AndElement root = new AndElement(); @Nullable private File outputFile; private final List resourceCollections = new ArrayList(); /** * @ant.typeGroupSubdir findExpressions * @ant.typeGroupName FIND expression * @ant.typeGroupHeading FIND expressions * @ant.typeTitleMf <{0}> * @ant.typeHeadingMf <{0}> */ public interface ExpressionElement { /** * Produces a FIND {@link Expression}. */ Expression toExpression(); } /** * Copies the contents of the current file to STDOUT and evaluates to TRUE. */ public static class CatElement implements ExpressionElement { @Override public Expression toExpression() { return new CatAction(System.out); } } /** * Calculates a "checksum" of the contents, prints it and returns true. */ public static class ChecksumElement implements ExpressionElement { private final Project project; private ChecksumType type = ChecksumAction.ChecksumType.CRC32; @Nullable private String propertyName; public ChecksumElement(Project project) { this.project = project; } /** * The checksum type to use * * @ant.defaultValue CRC32 */ public void setType(ChecksumAction.ChecksumType type) { this.type = type; } /** * The property to set * * @ant.defaultValue Print the checksum to STDOUT, instead of setting a property */ public void setProperty(String propertyName) { this.propertyName = propertyName; } @Override public Expression toExpression() { return AntTask.redirectInfoToProperty( this.project, new ChecksumAction(this.type), this.propertyName ); } } /** * Copies the contents of the current file to the named file and evaluates to TRUE. */ public static class CopyElement implements ExpressionElement { @Nullable private File tofile; private boolean mkdirs; /** * The file to copy to. * * @ant.mandatory */ public void setTofile(File value) { this.tofile = value; } /** * Whether to create any missing parent directories for the output file. */ public void setMkdirs(boolean value) { this.mkdirs = value; } @Override public Expression toExpression() { File tofile = this.tofile; if (tofile == null) throw new BuildException("Attribute 'tofile=\"\"' not set"); return new CopyAction(tofile, this.mkdirs); } } /** * Calculates a "message digest" of the contents, prints it and returns true. */ public static class DigestElement implements ExpressionElement { private final Project project; private String algorithm = "MD5"; @Nullable private String propertyName; public DigestElement(Project project) { this.project = project; } /** * The algorithm to use. * * @ant.defaultValue "MD5" */ public void setAlgorithm(String algorithm) { this.algorithm = algorithm; } /** * The property to set. * * @ant.defaultValue Print the checksum to STDOUT, instead of setting a property */ public void setProperty(String propertyName) { this.propertyName = propertyName; } @Override public Expression toExpression() { return AntTask.redirectInfoToProperty(this.project, new DigestAction(this.algorithm), this.propertyName); } } /** * Disassembles a Java .class file to STDOUT or a given file and evaluates to {@code true}. */ public static class DisassembleElement implements ExpressionElement { private boolean hideLines; private boolean hideVars; @Nullable private File toFile; /** * Whether to suppress line numbers in the disassembly output. */ public void setHidesLines(boolean hideLines) { this.hideLines = hideLines; } /** * Whether to suppress local variable names in the disassembly output. */ public void setHidesVars(boolean hideLines) { this.hideLines = hideLines; } /** * The file to redirect the disassembly output to. * * @ant.defaultValue Standard output */ public void setToFile(File toFile) { this.toFile = toFile; } @Override public Expression toExpression() { return new DisassembleAction(this.hideLines, this.hideVars, this.toFile); } } /** * Executes an external command; the special string '{}' within the command is replaced with the full path of the * current file/directory/archive entry. *

* Evaluates to {@code true} iff the command exists with status code '0'. *

*/ public static class ExecElement implements ExpressionElement { @Nullable private String command; /** * The command to execute; program name and command line arguments separated by whitespace. * * @ant.mandatory */ public void setCommand(String command) { this.command = command; } @Override public Expression toExpression() { String command = this.command; if (command == null) throw new BuildException("Attribute 'command' must be set"); return new ExecAction(Arrays.asList(command.split("\\s+"))); } } /** * Prints the file type ("{@code d}" or "{@code -}"), readability ("{@code r}" or "{@code -}"), writability ("{@code * w}" or "{@code -}"), executability ("{@code x}" or "{@code -}"), size, modification time and path, and evaluates * to {@code true}. */ public static class LsElement implements ExpressionElement { @Override public Expression toExpression() { return new LsAction(); } } /** * Prints the path of the current file/directory/archive entry and evaluates to {@code true}. */ public static class PrintElement implements ExpressionElement { @Override public Expression toExpression() { return new PrintAction(); } } /** * Prints a text and evaluates to {@code true}. *

* All occurrences of "{@code @{variable-name}}" in the text are replaced with the value of the named variable. * For the list of supported variables, see *here. *

*/ public static class EchoElement implements ExpressionElement { @Nullable private String message; /** * The text to print. */ public void setMessage(String text) { this.message = text; } @Override public Expression toExpression() { String message = this.message; if (message == null) throw new BuildException("Attribute 'message' must be set"); return new EchoAction(message); } } /** * Copies the contents of the current file/archive entry to the STDIN of a command and returns whether the command * exited with status 0. */ public static class PipeElement implements ExpressionElement { @Nullable private String command; /** * The command to execute; program name and command line arguments separated by whitespace. * * @ant.mandatory */ public void setCommand(String command) { this.command = command; } @Override public Expression toExpression() { String command = this.command; if (command == null) throw new BuildException("Attribute 'command' must be set"); return new PipeAction(Arrays.asList(command.split("\\s+")), null); } } /** * Sets an ANT property. */ public static class PropertyElement extends ProjectComponent implements ExpressionElement { @Nullable private String propertyName; @Nullable private String propertyValue; /** * The name of the property to set. * * @ant.mandatory */ public void setName(String propertyName) { this.propertyName = propertyName; } /** * The text to store in the property. */ public void setValue(String text) { this.propertyValue = text; } @Override public Expression toExpression() { String propertyName = this.propertyName; if (propertyName == null) throw new BuildException("Attribute 'propertyName' must be set"); String propertyValue = this.propertyValue; if (propertyValue == null) throw new BuildException("Attribute 'propertyValue' must be set"); return new PropertyAction(this.getProject(), propertyName, propertyValue); } } /** * Sets a particular property. */ static class PropertyAction implements Action { private final Project project; private final String propertyName; private final String propertyValue; PropertyAction(Project project, String propertyName, String propertyValue) { this.project = project; this.propertyName = propertyName; this.propertyValue = propertyValue; } @Override public boolean evaluate(Mapping properties) { String pn = Find.expandVariables(this.propertyName, properties); String pv = Find.expandVariables(this.propertyValue, properties); this.project.setProperty(pn, pv); return true; } @Override public String toString() { return "(set property \"" + this.propertyName + "\" to \"" + this.propertyValue + "\")"; } } /** * Combines subexpressions logically: Evaluates its subexpressions sequentially; if one of them evaluates to {@code * false}, then the remaining subexpressions are not evaluated, and {@code false} is returned. */ public static final class AndElement extends AbstractElementWithOperands implements ExpressionElement { private Expression predicate = Test.TRUE; @Override public void addConfigured(ExpressionElement operand) { this.predicate = new AndTest(this.predicate, operand.toExpression()); } @Override public Expression toExpression() { return this.predicate; } } /** * Combines subexpressions logically: Evaluates its subexpressions sequentially; if one of them evaluates to {@code * true}, then the remaining subexpressions are not evaluated, and {@code true} is returned. */ public static final class OrElement extends AbstractElementWithOperands implements ExpressionElement { private Expression predicate = Test.FALSE; @Override public void addConfigured(ExpressionElement operand) { this.predicate = new OrTest(this.predicate, operand.toExpression()); } @Override public Expression toExpression() { return this.predicate; } } /** * Evaluates subexpressions sequentially; returns the value of the last subexpression. */ public static final class CommaElement extends AbstractElementWithOperands implements ExpressionElement { private Expression predicate = Test.FALSE; @Override public void addConfigured(ExpressionElement operand) { this.predicate = new CommaTest(this.predicate, operand.toExpression()); } @Override public Expression toExpression() { return this.predicate; } } /** * Evaluates to {@code true} iff the name of the current file/directory/archive entry matches the given * glob. */ public static final class NameElement implements ExpressionElement { @Nullable private String value; /** * The glob to compare against. * * @ant.mandatory */ public void setValue(String glob) { this.value = glob; } @Override public Expression toExpression() { String value = this.value; if (value == null) throw new BuildException("Attribute 'value' must be set"); return new NameTest(value); } } /** * Evaluates to {@code true} iff the path of the current file/directory/archive entry matches the given * glob. *

* The underscore in the name is there to resolve the name collision with ANT's {@code } type, which, * because it is a valid {@linkplain #addConfigured(ResourceCollection) resource collection}, is also a valid * subelement of {@link AntTask}. *

*/ public static final class PathElement implements ExpressionElement { @Nullable private String value; /** * The glob to compare against. * * @ant.mandatory */ public void setValue(String glob) { this.value = glob; } @Override public Expression toExpression() { String value = this.value; if (value == null) throw new BuildException("Attribute 'value' must be set"); return new PathTest(value); } } /** * Evaluates to whether the type of the current file/directory/archive entry matches. *

* Actual types are: *

*
*
{@code directory}
*
A directory
*
{@code file}
*
A (non-archive, not-compressed) file
*
{@code archive-file}
*
An archive file
*
{@code compressed-file}
*
A compressed file
*
{@code archive}
*
A nested archive
*
{@code normal-contents}
*
Normal (non-archive, not-compressed) content
*
{@code directory-entry}
*
A 'directory entry' in an archive.
*
*/ public static final class TypeElement implements ExpressionElement { @Nullable private String value; /** * The glob to compare the type against. */ public void setValue(String glob) { this.value = glob; } @Override public Expression toExpression() { String value = this.value; if (value == null) throw new BuildException("Attribute 'value' must be set"); return new TypeTest(value); } } /** * Negates its subelement expression. */ public static class NotElement extends AbstractElementWithOperands implements ExpressionElement { @Nullable private Expression operand; /** * The expression to negate. * * @ant.mandatory */ @Override public void addConfigured(ExpressionElement operand) { if (this.operand != null) throw new IllegalArgumentException("No more than one subelement allowed"); this.operand = operand.toExpression(); } @Override public Expression toExpression() { Expression operand = this.operand; if (operand == null) throw new BuildException("One 'expressionElement' subelement must exist"); return new NotExpression(operand); } } /** * Evaluates to {@code true} iff the current file/directory is readable. */ public static class ReadableElement implements ExpressionElement { @Override public Expression toExpression() { return new ReadabilityTest(); } } /** * Evaluates to {@code true} iff the current file/directory is writable. */ public static class WritableElement implements ExpressionElement { @Override public Expression toExpression() { return new WritabilityTest(); } } /** * Evaluates to {@code true} iff the current file/directory is executable. */ public static class ExecutableElement implements ExpressionElement { @Override public Expression toExpression() { return new ExecutabilityTest(); } } /** * Checks the size of the current file or archive entry. */ public static class SizeElement implements ExpressionElement { @Nullable private Predicate predicate; /** * Specifies that the size of the current file is exactly (greater than, less than) N. * N is an integer, optionally followed by "{@code k}" (* 1000), "{@code M}" (* 1000000) or "{@code * G}" (* 1000000000). * * @ant.valueExplanation N|+N|-N * @ant.mandatory */ public void setValue(String value) { this.predicate = Parser.parseNumericArgument(value); } @Override public Expression toExpression() { Predicate p = this.predicate; if (p == null) throw new IllegalArgumentException("\"value\" attribute missing"); return new SizeTest(p); } } /** * Checks the modification time of the current file or archive entry. */ public static class ModificationTimeElement implements ExpressionElement { @Nullable private Predicate predicate; private long factor; /** * @deprecated Use {@link #setDays(String)} instead */ @Deprecated public void setValue(String value) { this.setDays(value); } /** * Specifies that the file or archive entry was last modified exactly (more than, less than) N * days ago. * "N days ago" means "between N*24h and N*24h+23:59:59.999h ago". * * @ant.valueExplanation N|+N|-N */ public void setDays(String value) { if (this.predicate != null) { throw new BuildException("\"days=...\" and \"-minutes=...\" are mutually exclusive"); } this.predicate = Parser.parseNumericArgument(value); this.factor = ModificationTimeTest.DAYS; } /** * Specifies that the file or archive entry was last modified exactly (more than, less than) N * minutes ago. * "N minutes ago" means "between N minutes and N minutes plus 59.999 seconds * ago". * * @ant.valueExplanation N|+N|-N */ public void setMinutes(String value) { if (this.predicate != null) { throw new BuildException("\"days=...\" and \"-minutes=...\" are mutually exclusive"); } this.predicate = Parser.parseNumericArgument(value); this.factor = ModificationTimeTest.MINUTES; } @Override public Expression toExpression() { Predicate p = this.predicate; if (p == null) { throw new IllegalArgumentException( "Exactly one of \"days=...\" and \"-minutes=...\" must be configured" ); } return new ModificationTimeTest(p, this.factor); } } /** * Evaluates to {@code true}. */ public static class TrueElement implements ExpressionElement { @Override public Expression toExpression() { return Test.TRUE; } } /** * Evaluates to {@code false}. */ public static class FalseElement implements ExpressionElement { @Override public Expression toExpression() { return Test.FALSE; } } // BEGIN CONFIGURATION SETTERS /** * Archive files, nested archives, compressed files and nested compressed content are introspected iff the * archive/compression format and the file's (resp. nested archive entry's) path match the given globs. *

* The default is to look into any recognized archive and comressed content. *

* * @ant.valueExplanation format-glob:path-glob */ public void setLookInto(String value) { this.find.setLookIntoFormat(Glob.compile(value, Pattern2.WILDCARD | Glob.INCLUDES_EXCLUDES)); } /** * 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.find.setDepth(value); } /** * Do not apply any tests or actions at levels less than levels. For example, "1" means "process all * files except the top level files". * * @ant.defaultValue 0 */ public void setMinDepth(int levels) { this.find.setMinDepth(levels); } /** * Descend at most levels of directories below the top level files and directories. For example, "0" * means "only apply the tests and actions to the top level files and directories". The default is to descend to * any nesting level. */ public void setMaxDepth(int levels) { this.find.setMaxDepth(levels); } /** * Also examine the given file. * * @see #setDir(File) * @see #addConfigured(ResourceCollection) */ public void setFile(File file) { this.resourceCollections.add(new FileResource(file)); } /** * Also examine the given directory. * * @see #setFile(File) * @see #addConfigured(ResourceCollection) */ public void setDir(File directory) { this.resourceCollections.add(new FileResource(directory)); } /** * Print the output to the given file instead of STDOUT. */ public void setOutputFile(File file) { this.outputFile = file; } /** * Another expression that will be evaluated for each directory, file and archive entry. */ @Override public void addConfigured(ExpressionElement operand) { this.root.addConfigured(operand); } /** * A {@code } subelement creates a custom {@link Expression} which is only available in the ANT * binding: A sequence of ANT tasks that evaluates to {@code true}. */ static NestedSequential newSequential(final AbstractElementWithOperands container) { final MacroDef macroDef = new MacroDef(); macroDef.setProject(container.getProject()); { MacroDef.Attribute attribute = new MacroDef.Attribute(); attribute.setName("name"); macroDef.addConfiguredAttribute(attribute); } { MacroDef.Attribute attribute = new MacroDef.Attribute(); attribute.setName("path"); macroDef.addConfiguredAttribute(attribute); } { MacroDef.Attribute attribute = new MacroDef.Attribute(); attribute.setName("entryName"); macroDef.addConfiguredAttribute(attribute); } ExpressionElement operand = new ExpressionElement() { @Override public Expression toExpression() { return new Expression() { @Override public boolean evaluate(Mapping properties) { MacroInstance instance = new MacroInstance(); instance.setProject(container.getProject()); // instance.setOwningTarget(xxx.getOwningTarget()); instance.setMacroDef(macroDef); for (String attributeName : new String[] { "name", "path" }) { Object attributeValue = properties.get(attributeName); if (attributeValue != null) { instance.setDynamicAttribute(attributeName, attributeValue.toString()); } } instance.execute(); return true; } @Override public String toString() { return ""; } }; } }; container.addConfigured(operand); return macroDef.createSequential(); } /** * Resources to examine. * * @see #setFile(File) * @see #setDir(File) */ public void addConfigured(ResourceCollection value) { new Throwable().printStackTrace(); System.err.println(value.getClass().getName()); this.resourceCollections.add(value); } // END CONFIGURATION SETTERS /** * The ANT task "execute" method. * * @see Task#execute */ public void execute() throws BuildException { try { this.execute2(); } catch (BuildException be) { throw be; } catch (Exception e) { throw new BuildException(e); } } private void execute2() { this.find.setExpression(this.root.toExpression()); final boolean[] hadExceptions = new boolean[1]; ConsumerWhichThrows exceptionHandler = new ConsumerWhichThrows() { @Override public void consume(IOException ioe) { AntTask.this.getProject().log(null, ioe, Project.MSG_ERR); hadExceptions[0] = true; } }; this.find.setExceptionHandler(exceptionHandler); for (ResourceCollection rc : this.resourceCollections) { for (@SuppressWarnings("unchecked") Iterator it = rc.iterator(); it.hasNext();) { Resource resource = it.next(); try { this.execute3(resource); } catch (IOException ioe) { AntTask.this.getProject().log(null, ioe, Project.MSG_ERR); hadExceptions[0] = true; } } } if (hadExceptions[0]) { throw new BuildException("One or more files had i/o exceptions"); } } private void execute3(final Resource resource) throws IOException { AntTask.execute4( new RunnableWhichThrows() { @Override public void run() throws IOException { if (resource instanceof FileProvider) { AntTask.this.find.findInFile(((FileProvider) resource).getFile()); } else { InputStream is = resource.getInputStream(); try { AntTask.this.find.findInStream(is); is.close(); } catch (IOException ioe) { try { is.close(); } catch (Exception e) {} } } } }, this.outputFile, this ); } /** * Runs the given runnable with printers redirected to ANT's logging mechanism. */ private static void execute4(RunnableWhichThrows runnable, @Nullable File outputFile, final ProjectComponent component) throws IOException { Printer printer = new AbstractPrinter() { @Override public void warn(@Nullable String message) { component.log(message, Project.MSG_WARN); } @Override public void verbose(@Nullable String message) { component.log(message, Project.MSG_VERBOSE); } @Override public void info(@Nullable String message) { component.log(message, Project.MSG_INFO); } @Override public void error(@Nullable String message) { component.log(message, Project.MSG_ERR); } @Override public void debug(@Nullable String message) { component.log(message, Project.MSG_DEBUG); } }; if (outputFile == null) { Printers.withPrinter(printer, runnable); } else { final PrintStream out = new PrintStream(outputFile); try { printer = new ProxyPrinter(printer) { @Override public void info(@Nullable String message) { out.println(message); } }; Printers.withPrinter(printer, runnable); out.close(); } finally { try { out.close(); } catch (Exception e) {} } } } /** * Iff the propertyName is not {@code null}, then the action is wrapped such that * when it is evaluated, its INFO output is stored in the named property. */ private static Action redirectInfoToProperty( final Project project, final Action action, @Nullable final String propertyName ) { if (propertyName == null) return action; return new Action() { @Override public boolean evaluate(final Mapping properties) { final boolean[] result = new boolean[1]; Printers.redirectInfo( new Consumer() { @Override public void consume(String subject) { project.setProperty(propertyName, subject); } }, new RunnableWhichThrows() { @Override public void run() { result[0] = action.evaluate(properties); } } ); return result[0]; } }; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy