// 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.val; import java.io.ByteArrayOutputStream; import java.io.DataInput; import java.io.DataOutput; import java.io.DataOutputStream; import java.io.IOException; import java.util.List; import pleocmd.Log; import pleocmd.exc.FormatException; import pleocmd.exc.InternalException; import pleocmd.pipe.Pipe; import pleocmd.pipe.data.AbstractDataConverter; import pleocmd.pipe.data.Data; import pleocmd.pipe.val.Syntax.Type; /** * Helper class for converting {@link Data} objects from and to Ascii. * <p> * Data must consist of fields (separated by '|') and followed by '\n'.<br> * Whitespaces are allowed and will be ignored.<br> * Fields may be prefixed by a type identifier followed by ':'. Allowed type * identifiers are 'I', 'F', 'S' and 'B' for int, float, string or binary * values, optionally followed by modifier 'x' for hexadecimal data.<br> * A data block may be preceded by a list of flags between '[' and ']'. Valid * flags are: * <table> * <tr> * <th align=left>Character</th> * <th align=left>Description</th> * </tr> * <tr> * <td>P</td> * <td>Priority:<br> * The next 2 characters must be digits and will be interpreted as the priority * (may be preceded by a '-' for negative numbers)</td> * </tr> * <tr> * <td>T</td> * <td>Time:<br> * The next up to 10 characters must be digits and will be interpreted as the * time in milliseconds if followed by 'ms' or seconds if followed by 's' after * starting the {@link Pipe} at which this {@link Data} should be executed</td> * </tr> * </table> * <p> * Examples:<br> * 25|7.3|Hello<br> * [P-05]I:3|F:2.0|S:Some String<br> * S: 12345678 | 100 | F: 100 | Bx: F0DD35007E | Sx: 48454C4C4F<br> * [T1sP99]S:Very High Priority, executed after 1 second<br> * * @author oliver */ public final class DataAsciiConverter extends AbstractDataConverter { /** * 08 => may be part of a decimal number (string otherwise)<br> * 09 => may be part of a floating point (string otherwise)<br> * 10 => valid decimal / floating point number<br> * 30 => valid string<br> * 40 => invalid */ private static final int[] TYPE_AUTODETECT_TABLE = { // /**//**/40, 40, 40, 40, 40, 40, 40, 40, // 00 - 07 40, 30, 40, 40, 40, 40, 40, 40, // 08 - 0F 40, 40, 40, 40, 40, 40, 40, 40, // 10 - 17 40, 40, 40, 40, 40, 40, 40, 40, // 18 - 1F 30, 30, 30, 30, 30, 30, 30, 30, // 20 - 27 30, 30, 30, +8, 30, +8, +9, 30, // 28 - 2F 10, 10, 10, 10, 10, 10, 10, 10, // 30 - 37 10, 10, 30, 30, 30, 30, 30, 30, // 38 - 3F 30, 30, 30, 30, 30, +9, 30, 30, // 40 - 47 30, 30, 30, 30, 30, 30, 30, 30, // 48 - 4F 30, 30, 30, 30, 30, 30, 30, 30, // 50 - 57 30, 30, 30, 30, 30, 30, 30, 30, // 58 - 5F 30, 30, 30, 30, 30, +9, 30, 30, // 60 - 67 30, 30, 30, 30, 30, 30, 30, 30, // 68 - 6F 30, 30, 30, 30, 30, 30, 30, 30, // 70 - 77 30, 30, 30, 30, 40, 30, 30, 40, // 78 - 7F 40, 40, 40, 40, 40, 40, 40, 40, // 80 - 87 40, 40, 40, 40, 40, 40, 40, 40, // 88 - 8F 40, 40, 40, 40, 40, 40, 40, 40, // 90 - 97 40, 40, 40, 40, 40, 40, 40, 40, // 98 - 9F 40, 40, 40, 40, 40, 40, 40, 40, // A0 - A7 40, 40, 40, 40, 40, 40, 40, 40, // A8 - AF 40, 40, 40, 40, 40, 40, 40, 40, // B0 - B7 40, 40, 40, 40, 40, 40, 40, 40, // B8 - BF 40, 40, 40, 40, 40, 40, 40, 40, // C0 - C7 40, 40, 40, 40, 40, 40, 40, 40, // C8 - CF 40, 40, 40, 40, 40, 40, 40, 40, // D0 - D7 40, 40, 40, 40, 40, 40, 40, 40, // D8 - DF 40, 40, 40, 40, 40, 40, 40, 40, // E0 - E7 40, 40, 40, 40, 40, 40, 40, 40, // E8 - EF 40, 40, 40, 40, 40, 40, 40, 40, // F0 - F7 40, 40, 40, 40, 40, 40, 40, 40, // F8 - FF }; private static final byte[] HEX_TABLE = new byte[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; private final List<Syntax> syntaxList; private byte[] buf; private int buflen; private ValueType type; private boolean isHex; private int index; /** * Creates a new {@link DataAsciiConverter} that wraps an existing * {@link Data} object. * * @param data * {@link Data} to read from * @param syntaxList * an (empty) list which receives all elements found during * parsing - may be <b>null</b> */ public DataAsciiConverter(final Data data, final List<Syntax> syntaxList) { super(data); this.syntaxList = syntaxList; } /** * Creates a new {@link DataAsciiConverter} and sets all its fields * according to the Ascii representation of a {@link Data} in the * {@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> * @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 */ public DataAsciiConverter(final DataInput in, final List<Syntax> syntaxList) throws IOException, FormatException { Log.detail("Started parsing an ASCII Data object"); this.syntaxList = syntaxList; buf = new byte[64]; index = -1; while (true) { ++index; final byte b; try { b = in.readByte(); } catch (final IOException e) { parseValue(getValues().isEmpty()); // this was the end of the data block trimValues(); Log.detail("Finished parsing an ASCII Data object"); return; } switch (b) { case '\n': parseValue(getValues().isEmpty()); // this was the end of the data block trimValues(); Log.detail("Finished parsing an ASCII Data object"); return; case '|': parseValue(false); if (this.syntaxList != null) this.syntaxList.add(new Syntax(Type.FieldDelim, index)); // prepare for the next value type = null; isHex = false; buflen = 0; break; case '[': if (index == 0) parseFlags(in); else // treat '[' on other positions as a normal character putByteIntoBuffer(b); break; case ':': if (type == null && (buflen == 1 || buflen == 2)) parseTypeIdentifier(); else // treat (second) ':' on other positions as // a normal character putByteIntoBuffer(b); break; case ' ': if (buflen > 0) // ignore whitespaces at the beginning putByteIntoBuffer(b); break; default: putByteIntoBuffer(b); break; } } } private void parseValue(final boolean ignoreIfEmpty) throws FormatException { // trim whitespaces final int orgbuflen = buflen; while (buflen > 0 && buf[buflen - 1] == ' ') --buflen; if (ignoreIfEmpty && buflen == 0) return; if (type == null) { // autodetect type isHex = false; type = detectDataType(buf, buflen, orgbuflen); Log.detail("Autodetecting resulted in: %s", type); } // create fitting value final Value val = Value.createForType(type); if (syntaxList != null) { final int si = index - orgbuflen; // start index // DataField has precedence over HexField if (isHex && type != ValueType.Data) syntaxList.add(new Syntax(Type.HexField, si)); else switch (type) { case Float32: case Float64: syntaxList.add(new Syntax(Type.FloatField, si)); break; case Int8: case Int32: case Int64: syntaxList.add(new Syntax(Type.IntField, si)); break; case NullTermString: case UTFString: syntaxList.add(new Syntax(Type.StringField, si)); break; case Data: syntaxList.add(new Syntax(Type.DataField, si)); break; } } if (isHex) { // we need to decode the data from a hex string Log.detail("Converting hex data with length %d", buflen); final byte[] buf2 = new byte[buflen / 2]; for (int i = 0, j = 0; i < buflen;) { final int d1 = Character.digit(buf[i++], 16); // CS_IGNORE final int si = index - orgbuflen + i; if (i == buflen) throw new FormatException(syntaxList, si - 1, "Broken hexadecimal data: Length must be " + "multiple of two but is %d", buflen); final int d2 = Character.digit(buf[i++], 16); // CS_IGNORE if (d1 == -1) throw new FormatException(syntaxList, si - 1, "Broken hexadecimal data: Invalid " + "character: 0x%02X", buf[i - 2]); if (d2 == -1) throw new FormatException(syntaxList, si, "Broken hexadecimal data: Invalid " + "character: 0x%02X", buf[i - 1]); buf2[j++] = (byte) (d1 << 4 | d2); // CS_IGNORE } try { val.readFromAscii(buf2, buf2.length); } catch (final Throwable t) { throw new FormatException(syntaxList, index - orgbuflen, t.getMessage()); } } else try { val.readFromAscii(buf, buflen); } catch (final Throwable t) { throw new FormatException(syntaxList, index - orgbuflen, t.getMessage()); } getValues().add(val); } public void writeToAscii(final DataOutput out, final boolean writeLF) throws IOException { Log.detail("Writing Data to ASCII output stream"); int pos = writeFlags(out); boolean first = true; for (final Value value : getValues()) { // write delimiter if needed if (!first) { if (syntaxList != null) syntaxList.add(new Syntax(Type.FieldDelim, pos)); out.writeByte(' '); out.writeByte('|'); out.writeByte(' '); pos += 3; } first = false; final boolean hex = value.mustWriteAsciiAsHex(); // write the field type identifier (and modifier if needed) if (hex) { if (syntaxList != null) syntaxList.add(new Syntax(Type.TypeIdent, pos)); out.writeByte(Value.getAsciiTypeChar(value)); out.writeByte('x'); out.writeByte(':'); out.writeByte(' '); pos += 4; } // write the field content in decimal or hex if (hex) { if (syntaxList != null) syntaxList.add(new Syntax(Type.HexField, pos)); final ByteArrayOutputStream hexOut = new ByteArrayOutputStream(); value.writeToAscii(new DataOutputStream(hexOut)); final byte[] ba = hexOut.toByteArray(); for (final byte b : ba) { out.write(HEX_TABLE[b >> 4 & 0x0F]); out.write(HEX_TABLE[b & 0x0F]); } pos += ba.length * 2; } else { if (syntaxList != null) switch (value.getType()) { case Float32: case Float64: syntaxList.add(new Syntax(Type.FloatField, pos)); break; case Int8: case Int32: case Int64: syntaxList.add(new Syntax(Type.IntField, pos)); break; case NullTermString: case UTFString: syntaxList.add(new Syntax(Type.StringField, pos)); break; case Data: syntaxList.add(new Syntax(Type.DataField, pos)); break; } pos += value.writeToAscii(out); } } // write the final block delimiter if (writeLF) out.writeByte('\n'); } private int writeFlags(final DataOutput out) throws IOException { int pos = 0; if (getPriority() == Data.PRIO_DEFAULT && getTime() == Data.TIME_NOTIME) return pos; if (syntaxList != null) syntaxList.add(new Syntax(Type.Flags, pos)); out.writeByte('['); out.writeByte(' '); pos += 2; if (getPriority() != Data.PRIO_DEFAULT) { if (syntaxList != null) syntaxList.add(new Syntax(Type.FlagPrio, pos)); out.writeByte('P'); if (getPriority() < 0) out.writeByte('-'); out.writeByte('0' + Math.abs(getPriority()) / 10); out.writeByte('0' + Math.abs(getPriority()) % 10); out.writeByte(' '); pos += getPriority() < 0 ? 5 : 4; } if (getTime() != Data.TIME_NOTIME) { if (syntaxList != null) syntaxList.add(new Syntax(Type.FlagTime, pos)); out.writeByte('T'); final boolean inSec = getTime() % 1000 == 0; final long val = inSec ? getTime() / 1000 : getTime(); final byte[] ba = String.valueOf(val).getBytes("ISO-8859-1"); out.write(ba); if (!inSec) out.write('m'); out.write('s'); out.writeByte(' '); pos += ba.length + (inSec ? 3 : 4); } if (syntaxList != null) syntaxList.add(new Syntax(Type.Flags, pos)); out.writeByte(']'); out.writeByte(' '); pos += 2; return pos; } private void parseFlags(final DataInput in) throws IOException, FormatException { if (syntaxList != null) syntaxList.add(new Syntax(Type.Flags, index)); while (true) { ++index; final byte b = in.readByte(); switch (b) { case ' ': // just ignore any spaces in flag list break; case ']': // end of flag list if (syntaxList != null) syntaxList.add(new Syntax(Type.Flags, index)); return; case 'P': case 'p': parseFlagPriority(in); break; case 'T': case 't': parseFlagTime(in); break; default: throw new FormatException(syntaxList, index, "Invalid character 0x%02X in flag list", b); } } } private void parseFlagPriority(final DataInput in) throws IOException, FormatException { if (syntaxList != null) syntaxList.add(new Syntax(Type.FlagPrio, index)); ++index; byte b = in.readByte(); final boolean neg = b == '-'; if (neg) { ++index; b = in.readByte(); } byte res = 0; if (b < '0' || b > '9') throw new FormatException(syntaxList, index, "Invalid character 0x%02X in priority", b); res += (b - '0') * 10; ++index; b = in.readByte(); if (b < '0' || b > '9') throw new FormatException(syntaxList, index, "Invalid character 0x%02X in priority", b); res += b - '0'; if (neg) res = (byte) -res; Log.detail("Parsed priority: %d", res); if (res < Data.PRIO_LOWEST || res > Data.PRIO_HIGHEST) throw new FormatException(syntaxList, index, "Priority is out of range: %d not between %d and %d", res, Data.PRIO_LOWEST, Data.PRIO_HIGHEST); setPriority(res); } private void parseFlagTime(final DataInput in) throws IOException, FormatException { if (syntaxList != null) syntaxList.add(new Syntax(Type.FlagTime, index)); long res = 0; while (true) { ++index; final byte b = in.readByte(); if (b == 'm') { ++index; final byte b2 = in.readByte(); if (b2 != 's') throw new FormatException(syntaxList, index, "Invalid " + "character 0x%02X in time at position %d", b2, index); break; } if (b == 's') { res *= 1000; break; } if (b < '0' || b > '9') throw new FormatException(syntaxList, index, "Invalid " + "character 0x%02X in time at position %d", b, index); res *= 10; res += b - '0'; } Log.detail("Parsed time: %d ms", res); if (res > 0xFFFFFFFFL) throw new FormatException(syntaxList, index, "Time is out of range: %d not between " + "0 and 0xFFFFFFFF", res); setTime(res); } private void parseTypeIdentifier() throws FormatException { if (syntaxList != null) syntaxList.add(new Syntax(Type.TypeIdent, index - buflen)); type = Value.detectFromTypeChar((char) buf[0]); if (type == null) throw new FormatException(syntaxList, index - buflen, "Invalid type identifier: 0x%02X", (int) buf[0]); if (buflen == 2) { if (buf[1] != 'x') throw new FormatException(syntaxList, index - 1, "Invalid type modifier: 0x%02X", (int) buf[1]); isHex = true; } else isHex = false; buflen = 0; Log.detail("Forced type '%s' - hex: %s", type, isHex); } private void putByteIntoBuffer(final byte b) { if (buflen == buf.length) { final int bufcap = buf.length * 2; final byte[] buf2 = new byte[bufcap]; System.arraycopy(buf, 0, buf2, 0, buflen); buf = buf2; } buf[buflen++] = b; } /** * Returns the most specific {@link ValueType} which can read the data. * * @param data * the Ascii data which should be converted * @param len * length of the data * @param orgbuflen * original length of the data (length including any removed * suffixed spaces) * @return one of {@link ValueType#Int64}, {@link ValueType#Float64} or * {@link ValueType#NullTermString} * @throws FormatException * if the data is not in one of the known data formats */ private ValueType detectDataType(final byte[] data, final int len, final int orgbuflen) throws FormatException { Log.detail("Autodetecting data type of %d bytes", len); int tat; int res = 0; final boolean[] found = new boolean[256]; for (int i = 0; i < len; ++i) { found[tat = TYPE_AUTODETECT_TABLE[data[i] & 0xFF]] = true; if ((res = Math.max(res, tat)) == 40) throw new FormatException(syntaxList, index - orgbuflen + i, "Invalid character for any known data type: 0x%02X", data[i]); } switch (res) { case 0: // treat empty data as string case 8: // treat incomplete decimal number as string case 9: // treat incomplete floating point as string case 30: // valid string characters return ValueType.NullTermString; case 10: // valid digits return found[9] ? ValueType.Float64 : ValueType.Int64; default: throw new InternalException( "Invalid entry in TYPE_AUTODETECT_TABLE: %d", res); } } public static String toHexString(final byte[] a, final int len) { final StringBuilder sb = new StringBuilder("["); for (int i = 0; i < len; ++i) sb.append(String.format("%02X", a[i])); return sb.append(']').toString(); } }