X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fbe%2Fnikiroo%2Futils%2Fserial%2FSerialUtils.java;h=6f851731fd034541b912d71046b0375e8ee7422b;hb=f8147a0ee57317e96d9ff0bf19573f7168d0354c;hp=c28faeddebd86cd783e4924460aa01cb749f4b83;hpb=e570f7eb0843d1074c3fc46dd759125715d68df0;p=nikiroo-utils.git diff --git a/src/be/nikiroo/utils/serial/SerialUtils.java b/src/be/nikiroo/utils/serial/SerialUtils.java index c28faed..6f85173 100644 --- a/src/be/nikiroo/utils/serial/SerialUtils.java +++ b/src/be/nikiroo/utils/serial/SerialUtils.java @@ -1,23 +1,55 @@ package be.nikiroo.utils.serial; -import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.NotSerializableException; +import java.io.OutputStream; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.net.URL; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; 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.Base64InputStream; +import be.nikiroo.utils.streams.Base64OutputStream; +import be.nikiroo.utils.streams.NextableInputStream; +import be.nikiroo.utils.streams.NextableInputStreamStep; /** * Small class to help with serialisation. *

* Note that we do not support inner classes (but we do support nested classes) * and all objects require an empty constructor to be deserialised. + *

+ * It is possible to add support to custom types (both the encoder and the + * decoder will require the custom classes) -- see {@link CustomSerializer}. + *

+ * Default supported types are: + *

* * @author niki */ @@ -30,79 +62,135 @@ public class SerialUtils { // 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 - builder.append(new Exporter().append(item) - .toString(true)); + // TODO: bad escaping? + write(out, "B64:"); + OutputStream out64 = new Base64OutputStream( + out, true); + new Exporter(out64).append(item); } catch (NotSerializableException e) { throw new UnknownFormatConversionException(e .getMessage()); } } - builder.append("\n"); } } catch (ArrayIndexOutOfBoundsException e) { // Done. } + } + + @Override + protected Object fromStream(InputStream in) throws IOException { + NextableInputStream stream = new NextableInputStream(in, + new NextableInputStreamStep('\r')); + + try { + List list = new ArrayList(); + stream.next(); + String type = IOUtils.readSmallStream(stream); + + while (stream.next()) { + Object value = new Importer().read(stream).getValue(); + list.add(value); + } - return builder.toString(); + Object array = Array.newInstance( + SerialUtils.getClass(type), list.size()); + for (int i = 0; i < list.size(); i++) { + Array.set(array, i, list.get(i)); + } + + return array; + } catch (Exception e) { + if (e instanceof IOException) { + throw (IOException) e; + } + throw new IOException(e.getMessage()); + } } @Override protected String getType() { return "[]"; } + }); + // URL: + customTypes.put("java.net.URL", new CustomSerializer() { @Override - protected Object fromString(String content) { - String[] tab = content.split("\n"); + protected void toStream(OutputStream out, Object value) + throws IOException { + String val = ""; + if (value != null) { + val = ((URL) value).toString(); + } - try { - 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); - } + out.write(StringUtils.getBytes(val)); + } - return array; - } catch (Exception e) { - throw new UnknownFormatConversionException(e.getMessage()); + @Override + protected Object fromStream(InputStream in) throws IOException { + String val = IOUtils.readSmallStream(in); + if (!val.isEmpty()) { + return new URL(val); } + + return null; + } + + @Override + protected String getType() { + return "java.net.URL"; } }); // Images (this is currently the only supported image type by default) - customTypes.put("java.awt.image.BufferedImage", new CustomSerializer() { + customTypes.put("be.nikiroo.utils.Image", new CustomSerializer() { @Override - protected String toString(Object value) { + protected void toStream(OutputStream out, Object value) + throws IOException { + Image img = (Image) value; + OutputStream encoded = new Base64OutputStream(out, true); try { - return StringUtils.fromImage((BufferedImage) value); - } catch (IOException e) { - throw new UnknownFormatConversionException(e.getMessage()); + InputStream in = img.newInputStream(); + try { + IOUtils.write(in, encoded); + } finally { + in.close(); + } + } finally { + encoded.flush(); + // Cannot close! } } @Override protected String getType() { - return "java.awt.image.BufferedImage"; + return "be.nikiroo.utils.Image"; } @Override - protected Object fromString(String content) { + protected Object fromStream(InputStream in) throws IOException { try { - return StringUtils.toImage(content); + // Cannot close it! + InputStream decoded = new Base64InputStream(in, false); + return new Image(decoded); } catch (IOException e) { throw new UnknownFormatConversionException(e.getMessage()); } @@ -126,27 +214,68 @@ public class SerialUtils { public static Object createObject(String type) throws ClassNotFoundException, NoSuchMethodException { + String desc = null; try { Class clazz = getClass(type); String className = clazz.getName(); - Object[] args = null; + List args = new ArrayList(); + List> classes = new ArrayList>(); Constructor ctor = null; if (className.contains("$")) { - Object javaParent = createObject(className.substring(0, - className.lastIndexOf('$'))); - args = new Object[] { javaParent }; - ctor = clazz.getDeclaredConstructor(new Class[] { javaParent - .getClass() }); + for (String parentName = className.substring(0, + className.lastIndexOf('$'));; parentName = parentName + .substring(0, parentName.lastIndexOf('$'))) { + Object parent = createObject(parentName); + args.add(parent); + classes.add(parent.getClass()); + + if (!parentName.contains("$")) { + break; + } + } + + // Better error description in case there is no empty + // constructor: + desc = ""; + String end = ""; + for (Class parent = clazz; parent != null + && !parent.equals(Object.class); parent = parent + .getSuperclass()) { + if (!desc.isEmpty()) { + desc += " [:"; + end += "]"; + } + desc += parent; + } + desc += end; + // + + try { + ctor = clazz.getDeclaredConstructor(classes + .toArray(new Class[] {})); + } catch (NoSuchMethodException nsme) { + // 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)); + Object firstParent = args.get(0); + args.clear(); + args.add(firstParent); + } + desc = null; } else { - args = new Object[] {}; ctor = clazz.getDeclaredConstructor(); } ctor.setAccessible(true); - return ctor.newInstance(args); + return ctor.newInstance(args.toArray()); } catch (ClassNotFoundException e) { throw e; } catch (NoSuchMethodException e) { + if (desc != null) { + throw new NoSuchMethodException("Empty constructor not found: " + + desc); + } throw e; } catch (Exception e) { throw new NoSuchMethodException("Cannot instantiate: " + type); @@ -165,14 +294,14 @@ public class SerialUtils { } /** - * Serialise the given object into this {@link StringBuilder}. + * Serialise the given object into this {@link OutputStream}. *

* Important: 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 @@ -184,9 +313,11 @@ public class SerialUtils { * 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 map) - throws NotSerializableException { + static void append(OutputStream out, Object o, Map map) + throws NotSerializableException, IOException { Field[] fields = new Field[] {}; String type = ""; @@ -197,10 +328,8 @@ public class SerialUtils { fields = o.getClass().getDeclaredFields(); type = o.getClass().getCanonicalName(); if (type == null) { - throw new NotSerializableException( - String.format( - "Cannot find the class for this object: %s (it could be an inner class, which is not supported)", - o)); + // Anonymous inner classes support + type = o.getClass().getName(); } id = Integer.toString(hash); if (map.containsKey(hash)) { @@ -210,9 +339,13 @@ public class SerialUtils { } } - 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); @@ -226,16 +359,15 @@ public class SerialUtils { 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) { @@ -245,42 +377,72 @@ public class SerialUtils { e.printStackTrace(); // should not happen (see // setAccessible) } + + write(out, "\n}"); } - builder.append("\n}"); } - // return true if encoded (supported) - static boolean encode(StringBuilder builder, Object value) { + /** + * Encode the object into the given {@link OutputStream} if possible and if + * supported. + *

+ * 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 + * considered supported, but compound objects are not supported here. + *

+ * For compound objects, you should use {@link Exporter}. + * + * @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 + * {@link OutputStream} won't be changed in case of failure) + * + * @throws IOException + * in case of I/O error + */ + static boolean encode(OutputStream out, Object value) throws IOException { if (value == null) { - builder.append("NULL"); - } else if (value.getClass().getCanonicalName().endsWith("[]")) { - customTypes.get("[]").encode(builder, value); + write(out, "NULL"); + } else if (value.getClass().getSimpleName().endsWith("[]")) { + // Simple name does support [] suffix and do not return NULL for + // inner anonymous classes + customTypes.get("[]").encode(out, value); } else if (customTypes.containsKey(value.getClass().getCanonicalName())) { customTypes.get(value.getClass().getCanonicalName())// - .encode(builder, value); + .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; } @@ -288,48 +450,94 @@ public class SerialUtils { return true; } - static Object decode(String encodedValue) { - String cut = ""; - if (encodedValue.length() > 1) { - cut = encodedValue.substring(0, encodedValue.length() - 1); - } + /** + * Decode the data into an equivalent supported source object. + *

+ * 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 + * considered supported, but compound objects are not supported here. + *

+ * For compound objects, you should use {@link Importer}. + * + * @param encodedValue + * the encoded data, cannot be NULL + * + * @return the object (can be NULL for NULL encoded values) + * + * @throws IOException + * if the content cannot be converted + */ + static Object decode(String encodedValue) throws IOException { + try { + String cut = ""; + if (encodedValue.length() > 1) { + cut = encodedValue.substring(0, encodedValue.length() - 1); + } - if (CustomSerializer.isCustom(encodedValue)) { - // custom:TYPE_NAME:"content is String-encoded" - String type = CustomSerializer.typeOf(encodedValue); - if (customTypes.containsKey(type)) { - return customTypes.get(type).decode(encodedValue); + if (CustomSerializer.isCustom(encodedValue)) { + // custom:TYPE_NAME:"content is String-encoded" + String type = CustomSerializer.typeOf(encodedValue); + if (customTypes.containsKey(type)) { + // TODO: we should start with a stream + InputStream streamEncodedValue = new ByteArrayInputStream( + StringUtils.getBytes(encodedValue)); + try { + return customTypes.get(type).decode(streamEncodedValue); + } finally { + streamEncodedValue.close(); + } + } + throw new IOException("Unknown custom type: " + type); + } else if (encodedValue.equals("NULL") + || encodedValue.equals("null")) { + return null; + } else if (encodedValue.endsWith("\"")) { + return decodeString(encodedValue); + } else if (encodedValue.equals("true")) { + return true; + } else if (encodedValue.equals("false")) { + return false; + } else if (encodedValue.endsWith("b")) { + return Byte.parseByte(cut); + } else if (encodedValue.endsWith("c")) { + return decodeString(cut).charAt(0); + } else if (encodedValue.endsWith("s")) { + return Short.parseShort(cut); + } else if (encodedValue.endsWith("L")) { + return Long.parseLong(cut); + } else if (encodedValue.endsWith("F")) { + return Float.parseFloat(cut); + } else if (encodedValue.endsWith("d")) { + return Double.parseDouble(cut); + } else if (encodedValue.endsWith(";")) { + return decodeEnum(encodedValue); } else { - throw new UnknownFormatConversionException( - "Unknown custom type: " + type); + return Integer.parseInt(encodedValue); } - } else if (encodedValue.equals("NULL") || encodedValue.equals("null")) { - return null; - } else if (encodedValue.endsWith("\"")) { - return decodeString(encodedValue); - } else if (encodedValue.equals("true")) { - return true; - } else if (encodedValue.equals("false")) { - return false; - } else if (encodedValue.endsWith("b")) { - return Byte.parseByte(cut); - } else if (encodedValue.endsWith("c")) { - return decodeString(cut).charAt(0); - } else if (encodedValue.endsWith("s")) { - return Short.parseShort(cut); - } else if (encodedValue.endsWith("L")) { - return Long.parseLong(cut); - } else if (encodedValue.endsWith("F")) { - return Float.parseFloat(cut); - } else if (encodedValue.endsWith("d")) { - return Double.parseDouble(cut); - } else if (encodedValue.endsWith(";")) { - return decodeEnum(encodedValue); - } else { - return Integer.parseInt(encodedValue); + } catch (Exception e) { + if (e instanceof IOException) { + throw (IOException) e; + } + 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 { + out.write(StringUtils.getBytes(data.toString())); + } + /** * Return the corresponding class or throw an {@link Exception} if it * cannot. @@ -381,7 +589,7 @@ public class SerialUtils { } @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); @@ -390,39 +598,63 @@ public class SerialUtils { try { return Enum.valueOf((Class) getClass(type), name); } catch (Exception e) { - e.printStackTrace(); throw new UnknownFormatConversionException("Unknown enum: <" + type + "> " + name); } } // 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; @@ -452,6 +684,6 @@ public class SerialUtils { } } - return builder.substring(1, builder.length() - 1).toString(); + return builder.substring(1, builder.length() - 1); } }