// This file is part of PleoCommand:
// Interactively control Pleo with psychobiological parameters
//
// Copyright (C) 2010 Oliver Hoffmann - Hoffmann_Oliver@gmx.de
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Boston, USA.
package pleocmd.pipe;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import pleocmd.Log;
import pleocmd.cfg.ConfigBoolean;
import pleocmd.cfg.ConfigInt;
import pleocmd.cfg.ConfigPath;
import pleocmd.cfg.ConfigPath.PathType;
import pleocmd.cfg.Configuration;
import pleocmd.cfg.ConfigurationInterface;
import pleocmd.cfg.Group;
import pleocmd.exc.ConfigurationException;
import pleocmd.exc.ConverterException;
import pleocmd.exc.InputException;
import pleocmd.exc.InternalException;
import pleocmd.exc.OutputException;
import pleocmd.exc.PipeException;
import pleocmd.exc.StateException;
import pleocmd.itfc.gui.MainFrame;
import pleocmd.itfc.gui.MainPipePanel;
import pleocmd.itfc.gui.PipeFlowVisualization;
import pleocmd.pipe.PipePart.HelpKind;
import pleocmd.pipe.cvt.Converter;
import pleocmd.pipe.data.Data;
import pleocmd.pipe.data.DataQueue;
import pleocmd.pipe.data.DataQueue.PutResult;
import pleocmd.pipe.in.Input;
import pleocmd.pipe.out.Output;
/**
* The central processing point of {@link Data} objects.
*
* @author oliver
*/
public final class Pipe extends StateHandling implements ConfigurationInterface {
private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat(
"HH:mm:ss.SSS");
/**
* Number of milliseconds to reduce waiting-time in the input thread for
* timed {@link Data}. before it's passed to the output thread's
* {@link #waitForOutputTime(Data)}
* <p>
* If too small (smaller than time needed to pass from Input thread via
* {@link #dataQueue} to Output thread), very short delays for timed
* {@link Data} may occur in some situations.<br>
* If too large, {@link Data}s may be dropped or interrupted without need if
* a timed {@link Data} with a different priority follows.<br>
* Typical values are in [0, 40] for fast and [25, 200] for a slow computer.
*/
private final ConfigInt cfgOverheadReductionTime = new ConfigInt(
"Overhead Reduction Time", 10, 0, 1000);
/**
* Number of milliseconds it approximately takes from
* {@link #waitForOutputTime(Data)} via {@link #writeDataToAllOutputs(Data)}
* to {@link Output#write(Data)}
* <p>
* If too small, very short delays for timed {@link Data} may occur.<br>
* If too large, timed {@link Data}s may be executed too early.<br>
* Typical values are in [0, 10].<br>
* Should not be larger than {@link #cfgOverheadReductionTime}.
*/
private final ConfigInt cfgOutputInitOverhead = new ConfigInt(
"Output Init Overhead", 2, 0, 1000);
/**
* Number of milliseconds which an output may be behind for a timed
* {@link Data} before a warning will be logged.
* <p>
* If too small, nearly every timed {@link Data} will be reported.<br>
* If too large, even very long delays may not be detected.<br>
* Typical values are in [50, 5000].
*/
private final ConfigInt cfgMaxBehind = new ConfigInt("Max Behind", 300, 0,
60000);
private final ConfigPath cfgLastSaveFile = new ConfigPath("Last Save-File",
PathType.FileForWriting);
private final ConfigBoolean cfgModifiedSinceSave = new ConfigBoolean(
"Modified Since Save", false);
private final List<Input> inputList = new ArrayList<Input>();
private final List<Output> outputList = new ArrayList<Output>();
private final List<Converter> converterList = new ArrayList<Converter>();
private final Set<PipePart> ignoredInputs = new HashSet<PipePart>();
private final Set<PipePart> ignoredConverter = new HashSet<PipePart>();
private final Set<PipePart> ignoredOutputs = new HashSet<PipePart>();
private final DataQueue dataQueue = new DataQueue();
private final List<Thread> thrsInput = new ArrayList<Thread>();
private Thread thrOutput;
private boolean inputThreadInterruped;
private PipeFeedback feedback;
private boolean pipeInitializing;
private boolean initPhaseInterrupted;
private final Configuration config;
private Thread mainInputThread;
private long lastTime;
/**
* Creates a new {@link Pipe}.
*
* @param config
* the {@link Configuration} to save the Pipe and all its
* PipeParts to.
*/
public Pipe(final Configuration config) {
this.config = config;
feedback = new PipeFeedback();
final Set<String> groupNames = new HashSet<String>();
groupNames.add(getClass().getSimpleName());
for (final Class<? extends PipePart> ppc : PipePartDetection.ALL_PIPEPART)
groupNames.add(getClass().getSimpleName() + ": "
+ ppc.getSimpleName());
try {
config.registerConfigurableObject(this, groupNames);
} catch (final ConfigurationException e) {
Log.error(e);
}
constructed();
}
/**
* @return an unmodifiable list of all currently connected {@link Input}s
*/
public List<Input> getInputList() {
return Collections.unmodifiableList(inputList);
}
/**
* @return an unmodifiable list of all currently connected {@link Output}s
*/
public List<Output> getOutputList() {
return Collections.unmodifiableList(outputList);
}
/**
* @return an unmodifiable list of all currently connected {@link Converter}
*/
public List<Converter> getConverterList() {
return Collections.unmodifiableList(converterList);
}
/**
* Adds a new {@link Input} to the list of currently connected ones.
*
* @param input
* the new {@link Input}
* @throws StateException
* if the {@link Pipe} is being constructed or already
* initialized
*/
public void addInput(final Input input) throws StateException {
ensureConstructed();
Log.detail("Connecting pipe with input '%s'", input);
inputList.add(input);
((PipePart) input).connectedToPipe(this);
}
/**
* Adds a new {@link Converter} to the list of currently connected ones.
*
* @param output
* the new {@link Converter}
* @throws StateException
* if the {@link Pipe} is being constructed or already
* initialized
*/
public void addOutput(final Output output) throws StateException {
ensureConstructed();
Log.detail("Connecting pipe with output '%s'", output);
outputList.add(output);
((PipePart) output).connectedToPipe(this);
}
/**
* Adds a new {@link Output} to the list of currently connected ones.
*
* @param converter
* the new {@link Output}
* @throws StateException
* if the {@link Pipe} is being constructed or already
* initialized
*/
public void addConverter(final Converter converter) throws StateException {
ensureConstructed();
Log.detail("Connecting pipe with converter '%s'", converter);
converterList.add(converter);
((PipePart) converter).connectedToPipe(this);
}
public void removeInput(final Input input) throws StateException {
ensureConstructed();
Log.detail("Disconnecting pipe from input '%s'", input);
if (inputList.remove(input))
((PipePart) input).disconnectedFromPipe(this);
}
public void removeOutput(final Output output) throws StateException {
ensureConstructed();
Log.detail("Disconnecting pipe from output '%s'", output);
if (outputList.remove(output))
((PipePart) output).disconnectedFromPipe(this);
}
public void removeConverter(final Converter converter)
throws StateException {
ensureConstructed();
Log.detail("Disconnecting pipe from converter '%s'", converter);
if (converterList.remove(converter))
((PipePart) converter).disconnectedFromPipe(this);
}
/**
* Rearranges all {@link Input}s according to the given list.
*
* @param ordered
* list of all {@link Input}s of the {@link Pipe} in the new
* order
* @throws StateException
* if the {@link Pipe} is being constructed or already
* initialized
* @throws IllegalArgumentException
* if the given list doesn't contain the same {@link Input}s as
* the {@link Pipe}.
*/
public void reorderInputs(final List<Input> ordered) throws StateException {
ensureConstructed();
if (inputList.size() != ordered.size())
throw new IllegalArgumentException("Size of lists differ");
for (final Input pp : inputList)
if (!ordered.contains(pp))
throw new IllegalArgumentException(String.format(
"Missed in ordered list: '%s'", pp));
inputList.clear();
inputList.addAll(ordered);
}
/**
* Rearranges all {@link Converter} according to the given list.
*
* @param ordered
* list of all {@link Converter} of the {@link Pipe} in the new
* order
* @throws StateException
* if the {@link Pipe} is being constructed or already
* initialized
* @throws IllegalArgumentException
* if the given list doesn't contain the same {@link Converter}
* as the {@link Pipe}.
*/
public void reorderConverter(final List<Converter> ordered)
throws StateException {
ensureConstructed();
if (converterList.size() != ordered.size())
throw new IllegalArgumentException("Size of lists differ");
for (final Converter pp : converterList)
if (!ordered.contains(pp))
throw new IllegalArgumentException(String.format(
"Missed in ordered list: '%s'", pp));
converterList.clear();
converterList.addAll(ordered);
}
/**
* Rearranges all {@link Output}s according to the given list.
*
* @param ordered
* list of all {@link Output}s of the {@link Pipe} in the new
* order
* @throws StateException
* if the {@link Pipe} is being constructed or already
* initialized
* @throws IllegalArgumentException
* if the given list doesn't contain the same {@link Output}s as
* the {@link Pipe}.
*/
public void reorderOutputs(final List<Output> ordered)
throws StateException {
ensureConstructed();
if (outputList.size() != ordered.size())
throw new IllegalArgumentException("Size of lists differ");
for (final Output pp : outputList)
if (!ordered.contains(pp))
throw new IllegalArgumentException(String.format(
"Missed in ordered list: '%s'", pp));
outputList.clear();
outputList.addAll(ordered);
}
private void resolveConnectionUIDs() throws StateException {
// create a map from UID to PipePart
final Map<Long, PipePart> map = new HashMap<Long, PipePart>();
for (final PipePart pp : inputList)
if (map.put(pp.getUID(), pp) != null)
throw new InternalException("UIDs not really unique");
for (final PipePart pp : converterList)
if (map.put(pp.getUID(), pp) != null)
throw new InternalException("UIDs not really unique");
for (final PipePart pp : outputList)
if (map.put(pp.getUID(), pp) != null)
throw new InternalException("UIDs not really unique");
// create the connections (if they are valid connections)
Log.detail("Resolving Connection-UIDs for all input");
for (final PipePart pp : inputList)
pp.resolveConnectionUIDs(map);
Log.detail("Resolving Connection-UIDs for all converter");
for (final PipePart pp : converterList)
pp.resolveConnectionUIDs(map);
Log.detail("Resolving Connection-UIDs for all output");
for (final PipePart pp : outputList)
pp.resolveConnectionUIDs(map);
}
/**
* @return all {@link PipePart}s which are sane according to
* {@link PipePart#topDownCheck(Map, Set, Set)}
*/
public Map<PipePart, String> getSanePipeParts() {
final Map<PipePart, String> sane = new HashMap<PipePart, String>();
final Set<PipePart> visited = new HashSet<PipePart>();
final Set<PipePart> deadLocked = new HashSet<PipePart>();
final List<PipePart> copy = new ArrayList<PipePart>(inputList);
Log.detail("Starting top-down check for sanity");
for (final PipePart pp : copy)
pp.topDownCheck(sane, visited, deadLocked);
for (final PipePart pp : deadLocked) {
final String sc = sane.get(pp);
sane.put(pp, sc == null ? "Deadlocked" : sc + "\nDeadlocked");
}
for (final PipePart pp : converterList)
if (!sane.containsKey(pp))
sane.put(pp, "Cannot be reached by any Input");
for (final PipePart pp : outputList)
if (!sane.containsKey(pp))
sane.put(pp, "Cannot be reached by any Input or Converter");
return sane;
}
private void checkSanity() throws PipeException {
final Map<PipePart, String> sane = getSanePipeParts();
final StringBuilder sb = new StringBuilder("The following PipeParts "
+ "are not correctly configured or connected:");
boolean found = false;
for (final Entry<PipePart, String> e : sane.entrySet())
if (e.getValue() != null) {
found = true;
sb.append("\n");
sb.append(e.getKey());
sb.append(": ");
sb.append(e.getValue());
}
if (found) throw new PipeException(this, true, sb.toString());
}
@Override
protected void configure0() throws PipeException {
Log.detail("Configuring all input");
for (final PipePart pp : inputList) {
pp.assertAllConnectionUIDsResolved();
if (!pp.tryConfigure()) ignoredInputs.add(pp);
}
Log.detail("Configuring all converter");
for (final PipePart pp : converterList) {
pp.assertAllConnectionUIDsResolved();
if (!pp.tryConfigure()) ignoredConverter.add(pp);
}
Log.detail("Configuring all output");
for (final PipePart pp : outputList) {
pp.assertAllConnectionUIDsResolved();
if (!pp.tryConfigure()) ignoredOutputs.add(pp);
}
}
@Override
protected void init0() throws PipeException {
checkSanity();
Log.detail("Initializing all input");
for (final PipePart pp : inputList) {
if (!pp.tryInit()) ignoredInputs.add(pp);
if (initPhaseInterrupted) return;
}
Log.detail("Initializing all converter");
for (final PipePart pp : converterList) {
if (!pp.tryInit()) ignoredConverter.add(pp);
if (initPhaseInterrupted) return;
}
Log.detail("Initializing all output");
for (final PipePart pp : outputList) {
if (!pp.tryInit()) ignoredOutputs.add(pp);
if (initPhaseInterrupted) return;
}
}
@Override
protected void close0() throws PipeException {
synchronized (this) {
if (!thrsInput.isEmpty() || thrOutput != null)
throw new PipeException(this, false,
"Background threads are still alive");
}
Log.detail("Closing all input");
for (final PipePart pp : inputList)
if (!ignoredInputs.contains(pp)
&& pp.getState() == State.Initialized) pp.tryClose();
Log.detail("Closing all converter");
for (final PipePart pp : converterList)
if (!ignoredConverter.contains(pp)
&& pp.getState() == State.Initialized) pp.tryClose();
Log.detail("Closing all output");
for (final PipePart pp : outputList)
if (!ignoredOutputs.contains(pp)
&& pp.getState() == State.Initialized) pp.tryClose();
ignoredInputs.clear();
ignoredConverter.clear();
ignoredOutputs.clear();
}
/**
* Starts two threads which pipe all data of all connected {@link Input}s
* through all connected {@link Converter} to all connected {@link Output}s.<br>
* Waits until both threads have finished.<br>
* The {@link Pipe} is initialized before starting and closed after
* finishing.
*
* @throws PipeException
* if the object is not already initialized
* @throws InterruptedException
* if any thread has interrupted the current thread while
* waiting for the two pipe threads
*/
public void pipeAllData() throws PipeException, InterruptedException {
ensureConfigured();
initPhaseInterrupted = false;
feedback = new PipeFeedback();
try {
pipeInitializing = true;
init();
} finally {
pipeInitializing = false;
}
if (initPhaseInterrupted) {
close();
return;
}
dataQueue.resetCache();
assert thrsInput.isEmpty();
createNewInputThread(new ArrayList<Input>(inputList));
thrOutput = new Thread("Pipe-Output-Thread") {
@Override
public void run() {
try {
runOutputThread();
} catch (final Throwable t) { // CS_IGNORE
Log.error(t, "Output-Thread died");
getFeedback().addError(t, true);
}
}
};
feedback.started();
thrOutput.start();
mainInputThread = thrsInput.get(0);
thrsInput.get(0).start();
Log.detail("Started waiting for threads");
while (thrOutput.isAlive())
Thread.sleep(100);
Log.detail("Output Thread no longer alive");
// wait up to 3 seconds ...
int remThrCnt = Integer.MAX_VALUE;
for (int i = 0; i < 30 && remThrCnt > 0; ++i) {
Thread.sleep(100);
synchronized (this) {
remThrCnt = thrsInput.size();
}
}
// ... then interrupt remaining input threads ...
if (remThrCnt > 0) {
Log.error("%d Input-Thread(s) still alive but "
+ "Output-Thread died", remThrCnt);
inputThreadInterruped = true;
synchronized (this) {
for (final Thread thr : thrsInput)
thr.interrupt();
}
while (true) {
synchronized (this) {
if (thrsInput.isEmpty()) break;
}
Thread.sleep(100);
}
}
// .. and wait till they finally finished
Log.detail("Input Thread no longer alive");
feedback.stopped();
assert thrsInput.isEmpty();
assert mainInputThread == null;
thrOutput = null;
close();
Log.info("Pipe finished and closed");
}
public Thread createNewInputThread(final List<Input> inputSubList) {
synchronized (this) {
for (final Input in : inputSubList)
in.incThreadReferenceCounter();
}
final Thread thr = new Thread("Pipe-Input-Thread") {
@Override
public void run() {
try {
runInputThread(inputSubList);
} catch (final Throwable t) { // CS_IGNORE
Log.error(t, "Input-Thread died");
getFeedback().addError(t, true);
}
}
};
thrsInput.add(thr);
return thr;
}
/**
* Aborts the pipe if one is currently running.<br>
* Note that {@link #pipeAllData()} itself blocks until the pipe has
* finished, so {@link #abortPipe()} only makes sense if
* {@link #pipeAllData()} is called from another thread. <br>
* This method waits until the abort has been accepted.
*
* @throws StateException
* if the object is not already initialized
* @throws InterruptedException
* if waiting has been interrupted
*/
public void abortPipe() throws StateException, InterruptedException {
Log.info("Aborting pipe");
inputThreadInterruped = true;
initPhaseInterrupted = true;
synchronized (this) {
for (final Thread thr : thrsInput)
thr.interrupt();
}
dataQueue.close();
if (thrOutput != null) thrOutput.interrupt();
Log.detail("Waiting for accepted abort in threads");
while (true) {
synchronized (this) {
if (!pipeInitializing && thrsInput.isEmpty()
&& thrOutput == null) break;
}
Thread.sleep(100);
}
Log.info("Pipe successfully aborted");
}
/**
* This is the run() method of the Input-Thread.<br>
* It fetches {@link Data} from the {@link Input}s, passes it to the
* {@link Converter} and puts it into the {@link DataQueue} in a loop until
* all {@link Input}s have finished or the thread gets interrupted.
*
* @param inputSubList
* the list of {@link Input}s for this Input-Thread
* @throws IOException
* if the {@link DataQueue} has been closed during looping
*/
protected void runInputThread(final List<Input> inputSubList)
throws IOException {
inputThreadInterruped = false;
try {
Log.info("Input-Thread started");
lastTime = 0;
while (!inputThreadInterruped) {
try {
ensureInitialized();
} catch (final StateException e) {
throw new InternalException(e);
}
// read next data block ...
final Data data = getFromInput(inputSubList);
if (data == null) {
Log.info("No more Inputs");
break; // marks end of all inputs
}
// ... and convert it
convertDataToDataList(data);
}
} finally {
synchronized (this) {
if (Thread.currentThread() == mainInputThread)
mainInputThread = null;
if (!thrsInput.remove(Thread.currentThread()))
Log.error("Internal error: "
+ "Input-Thread not found in thread-list");
if (thrsInput.isEmpty()) dataQueue.close();
}
Log.info("Input-Thread finished");
}
}
/**
* This is the run() method of the Output-Thread.<br>
* It fetches {@link Data} from the {@link DataQueue} and passes it to the
* {@link Output}s in a loop until the {@link DataQueue} has been closed.
* <p>
* If the thread gets interrupted, only writing of the current {@link Data}
* will be aborted. To interrupt the thread itself, one has to close the
* {@link DataQueue}.
*/
protected void runOutputThread() {
Log.info("Output-Thread started");
try {
while (true) {
try {
ensureInitialized();
} catch (final StateException e) {
throw new InternalException(e);
}
// fetch next data block ...
final Data data;
try {
data = dataQueue.get();
} catch (final InterruptedException e1) {
Log.detail("Reading next data has been interrupted");
feedback.incInterruptionCount();
continue;
}
if (data == null) break; // Input-Thread has finished piping
// There's no need to continue if we have no more outputs
if (ignoredOutputs.size() == outputList.size()) break;
// ... wait for the correct time, if needed ...
if (!waitForOutputTime(data)) continue;
// ... and send it to all currently registered outputs
writeDataToAllOutputs(data);
}
} finally {
Log.info("Output-Thread finished");
Log.detail("Sent %d data blocks to output",
feedback.getDataOutputCount());
}
}
private boolean waitForOutputTime(final Data data) {
if (data.getTime() == Data.TIME_NOTIME) return true;
final long execTime = feedback.getStartTime() + data.getTime();
final long delta = execTime - System.currentTimeMillis()
- cfgOutputInitOverhead.getContent();
if (delta > 0) {
Log.detail("Waiting %d ms", delta);
try {
Thread.sleep(delta);
} catch (final InterruptedException e) {
Log.error(e, "Failed to wait %d ms for "
+ "correct output time", delta);
// no incInterruptionCount() here
return false;
}
return true;
}
final boolean significant = delta < -cfgMaxBehind.getContent();
data.getOrigin().getFeedback().incBehindCount(-delta, significant);
feedback.incBehindCount(-delta, significant);
if (significant)
Log.warn("Output of '%s' is %d ms behind (should have been "
+ "executed at '%s')", data, -delta,
DATE_FORMATTER.format(new Date(execTime)));
else if (Log.canLog(Log.Type.Detail))
Log.detail("Output of '%s' is %d ms behind (should have been "
+ "executed at '%s')", data, -delta,
DATE_FORMATTER.format(new Date(execTime)));
return true;
}
/**
* Tries to read one {@link Data} block from the currently active
* {@link Input}.<br>
* If the {@link Input} has no more {@link Data} available or it fails, the
* next {@link Input} in the list will be used.<br>
* If there are no more available {@link Input}s, <b>null</b> will be
* returned.
*
* @param inputSubList
* the list of {@link Input}s for the current Input-Thread
* @return a new {@link Data} or <b>null</b> if no {@link Input} in the list
* has any more available {@link Data}
*/
private Data getFromInput(final List<Input> inputSubList) {
Log.detail("Reading one data block from input");
Input in;
while (true) {
if (inputThreadInterruped) return null;
if (inputSubList.isEmpty()) {
Log.detail("Finished InputList");
return null;
}
in = inputSubList.get(0);
if (ignoredInputs.contains(in)) {
Log.detail("Skipping input '%s' which failed "
+ "in config/init phase", in);
inputSubList.remove(0);
in.decThreadReferenceCounter();
continue;
}
Log.detail("Trying input '%s'", in);
try {
final Data res = in.readData();
if (res != null) {
feedback.incDataInputCount();
// found a valid data block
res.setOrigin(in);
lastTime = res.rememberTime(this, lastTime);
return res;
}
} catch (final InputException e) {
Log.error(e);
in.getFeedback().addError(e, e.isPermanent());
feedback.addError(e, e.isPermanent());
if (e.isPermanent()) {
Log.info("Skipping no longer working input '%s'", in);
removeFromInputList(inputSubList, in);
} else
Log.info("Skipping one data block from input '%s'", in);
// try next data block / try from next input
continue;
} catch (final Throwable e) { // CS_IGNORE catch any Input-problems
Log.error(e);
in.getFeedback().addError(e, false);
feedback.addError(e, false);
Log.info("Skipping one data block from input '%s'", in);
// try next data block / try from next input
continue;
}
// no more data available in this Input, so
// switch to the next one
Log.info("Switching to next input");
removeFromInputList(inputSubList, in);
}
}
private void removeFromInputList(final List<Input> inputSubList,
final Input in) {
inputSubList.remove(0);
if (in.decThreadReferenceCounter() == 0) in.tryClose();
}
/**
* Puts all the {@link Data} to the {@link DataQueue}.
* <p>
* Drops the {@link Data} if it's priority is lower than the one in the
* queue. <br>
* Clears the queue and interrupts the output thread if the {@link Data}'s
* priority is higher than the one in the queue.
* <p>
* If a time is specified for the {@link Data} this method will wait for the
* correct time before it decides whether the {@link Data} has to be dropped
* or the queue be cleared.<br>
* Immediately returns if sleeping for timed {@link Data} has been
* interrupted.
*
* @param data
* {@link Data} object to put into the {@link DataQueue}
* @throws IOException
* if the {@link DataQueue} has been closed
*/
private void putIntoOutputQueue(final Data data) throws IOException {
// if time-to-wait is positive we wait here before we are
// forced to immediately drop a data block or clear the queue
if (data.getTime() != Data.TIME_NOTIME) {
final long execTime = feedback.getStartTime() + data.getTime();
final long delta = execTime - System.currentTimeMillis()
- cfgOverheadReductionTime.getContent();
if (delta > 0) {
Log.detail("Waiting %d ms", delta);
try {
Thread.sleep(delta);
} catch (final InterruptedException e) {
Log.error(e, "Failed to wait %d ms for "
+ "correct output time", delta);
// no incInterruptionCount() here
return;
}
}
}
if (Log.canLogDetail())
Log.detail("Currently on queue: " + dataQueue.getAll());
final PutResult res = dataQueue.put(data);
switch (res) {
case ClearedAndPut:
Log.info("Canceling current command, "
+ "because of higher-priority command '%s'", data);
thrOutput.interrupt();
feedback.incDropCount(dataQueue.getSizeBeforeClear());
break;
case Dropped:
data.getOrigin().getFeedback().incDropCount();
feedback.incDropCount();
break;
case Put:
break;
default:
throw new InternalException(res);
}
}
/**
* Converts one {@link Data} object to a list of {@link Data} objects if a
* fitting {@link Converter} can be found. Otherwise the {@link Data} object
* itself is returned.<br>
* This method recursively calls {@link #convertOneData(Data, Converter)}
* which again calls this method.
*
* @param data
* The {@link Data} object to be converted.
* @return A single-element list holding the data object given by
* <b>data</b> if no fitting {@link Converter} could be found or a
* list of new {@link Data} objects returned by the first fitting
* {@link Converter}.
* @throws IOException
* if the DataQueue has been closed
*/
private List<Data> convertDataToDataList(final Data data)
throws IOException {
Log.detail("Converting data block to list of data blocks");
List<Data> res = null;
final List<Data> sum = new ArrayList<Data>();
boolean found = false;
boolean outputExists = false;
for (final PipePart pp : data.getOrigin().getConnectedPipeParts()) {
if (pp instanceof Converter && !ignoredConverter.contains(pp)
&& (res = convertOneData(data, (Converter) pp)) != null) {
found = true;
sum.addAll(res);
}
if (pp instanceof Output && !ignoredOutputs.contains(pp))
outputExists = true;
}
if (outputExists) putIntoOutputQueue(data);
if (found) return sum;
// no fitting (and not ignored and working) converter found
Log.detail("No Converter found, returning data as is: '%s'", data);
res = new ArrayList<Data>(1);
res.add(data);
return res;
}
/**
* Tries to convert the given {@link Data} block with the {@link Converter}.
* The converter is added to the {@link #ignoredConverter} list if it fails
* permanently during the conversion.<br>
* All {@link Data}s returned by the converter will immediately be converted
* again.<br>
* This method recursively calls {@link #convertDataToDataList(Data)} which
* again calls this method.
*
* @param data
* {@link Data} to convert
* @param cvt
* {@link Converter} to use for conversion
* @return list of {@link Data} created from {@link Converter} or
* <b>null</b> if the {@link Converter} could not handle the
* {@link Data} or an error occurred during conversion
*/
private List<Data> convertOneData(final Data data, final Converter cvt) {
try {
Log.detail("Converting '%s' with '%s'", data, cvt);
feedback.incDataConvertedCount();
visualizePipeFlow(data.getOrigin(), cvt);
final List<Data> newDatas = cvt.convert(data);
if (newDatas != null) {
final List<Data> res = new ArrayList<Data>(newDatas.size());
for (final Data newData : newDatas) {
newData.setOrigin(cvt);
res.addAll(convertDataToDataList(newData));
}
return res;
}
} catch (final ConverterException e) {
Log.error(e);
cvt.getFeedback().addError(e, e.isPermanent());
feedback.addError(e, e.isPermanent());
if (e.isPermanent()) {
Log.info("Removing no longer working converter '%s'", cvt);
cvt.tryClose();
ignoredConverter.add(cvt);
} else
Log.info("Skipping converter '%s' for one data block '%s'",
cvt, data);
} catch (final Throwable e) { // CS_IGNORE catch all what may go wrong
Log.error(e);
cvt.getFeedback().addError(e, false);
feedback.addError(e, false);
Log.info("Skipping converter '%s' for one data block '%s'", cvt,
data);
}
return null;
}
/**
* Writes the given {@link Data} to all {@link Output}s connected to
* {@link Data}'s origin, ignoring those which have permanently failed.<br>
* Complains if the {@link Data} has not been accepted by at least one
* {@link Output}.
*
* @param data
* {@link Data} to write
*/
private void writeDataToAllOutputs(final Data data) {
Log.detail("Writing data block '%s' to %d output(s)", data,
outputList.size());
boolean foundOne = false;
for (final PipePart trg : data.getOrigin().getConnectedPipeParts())
if (trg instanceof Output && !ignoredOutputs.contains(trg))
foundOne |= writeToOutput(data, (Output) trg);
if (!foundOne) {
final Throwable t = new OutputException(null, false,
"Skipping data block '%s' because no fitting "
+ "output has been found", data);
Log.error(t);
feedback.addError(t, false);
}
}
/**
* Writes one {@link Data} object to one {@link Output}.
*
* @param data
* {@link Data} to write
* @param out
* {@link Output} to write to
* @return true if the {@link Output} accepted the {@link Data}
*/
private boolean writeToOutput(final Data data, final Output out) {
try {
visualizePipeFlow(data.getOrigin(), out);
final boolean succeeded = out.write(data);
// data.setOrigin(out);
if (!succeeded) return false;
} catch (final OutputException e) {
// data.setOrigin(out);
Log.error(e);
if (!e.isPermanent()
&& e.getCause() instanceof InterruptedException) {
out.getFeedback().incExecutionInterruptedCount();
data.getOrigin().getFeedback().incInterruptionCount();
feedback.incInterruptionCount();
} else {
out.getFeedback().addError(e, e.isPermanent());
feedback.addError(e, e.isPermanent());
}
if (e.isPermanent()) {
Log.info("Removing no longer working output '%s'", out);
out.tryClose();
ignoredOutputs.add(out);
} else
Log.info("Skipping output '%s' for one data block '%s'", out,
data);
} catch (final Throwable e) { // CS_IGNORE catch all what may go wrong
// data.setOrigin(out);
Log.error(e);
out.getFeedback().addError(e, false);
feedback.addError(e, false);
Log.info("Skipping output '%s' for one data block '%s'", out, data);
}
feedback.incDataOutputCount(data.getPriority() >= Data.PRIO_DEFAULT);
return true;
}
/**
* Removes all connected {@link PipePart}s.
*
* @throws PipeException
* if {@link Pipe} is not configured or currently initialized
*/
public void reset() throws PipeException {
ensureNoLongerInitialized();
for (final PipePart pp : inputList)
pp.disconnectedFromPipe(this);
for (final PipePart pp : converterList)
pp.disconnectedFromPipe(this);
for (final PipePart pp : outputList)
pp.disconnectedFromPipe(this);
inputList.clear();
converterList.clear();
outputList.clear();
assert ignoredInputs.isEmpty();
assert ignoredConverter.isEmpty();
assert ignoredOutputs.isEmpty();
}
@Override
public Group getSkeleton(final String groupName) {
if (groupName.equals(getClass().getSimpleName()))
return new Group(groupName).add(cfgMaxBehind)
.add(cfgOutputInitOverhead).add(cfgOverheadReductionTime)
.add(cfgLastSaveFile).add(cfgModifiedSinceSave);
final String prefix = getClass().getSimpleName() + ":";
if (!groupName.startsWith(prefix))
throw new InternalException("Wrong groupName for "
+ "skeleton creation: '%s' should start with '%s'",
groupName, prefix);
final String name = groupName.substring(prefix.length()).trim();
try {
for (final Class<? extends PipePart> pp : PipePartDetection.ALL_PIPEPART)
if (pp.getSimpleName().equals(name))
return pp.newInstance().getGroup();
throw new ConfigurationException("Cannot find any PipePart "
+ "with class-name '%s'", name);
} catch (final ConfigurationException e) {
Log.error(e, "Skipped reading '%s' from config:", groupName);
return null;
} catch (final InstantiationException e) {
Log.error(e, "Skipped reading '%s' from config:", groupName);
return null;
} catch (final IllegalAccessException e) {
Log.error(e, "Skipped reading '%s' from config:", groupName);
return null;
}
}
@Override
public void configurationAboutToBeChanged() throws ConfigurationException {
try {
reset();
} catch (final PipeException e) {
throw new ConfigurationException(e, "Cannot change configuration");
}
}
@Override
public void configurationRead() {
final String prefix = getClass().getSimpleName() + ":";
synchronized (config) {
for (final Group g : config.getGroupsUnassigned())
if (g.getName().startsWith(prefix)) {
Log.error("Unknown PipePart which could not be "
+ "read from configuration: '%s'", g);
config.removeUnassignedGroup(g);
// need to recursively restart the loop because of
// modification
configurationRead();
break;
}
}
}
@Override
public void configurationChanged(final Group group)
throws ConfigurationException {
try {
ensureNoLongerInitialized();
} catch (final StateException e) {
throw new ConfigurationException(e, "Cannot change configuration");
}
if (group.getUser() instanceof PipePart) {
final PipePart pp = (PipePart) group.getUser();
if (pp instanceof Input)
inputList.add((Input) pp);
else if (pp instanceof Converter)
converterList.add((Converter) pp);
else if (pp instanceof Output)
outputList.add((Output) pp);
else
throw new InternalException(
"Superclass of PipePart '%s' unknown", pp);
try {
pp.connectedToPipe(this);
resolveConnectionUIDs();
pp.configure();
} catch (final PipeException e) {
throw new ConfigurationException(e,
"Cannot configure PipePart with group '%s'", group);
}
} else if (!group.getName().equals(getClass().getSimpleName()))
// this may occur if getSkeleton() returned null due to an error
// only detail here because getSkeleton() should have already
// printed some warning or error
Log.detail("Cannot handle unknown group '%s'", group.getName());
}
@Override
public List<Group> configurationWriteback() throws ConfigurationException {
final List<Group> res = new ArrayList<Group>();
res.add(getSkeleton(getClass().getSimpleName()));
for (final PipePart pp : inputList) {
pp.groupWriteback();
res.add(pp.getGroup());
}
for (final PipePart pp : converterList) {
pp.groupWriteback();
res.add(pp.getGroup());
}
for (final PipePart pp : outputList) {
pp.groupWriteback();
res.add(pp.getGroup());
}
return res;
}
public PipeFeedback getFeedback() {
return feedback;
}
public File getLastSaveFile() {
return cfgLastSaveFile.getContent();
}
public void setLastSaveFile(final File file) throws ConfigurationException {
cfgLastSaveFile.setContent(file);
cfgModifiedSinceSave.setContent(false);
}
@Override
public String toString() {
return String.format("%s: %s - %s - %s <%s> %s", getClass()
.getSimpleName(), inputList, converterList, outputList,
getState(), getFeedback());
}
public boolean isInitPhaseInterrupted() {
return initPhaseInterrupted;
}
public void modified() {
if (MainFrame.hasGUI()) {
final MainPipePanel mpp = MainFrame.the().getMainPipePanel();
if (mpp != null) {
final PipeFlowVisualization pfv = mpp
.getPipeFlowVisualization();
if (MainFrame.the().getPipe() == this)
mpp.timeUpdatePipeLabel();
if (pfv != null) pfv.modified();
}
}
cfgModifiedSinceSave.setContent(true);
}
private void visualizePipeFlow(final PipePart src, final PipePart dst) {
if (MainFrame.hasGUI()) {
final PipeFlowVisualization pfv = MainFrame.the()
.getMainPipePanel().getPipeFlowVisualization();
if (pfv != null) pfv.addPipeFlow(src, dst);
}
}
public String getTitle() {
String res = getLastSaveFile().getName();
if (res.contains(".")) res = res.substring(0, res.lastIndexOf('.'));
if (cfgModifiedSinceSave.getContent()) res += " [modified]";
return res;
}
public boolean isMainInputThreadFinished() {
return mainInputThread == null;
}
/**
* Returns the the {@link PipePart} with the given name. If there are more
* than one or none, <b>null</b> will be returned.
*
* @param part
* the full name of a {@link PipePart} as returned from
* {@link HelpKind#Name}
* @return {@link PipePart} or <b>null</b>
*/
public PipePart findByName(final String part) {
PipePart res = null;
for (final PipePart pp : getInputList())
if (pp.getName().equals(part)) {
if (res != null) return null;
res = pp;
}
for (final PipePart pp : getConverterList())
if (pp.getName().equals(part)) {
if (res != null) return null;
res = pp;
}
for (final PipePart pp : getOutputList())
if (pp.getName().equals(part)) {
if (res != null) return null;
res = pp;
}
return res;
}
/**
* Returns the the {@link PipePart} with the given simple class name. If
* there are more than one or none, <b>null</b> will be returned.
*
* @param className
* the simple name of a {@link PipePart}'s class
* @return {@link PipePart} or <b>null</b>
*/
public PipePart findByClassName(final String className) {
PipePart res = null;
for (final PipePart pp : getInputList())
if (pp.getClass().getSimpleName().equals(className)) {
if (res != null) return null;
res = pp;
}
for (final PipePart pp : getConverterList())
if (pp.getClass().getSimpleName().equals(className)) {
if (res != null) return null;
res = pp;
}
for (final PipePart pp : getOutputList())
if (pp.getClass().getSimpleName().equals(className)) {
if (res != null) return null;
res = pp;
}
return res;
}
/**
* Returns the the {@link PipePart} with the given UID. If there is none,
* <b>null</b> will be returned.
*
* @param uid
* the UID of a {@link PipePart} as returned from
* {@link PipePart#getUID()}
* @return {@link PipePart} or <b>null</b>
*/
public PipePart findByUID(final long uid) {
for (final PipePart pp : getInputList())
if (pp.getUID() == uid) return pp;
for (final PipePart pp : getConverterList())
if (pp.getUID() == uid) return pp;
for (final PipePart pp : getOutputList())
if (pp.getUID() == uid) return pp;
return null;
}
}