X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fbe%2Fnikiroo%2Futils%2Fserial%2FSerialUtils.java;h=73f2027abb75480957beb087681495fa75009363;hb=5bc55b5183dcc811d06ef7cf2e26b43329a0ae34;hp=3770c588ce53bc25e77b623ee715da6aa58b937e;hpb=aad145868879e293dbe391c005c1a6885341529b;p=nikiroo-utils.git diff --git a/src/be/nikiroo/utils/serial/SerialUtils.java b/src/be/nikiroo/utils/serial/SerialUtils.java index 3770c58..73f2027 100644 --- a/src/be/nikiroo/utils/serial/SerialUtils.java +++ b/src/be/nikiroo/utils/serial/SerialUtils.java @@ -1,18 +1,43 @@ package be.nikiroo.utils.serial; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; +import java.awt.image.BufferedImage; +import java.io.IOException; import java.io.NotSerializableException; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map; +import java.util.UnknownFormatConversionException; + +import be.nikiroo.utils.ImageUtils; /** - * Small class to help serialise/deserialise objects. + * 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 */ @@ -21,9 +46,90 @@ public class SerialUtils { static { customTypes = new HashMap(); - // TODO: add "default" custom serialisers + + // Array types: + customTypes.put("[]", new CustomSerializer() { + @Override + protected String toString(Object value) { + String type = value.getClass().getCanonicalName(); + type = type.substring(0, type.length() - 2); // remove the [] + + StringBuilder builder = new StringBuilder(); + builder.append(type).append("\n"); + try { + for (int i = 0; true; i++) { + Object item = Array.get(value, i); + // encode it normally if direct value + if (!SerialUtils.encode(builder, item)) { + try { + // use ZIP: if not + builder.append(new Exporter().append(item) + .toString(true)); + } 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) { + String[] tab = content.split("\n"); + + 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); + } + + return array; + } catch (Exception e) { + throw new UnknownFormatConversionException(e.getMessage()); + } + } + }); + + // Images (this is currently the only supported image type by default) + customTypes.put("java.awt.image.BufferedImage", new CustomSerializer() { + @Override + protected String toString(Object value) { + try { + return ImageUtils.toBase64((BufferedImage) value); + } catch (IOException e) { + throw new UnknownFormatConversionException(e.getMessage()); + } + } + + @Override + protected String getType() { + return "java.awt.image.BufferedImage"; + } + + @Override + protected Object fromString(String content) { + try { + return ImageUtils.fromBase64(content); + } catch (IOException e) { + throw new UnknownFormatConversionException(e.getMessage()); + } + } + }); } - + /** * Create an empty object of the given type. * @@ -32,18 +138,16 @@ public class SerialUtils { * * @return the new object * - * @throws NoSuchMethodException if the given class is not compatible with this code - * @throws ClassNotFoundException if the class cannot be found or created + * @throws ClassNotFoundException + * if the class cannot be found + * @throws NoSuchMethodException + * if the given class is not compatible with this code */ - public static Object createObject(String type) throws NoSuchMethodException, - ClassNotFoundException { + public static Object createObject(String type) + throws ClassNotFoundException, NoSuchMethodException { try { Class clazz = getClass(type); - if (clazz == null) { - throw new ClassNotFoundException("Class not found: " + type); - } - String className = clazz.getName(); Object[] args = null; Constructor ctor = null; @@ -60,25 +164,47 @@ public class SerialUtils { ctor.setAccessible(true); return ctor.newInstance(args); + } catch (ClassNotFoundException e) { + throw e; } catch (NoSuchMethodException e) { - throw new NoSuchMethodException( - String.format( - "Objects of type \"%s\" cannot be created by this code: maybe the class" - + " or its enclosing class doesn't have an empty constructor?", - type)); - + throw e; + } catch (Exception e) { + throw new NoSuchMethodException("Cannot instantiate: " + type); } - catch (SecurityException e) { throw new ClassNotFoundException("Cannot instantiate: " + type, e); } - catch (InstantiationException e) { throw new ClassNotFoundException("Cannot instantiate: " + type, e); } - catch (IllegalAccessException e) { throw new ClassNotFoundException("Cannot instantiate: " + type, e); } - catch (IllegalArgumentException e) { throw new ClassNotFoundException("Cannot instantiate: " + type, e); } - catch (InvocationTargetException e) { throw new ClassNotFoundException("Cannot instantiate: " + type, e); } } + /** + * Insert a custom serialiser that will take precedence over the default one + * or the target class. + * + * @param serializer + * the custom serialiser + */ static public void addCustomSerializer(CustomSerializer serializer) { customTypes.put(serializer.getType(), serializer); } + /** + * Serialise the given object into this {@link StringBuilder}. + *

+ * 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 o + * the object to serialise + * @param map + * the map of already serialised objects (if the given object or + * one of its descendant is already present in it, only an ID + * will be serialised) + * + * @throws NotSerializableException + * if the object cannot be serialised (in this case, the + * {@link StringBuilder} can contain bad, most probably not + * importable data) + */ static void append(StringBuilder builder, Object o, Map map) throws NotSerializableException { @@ -104,42 +230,63 @@ public class SerialUtils { } } - builder.append("{\nREF ").append(type).append("@").append(id); - try { - for (Field field : fields) { - field.setAccessible(true); + builder.append("{\nREF ").append(type).append("@").append(id) + .append(":"); + if (!encode(builder, o)) { // check if direct value + try { + for (Field field : fields) { + field.setAccessible(true); - if (field.getName().startsWith("this$")) { - // Do not keep this links of nested classes - continue; - } + if (field.getName().startsWith("this$") + || field.isSynthetic() + || (field.getModifiers() & Modifier.STATIC) == Modifier.STATIC) { + // Do not keep this links of nested classes + // Do not keep synthetic fields + // Do not keep final fields + continue; + } - builder.append("\n"); - builder.append(field.getName()); - builder.append(":"); - Object value; + builder.append("\n"); + builder.append(field.getName()); + builder.append(":"); + Object value; - value = field.get(o); + value = field.get(o); - if (!encode(builder, value)) { - builder.append("\n"); - append(builder, value, map); + if (!encode(builder, value)) { + builder.append("\n"); + append(builder, value, map); + } } + } catch (IllegalArgumentException e) { + e.printStackTrace(); // should not happen (see + // setAccessible) + } catch (IllegalAccessException e) { + e.printStackTrace(); // should not happen (see + // setAccessible) } - } catch (IllegalArgumentException e) { - e.printStackTrace(); // should not happen (see setAccessible) - } catch (IllegalAccessException e) { - e.printStackTrace(); // should not happen (see setAccessible) } builder.append("\n}"); } - // return true if encoded (supported) + /** + * Encode the object into the given builder if possible (if supported). + * + * @param builder + * the builder 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) + */ static boolean encode(StringBuilder builder, Object value) { if (value == null) { builder.append("NULL"); + } else if (value.getClass().getCanonicalName().endsWith("[]")) { + return customTypes.get("[]").encode(builder, value); } else if (customTypes.containsKey(value.getClass().getCanonicalName())) { - customTypes.get(value.getClass().getCanonicalName())// + return customTypes.get(value.getClass().getCanonicalName())// .encode(builder, value); } else if (value instanceof String) { encodeString(builder, (String) value); @@ -148,7 +295,7 @@ public class SerialUtils { } else if (value instanceof Byte) { builder.append(value).append('b'); } else if (value instanceof Character) { - encodeString(builder, (String) value); + encodeString(builder, "" + value); builder.append('c'); } else if (value instanceof Short) { builder.append(value).append('s'); @@ -160,6 +307,10 @@ public class SerialUtils { builder.append(value).append('F'); } else if (value instanceof Double) { builder.append(value).append('d'); + } else if (value instanceof Enum) { + String type = value.getClass().getCanonicalName(); + builder.append(type).append(".").append(((Enum) value).name()) + .append(";"); } else { return false; } @@ -167,48 +318,83 @@ public class SerialUtils { return true; } + /** + * Decode the data into an equivalent source object. + * + * @param encodedValue + * the encoded data, cannot be NULL + * + * @return the object (can be NULL for NULL encoded values) + * + * @throws UnknownFormatConversionException + * if the content cannot be converted + */ static Object decode(String encodedValue) { 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); - } else { - throw new java.util.UnknownFormatConversionException( + try { + 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); + } + throw new UnknownFormatConversionException( "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 { + 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 { - return Integer.parseInt(encodedValue); + } catch (Exception e) { + if (e instanceof UnknownFormatConversionException) { + throw (UnknownFormatConversionException) e; + } + throw new UnknownFormatConversionException(e.getMessage()); } } - - static private Class getClass(String type) throws ClassNotFoundException, - NoSuchMethodException { + + /** + * Return the corresponding class or throw an {@link Exception} if it + * cannot. + * + * @param type + * the class name to look for + * + * @return the class (will never be NULL) + * + * @throws ClassNotFoundException + * if the class cannot be found + * @throws NoSuchMethodException + * if the class cannot be created (usually because it or its + * enclosing class doesn't have an empty constructor) + */ + static private Class getClass(String type) + throws ClassNotFoundException, NoSuchMethodException { Class clazz = null; try { clazz = Class.forName(type); @@ -235,9 +421,28 @@ public class SerialUtils { } } + if (clazz == null) { + throw new ClassNotFoundException("Class not found: " + type); + } + return clazz; } - + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static Enum decodeEnum(String escaped) { + // escaped: be.xxx.EnumType.VALUE; + int pos = escaped.lastIndexOf("."); + String type = escaped.substring(0, pos); + String name = escaped.substring(pos + 1, escaped.length() - 1); + + try { + return Enum.valueOf((Class) getClass(type), name); + } catch (Exception e) { + throw new UnknownFormatConversionException("Unknown enum: <" + type + + "> " + name); + } + } + // aa bb -> "aa\tbb" private static void encodeString(StringBuilder builder, String raw) { builder.append('\"');