-4.7.2-dev
+4.7.2-streamify-dev
- new: CryptUtils
- new: streams classes
- fix: IOUtils.readSmallStream and \n at the end
+- fix: Base64 implementation changed
- change: serial: SSL -> CryptUtils
- change: MarkableFileInputStream moved to nikiroo.utils.streams
+- change: Break compat with package utils.server (Version not used anymore)
## Version 4.7.2
package be.nikiroo.utils.serial;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.streams.NextableInputStream;
+import be.nikiroo.utils.streams.NextableInputStreamStep;
+import be.nikiroo.utils.streams.ReplaceInputStream;
+import be.nikiroo.utils.streams.ReplaceOutputStream;
public abstract class CustomSerializer {
- protected abstract String toString(Object value);
+ protected abstract void toStream(OutputStream out, Object value)
+ throws IOException;
- protected abstract Object fromString(String content) throws IOException;
+ protected abstract Object fromStream(InputStream in) throws IOException;
protected abstract String getType();
/**
- * Encode the object into the given builder if possible (if supported).
+ * Encode the object into the given {@link OutputStream} if supported.
*
- * @param builder
+ * @param out
* the builder to append to
* @param value
* the object to encode
- * @return TRUE if success, FALSE if not (the content of the builder won't
- * be changed in case of failure)
+ *
+ * @throws IOException
+ * in case of I/O error
*/
- public boolean encode(StringBuilder builder, Object value) {
- int prev = builder.length();
- String customString = toString(value);
- builder.append("custom^").append(getType()).append("^");
- if (!SerialUtils.encode(builder, customString)) {
- builder.delete(prev, builder.length());
- return false;
- }
+ public void encode(OutputStream out, Object value) throws IOException {
+ ReplaceOutputStream replace = new ReplaceOutputStream(out, //
+ new String[] { "\\", "\n" }, //
+ new String[] { "\\\\", "\\n" });
- return true;
+ try {
+ SerialUtils.write(replace, "custom^");
+ SerialUtils.write(replace, getType());
+ SerialUtils.write(replace, "^");
+ toStream(replace, value);
+ } finally {
+ replace.close(false);
+ }
}
- public Object decode(String encodedValue) throws IOException {
- return fromString((String) SerialUtils.decode(contentOf(encodedValue)));
+ public Object decode(InputStream in) throws IOException {
+ ReplaceInputStream replace = new ReplaceInputStream(in, //
+ new String[] { "\\\\", "\\n" }, //
+ new String[] { "\\", "\n" });
+
+ try {
+ NextableInputStream stream = new NextableInputStream(
+ replace.open(), new NextableInputStreamStep('^'));
+ try {
+ if (!stream.next()) {
+ throw new IOException(
+ "Cannot find the first custom^ element");
+ }
+
+ String custom = IOUtils.readSmallStream(stream);
+ if (!"custom".equals(custom)) {
+ throw new IOException(
+ "Cannot find the first custom^ element, it is: "
+ + custom + "^");
+ }
+
+ if (!stream.next()) {
+ throw new IOException("Cannot find the second custom^"
+ + getType() + " element");
+ }
+
+ String type = IOUtils.readSmallStream(stream);
+ if (!getType().equals(type)) {
+ throw new IOException("Cannot find the second custom^"
+ + getType() + " element, it is: custom^" + type
+ + "^");
+ }
+
+ if (!stream.nextAll()) {
+ throw new IOException("Cannot find the third custom^"
+ + getType() + "^value element");
+ }
+
+ return fromStream(stream);
+ } finally {
+ stream.close();
+ }
+ } finally {
+ replace.close(false);
+ }
}
public static boolean isCustom(String encodedValue) {
import java.io.IOException;
import java.io.NotSerializableException;
+import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
-import be.nikiroo.utils.StringUtils;
-
/**
* A simple class to serialise objects to {@link String}.
* <p>
*/
public class Exporter {
private Map<Integer, Object> map;
- private StringBuilder builder;
+ private OutputStream out;
/**
* Create a new {@link Exporter}.
+ *
+ * @param out
+ * export the data to this stream
*/
- public Exporter() {
+ public Exporter(OutputStream out) {
+ if (out == null) {
+ throw new NullPointerException(
+ "Cannot create an be.nikiroo.utils.serials.Exporter that will export to NULL");
+ }
+
+ this.out = out;
map = new HashMap<Integer, Object>();
- builder = new StringBuilder();
}
/**
* if the object cannot be serialised (in this case, the
* {@link Exporter} can contain bad, most probably not
* importable data)
+ * @throws IOException
+ * in case of I/O error
*/
- public Exporter append(Object o) throws NotSerializableException {
- SerialUtils.append(builder, o, map);
+ public Exporter append(Object o) throws NotSerializableException,
+ IOException {
+ SerialUtils.append(out, o, map);
return this;
}
-
- /**
- * Clear the current content.
- */
- public void clear() {
- builder.setLength(0);
- map.clear();
- }
-
- /**
- * Append the exported items in a serialised form into the given
- * {@link StringBuilder}.
- *
- * @param toBuilder
- * the {@link StringBuilder}
- * @param b64
- * TRUE to have BASE64-coded content, FALSE to have raw content,
- * NULL to let the system decide
- * @param zip
- * TRUE to zip the BASE64 output if the output is indeed in
- * BASE64 format, FALSE not to
- */
- public void appendTo(StringBuilder toBuilder, Boolean b64, boolean zip) {
- if (b64 == null && builder.length() < 128) {
- b64 = false;
- }
-
- if (b64 == null || b64) {
- try {
- String zipped = StringUtils.base64(builder.toString(), zip);
- if (b64 != null || zipped.length() < builder.length() - 4) {
- toBuilder.append(zip ? "ZIP:" : "B64:");
- toBuilder.append(zipped);
- return;
- }
- } catch (IOException e) {
- throw new RuntimeException(
- "Base64 conversion of data failed, maybe not enough memory?",
- e);
- }
- }
-
- toBuilder.append(builder);
- }
-
- /**
- * The exported items in a serialised form.
- *
- * @deprecated use {@link Exporter#toString(Boolean, boolean)} instead
- *
- * @param zip
- * TRUE to have zipped (and BASE64-coded) content, FALSE to have
- * raw content, NULL to let the system decide
- *
- * @return the items currently in this {@link Exporter}
- */
- @Deprecated
- public String toString(Boolean zip) {
- return toString(zip, zip == null || zip);
- }
-
- /**
- * The exported items in a serialised form.
- *
- * @param b64
- * TRUE to have BASE64-coded content, FALSE to have raw content,
- * NULL to let the system decide
- * @param zip
- * TRUE to zip the BASE64 output if the output is indeed in
- * BASE64 format, FALSE not to
- *
- * @return the items currently in this {@link Exporter}
- */
- public String toString(Boolean b64, boolean zip) {
- StringBuilder toBuilder = new StringBuilder();
- appendTo(toBuilder, b64, zip);
- return toBuilder.toString();
- }
-
- /**
- * The exported items in a serialised form (possibly BASE64-coded, possibly
- * zipped).
- *
- * @return the items currently in this {@link Exporter}
- */
- @Override
- public String toString() {
- return toString(null, true);
- }
}
\ No newline at end of file
package be.nikiroo.utils.serial;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
+import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
+import be.nikiroo.utils.IOUtils;
import be.nikiroo.utils.StringUtils;
+import be.nikiroo.utils.streams.NextableInputStream;
+import be.nikiroo.utils.streams.NextableInputStreamStep;
/**
* A simple class that can accept the output of {@link Exporter} to recreate
* @author niki
*/
public class Importer {
- static private Integer SIZE_ID = null;
- static private byte[] NEWLINE = null;
-
private Boolean link;
private Object me;
private Importer child;
private String currentFieldName;
- static {
- try {
- SIZE_ID = "EXT:".getBytes("UTF-8").length;
- NEWLINE = "\n".getBytes("UTF-8");
- } catch (UnsupportedEncodingException e) {
- // UTF-8 is mandated to exist on confirming jre's
- }
- }
-
/**
* Create a new {@link Importer}.
*/
* content, or a number of lines of it (any given line <b>MUST</b> be
* complete though) and accumulate it with the already present data.
*
- * @param data
+ * @param in
* the data to parse
*
* @return itself so it can be chained
* if a class described in the serialised data cannot be found
* @throws IOException
* if the content cannot be read (for instance, corrupt data)
+ * @throws NullPointerException
+ * if the stream is empty
*/
- public Importer read(String data) throws NoSuchFieldException,
- NoSuchMethodException, ClassNotFoundException, IOException {
- return read(data.getBytes("UTF-8"), 0);
- }
+ public Importer read(InputStream in) throws NoSuchFieldException,
+ NoSuchMethodException, ClassNotFoundException, IOException,
+ NullPointerException {
- /**
- * Read some data into this {@link Importer}: it can be the full serialised
- * content, or a number of lines of it (any given line <b>MUST</b> be
- * complete though) and accumulate it with the already present data.
- *
- * @param data
- * the data to parse
- * @param offset
- * the offset at which to start reading the data (we ignore
- * anything that goes before that offset)
- *
- * @return itself so it can be chained
- *
- * @throws NoSuchFieldException
- * if the serialised data contains information about a field
- * which does actually not exist in the class we know of
- * @throws NoSuchMethodException
- * if a class described in the serialised data cannot be created
- * because it is not compatible with this code
- * @throws ClassNotFoundException
- * if a class described in the serialised data cannot be found
- * @throws IOException
- * if the content cannot be read (for instance, corrupt data)
- */
- private Importer read(byte[] data, int offset) throws NoSuchFieldException,
- NoSuchMethodException, ClassNotFoundException, IOException {
+ NextableInputStream stream = new NextableInputStream(in,
+ new NextableInputStreamStep('\n'));
- int dataStart = offset;
- while (dataStart < data.length) {
- String id = "";
- if (data.length - dataStart >= SIZE_ID) {
- id = new String(data, dataStart, SIZE_ID);
+ try {
+ if (in == null) {
+ throw new NullPointerException("InputStream is null");
}
- boolean zip = id.equals("ZIP:");
- boolean b64 = id.equals("B64:");
- if (zip || b64) {
- dataStart += SIZE_ID;
- }
+ boolean first = true;
+ while (stream.next()) {
+ if (stream.eof()) {
+ if (first) {
+ throw new NullPointerException(
+ "InputStream empty, normal termination");
+ }
+ return this;
+ }
+ first = false;
- int count = find(data, dataStart, NEWLINE);
- count -= dataStart;
- if (count < 0) {
- count = data.length - dataStart;
- }
+ boolean zip = stream.startsWiths("ZIP:");
+ boolean b64 = stream.startsWiths("B64:");
- if (zip || b64) {
- boolean unpacked = false;
- try {
- byte[] line = StringUtils.unbase64(data, dataStart, count,
+ if (zip || b64) {
+ stream.skip("XXX:".length());
+ InputStream decoded = StringUtils.unbase64(stream.open(),
zip);
- unpacked = true;
- read(line, 0);
- } catch (IOException e) {
- throw new IOException("Internal error when decoding "
- + (unpacked ? "unpacked " : "")
- + (zip ? "ZIP" : "B64")
- + " content: input may be corrupt");
+ try {
+ read(decoded);
+ } finally {
+ decoded.close();
+ }
+ } else {
+ processLine(stream);
}
- } else {
- String line = new String(data, dataStart, count, "UTF-8");
- processLine(line);
}
-
- dataStart += count + NEWLINE.length;
+ } finally {
+ stream.close(false);
}
return this;
* Read a single (whole) line of serialised data into this {@link Importer}
* and accumulate it with the already present data.
*
- * @param line
+ * @param in
* the line to parse
*
* @return TRUE if we are just done with one object or sub-object
* @throws IOException
* if the content cannot be read (for instance, corrupt data)
*/
- private boolean processLine(String line) throws NoSuchFieldException,
+ private boolean processLine(InputStream in) throws NoSuchFieldException,
NoSuchMethodException, ClassNotFoundException, IOException {
+
// Defer to latest child if any
if (child != null) {
- if (child.processLine(line)) {
+ if (child.processLine(in)) {
if (currentFieldName != null) {
setField(currentFieldName, child.getValue());
currentFieldName = null;
return false;
}
+ // TODO use the stream, Luke
+ String line = IOUtils.readSmallStream(in);
+
+ if (line.isEmpty()) {
+ return false;
+ }
+
if (line.equals("{")) { // START: new child if needed
if (link != null) {
child = new Importer(map);
}
}
- /**
- * Find the given needle in the data and return its position (or -1 if not
- * found).
- *
- * @param data
- * the data to look through
- * @param offset
- * the offset at wich to start searching
- * @param needle
- * the needle to find
- *
- * @return the position of the needle if found, -1 if not found
- */
- private int find(byte[] data, int offset, byte[] needle) {
- for (int i = offset; i + needle.length - 1 < data.length; i++) {
- boolean same = true;
- for (int j = 0; j < needle.length; j++) {
- if (data[i + j] != needle[j]) {
- same = false;
- break;
- }
- }
-
- if (same) {
- return i;
- }
- }
-
- return -1;
- }
-
/**
* Return the current deserialised value.
*
package be.nikiroo.utils.serial;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.NotSerializableException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.UnknownFormatConversionException;
+import be.nikiroo.utils.IOUtils;
import be.nikiroo.utils.Image;
+import be.nikiroo.utils.StringUtils;
+import be.nikiroo.utils.streams.NextableInputStream;
+import be.nikiroo.utils.streams.NextableInputStreamStep;
/**
* Small class to help with serialisation.
// Array types:
customTypes.put("[]", new CustomSerializer() {
@Override
- protected String toString(Object value) {
+ protected void toStream(OutputStream out, Object value)
+ throws IOException {
+
+ // TODO: we use \n to separate, and b64 to un-\n
+ // -- but we could use \\n ?
String type = value.getClass().getCanonicalName();
type = type.substring(0, type.length() - 2); // remove the []
- StringBuilder builder = new StringBuilder();
- builder.append(type).append("\n");
+ write(out, type);
try {
for (int i = 0; true; i++) {
Object item = Array.get(value, i);
+
// encode it normally if direct value
- if (!SerialUtils.encode(builder, item)) {
+ write(out, "\r");
+ if (!SerialUtils.encode(out, item)) {
try {
- // use ZIP: if not
- new Exporter().append(item).appendTo(builder,
- true, false);
+ // TODO: bad escaping?
+ write(out, "B64:");
+ OutputStream bout = StringUtils.base64(out,
+ false, false);
+ new Exporter(bout).append(item);
} catch (NotSerializableException e) {
throw new UnknownFormatConversionException(e
.getMessage());
}
}
- builder.append("\n");
}
} catch (ArrayIndexOutOfBoundsException e) {
// Done.
}
-
- return builder.toString();
}
@Override
- protected String getType() {
- return "[]";
- }
-
- @Override
- protected Object fromString(String content) throws IOException {
- String[] tab = content.split("\n");
+ protected Object fromStream(InputStream in) throws IOException {
+ NextableInputStream stream = new NextableInputStream(in,
+ new NextableInputStreamStep('\r'));
try {
+ List<Object> list = new ArrayList<Object>();
+ stream.next();
+ String type = IOUtils.readSmallStream(stream);
+
+ while (stream.next()) {
+ Object value = new Importer().read(stream).getValue();
+ list.add(value);
+ }
+
Object array = Array.newInstance(
- SerialUtils.getClass(tab[0]), tab.length - 1);
- for (int i = 1; i < tab.length; i++) {
- Object value = new Importer().read(tab[i]).getValue();
- Array.set(array, i - 1, value);
+ SerialUtils.getClass(type), list.size());
+ for (int i = 0; i < list.size(); i++) {
+ Array.set(array, i, list.get(i));
}
return array;
throw new IOException(e.getMessage());
}
}
+
+ @Override
+ protected String getType() {
+ return "[]";
+ }
});
// URL:
customTypes.put("java.net.URL", new CustomSerializer() {
@Override
- protected String toString(Object value) {
+ protected void toStream(OutputStream out, Object value)
+ throws IOException {
+ String val = "";
if (value != null) {
- return ((URL) value).toString();
+ val = ((URL) value).toString();
}
- return null;
+
+ out.write(val.getBytes("UTF-8"));
}
@Override
- protected Object fromString(String content) throws IOException {
- if (content != null) {
- return new URL(content);
+ protected Object fromStream(InputStream in) throws IOException {
+ String val = IOUtils.readSmallStream(in);
+ if (!val.isEmpty()) {
+ return new URL(val);
}
+
return null;
}
// Images (this is currently the only supported image type by default)
customTypes.put("be.nikiroo.utils.Image", new CustomSerializer() {
@Override
- protected String toString(Object value) {
- return ((Image) value).toBase64();
+ protected void toStream(OutputStream out, Object value)
+ throws IOException {
+ Image img = (Image) value;
+ OutputStream encoded = StringUtils.base64(out, false, false);
+ try {
+ InputStream in = img.newInputStream();
+ try {
+ IOUtils.write(in, encoded);
+ } finally {
+ in.close();
+ }
+ } finally {
+ encoded.flush();
+ // Cannot close!
+ }
}
@Override
}
@Override
- protected Object fromString(String content) {
+ protected Object fromStream(InputStream in) throws IOException {
try {
- return new Image(content);
+ // Cannot close it!
+ InputStream decoded = StringUtils.unbase64(in, false);
+ return new Image(decoded);
} catch (IOException e) {
throw new UnknownFormatConversionException(e.getMessage());
}
ctor = clazz.getDeclaredConstructor(classes
.toArray(new Class[] {}));
} catch (NoSuchMethodException nsme) {
- // TODO: it seems e do not always need a parameter for each
+ // TODO: it seems we do not always need a parameter for each
// level, so we currently try "ALL" levels or "FIRST" level
// only -> we should check the actual rule and use it
ctor = clazz.getDeclaredConstructor(classes.get(0));
}
/**
- * Serialise the given object into this {@link StringBuilder}.
+ * Serialise the given object into this {@link OutputStream}.
* <p>
* <b>Important: </b>If the operation fails (with a
* {@link NotSerializableException}), the {@link StringBuilder} will be
* corrupted (will contain bad, most probably not importable data).
*
- * @param builder
- * the output {@link StringBuilder} to serialise to
+ * @param out
+ * the output {@link OutputStream} to serialise to
* @param o
* the object to serialise
* @param map
* if the object cannot be serialised (in this case, the
* {@link StringBuilder} can contain bad, most probably not
* importable data)
+ * @throws IOException
+ * in case of I/O errors
*/
- static void append(StringBuilder builder, Object o, Map<Integer, Object> map)
- throws NotSerializableException {
+ static void append(OutputStream out, Object o, Map<Integer, Object> map)
+ throws NotSerializableException, IOException {
Field[] fields = new Field[] {};
String type = "";
}
}
- builder.append("{\nREF ").append(type).append("@").append(id)
- .append(":");
- if (!encode(builder, o)) { // check if direct value
+ write(out, "{\nREF ");
+ write(out, type);
+ write(out, "@");
+ write(out, id);
+ write(out, ":");
+
+ if (!encode(out, o)) { // check if direct value
try {
for (Field field : fields) {
field.setAccessible(true);
continue;
}
- builder.append("\n");
- builder.append(field.getName());
- builder.append(":");
- Object value;
+ write(out, "\n");
+ write(out, field.getName());
+ write(out, ":");
- value = field.get(o);
+ Object value = field.get(o);
- if (!encode(builder, value)) {
- builder.append("\n");
- append(builder, value, map);
+ if (!encode(out, value)) {
+ write(out, "\n");
+ append(out, value, map);
}
}
} catch (IllegalArgumentException e) {
e.printStackTrace(); // should not happen (see
// setAccessible)
}
+
+ write(out, "\n}");
}
- builder.append("\n}");
}
/**
- * Encode the object into the given builder if possible and if supported.
+ * Encode the object into the given {@link OutputStream} if possible and if
+ * supported.
* <p>
* A supported object in this context means an object we can directly
* encode, like an Integer or a String. Custom objects and arrays are also
* <p>
* For compound objects, you should use {@link Exporter}.
*
- * @param builder
- * the builder to append to
+ * @param out
+ * the {@link OutputStream} to append to
* @param value
* the object to encode (can be NULL, which will be encoded)
*
- * @return TRUE if success, FALSE if not (the content of the builder won't
- * be changed in case of failure)
+ * @return TRUE if success, FALSE if not (the content of the
+ * {@link OutputStream} won't be changed in case of failure)
+ *
+ * @throws IOException
+ * in case of I/O error
*/
- static boolean encode(StringBuilder builder, Object value) {
+ static boolean encode(OutputStream out, Object value) throws IOException {
if (value == null) {
- builder.append("NULL");
+ write(out, "NULL");
} else if (value.getClass().getSimpleName().endsWith("[]")) {
// Simple name does support [] suffix and do not return NULL for
// inner anonymous classes
- return customTypes.get("[]").encode(builder, value);
+ customTypes.get("[]").encode(out, value);
} else if (customTypes.containsKey(value.getClass().getCanonicalName())) {
- return customTypes.get(value.getClass().getCanonicalName())//
- .encode(builder, value);
+ customTypes.get(value.getClass().getCanonicalName())//
+ .encode(out, value);
} else if (value instanceof String) {
- encodeString(builder, (String) value);
+ encodeString(out, (String) value);
} else if (value instanceof Boolean) {
- builder.append(value);
+ write(out, value);
} else if (value instanceof Byte) {
- builder.append(value).append('b');
+ write(out, value);
+ write(out, "b");
} else if (value instanceof Character) {
- encodeString(builder, "" + value);
- builder.append('c');
+ encodeString(out, "" + value);
+ write(out, "c");
} else if (value instanceof Short) {
- builder.append(value).append('s');
+ write(out, value);
+ write(out, "s");
} else if (value instanceof Integer) {
- builder.append(value);
+ write(out, value);
} else if (value instanceof Long) {
- builder.append(value).append('L');
+ write(out, value);
+ write(out, "L");
} else if (value instanceof Float) {
- builder.append(value).append('F');
+ write(out, value);
+ write(out, "F");
} else if (value instanceof Double) {
- builder.append(value).append('d');
+ write(out, value);
+ write(out, "d");
} else if (value instanceof Enum) {
String type = value.getClass().getCanonicalName();
- builder.append(type).append(".").append(((Enum<?>) value).name())
- .append(";");
+ write(out, type);
+ write(out, ".");
+ write(out, ((Enum<?>) value).name());
+ write(out, ";");
} else {
return false;
}
// custom:TYPE_NAME:"content is String-encoded"
String type = CustomSerializer.typeOf(encodedValue);
if (customTypes.containsKey(type)) {
- return customTypes.get(type).decode(encodedValue);
+ // TODO: we should start with a stream
+ InputStream streamEncodedValue = new ByteArrayInputStream(
+ encodedValue.getBytes("UTF-8"));
+ try {
+ return customTypes.get(type).decode(streamEncodedValue);
+ } finally {
+ streamEncodedValue.close();
+ }
}
throw new IOException("Unknown custom type: " + type);
} else if (encodedValue.equals("NULL")
if (e instanceof IOException) {
throw (IOException) e;
}
- throw new IOException(e.getMessage());
+ throw new IOException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Write the given {@link String} into the given {@link OutputStream} in
+ * UTF-8.
+ *
+ * @param out
+ * the {@link OutputStream}
+ * @param data
+ * the data to write, cannot be NULL
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ static void write(OutputStream out, Object data) throws IOException {
+ try {
+ out.write(data.toString().getBytes("UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ // A conforming JVM is required to support UTF-8
+ e.printStackTrace();
}
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
- private static Enum<?> decodeEnum(String escaped) {
+ static private Enum<?> decodeEnum(String escaped) {
// escaped: be.xxx.EnumType.VALUE;
int pos = escaped.lastIndexOf(".");
String type = escaped.substring(0, pos);
}
// aa bb -> "aa\tbb"
- private static void encodeString(StringBuilder builder, String raw) {
- builder.append('\"');
+ static void encodeString(OutputStream out, String raw) throws IOException {
+ // TODO: not. efficient.
+ out.write('\"');
+ // TODO !! utf-8 required
for (char car : raw.toCharArray()) {
- switch (car) {
- case '\\':
- builder.append("\\\\");
- break;
- case '\r':
- builder.append("\\r");
- break;
- case '\n':
- builder.append("\\n");
- break;
- case '"':
- builder.append("\\\"");
- break;
- default:
- builder.append(car);
- break;
+ encodeString(out, car);
+ }
+ out.write('\"');
+ }
+
+ // aa bb -> "aa\tbb"
+ static void encodeString(OutputStream out, InputStream raw)
+ throws IOException {
+ out.write('\"');
+ byte buffer[] = new byte[4096];
+ for (int len = 0; (len = raw.read(buffer)) > 0;) {
+ for (int i = 0; i < len; i++) {
+ // TODO: not 100% correct, look up howto for UTF-8
+ encodeString(out, (char) buffer[i]);
}
}
- builder.append('\"');
+ out.write('\"');
+ }
+
+ // for encode string, NOT to encode a char by itself!
+ static void encodeString(OutputStream out, char raw) throws IOException {
+ switch (raw) {
+ case '\\':
+ out.write('\\');
+ out.write('\\');
+ break;
+ case '\r':
+ out.write('\\');
+ out.write('r');
+ break;
+ case '\n':
+ out.write('\\');
+ out.write('n');
+ break;
+ case '"':
+ out.write('\\');
+ out.write('\"');
+ break;
+ default:
+ out.write(raw);
+ break;
+ }
}
// "aa\tbb" -> aa bb
- private static String decodeString(String escaped) {
+ static String decodeString(String escaped) {
StringBuilder builder = new StringBuilder();
boolean escaping = false;
package be.nikiroo.utils.serial.server;
-import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
-import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import javax.net.ssl.SSLException;
import be.nikiroo.utils.CryptUtils;
-import be.nikiroo.utils.Version;
+import be.nikiroo.utils.IOUtils;
import be.nikiroo.utils.serial.Exporter;
import be.nikiroo.utils.serial.Importer;
+import be.nikiroo.utils.streams.BufferedOutputStream;
+import be.nikiroo.utils.streams.NextableInputStream;
+import be.nikiroo.utils.streams.NextableInputStreamStep;
+import be.nikiroo.utils.streams.ReplaceInputStream;
+import be.nikiroo.utils.streams.ReplaceOutputStream;
/**
* Base class used for the client/server basic handling.
abstract class ConnectAction {
private Socket s;
private boolean server;
- private Version version;
- private Version clientVersion;
private CryptUtils crypt;
private Object lock = new Object();
- private InputStream in;
- private OutputStream out;
+ private NextableInputStream in;
+ private BufferedOutputStream out;
private boolean contentToSend;
- private long bytesReceived;
- private long bytesSent;
-
/**
* Method that will be called when an action is performed on either the
* client or server this {@link ConnectAction} represent.
*
- * @param version
- * the counter part version
- *
* @throws Exception
* in case of I/O error
*/
- abstract protected void action(Version version) throws Exception;
-
- /**
- * Method called when we negotiate the version with the client.
- * <p>
- * Thus, it is only called on the server.
- * <p>
- * Will return the actual server version by default.
- *
- * @param clientVersion
- * the client version
- *
- * @return the version to send to the client
- */
- abstract protected Version negotiateVersion(Version clientVersion);
+ abstract protected void action() throws Exception;
/**
* Handler called when an unexpected error occurs in the code.
* @param key
* an optional key to encrypt all the communications (if NULL,
* everything will be sent in clear text)
- * @param version
- * the version of this client-or-server
*/
- protected ConnectAction(Socket s, boolean server, String key,
- Version version) {
+ protected ConnectAction(Socket s, boolean server, String key) {
this.s = s;
this.server = server;
if (key != null) {
crypt = new CryptUtils(key);
}
-
- if (version == null) {
- this.version = new Version();
- } else {
- this.version = version;
- }
-
- clientVersion = new Version();
- }
-
- /**
- * The version of this client-or-server.
- *
- * @return the version
- */
- public Version getVersion() {
- return version;
}
/**
* @return the amount of bytes received
*/
public long getBytesReceived() {
- return bytesReceived;
+ return in.getBytesRead();
}
/**
*
* @return the amount of bytes sent
*/
- public long getBytesSent() {
- return bytesSent;
+ public long getBytesWritten() {
+ return out.getBytesWritten();
}
/**
*/
public void connect() {
try {
- in = s.getInputStream();
- try {
- out = s.getOutputStream();
- try {
- if (server) {
- String line;
- try {
- line = readLine(in);
- } catch (SSLException e) {
- out.write("Unauthorized\n".getBytes());
- throw e;
- }
+ // TODO: assure that \b is never used, make sure \n usage is OK
+ in = new NextableInputStream(s.getInputStream(),
+ new NextableInputStreamStep('\b'));
- if (line != null && line.startsWith("VERSION ")) {
- // "VERSION client-version" (VERSION 1.0.0)
- Version clientVersion = new Version(
- line.substring("VERSION ".length()));
- this.clientVersion = clientVersion;
- Version v = negotiateVersion(clientVersion);
- if (v == null) {
- v = new Version();
- }
-
- sendString("VERSION " + v.toString());
- }
-
- action(clientVersion);
- } else {
- String v = sendString("VERSION " + version.toString());
- if (v != null && v.startsWith("VERSION ")) {
- v = v.substring("VERSION ".length());
- }
+ try {
+ out = new BufferedOutputStream(s.getOutputStream());
- action(new Version(v));
- }
+ try {
+ action();
} finally {
out.close();
- out = null;
}
} finally {
in.close();
- in = null;
}
} catch (Exception e) {
onError(e);
* @param data
* the data to send
*
- * @return the answer (which can be NULL) if this action is a client, always
- * NULL if it is a server
+ * @return the answer (which can be NULL if no answer, or NULL for an answer
+ * which is NULL) if this action is a client, always NULL if it is a
+ * server
*
* @throws IOException
* in case of I/O error
*/
protected Object sendObject(Object data) throws IOException,
NoSuchFieldException, NoSuchMethodException, ClassNotFoundException {
- synchronized (lock) {
- String rep = sendString(new Exporter().append(data).toString(true,
- true));
- if (rep != null) {
- return new Importer().read(rep).getValue();
- }
-
- return null;
- }
+ return send(out, data, false);
}
/**
protected Object recObject() throws IOException, NoSuchFieldException,
NoSuchMethodException, ClassNotFoundException,
java.lang.NullPointerException {
- String str = recString();
- if (str == null) {
- throw new NullPointerException("No more data available");
- }
-
- return new Importer().read(str).getValue();
+ return rec(false);
}
/**
* in case of crypt error
*/
protected String sendString(String line) throws IOException {
- synchronized (lock) {
- writeLine(out, line);
-
- if (server) {
- out.flush();
- return null;
- }
-
- contentToSend = true;
- return recString();
+ try {
+ return (String) send(out, line, true);
+ } catch (NoSuchFieldException e) {
+ // Cannot happen
+ e.printStackTrace();
+ } catch (NoSuchMethodException e) {
+ // Cannot happen
+ e.printStackTrace();
+ } catch (ClassNotFoundException e) {
+ // Cannot happen
+ e.printStackTrace();
}
+
+ return null;
}
/**
* <p>
* Will only flush the data if there is contentToSend.
*
- * @return the answer (which can be NULL)
+ * @return the answer (which can be NULL if no more content)
*
* @throws IOException
* in case of I/O error
* in case of crypt error
*/
protected String recString() throws IOException {
- synchronized (lock) {
- if (server || contentToSend) {
- if (contentToSend) {
- out.flush();
- contentToSend = false;
- }
-
- return readLine(in);
- }
-
- return null;
+ try {
+ return (String) rec(true);
+ } catch (NoSuchFieldException e) {
+ // Cannot happen
+ e.printStackTrace();
+ } catch (NoSuchMethodException e) {
+ // Cannot happen
+ e.printStackTrace();
+ } catch (ClassNotFoundException e) {
+ // Cannot happen
+ e.printStackTrace();
+ } catch (NullPointerException e) {
+ // Should happen
+ e.printStackTrace();
}
+
+ return null;
}
/**
- * Read a possibly encrypted line.
+ * Serialise and send the given object to the counter part (and, only for
+ * client, return the deserialised answer -- the server will always receive
+ * NULL).
*
- * @param in
- * the stream to read from
- * @return the unencrypted line
+ * @param out
+ * the stream to write to
+ * @param data
+ * the data to write
+ * @param asString
+ * TRUE to write it as a String, FALSE to write it as an Object
*
+ * @return the answer (which can be NULL if no answer, or NULL for an answer
+ * which is NULL) if this action is a client, always NULL if it is a
+ * server
*
* @throws IOException
* in case of I/O error
* @throws SSLException
* in case of crypt error
+ * @throws IOException
+ * in case of I/O error
+ * @throws NoSuchFieldException
+ * if the serialised data contains information about a field
+ * which does actually not exist in the class we know of
+ * @throws NoSuchMethodException
+ * if a class described in the serialised data cannot be created
+ * because it is not compatible with this code
+ * @throws ClassNotFoundException
+ * if a class described in the serialised data cannot be found
*/
- private String readLine(InputStream in) throws IOException {
- if (inReader == null) {
- inReader = new BufferedReader(new InputStreamReader(in));
- }
- String line = inReader.readLine();
- if (line != null) {
- bytesReceived += line.length();
+ private Object send(BufferedOutputStream out, Object data, boolean asString)
+ throws IOException, NoSuchFieldException, NoSuchMethodException,
+ ClassNotFoundException, java.lang.NullPointerException {
+
+ synchronized (lock) {
+ OutputStream sub;
if (crypt != null) {
- line = crypt.decrypt64s(line, false);
+ sub = crypt.encrypt64(out.open(), false);
+ } else {
+ sub = out.open();
}
- }
- return line;
- }
+ // TODO: could be possible to check for non-crypt and only
+ // do it for crypt
+ sub = new ReplaceOutputStream(sub, //
+ new String[] { "\\", "\b" }, //
+ new String[] { "\\\\", "\\b" });
+
+ try {
+ if (asString) {
+ sub.write(data.toString().getBytes("UTF-8"));
+ } else {
+ new Exporter(sub).append(data);
+ }
+ } finally {
+ sub.close();
+ }
- private BufferedReader inReader;
+ out.write('\b');
+
+ if (server) {
+ out.flush();
+ return null;
+ }
+
+ contentToSend = true;
+ try {
+ return rec(asString);
+ } catch (NullPointerException e) {
+ // We accept no data here for Objects
+ }
+
+ return null;
+ }
+ }
/**
- * Write a line, possible encrypted.
+ * Reserved for the server: flush the data to the client and retrieve its
+ * answer.
+ * <p>
+ * Also used internally for the client (only do something if there is
+ * contentToSend).
+ * <p>
+ * Will only flush the data if there is contentToSend.
+ * <p>
+ * Note that the behaviour is slightly different for String and Object
+ * reading regarding exceptions:
+ * <ul>
+ * <li>NULL means that the counter part has no more data to send</li>
+ * <li>All the exceptions except {@link IOException} are there for Object
+ * conversion</li>
+ * </ul>
+ *
+ * @param asString
+ * TRUE for String reading, FALSE for Object reading (which can
+ * still be a String)
+ *
+ * @return the deserialised answer (which can actually be NULL)
*
- * @param out
- * the stream to write to
- * @param line
- * the line to write
* @throws IOException
* in case of I/O error
- * @throws SSLException
- * in case of crypt error
+ * @throws NoSuchFieldException
+ * if the serialised data contains information about a field
+ * which does actually not exist in the class we know of
+ * @throws NoSuchMethodException
+ * if a class described in the serialised data cannot be created
+ * because it is not compatible with this code
+ * @throws ClassNotFoundException
+ * if a class described in the serialised data cannot be found
+ * @throws java.lang.NullPointerException
+ * for Objects only: if the counter part has no data to send
*/
- private void writeLine(OutputStream out, String line) throws IOException {
- if (crypt == null) {
- out.write(line.getBytes());
- bytesSent += line.length();
- } else {
- // TODO: how NOT to create so many big Strings?
- String b64 = crypt.encrypt64(line, false);
- out.write(b64.getBytes());
- bytesSent += b64.length();
+ private Object rec(boolean asString) throws IOException,
+ NoSuchFieldException, NoSuchMethodException,
+ ClassNotFoundException, java.lang.NullPointerException {
+
+ synchronized (lock) {
+ if (server || contentToSend) {
+ if (contentToSend) {
+ out.flush();
+ contentToSend = false;
+ }
+
+ if (in.next()) {
+ // TODO: could be possible to check for non-crypt and only
+ // do it for crypt
+ InputStream read = new ReplaceInputStream(in.open(), //
+ new String[] { "\\\\", "\\b" }, //
+ new String[] { "\\", "\b" });
+
+ try {
+ if (crypt != null) {
+ read = crypt.decrypt64(read, false);
+ }
+
+ if (asString) {
+ return IOUtils.readSmallStream(read);
+ }
+
+ return new Importer().read(read).getValue();
+ } finally {
+ read.close();
+ }
+ }
+
+ if (!asString) {
+ throw new NullPointerException();
+ }
+ }
+
+ return null;
}
- out.write("\n".getBytes());
- bytesSent++;
}
}
\ No newline at end of file
import java.net.Socket;
import java.net.UnknownHostException;
-import be.nikiroo.utils.Version;
-
/**
* Base class used for the client basic handling.
* <p>
protected ConnectAction action;
/**
- * Create a new {@link ConnectActionClient} with the current application
- * version (see {@link Version#getCurrentVersion()}) as the client version.
- *
- * @param s
- * the socket to bind to
- * @param key
- * an optional key to encrypt all the communications (if NULL,
- * everything will be sent in clear text)
- */
- public ConnectActionClient(Socket s, String key) {
- this(s, key, Version.getCurrentVersion());
- }
-
- /**
- * Create a new {@link ConnectActionClient} with the current application
- * version (see {@link Version#getCurrentVersion()}) as the client version.
+ * Create a new {@link ConnectActionClient}.
*
* @param host
* the host to bind to
*/
public ConnectActionClient(String host, int port, String key)
throws IOException {
- this(new Socket(host, port), key, Version.getCurrentVersion());
- }
-
- /**
- * Create a new {@link ConnectActionClient}.
- *
- * @param host
- * the host to bind to
- * @param port
- * the port to bind to
- * @param key
- * an optional key to encrypt all the communications (if NULL,
- * everything will be sent in clear text)
- * @param version
- * the client version
- *
- * @throws IOException
- * in case of I/O error
- * @throws UnknownHostException
- * if the host is not known
- * @throws IllegalArgumentException
- * if the port parameter is outside the specified range of valid
- * port values, which is between 0 and 65535, inclusive
- */
- public ConnectActionClient(String host, int port, String key,
- Version version) throws IOException {
- this(new Socket(host, port), key, version);
+ this(new Socket(host, port), key);
}
/**
* @param key
* an optional key to encrypt all the communications (if NULL,
* everything will be sent in clear text)
- * @param version
- * the client version
*/
- public ConnectActionClient(Socket s, String key, Version version) {
- action = new ConnectAction(s, false, key, version) {
+ public ConnectActionClient(Socket s, String key) {
+ action = new ConnectAction(s, false, key) {
@Override
- protected void action(Version serverVersion) throws Exception {
- ConnectActionClient.this.action(serverVersion);
+ protected void action() throws Exception {
+ ConnectActionClient.this.action();
}
@Override
protected void onError(Exception e) {
ConnectActionClient.this.onError(e);
}
-
- @Override
- protected Version negotiateVersion(Version clientVersion) {
- new Exception("Should never be called on a client")
- .printStackTrace();
- return null;
- }
};
}
/**
* Method that will be called when an action is performed on the client.
*
- * @param serverVersion
- * the server version
- *
* @throws Exception
* in case of I/O error
*/
@SuppressWarnings("unused")
- public void action(Version serverVersion) throws Exception {
+ public void action() throws Exception {
}
/**
super(host, port, key);
}
- /**
- * Create a new {@link ConnectActionClientObject}.
- *
- * @param host
- * the host to bind to
- * @param port
- * the port to bind to
- * @param key
- * an optional key to encrypt all the communications (if NULL,
- * everything will be sent in clear text)
- * @param version
- * the client version
- *
- * @throws IOException
- * in case of I/O error
- * @throws UnknownHostException
- * if the IP address of the host could not be determined
- * @throws IllegalArgumentException
- * if the port parameter is outside the specified range of valid
- * port values, which is between 0 and 65535, inclusive
- */
- public ConnectActionClientObject(String host, int port, String key,
- Version version) throws IOException {
- super(host, port, key, version);
- }
-
- /**
- * Create a new {@link ConnectActionClientObject}.
- *
- * @param s
- * the socket to bind to
- * @param version
- * the client version
- * @param key
- * an optional key to encrypt all the communications (if NULL,
- * everything will be sent in clear text)
- */
- public ConnectActionClientObject(Socket s, String key, Version version) {
- super(s, key, version);
- }
-
/**
* Serialise and send the given object to the server (and return the
* deserialised answer).
super(host, port, key);
}
- /**
- * Create a new {@link ConnectActionClientString}.
- *
- * @param host
- * the host to bind to
- * @param port
- * the port to bind to
- * @param key
- * an optional key to encrypt all the communications (if NULL,
- * everything will be sent in clear text)
- * @param version
- * the client version
- *
- * @throws IOException
- * in case of I/O error
- * @throws UnknownHostException
- * if the IP address of the host could not be determined
- * @throws IllegalArgumentException
- * if the port parameter is outside the specified range of valid
- * port values, which is between 0 and 65535, inclusive
- */
- public ConnectActionClientString(String host, int port, String key,
- Version version) throws IOException {
- super(host, port, key, version);
- }
-
- /**
- * Create a new {@link ConnectActionClientString}.
- *
- * @param s
- * the socket to bind to
- * @param key
- * an optional key to encrypt all the communications (if NULL,
- * everything will be sent in clear text)
- * @param version
- * the client version
- */
- public ConnectActionClientString(Socket s, String key, Version version) {
- super(s, key, version);
- }
-
/**
* Send the given object to the server (and return the answer).
*
import java.net.Socket;
-import be.nikiroo.utils.Version;
-
/**
* Base class used for the server basic handling.
* <p>
*/
protected ConnectAction action;
- /**
- * Create a new {@link ConnectActionServer} with the current application
- * version (see {@link Version#getCurrentVersion()}) as the server version.
- *
- * @param s
- * the socket to bind to
- * @param key
- * an optional key to encrypt all the communications (if NULL,
- * everything will be sent in clear text)
- */
- public ConnectActionServer(Socket s, String key) {
- this(s, key, Version.getCurrentVersion());
- }
-
/**
* Create a new {@link ConnectActionServer}.
*
* @param key
* an optional key to encrypt all the communications (if NULL,
* everything will be sent in clear text)
- * @param version
- * the server version
*/
- public ConnectActionServer(Socket s, String key, Version version) {
- action = new ConnectAction(s, true, key, version) {
+ public ConnectActionServer(Socket s, String key) {
+ action = new ConnectAction(s, true, key) {
@Override
- protected void action(Version clientVersion) throws Exception {
- ConnectActionServer.this.action(clientVersion);
+ protected void action() throws Exception {
+ ConnectActionServer.this.action();
}
@Override
protected void onError(Exception e) {
ConnectActionServer.this.onError(e);
}
-
- @Override
- protected Version negotiateVersion(Version clientVersion) {
- return ConnectActionServer.this.negotiateVersion(clientVersion);
- }
};
}
* @return the amount of bytes sent
*/
public long getBytesSent() {
- return action.getBytesSent();
+ return action.getBytesWritten();
}
/**
* Method that will be called when an action is performed on the server.
*
- * @param clientVersion
- * the client version
- *
* @throws Exception
* in case of I/O error
*/
@SuppressWarnings("unused")
- public void action(Version clientVersion) throws Exception {
+ public void action() throws Exception {
}
/**
*/
protected void onError(@SuppressWarnings("unused") Exception e) {
}
-
- /**
- * Method called when we negotiate the version with the client.
- * <p>
- * Will return the actual server version by default.
- *
- * @param clientVersion
- * the client version
- *
- * @return the version to send to the client
- */
- protected Version negotiateVersion(
- @SuppressWarnings("unused") Version clientVersion) {
- return action.getVersion();
- }
}
\ No newline at end of file
super(s, key);
}
- /**
- * Create a new {@link ConnectActionServerObject}.
- *
- * @param s
- * the socket to bind to
- * @param key
- * an optional key to encrypt all the communications (if NULL,
- * everything will be sent in clear text)
- * @param version
- * the server version
- */
- public ConnectActionServerObject(Socket s, String key, Version version) {
- super(s, key, version);
- }
-
/**
* Serialise and send the given object to the client.
*
super(s, key);
}
- /**
- * Create a new {@link ConnectActionServerString}.
- *
- * @param s
- * the socket to bind to
- * @param key
- * an optional key to encrypt all the communications (if NULL,
- * everything will be sent in clear text)
- * @param version
- * the server version
- */
- public ConnectActionServerString(Socket s, String key, Version version) {
- super(s, key, version);
- }
-
/**
* Serialise and send the given object to the client.
*
package be.nikiroo.utils.serial.server;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.lang.reflect.Array;
import java.net.Socket;
import java.net.UnknownHostException;
import be.nikiroo.utils.StringUtils;
import be.nikiroo.utils.TraceHandler;
-import be.nikiroo.utils.Version;
import be.nikiroo.utils.serial.Importer;
/**
@Override
protected ConnectActionServer createConnectActionServer(Socket s) {
+ // Bad impl, not up to date (should work, but not efficient)
return new ConnectActionServerString(s, key) {
@Override
- public void action(final Version clientVersion) throws Exception {
- onClientContact(clientVersion);
+ public void action() throws Exception {
+ onClientContact();
final ConnectActionServerString bridge = this;
try {
new ConnectActionClientString(forwardToHost, forwardToPort,
- forwardToKey, clientVersion) {
+ forwardToKey) {
@Override
- public void action(final Version serverVersion)
- throws Exception {
- onServerContact(serverVersion);
+ public void action() throws Exception {
+ onServerContact();
for (String fromClient = bridge.rec(); fromClient != null; fromClient = bridge
.rec()) {
- onRec(clientVersion, fromClient);
+ onRec(fromClient);
String fromServer = send(fromClient);
- onSend(serverVersion, fromServer);
+ onSend(fromServer);
bridge.send(fromServer);
}
/**
* This is the method that is called each time a client contact us.
- *
- * @param clientVersion
- * the client version
*/
- protected void onClientContact(Version clientVersion) {
- getTraceHandler().trace(">>> CLIENT " + clientVersion);
+ protected void onClientContact() {
+ getTraceHandler().trace(">>> CLIENT ");
}
/**
* This is the method that is called each time a client contact us.
- *
- * @param serverVersion
- * the server version
*/
- protected void onServerContact(Version serverVersion) {
- getTraceHandler().trace("<<< SERVER " + serverVersion);
+ protected void onServerContact() {
+ getTraceHandler().trace("<<< SERVER");
getTraceHandler().trace("");
}
/**
* This is the method that is called each time a client contact us.
*
- * @param clientVersion
- * the client version
* @param data
* the data sent by the client
*/
- protected void onRec(Version clientVersion, String data) {
- trace(">>> CLIENT (" + clientVersion + ")", data);
+ protected void onRec(String data) {
+ trace(">>> CLIENT", data);
}
/**
* This is the method that is called each time the forwarded server contact
* us.
*
- * @param serverVersion
- * the client version
* @param data
* the data sent by the client
*/
- protected void onSend(Version serverVersion, String data) {
- trace("<<< SERVER (" + serverVersion + ")", data);
+ protected void onSend(String data) {
+ trace("<<< SERVER", data);
}
@Override
* the data to trace
*/
private void trace(String prefix, String data) {
+ // TODO: we convert to string and back
int size = data == null ? 0 : data.length();
String ssize = StringUtils.formatNumber(size) + "bytes";
}
}
- Object obj = new Importer().read(data).getValue();
- if (obj == null) {
- getTraceHandler().trace("NULL", 2);
- getTraceHandler().trace("NULL", 3);
- getTraceHandler().trace("NULL", 4);
- } else {
- if (obj.getClass().isArray()) {
- getTraceHandler().trace(
- "(" + obj.getClass() + ") with "
- + Array.getLength(obj) + "element(s)",
- 3);
+ InputStream stream = new ByteArrayInputStream(
+ data.getBytes("UTF-8"));
+ try {
+ Object obj = new Importer().read(stream).getValue();
+ if (obj == null) {
+ getTraceHandler().trace("NULL", 2);
+ getTraceHandler().trace("NULL", 3);
+ getTraceHandler().trace("NULL", 4);
} else {
- getTraceHandler().trace("(" + obj.getClass() + ")", 2);
+ if (obj.getClass().isArray()) {
+ getTraceHandler().trace(
+ "(" + obj.getClass() + ") with "
+ + Array.getLength(obj)
+ + "element(s)", 3);
+ } else {
+ getTraceHandler().trace("(" + obj.getClass() + ")",
+ 2);
+ }
+ getTraceHandler().trace("" + obj.toString(), 3);
+ getTraceHandler().trace(data, 4);
}
- getTraceHandler().trace("" + obj.toString(), 3);
- getTraceHandler().trace(data, 4);
+ } finally {
+ stream.close();
}
} catch (NoSuchMethodException e) {
getTraceHandler().trace("(not an object)", 2);
import java.net.Socket;
import java.net.UnknownHostException;
-import be.nikiroo.utils.Version;
-
/**
* This class implements a simple server that can listen for connections and
* send/receive objects.
protected ConnectActionServer createConnectActionServer(Socket s) {
return new ConnectActionServerObject(s, key) {
@Override
- public void action(Version clientVersion) throws Exception {
+ public void action() throws Exception {
try {
for (Object data = rec(); true; data = rec()) {
Object rep = null;
try {
- rep = onRequest(this, clientVersion, data);
+ rep = onRequest(this, data);
if (isClosing()) {
return;
}
} catch (Exception e) {
onError(e);
}
+
send(rep);
}
} catch (NullPointerException e) {
*
* @param action
* the client action
- * @param clientVersion
- * the client version
* @param data
* the data sent by the client (which can be NULL)
*
* in case of an exception, the error will only be logged
*/
abstract protected Object onRequest(ConnectActionServerObject action,
- Version clientVersion, Object data) throws Exception;
+ Object data) throws Exception;
}
import java.net.Socket;
import java.net.UnknownHostException;
-import be.nikiroo.utils.Version;
-
/**
* This class implements a simple server that can listen for connections and
* send/receive Strings.
protected ConnectActionServer createConnectActionServer(Socket s) {
return new ConnectActionServerString(s, key) {
@Override
- public void action(Version clientVersion) throws Exception {
+ public void action() throws Exception {
for (String data = rec(); data != null; data = rec()) {
String rep = null;
try {
- rep = onRequest(this, clientVersion, data);
+ rep = onRequest(this, data);
if (isClosing()) {
return;
}
*
* @param action
* the client action
- * @param clientVersion
- * the client version
* @param data
* the data sent by the client
*
* in case of an exception, the error will only be logged
*/
abstract protected String onRequest(ConnectActionServerString action,
- Version clientVersion, String data) throws Exception;
+ String data) throws Exception;
}
addTest(new TestCase("Simple test") {
@Override
public void test() throws Exception {
- InputStream in = new ByteArrayInputStream(new byte[] {42, 127, 12});
+ InputStream in = new ByteArrayInputStream(new byte[] { 42, 127,
+ 12 });
crypt.encrypt(in);
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.write(in, out);
byte[] result = out.toByteArray();
-
- assertEquals("We wrote 3 bytes, we expected 3 bytes back but got: "
- + result.length, result.length, result.length);
-
+
+ assertEquals(
+ "We wrote 3 bytes, we expected 3 bytes back but got: "
+ + result.length, result.length, result.length);
+
assertEquals(42, result[0]);
assertEquals(127, result[1]);
assertEquals(12, result[2]);
import java.net.URL;
-import be.nikiroo.utils.Version;
import be.nikiroo.utils.serial.server.ConnectActionClientObject;
import be.nikiroo.utils.serial.server.ConnectActionClientString;
import be.nikiroo.utils.serial.server.ConnectActionServerObject;
public SerialServerTest(String[] args) {
super("SerialServer test", args);
- for (String key : new String[] { null, "",
- "some real key with a few bytes in it" }) {
+ for (String key : new String[] { null,
+ "some super secret encryption key" }) {
for (boolean bridge : new Boolean[] { false, true }) {
final String skey = (key != null ? "(encrypted)"
: "(plain text)");
ServerString server = new ServerString(this.getName(), 0, key) {
@Override
protected String onRequest(
- ConnectActionServerString action,
- Version clientVersion, String data)
+ ConnectActionServerString action, String data)
throws Exception {
return null;
}
try {
new ConnectActionClientObject(null, port, key) {
@Override
- public void action(Version serverVersion)
- throws Exception {
+ public void action() throws Exception {
rec[0] = "ok";
}
}.connect();
ServerString server = new ServerString(this.getName(), 0, key) {
@Override
protected String onRequest(
- ConnectActionServerString action,
- Version clientVersion, String data)
+ ConnectActionServerString action, String data)
throws Exception {
sent[0] = data;
return "pong";
try {
new ConnectActionClientString(null, port, key) {
@Override
- public void action(Version serverVersion)
- throws Exception {
+ public void action() throws Exception {
recd[0] = send("ping");
}
}.connect();
}
if (err[0] != null) {
- fail("An exception was thrown: " + err[0].getMessage());
+ fail("An exception was thrown: " + err[0].getMessage(),
+ err[0]);
}
assertEquals("ping", sent[0]);
ServerString server = new ServerString(this.getName(), 0, key) {
@Override
protected String onRequest(
- ConnectActionServerString action,
- Version clientVersion, String data)
+ ConnectActionServerString action, String data)
throws Exception {
sent[0] = data;
action.send("pong");
try {
new ConnectActionClientString(null, port, key) {
@Override
- public void action(Version serverVersion)
- throws Exception {
+ public void action() throws Exception {
recd[0] = send("ping");
recd[1] = send("ping2");
}
}
if (err[0] != null) {
- fail("An exception was thrown: " + err[0].getMessage());
+ fail("An exception was thrown: " + err[0].getMessage(),
+ err[0]);
}
assertEquals("ping", sent[0]);
ServerString server = new ServerString(this.getName(), 0, key) {
@Override
protected String onRequest(
- ConnectActionServerString action,
- Version clientVersion, String data)
+ ConnectActionServerString action, String data)
throws Exception {
sent[Integer.parseInt(data)] = data;
return "" + (Integer.parseInt(data) * 2);
try {
new ConnectActionClientString(null, port, key) {
@Override
- public void action(Version serverVersion)
- throws Exception {
+ public void action() throws Exception {
for (int i = 0; i < 3; i++) {
recd[i] = send("" + i);
}
}
if (err[0] != null) {
- fail("An exception was thrown: " + err[0].getMessage());
+ fail("An exception was thrown: " + err[0].getMessage(),
+ err[0]);
}
assertEquals("0", sent[0]);
ServerObject server = new ServerObject(this.getName(), 0, key) {
@Override
protected Object onRequest(
- ConnectActionServerObject action,
- Version clientVersion, Object data)
+ ConnectActionServerObject action, Object data)
throws Exception {
return null;
}
try {
new ConnectActionClientObject(null, port, key) {
@Override
- public void action(Version serverVersion)
- throws Exception {
+ public void action() throws Exception {
rec[0] = true;
}
ServerObject server = new ServerObject(this.getName(), 0, key) {
@Override
protected Object onRequest(
- ConnectActionServerObject action,
- Version clientVersion, Object data)
+ ConnectActionServerObject action, Object data)
throws Exception {
sent[0] = data;
return "pong";
try {
new ConnectActionClientObject(null, port, key) {
@Override
- public void action(Version serverVersion)
- throws Exception {
+ public void action() throws Exception {
recd[0] = send("ping");
}
}.connect();
}
if (err[0] != null) {
- fail("An exception was thrown: " + err[0].getMessage());
+ fail("An exception was thrown: " + err[0].getMessage(),
+ err[0]);
}
assertEquals("ping", sent[0]);
ServerObject server = new ServerObject(this.getName(), 0, key) {
@Override
protected Object onRequest(
- ConnectActionServerObject action,
- Version clientVersion, Object data)
+ ConnectActionServerObject action, Object data)
throws Exception {
sent[0] = data;
action.send("pong");
try {
new ConnectActionClientObject(null, port, key) {
@Override
- public void action(Version serverVersion)
- throws Exception {
+ public void action() throws Exception {
recd[0] = send("ping");
recd[1] = send("ping2");
}
}
if (err[0] != null) {
- fail("An exception was thrown: " + err[0].getMessage());
+ fail("An exception was thrown: " + err[0].getMessage(),
+ err[0]);
}
assertEquals("ping", sent[0]);
ServerObject server = new ServerObject(this.getName(), 0, key) {
@Override
protected Object onRequest(
- ConnectActionServerObject action,
- Version clientVersion, Object data)
+ ConnectActionServerObject action, Object data)
throws Exception {
sent[0] = data;
return new Object[] { "ACK" };
try {
new ConnectActionClientObject(null, port, key) {
@Override
- public void action(Version serverVersion)
- throws Exception {
+ public void action() throws Exception {
recd[0] = send(new Object[] {
"key",
new URL(
}
if (err[0] != null) {
- fail("An exception was thrown: " + err[0].getMessage());
+ fail("An exception was thrown: " + err[0].getMessage(),
+ err[0]);
}
Object[] sento = (Object[]) (sent[0]);
ServerObject server = new ServerObject(this.getName(), 0, key) {
@Override
protected Object onRequest(
- ConnectActionServerObject action,
- Version clientVersion, Object data)
+ ConnectActionServerObject action, Object data)
throws Exception {
sent[(Integer) data] = data;
return ((Integer) data) * 2;
try {
new ConnectActionClientObject(null, port, key) {
@Override
- public void action(Version serverVersion)
- throws Exception {
+ public void action() throws Exception {
for (int i = 0; i < 3; i++) {
recd[i] = send(i);
}
}
if (err[0] != null) {
- fail("An exception was thrown: " + err[0].getMessage());
+ fail("An exception was thrown: " + err[0].getMessage(),
+ err[0]);
}
assertEquals(0, sent[0]);
package be.nikiroo.utils.test_code;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.NotSerializableException;
import java.net.URL;
+import java.util.Arrays;
import be.nikiroo.utils.serial.Exporter;
import be.nikiroo.utils.serial.Importer;
this(null);
}
+ private void encodeRecodeTest(TestCase test, Object data) throws Exception {
+ byte[] encoded = toBytes(data, true);
+ Object redata = fromBytes(toBytes(data, false));
+ byte[] reencoded = toBytes(redata, true);
+
+ // We suppose text mode
+ if (encoded.length < 256 && reencoded.length < 256) {
+ test.assertEquals("Different data after encode/decode/encode",
+ new String(encoded, "UTF-8"),
+ new String(reencoded, "UTF-8"));
+ } else {
+ test.assertEquals("Different data after encode/decode/encode",
+ true, Arrays.equals(encoded, reencoded));
+ }
+ }
+
+ // try to remove pointer addresses
+ private byte[] toBytes(Object data, boolean clearRefs)
+ throws NotSerializableException, IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ new Exporter(out).append(data);
+ out.flush();
+
+ if (clearRefs) {
+ String tmp = new String(out.toByteArray(), "UTF-8");
+ tmp = tmp.replaceAll("@[0-9]*", "@REF");
+ return tmp.getBytes("UTF-8");
+ }
+
+ return out.toByteArray();
+ }
+
+ private Object fromBytes(byte[] data) throws NoSuchFieldException,
+ NoSuchMethodException, ClassNotFoundException,
+ NullPointerException, IOException {
+
+ InputStream in = new ByteArrayInputStream(data);
+ try {
+ return new Importer().read(in).getValue();
+ } finally {
+ in.close();
+ }
+ }
+
public SerialTest(String[] args) {
super("Serial test", args);
@Override
public void test() throws Exception {
Data data = new Data(42);
- String encoded = new Exporter().append(data).toString(false,
- false);
- Object redata = new Importer().read(encoded).getValue();
- String reencoded = new Exporter().append(redata).toString(
- false, false);
-
- assertEquals(encoded.replaceAll("@[0-9]*", "@REF"),
- reencoded.replaceAll("@[0-9]*", "@REF"));
+ encodeRecodeTest(this, data);
}
});
-
addTest(new TestCase() {
@SuppressWarnings("unused")
private TestCase me = setName("Anonymous inner class");
@SuppressWarnings("unused")
int value = 42;
};
-
- String encoded = new Exporter().append(data).toString(false,
- false);
- Object redata = new Importer().read(encoded).getValue();
- String reencoded = new Exporter().append(redata).toString(
- false, false);
-
- assertEquals(encoded.replaceAll("@[0-9]*", "@REF"),
- reencoded.replaceAll("@[0-9]*", "@REF"));
+ encodeRecodeTest(this, data);
}
});
-
addTest(new TestCase() {
@SuppressWarnings("unused")
private TestCase me = setName("Array of anonymous inner classes");
int value = 42;
} };
- String encoded = new Exporter().append(data).toString(false,
- false);
- Object redata = new Importer().read(encoded).getValue();
- String reencoded = new Exporter().append(redata).toString(
- false, false);
-
- // Comparing the 2 strings won't be useful, because the @REFs
- // will be ZIP-encoded; so we parse and re-encode the object
- encoded = new Exporter().append(data[0]).toString(false, false);
- try {
- reencoded = new Exporter().append(((Data[]) redata)[0])
- .toString(false, false);
- } catch (Exception e) {
- fail("Cannot cast the returned data into its original object",
- e);
- }
-
- assertEquals(encoded.replaceAll("@[0-9]*", "@REF"),
- reencoded.replaceAll("@[0-9]*", "@REF"));
+ byte[] encoded = toBytes(data, false);
+ Object redata = fromBytes(encoded);
+
+ // Comparing the 2 arrays won't be useful, because the @REFs
+ // will be ZIP-encoded; so we parse and re-encode each object
+
+ byte[] encoded1 = toBytes(data[0], true);
+ byte[] reencoded1 = toBytes(((Object[]) redata)[0], true);
+
+ assertEquals("Different data after encode/decode/encode", true,
+ Arrays.equals(encoded1, reencoded1));
}
});
-
addTest(new TestCase("URL Import/Export") {
@Override
public void test() throws Exception {
URL data = new URL("https://fanfan.be/");
- String encoded = new Exporter().append(data).toString(false,
- false);
- Object redata = new Importer().read(encoded).getValue();
- String reencoded = new Exporter().append(redata).toString(
- false, false);
-
- assertEquals(encoded.replaceAll("@[0-9]*", "@REF"),
- reencoded.replaceAll("@[0-9]*", "@REF"));
+ encodeRecodeTest(this, data);
}
});
-
addTest(new TestCase("URL-String Import/Export") {
@Override
public void test() throws Exception {
String data = new URL("https://fanfan.be/").toString();
- String encoded = new Exporter().append(data).toString(false,
- false);
- Object redata = new Importer().read(encoded).getValue();
- String reencoded = new Exporter().append(redata).toString(
- false, false);
-
- assertEquals(encoded.replaceAll("@[0-9]*", "@REF"),
- reencoded.replaceAll("@[0-9]*", "@REF"));
- assertEquals(data, redata);
+ encodeRecodeTest(this, data);
}
});
-
addTest(new TestCase("URL/URL-String arrays Import/Export") {
@Override
public void test() throws Exception {
final String url = "https://fanfan.be/";
-
Object[] data = new Object[] { new URL(url), url };
- String encoded = new Exporter().append(data).toString(false,
- false);
- Object redata = new Importer().read(encoded).getValue();
- String reencoded = new Exporter().append(redata).toString(
- false, false);
-
- assertEquals(encoded.replaceAll("@[0-9]*", "@REF"),
- reencoded.replaceAll("@[0-9]*", "@REF"));
- assertEquals(data[0], ((Object[]) redata)[0]);
- assertEquals(data[1], ((Object[]) redata)[1]);
+
+ byte[] encoded = toBytes(data, false);
+ Object redata = fromBytes(encoded);
+
+ // Comparing the 2 arrays won't be useful, because the @REFs
+ // will be ZIP-encoded; so we parse and re-encode each object
+
+ byte[] encoded1 = toBytes(data[0], true);
+ byte[] reencoded1 = toBytes(((Object[]) redata)[0], true);
+ byte[] encoded2 = toBytes(data[1], true);
+ byte[] reencoded2 = toBytes(((Object[]) redata)[1], true);
+
+ assertEquals("Different data 1 after encode/decode/encode",
+ true, Arrays.equals(encoded1, reencoded1));
+ assertEquals("Different data 2 after encode/decode/encode",
+ true, Arrays.equals(encoded2, reencoded2));
}
});
-
addTest(new TestCase("Import/Export with nested objects") {
@Override
public void test() throws Exception {
Data data = new DataObject(new Data(21));
- String encoded = new Exporter().append(data).toString(false,
- false);
- Object redata = new Importer().read(encoded).getValue();
- String reencoded = new Exporter().append(redata).toString(
- false, false);
-
- assertEquals(encoded.replaceAll("@[0-9]*", "@REF"),
- reencoded.replaceAll("@[0-9]*", "@REF"));
+ encodeRecodeTest(this, data);
}
});
-
addTest(new TestCase("Import/Export with nested objects forming a loop") {
@Override
public void test() throws Exception {
DataLoop data = new DataLoop("looping");
data.next = new DataLoop("level 2");
data.next.next = data;
-
- String encoded = new Exporter().append(data).toString(false,
- false);
- Object redata = new Importer().read(encoded).getValue();
- String reencoded = new Exporter().append(redata).toString(
- false, false);
-
- assertEquals(encoded.replaceAll("@[0-9]*", "@REF"),
- reencoded.replaceAll("@[0-9]*", "@REF"));
+ encodeRecodeTest(this, data);
}
});
-
addTest(new TestCase("Array in Object Import/Export") {
@Override
public void test() throws Exception {
Object data = new DataArray();// new String[] { "un", "deux" };
- String encoded = new Exporter().append(data).toString(false,
- false);
- Object redata = new Importer().read(encoded).getValue();
- String reencoded = new Exporter().append(redata).toString(
- false, false);
-
- assertEquals(encoded.replaceAll("@[0-9]*", "@REF"),
- reencoded.replaceAll("@[0-9]*", "@REF"));
+ encodeRecodeTest(this, data);
}
});
-
addTest(new TestCase("Array Import/Export") {
@Override
public void test() throws Exception {
Object data = new String[] { "un", "deux" };
- String encoded = new Exporter().append(data).toString(false,
- false);
- Object redata = new Importer().read(encoded).getValue();
- String reencoded = new Exporter().append(redata).toString(
- false, false);
-
- assertEquals(encoded.replaceAll("@[0-9]*", "@REF"),
- reencoded.replaceAll("@[0-9]*", "@REF"));
+ encodeRecodeTest(this, data);
}
});
-
addTest(new TestCase("Enum Import/Export") {
@Override
public void test() throws Exception {
Object data = EnumToSend.FANFAN;
- String encoded = new Exporter().append(data).toString(false,
- false);
- Object redata = new Importer().read(encoded).getValue();
- String reencoded = new Exporter().append(redata).toString(
- false, false);
-
- assertEquals(encoded.replaceAll("@[0-9]*", "@REF"),
- reencoded.replaceAll("@[0-9]*", "@REF"));
- }
- });
-
- addTest(new TestCase("B64 and ZIP String test") {
- @Override
- public void test() throws Exception {
- Object data = "Fanfan la tulipe";
- String encoded = new Exporter().append(data).toString(true,
- false);
- String redata = (String) new Importer().read(encoded)
- .getValue();
-
- assertEquals("Items not identical after B64", data, redata);
-
- encoded = new Exporter().append(data).toString(true, true);
- redata = (String) new Importer().read(encoded).getValue();
-
- assertEquals("Items not identical after ZIP", data, redata);
- }
- });
-
- addTest(new TestCase("B64 and ZIP Data test") {
- @Override
- public void test() throws Exception {
- Object data = new Data(55);
- String encoded = new Exporter().append(data).toString(true,
- false);
- Data redata = (Data) new Importer().read(encoded).getValue();
-
- assertEquals("Items not identical after B64", data, redata);
-
- encoded = new Exporter().append(data).toString(true, true);
- redata = (Data) new Importer().read(encoded).getValue();
-
- assertEquals("Items not identical after ZIP", data, redata);
- }
- });
-
- addTest(new TestCase("B64 and ZIP 70000 chars test") {
- @Override
- public void test() throws Exception {
- StringBuilder builder = new StringBuilder();
- for (int i = 0; i < 7000; i++) {
- builder.append("0123456789");
- }
-
- Object data = builder.toString();
- String encoded = new Exporter().append(data).toString(true,
- false);
- String redata = (String) new Importer().read(encoded)
- .getValue();
-
- assertEquals("Items not identical after B64", data, redata);
-
- encoded = new Exporter().append(data).toString(true, true);
- redata = (String) new Importer().read(encoded).getValue();
-
- assertEquals("Items not identical after ZIP", data, redata);
+ encodeRecodeTest(this, data);
}
});
}