// 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.awt.Rectangle; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import javax.swing.Icon; import pleocmd.ImmutableRectangle; import pleocmd.Log; import pleocmd.cfg.ConfigBoolean; import pleocmd.cfg.ConfigBounds; import pleocmd.cfg.ConfigCollection; import pleocmd.cfg.ConfigCollection.Type; import pleocmd.cfg.ConfigLong; import pleocmd.cfg.ConfigValue; import pleocmd.cfg.Group; import pleocmd.exc.ConfigurationException; import pleocmd.exc.InternalException; import pleocmd.exc.PipeException; import pleocmd.exc.StateException; import pleocmd.itfc.gui.dgr.Diagram; import pleocmd.itfc.gui.dgr.DiagramDataSet; import pleocmd.itfc.gui.dgr.PipeVisualizationDialog; import pleocmd.itfc.gui.icons.IconLoader; import pleocmd.pipe.cvt.Converter; import pleocmd.pipe.data.Data; import pleocmd.pipe.in.Input; import pleocmd.pipe.out.Output; /** * Base class of all {@link Input}s, {@link Converter} and {@link Output}s which * can be connected to the {@link Pipe}. * * @author oliver */ public abstract class PipePart extends StateHandling { private static final Random RAND = new Random(); private static final int BUILTIN_VIS = 0; public enum HelpKind { /** * Name of the Pipe-Part (only one line) */ Name, /** * A short description of this Pipe-Part (a few lines) */ Description, /** * Name of an HTML file displayed when clicking "Help" in the * configuration dialog (defaults to the class-name + ".html") */ HelpFile, /** * Name of an icon for this Pipe-Part (should be 16x16 pixels, defaults * to the class-name + "-icon.png") */ Icon, /** * Name of an image displayed on the left side of the configuration * dialog (should be no larger than 300x500 pixels, defaults to the * class-name + "-cfg.png") */ ConfigImage, /** * Short description of a configuration entry (a few lines) */ Config1, Config2, Config3, Config4, Config5, // Config6, Config7, Config8, Config9, Config10, // Config11, Config12, Config13, Config14, Config15, // Config16, Config17, Config18, Config19, Config20, // // Config... must be the last entries } private final ConfigLong cfgUID; private final Group group; private final List<ConfigValue> guiConfigs; private Pipe pipe; private final Set<PipePart> connected; private final ConfigCollection<Long> cfgConnectedUIDs; private final ConfigBounds cfgGuiPosition; private PipeVisualizationDialog visualizationDialog; private final ConfigBoolean cfgVisualize; private final PipePartVisualizationConfig visualizationConfig; private final ImmutableRectangle immutableGUIPosition; private String sanityCache; private String shortConfigDescr; private final PipePartFeedback feedback; public PipePart() { feedback = new PipePartFeedback(); group = new Group(Pipe.class.getSimpleName() + ": " + getClass().getSimpleName(), this); guiConfigs = new ArrayList<ConfigValue>(); connected = new HashSet<PipePart>(); group.add(cfgUID = new ConfigLong("UID", RAND.nextLong())); group.add(cfgConnectedUIDs = new ConfigCollection<Long>( "Connected UIDs", Type.Set) { @Override protected Long createItem(final String itemAsString) throws ConfigurationException { try { return Long.parseLong(itemAsString); } catch (final NumberFormatException e) { throw new ConfigurationException("Not a valid UID: " + itemAsString, e); } } }); group.add(cfgGuiPosition = new ConfigBounds("GUI-Position")); cfgGuiPosition.getContent().setBounds(0, 0, 0, 0); visualizationConfig = new PipePartVisualizationConfig(this, group); group.add(cfgVisualize = new ConfigBoolean("Visualization", false)); immutableGUIPosition = new ImmutableRectangle( cfgGuiPosition.getContent()); } final long getUID() { return cfgUID.getContent(); } /** * @return the {@link Group} with all available configuration for this * {@link PipePart}. Changes made to it will be ignored until * {@link #configure()} is called (again). */ public final Group getGroup() { return group; } public final void addConfig(final ConfigValue value) { addConfig(value, true); } protected final void addConfig(final ConfigValue value, final boolean visibleInGUI) { try { ensureConstructing(); } catch (final StateException e) { throw new IllegalStateException("Cannot add ConfigValue", e); } group.add(value); if (visibleInGUI) guiConfigs.add(value); } public final List<ConfigValue> getGuiConfigs() { return Collections.unmodifiableList(guiConfigs); } public final void addConfigToGUI(final ConfigValue value) { if (group.get(value.getLabel()) != value) throw new IllegalArgumentException("ConfigValue is not registered"); if (guiConfigs.contains(value)) throw new IllegalArgumentException("ConfigValue is already in GUI"); guiConfigs.add(value); } /** * @return <b>null</b> if everything is all-right or a {@link String} * describing why this {@link PipePart} will probably fail to * initialize if it would be initialized with it's current * configuration. */ protected abstract String isConfigurationSane(); /** * @return <b>null</b> if everything is all-right or a {@link String} * describing why this {@link PipePart} will probably fail to * initialize if it would be initialized with it's current * configuration. */ public final String isCachedConfigSane() { if (sanityCache == null) { sanityCache = isConfigurationSane(); if (sanityCache == null) sanityCache = ""; } return sanityCache; } public final void configValuesChanged() { sanityCache = null; shortConfigDescr = null; } /** * Tries to call {@link #configure()} and writes an error message to the * {@link Log} if configuring fails. * * @return true if configuring was successful */ protected final boolean tryConfigure() { try { feedback.incConfiguredCount(); configure(); return true; } catch (final PipeException e) { feedback.addError(e, e.isPermanent()); pipe.getFeedback().addError(e, e.isPermanent()); Log.error(e, "Cannot configure '%s'", getName()); return false; } } /** * Tries to call {@link #init()} and writes an error message to the * {@link Log} if initializing fails. * * @return true if initializing was successful */ protected final boolean tryInit() { try { init(); feedback.incInitializedCount(); feedback.started(); if (cfgVisualize.getContent()) createVisualization(); return true; } catch (final PipeException e) { feedback.addError(e, e.isPermanent()); pipe.getFeedback().addError(e, e.isPermanent()); Log.error(e, "Cannot initialize '%s'", getName()); return false; } } /** * Tries to call {@link #close()} and writes an error message to the * {@link Log} if closing fails. * * @return true if closing was successful */ protected final boolean tryClose() { try { feedback.stopped(); feedback.incClosedCount(); close(); return true; } catch (final PipeException e) { feedback.addError(e, e.isPermanent()); pipe.getFeedback().addError(e, e.isPermanent()); Log.error(e, "Cannot close '%s'", getName()); return false; } } @Override protected final void configure0() throws PipeException, IOException { configValuesChanged(); configure1(); } /** * Can contain special code which should be invoked in sub-classes during * configuration. * * @throws PipeException * if configuration fails * @throws IOException * if configuration fails */ protected void configure1() throws PipeException, IOException { // do nothing by default } public abstract String getInputDescription(); public abstract String getOutputDescription(); @Override public String toString() { // CS_IGNORE_PREV keep overridable final StringBuilder sb = new StringBuilder(); sb.append(getClass().getSimpleName()); if (!guiConfigs.isEmpty()) { sb.append(" ["); boolean first = true; for (final ConfigValue cv : guiConfigs) { if (!first) sb.append(", "); first = false; sb.append(cv.toString()); } sb.append("]"); } return sb.toString(); } /** * Returns the {@link Pipe} to which this {@link PipePart} is currently * connected. * * @return a {@link Pipe} * @throws IllegalArgumentException * if this {@link PipePart} is not connected to any {@link Pipe} */ public final Pipe getPipe() { if (pipe == null) throw new IllegalArgumentException("Not connected to any pipe"); return pipe; } /** * @return true if this {@link PipePart} is connected to a {@link Pipe}. */ public final boolean isConnected() { return pipe != null; } final void connectedToPipe(final Pipe newPipe) { String s1; assert (s1 = new Throwable().getStackTrace()[1].getClassName()) .equals(Pipe.class.getName()) : s1; if (newPipe == null) throw new NullPointerException("newPipe"); if (pipe != null) throw new IllegalArgumentException(String.format( "Cannot connect to pipe '%s': Already connected to '%s'", newPipe, pipe)); pipe = newPipe; Log.detail("Connected '%s' to '%s'", this, pipe); pipe.modified(); } final void disconnectedFromPipe(final Pipe curPipe) { String s1; assert (s1 = new Throwable().getStackTrace()[1].getClassName()) .equals(Pipe.class.getName()) : s1; closeVisualization(); if (curPipe == null) throw new NullPointerException("curPipe"); if (pipe == null) throw new IllegalArgumentException(String.format( "Cannot disconnect from pipe '%s': Not connected", curPipe)); if (pipe != curPipe) throw new IllegalArgumentException(String.format( "Cannot disconnect from pipe '%s': Connected to '%s'", curPipe, pipe)); pipe = null; Log.detail("Disconnected '%s' from '%s'", this, curPipe); curPipe.modified(); } public final Set<PipePart> getConnectedPipeParts() { return Collections.unmodifiableSet(connected); } public final void connectToPipePart(final PipePart target) throws StateException { ensureConstructed(); try { cfgConnectedUIDs.addContent(target.getUID()); } catch (final ConfigurationException e) { throw new InternalException(e); } connected.add(target); if (pipe != null) pipe.modified(); } public final void disconnectFromPipePart(final PipePart target) throws StateException { ensureConstructed(); connected.remove(target); cfgConnectedUIDs.removeContent(target.getUID()); if (pipe != null) pipe.modified(); } /** * @param s1 * first string * @param s2 * second string * @return true only if both strings contain something and that is not the * same. */ private static boolean strDiffer(final String s1, final String s2) { return s1 != null && s2 != null && !s1.isEmpty() && !s2.isEmpty() && !s1.equals(s2); } public final boolean isConnectionAllowed(final PipePart trg) { return this != trg && !(trg instanceof Input) && isConnectionAllowed0(trg) && !strDiffer(getOutputDescription(), trg.getInputDescription()); } protected abstract boolean isConnectionAllowed0(final PipePart trg); /** * Throws an exception when there are any unresolved connections.<br> * The unresolved UIDs will be removed from the internal UID-list. * * @throws PipeException * an exception containing the list of unresolved connections. */ protected final void assertAllConnectionUIDsResolved() throws PipeException { if (cfgConnectedUIDs.getContent().size() != connected.size()) { final Set<Long> goodUIDs = new HashSet<Long>(); final Set<Long> badUIDs = new HashSet<Long>(); for (final Long trgUID : cfgConnectedUIDs.getContent()) { boolean found = false; for (final PipePart pp : connected) if (pp.getUID() == trgUID) { found = true; break; } if (found) goodUIDs.add(trgUID); else badUIDs.add(trgUID); } try { cfgConnectedUIDs.setContent(goodUIDs); } catch (final ConfigurationException e) { throw new InternalException(e); } throw new PipeException(this, true, "Some UIDs could not " + "be resolved: %s. Check connections of Pipe.", badUIDs); } } /** * Resolves all UIDs from connected {@link PipePart}s, while missing ones * are ignored. The internal connection-list only contains those that could * be resolved afterwards. * * @param map * a mapping between UIDs and {@link PipePart}s currently known * to the {@link Pipe} * @throws StateException * if the {@link PipePart} is being constructed or already * initialized */ protected final void resolveConnectionUIDs(final Map<Long, PipePart> map) throws StateException { ensureConstructed(); connected.clear(); for (final Long trgUID : cfgConnectedUIDs.getContent()) if (map.containsKey(trgUID)) connected.add(map.get(trgUID)); } public final void setGuiPosition(final Rectangle guiPosition) { cfgGuiPosition.setContent(guiPosition); if (pipe != null) pipe.modified(); } public final ImmutableRectangle getGuiPosition() { return immutableGUIPosition; } protected final void groupWriteback() throws ConfigurationException { visualizationConfig.writeback(); groupWriteback0(); } protected void groupWriteback0() { // do nothing by default } public final boolean isVisualize() { return cfgVisualize.getContent(); } public final void setVisualize(final boolean visualize) { cfgVisualize.setContent(visualize); if (getState() == State.Initialized) if (visualize) createVisualization(); else closeVisualization(); if (pipe != null) pipe.modified(); } private void createVisualization() { final int visDataCount = getVisualizeDataSetCount() + BUILTIN_VIS; if (visualizationDialog != null) { visualizationDialog.reset(visDataCount); initVisualize0(); return; } visualizationDialog = new PipeVisualizationDialog(this, visDataCount); visualizationConfig.assignConfig(); initVisualize0(); visualizationDialog.setVisible(true); } protected void initVisualize0() { // do nothing by default } private void closeVisualization() { if (visualizationDialog == null) return; try { visualizationConfig.writeback(); } catch (final ConfigurationException e) { Log.error(e, "Cannot write back visualization configuration"); } visualizationDialog.dispose(); visualizationDialog = null; } /** * Invoked directly after {@link #init0()} and if the user enables * visualization while the {@link Pipe} is already running.<br> * Must return a constant value while being initialized. * * @return the number of {@link DiagramDataSet}s which will be plotted via * {@link #plot(int, double)}. */ protected abstract int getVisualizeDataSetCount(); public final PipeVisualizationDialog getVisualizationDialog() { return visualizationDialog; } protected final DiagramDataSet getVisualizeDataSet(final int index) { return visualizationDialog == null ? null : visualizationDialog .getDataSet(index + BUILTIN_VIS); } /** * Don't use this from any other than {@link Input}, {@link Output} and * {@link Converter}. * * @param index * real index of {@link DiagramDataSet} * @param x * value for x-axis * @param y * value for y-axis */ protected final void plot0(final int index, final double x, final double y) { String s1; assert (s1 = new Throwable().getStackTrace()[1].getClassName()) .equals(Pipe.class.getName()) || s1.equals(Input.class.getName()) || s1.equals(Converter.class.getName()) || s1.equals(Output.class.getName()) : s1; if (visualizationDialog != null) { visualizationDialog.plot(index, x, y); feedback.incDataPlotCount(); } } /** * Plots a value to the visualization {@link Diagram}.<br> * Because this x-axis always represents the currently elapsed time, * {@link #plot(int, double)} may be more appropriate in most cases. * * @param index * index of {@link DiagramDataSet} * @param x * value for x-axis * @param y * value for y-axis */ protected final void plot(final int index, final double x, final double y) { plot0(index + BUILTIN_VIS, x, y); } /** * Plots a value to the visualization {@link Diagram} with the currently * elapsed time of the {@link Pipe} as value for x-axis. * * @param index * index of {@link DiagramDataSet} * @param y * value for y-axis */ protected final void plot(final int index, final double y) { plot0(index + BUILTIN_VIS, Double.NaN, y); } /** * Checks if this {@link PipePart} is sane. Sane means that the * {@link PipePart} can be reached from an {@link Input}, has a path to an * {@link Output}, doesn't contain a dead-lock in it's path and is correctly * configured. * * @param sane * {@link PipePart} is added to this map. The value assigned to * it will be <b>null</b> if it is sane or some {@link String} * otherwise. * @param visited * a set of already visited {@link PipePart}s during the current * recursion (handled like a kind of stack) to detect dead-locks * @param deadLocked * a set of already detected dead-locks * @return true if an {@link Output} can be reached from the * {@link PipePart}. */ final boolean topDownCheck(final Map<PipePart, String> sane, final Set<PipePart> visited, final Set<PipePart> deadLocked) { if (visited.contains(this)) { deadLocked.add(this); return false; } boolean outputReached = topDownCheck_outputReached(); boolean validConns = true; visited.add(this); final List<PipePart> copy = new ArrayList<PipePart>( getConnectedPipeParts()); for (final PipePart ppSub : copy) { outputReached |= ppSub.topDownCheck(sane, visited, deadLocked); validConns &= isConnectionAllowed(ppSub); } visited.remove(this); final StringBuilder sbError = new StringBuilder(); if (!outputReached) sbError.append("An Output-PipePart cannot be reached\n"); if (!validConns) sbError.append("One or more invalid connections attached\n"); final String scc = isCachedConfigSane(); if (!scc.isEmpty()) sbError.append(scc + "\n"); if (sbError.length() == 0) sane.put(this, null); else { sbError.delete(sbError.length() - 1, sbError.length()); sane.put(this, sbError.toString()); } return outputReached; } protected boolean topDownCheck_outputReached() { // CS_IGNORE // design for extension: *is* empty return false; } protected static List<Data> asList(final Data data) { final List<Data> res = new ArrayList<Data>(1); res.add(data); return res; } protected static List<Data> emptyList() { return new ArrayList<Data>(0); } public final String getShortConfigDescr() { if (shortConfigDescr == null) shortConfigDescr = getShortConfigDescr0(); return shortConfigDescr; } protected abstract String getShortConfigDescr0(); public final String getName() { return getName(getClass()); } public static final String getName(final Class<? extends PipePart> ppc) { return PipePartDetection.callHelp(ppc, HelpKind.Name); } public final String getDescription() { return getDescription(getClass()); } public static final String getDescription( final Class<? extends PipePart> ppc) { return PipePartDetection.callHelp(ppc, HelpKind.Description); } public final String getHelpFile() { return getHelpFile(getClass()); } public static final String getHelpFile(final Class<? extends PipePart> ppc) { final String name = PipePartDetection.callHelp(ppc, HelpKind.HelpFile); return name == null ? ppc.getSimpleName() + ".html" : name; } public final String getConfigHelp(final int index) { return getConfigHelp(getClass(), index); } public static final String getConfigHelp( final Class<? extends PipePart> ppc, final int index) { final int cfgIndex = HelpKind.Config1.ordinal() + index; if (cfgIndex >= HelpKind.values().length) return null; return PipePartDetection.callHelp(ppc, HelpKind.values()[cfgIndex]); } public final Icon getConfigImage() { return getConfigImage(getClass()); } public static final Icon getConfigImage(final Class<? extends PipePart> ppc) { String name = PipePartDetection.callHelp(ppc, HelpKind.ConfigImage); if (name == null) name = ppc.getSimpleName() + "-cfg.png"; return IconLoader.isIconAvailable(name) ? IconLoader.getIcon(name) : null; } public final Icon getIcon() { return getIcon(getClass()); } public static final Icon getIcon(final Class<? extends PipePart> ppc) { String name = PipePartDetection.callHelp(ppc, HelpKind.Icon); if (name == null) name = ppc.getSimpleName() + "-icon.png"; return IconLoader.isIconAvailable(name) ? IconLoader.getIcon(name) : null; } public final PipePartFeedback getFeedback() { return feedback; } }