/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.utils.common.textstream; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Future; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.TeeInputStream; import org.apache.commons.logging.LogFactory; import de.rcenvironment.toolkit.modules.concurrency.api.AsyncTaskService; import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription; /** * A utility class to read and forward line-based text data from an {@link InputStream}. As it requires an {@link AsyncTaskService} to run, * an instance must be provided via the constructor. If available in the project scope, consider using * de.rcenvironment.core.toolkitbridge.transitional.TextStreamWatcherFactory for convenience. * * TODO consider reworking this class to a plain {@link Runnable} to avoid this dependency, which would also allow using this class with a * plain Java {@link Executor}. * * TODO add more specific unit tests? currently covered indirectly by executor tests * * @author Robert Mischke */ public class TextStreamWatcher { private final TextOutputReceiver[] receivers; private InputStream inputStream; private BufferedReader bufferedReader; /** * An optional file where the input stream is mirrored to. */ private File logFile; private volatile Future<?> watcherTaskFuture; private volatile boolean streamClosed = false; private volatile FileOutputStream logFileStream; private final AsyncTaskService asyncTaskService; /** * The actual {@link Runnable} that performs the output capture. Uses the outer class fields for delegation. * * @author Robert Mischke */ private final class WatcherRunnable implements Runnable { @Override @TaskDescription("Text stream watching/reading") public void run() { for (TextOutputReceiver receiver : receivers) { receiver.onStart(); } String line; try { while ((line = bufferedReader.readLine()) != null) { for (TextOutputReceiver receiver : receivers) { receiver.addOutput(line); } } for (TextOutputReceiver receiver : receivers) { receiver.onFinished(); } } catch (IOException e) { for (TextOutputReceiver receiver : receivers) { try { receiver.onFatalError(e); } catch (RuntimeException e1) { // catch this to make sure the other "onException" handlers are called LogFactory.getLog(getClass()).error("Exception in onException() callback", e1); } } } IOUtils.closeQuietly(bufferedReader); streamClosed = true; } } /** * Creates an instance (which is not started automatically; see {@link #start()}). * * @param input the {@link InputStream} to read from * @param receivers the {@link TextOutputReceiver} to send the generated events to */ public TextStreamWatcher(InputStream input, AsyncTaskService asyncTaskService, TextOutputReceiver... receivers) { this.receivers = receivers; this.inputStream = input; this.asyncTaskService = asyncTaskService; // guard against "null" listeners for (TextOutputReceiver r : receivers) { if (r == null) { throw new IllegalArgumentException("A 'null' receiver was passed as an argument"); } } } /** * Defines that the raw output should be mirrored to the given file. The target file will be created, lines appended or overwritten if * it already exists. This method is meant to be called no more than once; repeated calls will cause a {@link IllegalStateException}. * * @param file the target file to write to * @param append append lines to file if true; otherwise overwrite file if already exists * * @throws IOException on I/O errors while setting up the log file */ public void enableLogFile(File file, boolean append) throws IOException { if (watcherTaskFuture != null) { throw new IllegalStateException("Already started"); } // sanity check if (logFile != null) { throw new IllegalStateException("Log file was already defined"); } logFile = file; logFileStream = new FileOutputStream(logFile, append); // true = auto close stream inputStream = new TeeInputStream(inputStream, logFileStream, true); } /** * Returns the file set by {@link #enableLogFile(File)}, or null if none was set. * * @return the defined log file */ public File getLogFile() { return logFile; } /** * Starts the watcher thread. * * @return the "self" instance for convenient chaining */ public TextStreamWatcher start() { this.bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); watcherTaskFuture = asyncTaskService.submit(new WatcherRunnable()); return this; } /** * Blocks until the stream watcher thread has terminated. * */ public void waitForTermination() { try { watcherTaskFuture.get(); } catch (InterruptedException e) { LogFactory.getLog(getClass()).debug("Interrupted while waiting for stream watcher task to finish"); } catch (ExecutionException e) { LogFactory.getLog(getClass()).warn("Exception while waiting for stream watcher task to finish", e); } if (logFileStream != null) { IOUtils.closeQuietly(logFileStream); } } /** * Interrupts the stream watcher thread. */ public void cancel() { if (watcherTaskFuture == null) { throw new IllegalStateException("Watcher task was not started yet"); } watcherTaskFuture.cancel(true); } /** * Returns whether the watched output stream has ended, either by reaching EOF or because an exception has occurred. * * @return true if the watched stream has ended */ public boolean isStreamClosed() { return streamClosed; } }