/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.ant.taskdefs; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PipedOutputStream; import java.io.PrintStream; import java.io.Reader; import java.io.StringReader; import java.util.Arrays; import java.util.Vector; 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.filters.util.ChainReaderHelper; import org.apache.tools.ant.types.FilterChain; import org.apache.tools.ant.util.ConcatFileInputStream; import org.apache.tools.ant.util.FileUtils; import org.apache.tools.ant.util.KeepAliveOutputStream; import org.apache.tools.ant.util.LazyFileOutputStream; import org.apache.tools.ant.util.LeadPipeInputStream; import org.apache.tools.ant.util.LineOrientedOutputStreamRedirector; import org.apache.tools.ant.util.OutputStreamFunneler; import org.apache.tools.ant.util.ReaderInputStream; import org.apache.tools.ant.util.StringUtils; import org.apache.tools.ant.util.TeeOutputStream; /** * The Redirector class manages the setup and connection of input and output * redirection for an Ant project component. * * @since Ant 1.6 */ public class Redirector { private static final int STREAMPUMPER_WAIT_INTERVAL = 1000; private static final String DEFAULT_ENCODING = System .getProperty("file.encoding"); private class PropertyOutputStream extends ByteArrayOutputStream { private final String property; private boolean closed = false; PropertyOutputStream(final String property) { super(); this.property = property; } @Override public void close() throws IOException { synchronized (outMutex) { if (!closed && !(appendOut && appendProperties)) { setPropertyFromBAOS(this, property); closed = true; } } } } /** * The file(s) from which standard input is being taken. If > 1, files' * content will be concatenated in the order received. */ private File[] input; /** * The file(s) receiving standard output. Will also receive standard error * unless standard error is redirected or logError is true. */ private File[] out; /** * The file(s) to which standard error is being redirected */ private File[] error; /** * Indicates if standard error should be logged to Ant's log system rather * than the output. This has no effect if standard error is redirected to a * file or property. */ private boolean logError = false; /** * Buffer used to capture output for storage into a property */ private PropertyOutputStream baos = null; /** * Buffer used to capture error output for storage into a property */ private PropertyOutputStream errorBaos = null; /** The name of the property into which output is to be stored */ private String outputProperty; /** The name of the property into which error output is to be stored */ private String errorProperty; /** String from which input is taken */ private String inputString; /** Flag which indicates if error and output files are to be appended. */ private boolean appendOut = false; private boolean appendErr = false; /** Flag which indicates that output should be always sent to the log */ private boolean alwaysLogOut = false; private boolean alwaysLogErr = false; /** Flag which indicates whether files should be created even when empty. */ private boolean createEmptyFilesOut = true; private boolean createEmptyFilesErr = true; /** The task for which this redirector is working */ private final ProjectComponent managingTask; /** The stream for output data */ private OutputStream outputStream = null; /** The stream for error output */ private OutputStream errorStream = null; /** The stream for input */ private InputStream inputStream = null; /** Stream which is used for line oriented output */ private PrintStream outPrintStream = null; /** Stream which is used for line oriented error output */ private PrintStream errorPrintStream = null; /** The output filter chains */ private Vector<FilterChain> outputFilterChains; /** The error filter chains */ private Vector<FilterChain> errorFilterChains; /** The input filter chains */ private Vector<FilterChain> inputFilterChains; /** The output encoding */ private String outputEncoding = DEFAULT_ENCODING; /** The error encoding */ private String errorEncoding = DEFAULT_ENCODING; /** The input encoding */ private String inputEncoding = DEFAULT_ENCODING; /** Whether to complete properties settings **/ private boolean appendProperties = true; /** The thread group used for starting <code>StreamPumper</code> threads */ private final ThreadGroup threadGroup = new ThreadGroup("redirector"); /** whether to log the inputstring */ private boolean logInputString = true; /** Mutex for in */ private final Object inMutex = new Object(); /** Mutex for out */ private final Object outMutex = new Object(); /** Mutex for err */ private final Object errMutex = new Object(); /** Is the output binary or can we safely split it into lines? */ private boolean outputIsBinary = false; /** * Create a redirector instance for the given task * * @param managingTask * the task for which the redirector is to work */ public Redirector(final Task managingTask) { this((ProjectComponent) managingTask); } /** * Create a redirector instance for the given task * * @param managingTask * the project component for which the redirector is to work * @since Ant 1.6.3 */ public Redirector(final ProjectComponent managingTask) { this.managingTask = managingTask; } /** * Set the input to use for the task * * @param input * the file from which input is read. */ public void setInput(final File input) { setInput((input == null) ? null : new File[] {input}); } /** * Set the input to use for the task * * @param input * the files from which input is read. */ public void setInput(final File[] input) { synchronized (inMutex) { if (input == null) { this.input = null; } else { this.input = input.clone(); } } } /** * Set the string to use as input * * @param inputString * the string which is used as the input source */ public void setInputString(final String inputString) { synchronized (inMutex) { this.inputString = inputString; } } /** * Set whether to include the value of the input string in log messages. * Defaults to true. * * @param logInputString * true or false. * @since Ant 1.7 */ public void setLogInputString(final boolean logInputString) { this.logInputString = logInputString; } /** * Set a stream to use as input. * * @param inputStream * the stream from which input will be read * @since Ant 1.6.3 */ /* public */void setInputStream(final InputStream inputStream) { synchronized (inMutex) { this.inputStream = inputStream; } } /** * File the output of the process is redirected to. If error is not * redirected, it too will appear in the output * * @param out * the file to which output stream is written */ public void setOutput(final File out) { setOutput((out == null) ? null : new File[] {out}); } /** * Files the output of the process is redirected to. If error is not * redirected, it too will appear in the output * * @param out * the files to which output stream is written */ public void setOutput(final File[] out) { synchronized (outMutex) { if (out == null) { this.out = null; } else { this.out = out.clone(); } } } /** * Set the output encoding. * * @param outputEncoding * <code>String</code>. */ public void setOutputEncoding(final String outputEncoding) { if (outputEncoding == null) { throw new IllegalArgumentException( "outputEncoding must not be null"); } synchronized (outMutex) { this.outputEncoding = outputEncoding; } } /** * Set the error encoding. * * @param errorEncoding * <code>String</code>. */ public void setErrorEncoding(final String errorEncoding) { if (errorEncoding == null) { throw new IllegalArgumentException("errorEncoding must not be null"); } synchronized (errMutex) { this.errorEncoding = errorEncoding; } } /** * Set the input encoding. * * @param inputEncoding * <code>String</code>. */ public void setInputEncoding(final String inputEncoding) { if (inputEncoding == null) { throw new IllegalArgumentException("inputEncoding must not be null"); } synchronized (inMutex) { this.inputEncoding = inputEncoding; } } /** * Controls whether error output of exec is logged. This is only useful when * output is being redirected and error output is desired in the Ant log * * @param logError * if true the standard error is sent to the Ant log system and * not sent to output. */ public void setLogError(final boolean logError) { synchronized (errMutex) { this.logError = logError; } } /** * This <code>Redirector</code>'s subordinate * <code>PropertyOutputStream</code>s will not set their respective * properties <code>while (appendProperties && append)</code>. * * @param appendProperties * whether to append properties. */ public void setAppendProperties(final boolean appendProperties) { synchronized (outMutex) { this.appendProperties = appendProperties; } } /** * Set the file to which standard error is to be redirected. * * @param error * the file to which error is to be written */ public void setError(final File error) { setError((error == null) ? null : new File[] {error}); } /** * Set the files to which standard error is to be redirected. * * @param error * the file to which error is to be written */ public void setError(final File[] error) { synchronized (errMutex) { if (error == null) { this.error = null; } else { this.error = error.clone(); } } } /** * Property name whose value should be set to the output of the process. * * @param outputProperty * the name of the property to be set with the task's output. */ public void setOutputProperty(final String outputProperty) { if (outputProperty == null || !(outputProperty.equals(this.outputProperty))) { synchronized (outMutex) { this.outputProperty = outputProperty; baos = null; } } } /** * Whether output should be appended to or overwrite an existing file. * Defaults to false. * * @param append * if true output and error streams are appended to their * respective files, if specified. */ public void setAppend(final boolean append) { synchronized (outMutex) { appendOut = append; } synchronized (errMutex) { appendErr = append; } } /** * If true, (error and non-error) output will be "teed", redirected as * specified while being sent to Ant's logging mechanism as if no * redirection had taken place. Defaults to false. * * @param alwaysLog * <code>boolean</code> * @since Ant 1.6.3 */ public void setAlwaysLog(final boolean alwaysLog) { synchronized (outMutex) { alwaysLogOut = alwaysLog; } synchronized (errMutex) { alwaysLogErr = alwaysLog; } } /** * Whether output and error files should be created even when empty. * Defaults to true. * * @param createEmptyFiles * <code>boolean</code>. */ public void setCreateEmptyFiles(final boolean createEmptyFiles) { synchronized (outMutex) { createEmptyFilesOut = createEmptyFiles; } synchronized (outMutex) { createEmptyFilesErr = createEmptyFiles; } } /** * Property name whose value should be set to the error of the process. * * @param errorProperty * the name of the property to be set with the error output. */ public void setErrorProperty(final String errorProperty) { synchronized (errMutex) { if (errorProperty == null || !(errorProperty.equals(this.errorProperty))) { this.errorProperty = errorProperty; errorBaos = null; } } } /** * Set the input <code>FilterChain</code>s. * * @param inputFilterChains * <code>Vector</code> containing <code>FilterChain</code>. */ public void setInputFilterChains(final Vector<FilterChain> inputFilterChains) { synchronized (inMutex) { this.inputFilterChains = inputFilterChains; } } /** * Set the output <code>FilterChain</code>s. * * @param outputFilterChains * <code>Vector</code> containing <code>FilterChain</code>. */ public void setOutputFilterChains(final Vector<FilterChain> outputFilterChains) { synchronized (outMutex) { this.outputFilterChains = outputFilterChains; } } /** * Set the error <code>FilterChain</code>s. * * @param errorFilterChains * <code>Vector</code> containing <code>FilterChain</code>. */ public void setErrorFilterChains(final Vector<FilterChain> errorFilterChains) { synchronized (errMutex) { this.errorFilterChains = errorFilterChains; } } /** * Whether to consider the output created by the process binary. * * <p>Binary output will not be split into lines which may make * error and normal output look mixed up when they get written to * the same stream.</p> * @since 1.9.4 */ public void setBinaryOutput(final boolean b) { outputIsBinary = b; } /** * Set a property from a ByteArrayOutputStream * * @param baos * contains the property value. * @param propertyName * the property name. * * @exception IOException * if the value cannot be read form the stream. */ private void setPropertyFromBAOS(final ByteArrayOutputStream baos, final String propertyName) throws IOException { final BufferedReader in = new BufferedReader(new StringReader(Execute .toString(baos))); String line = null; final StringBuffer val = new StringBuffer(); while ((line = in.readLine()) != null) { if (val.length() != 0) { val.append(StringUtils.LINE_SEP); } val.append(line); } managingTask.getProject().setNewProperty(propertyName, val.toString()); } /** * Create the input, error and output streams based on the configuration * options. */ public void createStreams() { synchronized (outMutex) { outStreams(); if (alwaysLogOut || outputStream == null) { final OutputStream outputLog = new LogOutputStream(managingTask, Project.MSG_INFO); outputStream = (outputStream == null) ? outputLog : new TeeOutputStream(outputLog, outputStream); } if ((outputFilterChains != null && outputFilterChains.size() > 0) || !(outputEncoding.equalsIgnoreCase(inputEncoding))) { try { final LeadPipeInputStream snk = new LeadPipeInputStream(); snk.setManagingComponent(managingTask); InputStream outPumpIn = snk; Reader reader = new InputStreamReader(outPumpIn, inputEncoding); if (outputFilterChains != null && outputFilterChains.size() > 0) { final ChainReaderHelper helper = new ChainReaderHelper(); helper.setProject(managingTask.getProject()); helper.setPrimaryReader(reader); helper.setFilterChains(outputFilterChains); reader = helper.getAssembledReader(); } outPumpIn = new ReaderInputStream(reader, outputEncoding); final Thread t = new Thread(threadGroup, new StreamPumper( outPumpIn, outputStream, true), "output pumper"); t.setPriority(Thread.MAX_PRIORITY); outputStream = new PipedOutputStream(snk); t.start(); } catch (final IOException eyeOhEx) { throw new BuildException("error setting up output stream", eyeOhEx); } } } synchronized (errMutex) { errorStreams(); if (alwaysLogErr || errorStream == null) { final OutputStream errorLog = new LogOutputStream(managingTask, Project.MSG_WARN); errorStream = (errorStream == null) ? errorLog : new TeeOutputStream(errorLog, errorStream); } if ((errorFilterChains != null && errorFilterChains.size() > 0) || !(errorEncoding.equalsIgnoreCase(inputEncoding))) { try { final LeadPipeInputStream snk = new LeadPipeInputStream(); snk.setManagingComponent(managingTask); InputStream errPumpIn = snk; Reader reader = new InputStreamReader(errPumpIn, inputEncoding); if (errorFilterChains != null && errorFilterChains.size() > 0) { final ChainReaderHelper helper = new ChainReaderHelper(); helper.setProject(managingTask.getProject()); helper.setPrimaryReader(reader); helper.setFilterChains(errorFilterChains); reader = helper.getAssembledReader(); } errPumpIn = new ReaderInputStream(reader, errorEncoding); final Thread t = new Thread(threadGroup, new StreamPumper( errPumpIn, errorStream, true), "error pumper"); t.setPriority(Thread.MAX_PRIORITY); errorStream = new PipedOutputStream(snk); t.start(); } catch (final IOException eyeOhEx) { throw new BuildException("error setting up error stream", eyeOhEx); } } } synchronized (inMutex) { // if input files are specified, inputString and inputStream are // ignored; // classes that work with redirector attributes can enforce // whatever warnings are needed if (input != null && input.length > 0) { managingTask .log("Redirecting input from file" + ((input.length == 1) ? "" : "s"), Project.MSG_VERBOSE); try { inputStream = new ConcatFileInputStream(input); } catch (final IOException eyeOhEx) { throw new BuildException(eyeOhEx); } ((ConcatFileInputStream) inputStream) .setManagingComponent(managingTask); } else if (inputString != null) { final StringBuffer buf = new StringBuffer("Using input "); if (logInputString) { buf.append('"').append(inputString).append('"'); } else { buf.append("string"); } managingTask.log(buf.toString(), Project.MSG_VERBOSE); inputStream = new ByteArrayInputStream(inputString.getBytes()); } if (inputStream != null && inputFilterChains != null && inputFilterChains.size() > 0) { final ChainReaderHelper helper = new ChainReaderHelper(); helper.setProject(managingTask.getProject()); try { helper.setPrimaryReader(new InputStreamReader(inputStream, inputEncoding)); } catch (final IOException eyeOhEx) { throw new BuildException("error setting up input stream", eyeOhEx); } helper.setFilterChains(inputFilterChains); inputStream = new ReaderInputStream( helper.getAssembledReader(), inputEncoding); } } } /** outStreams */ private void outStreams() { if (out != null && out.length > 0) { final String logHead = new StringBuffer("Output ").append( ((appendOut) ? "appended" : "redirected")).append(" to ") .toString(); outputStream = foldFiles(out, logHead, Project.MSG_VERBOSE, appendOut, createEmptyFilesOut); } if (outputProperty != null) { if (baos == null) { baos = new PropertyOutputStream(outputProperty); managingTask.log("Output redirected to property: " + outputProperty, Project.MSG_VERBOSE); } // shield it from being closed by a filtering StreamPumper final OutputStream keepAliveOutput = new KeepAliveOutputStream(baos); outputStream = (outputStream == null) ? keepAliveOutput : new TeeOutputStream(outputStream, keepAliveOutput); } else { baos = null; } } private void errorStreams() { if (error != null && error.length > 0) { final String logHead = new StringBuffer("Error ").append( ((appendErr) ? "appended" : "redirected")).append(" to ") .toString(); errorStream = foldFiles(error, logHead, Project.MSG_VERBOSE, appendErr, createEmptyFilesErr); } else if (!(logError || outputStream == null) && errorProperty == null) { final long funnelTimeout = 0L; final OutputStreamFunneler funneler = new OutputStreamFunneler( outputStream, funnelTimeout); try { outputStream = funneler.getFunnelInstance(); errorStream = funneler.getFunnelInstance(); if (!outputIsBinary) { outputStream = new LineOrientedOutputStreamRedirector(outputStream); errorStream = new LineOrientedOutputStreamRedirector(errorStream); } } catch (final IOException eyeOhEx) { throw new BuildException( "error splitting output/error streams", eyeOhEx); } } if (errorProperty != null) { if (errorBaos == null) { errorBaos = new PropertyOutputStream(errorProperty); managingTask.log("Error redirected to property: " + errorProperty, Project.MSG_VERBOSE); } // shield it from being closed by a filtering StreamPumper final OutputStream keepAliveError = new KeepAliveOutputStream(errorBaos); errorStream = (error == null || error.length == 0) ? keepAliveError : new TeeOutputStream(errorStream, keepAliveError); } else { errorBaos = null; } } /** * Create the StreamHandler to use with our Execute instance. * * @return the execute stream handler to manage the input, output and error * streams. * * @throws BuildException * if the execute stream handler cannot be created. */ public ExecuteStreamHandler createHandler() throws BuildException { createStreams(); final boolean nonBlockingRead = input == null && inputString == null; return new PumpStreamHandler(getOutputStream(), getErrorStream(), getInputStream(), nonBlockingRead); } /** * Pass output sent to System.out to specified output. * * @param output * the data to be output */ protected void handleOutput(final String output) { synchronized (outMutex) { if (outPrintStream == null) { outPrintStream = new PrintStream(outputStream); } outPrintStream.print(output); } } /** * Handle an input request * * @param buffer * the buffer into which data is to be read. * @param offset * the offset into the buffer at which data is stored. * @param length * the amount of data to read * * @return the number of bytes read * * @exception IOException * if the data cannot be read */ protected int handleInput(final byte[] buffer, final int offset, final int length) throws IOException { synchronized (inMutex) { if (inputStream == null) { return managingTask.getProject().defaultInput(buffer, offset, length); } return inputStream.read(buffer, offset, length); } } /** * Process data due to a flush operation. * * @param output * the data being flushed. */ protected void handleFlush(final String output) { synchronized (outMutex) { if (outPrintStream == null) { outPrintStream = new PrintStream(outputStream); } outPrintStream.print(output); outPrintStream.flush(); } } /** * Process error output * * @param output * the error output data. */ protected void handleErrorOutput(final String output) { synchronized (errMutex) { if (errorPrintStream == null) { errorPrintStream = new PrintStream(errorStream); } errorPrintStream.print(output); } } /** * Handle a flush operation on the error stream * * @param output * the error information being flushed. */ protected void handleErrorFlush(final String output) { synchronized (errMutex) { if (errorPrintStream == null) { errorPrintStream = new PrintStream(errorStream); } errorPrintStream.print(output); errorPrintStream.flush(); } } /** * Get the output stream for the redirector * * @return the redirector's output stream or null if no output has been * configured */ public OutputStream getOutputStream() { synchronized (outMutex) { return outputStream; } } /** * Get the error stream for the redirector * * @return the redirector's error stream or null if no output has been * configured */ public OutputStream getErrorStream() { synchronized (errMutex) { return errorStream; } } /** * Get the input stream for the redirector * * @return the redirector's input stream or null if no output has been * configured */ public InputStream getInputStream() { synchronized (inMutex) { return inputStream; } } /** * Complete redirection. * * This operation will close any streams and create any specified property * values. * * @throws IOException * if the output properties cannot be read from their output * streams. */ public void complete() throws IOException { System.out.flush(); System.err.flush(); synchronized (inMutex) { if (inputStream != null) { inputStream.close(); } } synchronized (outMutex) { outputStream.flush(); outputStream.close(); } synchronized (errMutex) { errorStream.flush(); errorStream.close(); } // wait for the StreamPumpers to finish synchronized (this) { while (threadGroup.activeCount() > 0) { try { managingTask.log("waiting for " + threadGroup.activeCount() + " Threads:", Project.MSG_DEBUG); final Thread[] thread = new Thread[threadGroup.activeCount()]; threadGroup.enumerate(thread); for (int i = 0; i < thread.length && thread[i] != null; i++) { try { managingTask.log(thread[i].toString(), Project.MSG_DEBUG); } catch (final NullPointerException enPeaEx) { // Ignore exception } } wait(STREAMPUMPER_WAIT_INTERVAL); } catch (final InterruptedException eyeEx) { final Thread[] thread = new Thread[threadGroup.activeCount()]; threadGroup.enumerate(thread); for (int i = 0; i < thread.length && thread[i] != null; i++) { thread[i].interrupt(); } } } } setProperties(); synchronized (inMutex) { inputStream = null; } synchronized (outMutex) { outputStream = null; outPrintStream = null; } synchronized (errMutex) { errorStream = null; errorPrintStream = null; } } /** * Notify the <code>Redirector</code> that it is now okay to set any output * and/or error properties. */ public void setProperties() { synchronized (outMutex) { FileUtils.close(baos); } synchronized (errMutex) { FileUtils.close(errorBaos); } } private OutputStream foldFiles(final File[] file, final String logHead, final int loglevel, final boolean append, final boolean createEmptyFiles) { final OutputStream result = new LazyFileOutputStream(file[0], append, createEmptyFiles); managingTask.log(logHead + file[0], loglevel); final char[] c = new char[logHead.length()]; Arrays.fill(c, ' '); final String indent = new String(c); for (int i = 1; i < file.length; i++) { outputStream = new TeeOutputStream(outputStream, new LazyFileOutputStream(file[i], append, createEmptyFiles)); managingTask.log(indent + file[i], loglevel); } return result; } }