// 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.data; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInput; import java.io.DataInputStream; import java.io.DataOutput; import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.AbstractList; import java.util.ArrayList; import java.util.List; import pleocmd.Log; import pleocmd.exc.FormatException; import pleocmd.pipe.Pipe; import pleocmd.pipe.PipePart; import pleocmd.pipe.cvt.Converter; import pleocmd.pipe.in.Input; import pleocmd.pipe.out.Output; import pleocmd.pipe.val.DataAsciiConverter; import pleocmd.pipe.val.DataBinaryConverter; import pleocmd.pipe.val.DummyValue; import pleocmd.pipe.val.Syntax; import pleocmd.pipe.val.Value; /** * Contains information about one command which will be created from * {@link Input}s, converted by {@link Converter}s and then written out by * {@link Output}s inside the {@link Pipe}.<br> * Is immutable. * * @author oliver */ public class Data extends AbstractList<Value> { /** * The priority which will be used if no special one is specified. */ public static final byte PRIO_DEFAULT = 0; /** * The lowest possible priority.<br> * If this constant changes, reading and writing in the * {@link DataAsciiConverter} must be updated. */ public static final byte PRIO_LOWEST = -99; /** * The highest possible priority.<br> * If this constant changes, reading and writing in the * {@link DataAsciiConverter} must be updated. */ public static final byte PRIO_HIGHEST = 99; /** * This specifies that a {@link Data} object doesn't have a specific time at * which it should be executed - it will just be executed when it reaches * the top position in the {@link DataQueue}. */ public static final long TIME_NOTIME = -1; protected static final long CTOR_DIRECT = 8297464393242L; protected static final char[] HEX_TABLE = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; private final List<Value> values; private PipePart origin; private final Data parent; private final byte priority; private long time; /** * Creates a new {@link Data} object where all fields can be set * individually. * * @param values * list of {@link Value} - will be <b>shallow-copied</b> * @param parent * the parent which was the cause for this {@link Data} being * created - may be <b>null</b> * @param priority * priority of the new {@link Data} (must be between * {@link #PRIO_LOWEST} and {@link #PRIO_HIGHEST}) - if * {@link #PRIO_DEFAULT}, it will inherit the parent's priority * if a parent is defined * @param time * time at which this {@link Data} has been created relative to * the start of the {@link Pipe} - if {@link #TIME_NOTIME} it * will inherit the parent's time if a parent is defined */ public Data(final List<Value> values, final Data parent, final byte priority, final long time) { this(new ArrayList<Value>(values), parent, priority, time, CTOR_DIRECT); } /** * Creates a new {@link Data} object where all fields will be inherited from * the parent if one is defined or set to defaults otherwise. * * @param values * list of {@link Value} - will be <b>shallow-copied</b> * @param parent * the parent which was the cause for this {@link Data} being * created - may be <b>null</b> */ public Data(final List<Value> values, final Data parent) { this(new ArrayList<Value>(values), parent, CTOR_DIRECT); } /** * Internal constructor which directly uses the values list * * @param values * list of {@link Value} - will be directly used * @param parent * the parent which was the cause for this {@link Data} being * created - may be <b>null</b> * @param priority * priority of the new {@link Data} * @param time * relative time at which this {@link Data} has been created * @param dummy * just for distinction between other constructors */ protected Data(final List<Value> values, final Data parent, final byte priority, final long time, final long dummy) { assert dummy == CTOR_DIRECT; this.values = values; this.parent = parent; this.priority = priority == PRIO_DEFAULT && parent != null ? parent .getPriority() : priority; this.time = time == TIME_NOTIME && parent != null ? parent.getTime() : time; Log.detail("New Data created: %s (parent: '%s' priority: %d time: %d)", this, parent, priority, time); } /** * Internal constructor which directly uses the values list * * @param values * list of {@link Value} - will be directly used * @param parent * the parent which was the cause for this {@link Data} being * created - may be <b>null</b> * @param dummy * just for distinction between other constructors */ protected Data(final List<Value> values, final Data parent, final long dummy) { this(values, parent, PRIO_DEFAULT, TIME_NOTIME, dummy); } /** * Returns the {@link Value} at the given position. * * @param index * index of the {@link Value} to return * @return {@link Value} at this position * @throws IndexOutOfBoundsException * if the given index if invalid */ @Override // CS_IGNORE_NEXT unchecked exception thrown intentionally public final Value get(final int index) throws IndexOutOfBoundsException { if (index < 0 || index >= values.size()) throw new IndexOutOfBoundsException(String.format( "Argument %d does not exist in data '%s'", index, this)); return values.get(index); } /** * Returns the {@link Value} at the given position or a {@link DummyValue} * if the position is invalid.<br> * Never throws an {@link IndexOutOfBoundsException}. * * @param index * index of the {@link Value} to return * @return {@link Value} at this position or {@link DummyValue} */ public final Value getSafe(final int index) { return index < 0 || index >= values.size() ? new DummyValue() : values .get(index); } @Override public final int size() { return values.size(); } /** * @return the {@link PipePart} which has created or most recently "touched" * this {@link Data}. */ public final PipePart getOrigin() { return origin; } /** * Sets the {@link PipePart} which has most recently "touched" this * {@link Data}. * * @param origin * creator or processor of this {@link Data}. */ public final void setOrigin(final PipePart origin) { this.origin = origin; } /** * @return the parent of this {@link Data} - it is the {@link Data} which * was the cause why this one has been created. Recursively moving * up the parents will return the root of the tree-structured * hierarchy.<br> * May be <b>null</b>, if this one is the root (i.e. it has directly * been read from an {@link Input}). */ public final Data getParent() { return parent; } /** * @return the priority of this {@link Data} which is between * {@link #PRIO_LOWEST} and {@link #PRIO_HIGHEST}. {@link Data}s * with lower priority will be removed from the {@link DataQueue} if * one with a higher priority arrives. {@link Data}s with a lower * priority than other ones already in the queue will be ignored. */ public final byte getPriority() { return priority; } /** * @return the specific time at which this {@link Data} should be executed * (i.e. passed on to the {@link Output}s in the {@link Pipe}). If * {@link #TIME_NOTIME} it will just be executed when it reaches the * top position in the {@link DataQueue}. */ public final long getTime() { return time; } /** * Creates a new {@link Data} object from a {@link DataInput}. * * @param in * Input Stream with binary data * @return new {@link Data} with a list of {@link Value}s read from stream * @throws IOException * if data could not be read from {@link DataInput} * @throws FormatException * if data is of an invalid type or is of an invalid format for * its type * @see DataBinaryConverter */ public static Data createFromBinary(final DataInput in) throws IOException, FormatException { return new DataBinaryConverter(in, null).createDataFromFields(); } /** * Creates a new {@link Data} object from a {@link DataInput}. * * @param in * Input Stream with binary data * @param syntaxList * an (empty) list which receives all elements found during * parsing - may be <b>null</b> * @return new {@link Data} with a list of {@link Value}s read from stream * @throws IOException * if data could not be read from {@link DataInput} * @throws FormatException * if data is of an invalid type or is of an invalid format for * its type * @see DataBinaryConverter */ public static Data createFromBinary(final DataInput in, final List<Syntax> syntaxList) throws IOException, FormatException { return new DataBinaryConverter(in, syntaxList).createDataFromFields(); } /** * Creates a new {@link Data} object from a {@link DataInput}. * * @param in * Input Stream with text data in ISO-8859-1 encoding * @return new {@link Data} with a list of {@link Value}s read from stream * @throws IOException * if data could not be read from {@link DataInput} * @throws FormatException * if data is of an invalid type or is of an invalid format for * its type * @see DataAsciiConverter */ public static Data createFromAscii(final DataInput in) throws IOException, FormatException { return new DataAsciiConverter(in, null).createDataFromFields(); } /** * Creates a new {@link Data} object from a {@link DataInput}. * * @param in * Input Stream with text data in ISO-8859-1 encoding * @param syntaxList * an (empty) list which receives all elements found during * parsing - may be <b>null</b> * @return new {@link Data} with a list of {@link Value}s read from stream * @throws IOException * if data could not be read from {@link DataInput} * @throws FormatException * if data is of an invalid type or is of an invalid format for * its type * @see DataAsciiConverter */ public static Data createFromAscii(final DataInput in, final List<Syntax> syntaxList) throws IOException, FormatException { return new DataAsciiConverter(in, syntaxList).createDataFromFields(); } /** * Creates a new {@link Data} object from a {@link String}. * * @param string * {@link String} to read the data block from (optionally with a * line-break) * @return new {@link Data} with a list of {@link Value}s read from * {@link String} * @throws IOException * if data could not be read from {@link String} * @throws FormatException * if data is of an invalid type or is of an invalid format for * its type * @see DataAsciiConverter */ public static Data createFromAscii(final String string) throws IOException, FormatException { return new DataAsciiConverter( new DataInputStream(new ByteArrayInputStream( (string + '\n').getBytes("ISO-8859-1"))), null) .createDataFromFields(); } /** * Creates a new {@link Data} object from a {@link String}. * * @param string * {@link String} to read the data block from (optionally with a * line-break) * @param syntaxList * an (empty) list which receives all elements found during * parsing - may be <b>null</b> * @return new {@link Data} with a list of {@link Value}s read from * {@link String} * @throws IOException * if data could not be read from {@link String} * @throws FormatException * if data is of an invalid type or is of an invalid format for * its type * @see DataAsciiConverter */ public static Data createFromAscii(final String string, final List<Syntax> syntaxList) throws IOException, FormatException { return new DataAsciiConverter( new DataInputStream(new ByteArrayInputStream( (string + '\n').getBytes("ISO-8859-1"))), syntaxList) .createDataFromFields(); } /** * Writes this {@link Data} to a {@link DataOutput}. * * @param out * the {@link DataOutput} to which this {@link Data} will be * written in a binary form * @throws IOException * if writing to {@link DataOutput} failed or this {@link Data} * 's fields cannot be put into binary representation (for * example more than eight values associated) * @see DataBinaryConverter */ public final void writeToBinary(final DataOutput out) throws IOException { new DataBinaryConverter(this).writeToBinary(out, null); } /** * Writes this {@link Data} to a {@link DataOutput}. * * @param out * the {@link DataOutput} to which this {@link Data} will be * written in a binary form * @param syntaxList * an (empty) list which receives all elements created during * printing - may be <b>null</b> * @throws IOException * if writing to {@link DataOutput} failed or this {@link Data} * 's fields cannot be put into binary representation (for * example more than eight values associated) * @see DataBinaryConverter */ public final void writeToBinary(final DataOutput out, final List<Syntax> syntaxList) throws IOException { new DataBinaryConverter(this).writeToBinary(out, syntaxList); } /** * Writes this {@link Data} to a {@link DataOutput}. * * @param append * an {@link Appendable} like a {@link StringBuilder} to which * this {@link Data} will be written in a binary form, * represented with hexadecimal values * @param syntaxList * an (empty) list which receives all elements created during * printing - may be <b>null</b> * @throws IOException * if writing to {@link DataOutput} failed or this {@link Data} * 's fields cannot be put into binary representation (for * example more than eight values associated) * @see DataBinaryConverter */ public final void writeToBinary(final Appendable append, final List<Syntax> syntaxList) throws IOException { final DataOutputStream out = new DataOutputStream(new OutputStream() { @Override public void write(final int b) throws IOException { append.append(HEX_TABLE[b >> 4 & 0x0F]); append.append(HEX_TABLE[b & 0x0F]); } }); new DataBinaryConverter(this).writeToBinary(out, syntaxList); if (syntaxList != null) for (final Syntax sy : syntaxList) sy.duplicatePosition(); } /** * Writes this {@link Data} to a {@link DataOutput}. * * @param out * the {@link DataOutput} to which this {@link Data} will be * written in ISO-8859-1 encoding * @param writeLF * if a line-feed should be appended * @throws IOException * if writing to {@link DataOutput} failed * @see DataAsciiConverter */ public final void writeToAscii(final DataOutput out, final boolean writeLF) throws IOException { new DataAsciiConverter(this, null).writeToAscii(out, writeLF); } /** * Writes this {@link Data} to a {@link DataOutput}. * * @param out * the {@link DataOutput} to which this {@link Data} will be * written in ISO-8859-1 encoding * @param writeLF * if a line-feed should be appended * @param syntaxList * an (empty) list which receives all elements created during * printing - may be <b>null</b> * @throws IOException * if writing to {@link DataOutput} failed * @see DataAsciiConverter */ public final void writeToAscii(final DataOutput out, final boolean writeLF, final List<Syntax> syntaxList) throws IOException { new DataAsciiConverter(this, syntaxList).writeToAscii(out, writeLF); } public final String asString() { final ByteArrayOutputStream out = new ByteArrayOutputStream(128); try { writeToAscii(new DataOutputStream(out), false); return out.toString("ISO-8859-1"); } catch (final IOException e) { Log.error(e); return String.format("S:%s", e.getMessage()); } } @Override public final String toString() { return String.format("%s from %s", asString(), origin == null ? "unknown origin" : origin); } /** * @return the root of the tree-structured hierarchy of this {@link Data} - * it is the {@link Data} which was the first cause why this one has * been created.<br> * May be the {@link Data} itself, if this one is the root (i.e. it * has directly been read from an {@link Input}). */ public final Data getRoot() { return parent == null ? this : parent.getRoot(); } @Override public final boolean equals(final Object o) { if (o == this) return true; if (!(o instanceof Data)) return false; final Data d = (Data) o; return values.equals(d.values) && parent == d.parent && priority == d.priority && time == d.time; } @Override public final int hashCode() { int res = values.hashCode(); res = res * 31 + (parent == null ? 0 : parent.hashCode()); res = res * 31 + priority; res = res * 31 + (int) (time ^ time >>> 32); return res; } /** * Store the current time if the data block itself currently has no * timestamp and we are at least 2 ms past the last data block. * * @param pipe * the currently running {@link Pipe}. * @param lastTime * the time when the last data block has been processed by the * {@link Pipe}. * @return the current time of the {@link Pipe}. */ public long rememberTime(final Pipe pipe, final long lastTime) { final long elapsed = pipe.getFeedback().getElapsed(); if (time == Data.TIME_NOTIME && elapsed >= lastTime + 2) time = elapsed; return elapsed; } }