1 package be
.nikiroo
.utils
.serial
;
3 import java
.io
.IOException
;
4 import java
.io
.NotSerializableException
;
5 import java
.lang
.reflect
.Array
;
6 import java
.lang
.reflect
.Constructor
;
7 import java
.lang
.reflect
.Field
;
8 import java
.lang
.reflect
.Modifier
;
10 import java
.util
.ArrayList
;
11 import java
.util
.HashMap
;
12 import java
.util
.List
;
14 import java
.util
.UnknownFormatConversionException
;
16 import be
.nikiroo
.utils
.Image
;
19 * Small class to help with serialisation.
21 * Note that we do not support inner classes (but we do support nested classes)
22 * and all objects require an empty constructor to be deserialised.
24 * It is possible to add support to custom types (both the encoder and the
25 * decoder will require the custom classes) -- see {@link CustomSerializer}.
27 * Default supported types are:
29 * <li>NULL (as a null value)</li>
39 * <li>Enum (any enum whose name and value is known by the caller)</li>
40 * <li>java.awt.image.BufferedImage (as a {@link CustomSerializer})</li>
41 * <li>An array of the above (as a {@link CustomSerializer})</li>
47 public class SerialUtils
{
48 private static Map
<String
, CustomSerializer
> customTypes
;
51 customTypes
= new HashMap
<String
, CustomSerializer
>();
54 customTypes
.put("[]", new CustomSerializer() {
56 protected String
toString(Object value
) {
57 String type
= value
.getClass().getCanonicalName();
58 type
= type
.substring(0, type
.length() - 2); // remove the []
60 StringBuilder builder
= new StringBuilder();
61 builder
.append(type
).append("\n");
63 for (int i
= 0; true; i
++) {
64 Object item
= Array
.get(value
, i
);
65 // encode it normally if direct value
66 if (!SerialUtils
.encode(builder
, item
)) {
69 builder
.append(new Exporter().append(item
)
71 } catch (NotSerializableException e
) {
72 throw new UnknownFormatConversionException(e
78 } catch (ArrayIndexOutOfBoundsException e
) {
82 return builder
.toString();
86 protected String
getType() {
91 protected Object
fromString(String content
) throws IOException
{
92 String
[] tab
= content
.split("\n");
95 Object array
= Array
.newInstance(
96 SerialUtils
.getClass(tab
[0]), tab
.length
- 1);
97 for (int i
= 1; i
< tab
.length
; i
++) {
98 Object value
= new Importer().read(tab
[i
]).getValue();
99 Array
.set(array
, i
- 1, value
);
103 } catch (Exception e
) {
104 if (e
instanceof IOException
) {
105 throw (IOException
) e
;
107 throw new IOException(e
.getMessage());
113 customTypes
.put("java.net.URL", new CustomSerializer() {
115 protected String
toString(Object value
) {
117 return ((URL
) value
).toString();
123 protected Object
fromString(String content
) throws IOException
{
124 if (content
!= null) {
125 return new URL(content
);
131 protected String
getType() {
132 return "java.net.URL";
136 // Images (this is currently the only supported image type by default)
137 customTypes
.put("be.nikiroo.utils.Image", new CustomSerializer() {
139 protected String
toString(Object value
) {
140 return ((Image
) value
).toBase64();
144 protected String
getType() {
145 return "be.nikiroo.utils.Image";
149 protected Object
fromString(String content
) {
151 return new Image(content
);
152 } catch (IOException e
) {
153 throw new UnknownFormatConversionException(e
.getMessage());
160 * Create an empty object of the given type.
163 * the object type (its class name)
165 * @return the new object
167 * @throws ClassNotFoundException
168 * if the class cannot be found
169 * @throws NoSuchMethodException
170 * if the given class is not compatible with this code
172 public static Object
createObject(String type
)
173 throws ClassNotFoundException
, NoSuchMethodException
{
177 Class
<?
> clazz
= getClass(type
);
178 String className
= clazz
.getName();
179 List
<Object
> args
= new ArrayList
<Object
>();
180 List
<Class
<?
>> classes
= new ArrayList
<Class
<?
>>();
181 Constructor
<?
> ctor
= null;
182 if (className
.contains("$")) {
183 for (String parentName
= className
.substring(0,
184 className
.lastIndexOf('$'));; parentName
= parentName
185 .substring(0, parentName
.lastIndexOf('$'))) {
186 Object parent
= createObject(parentName
);
188 classes
.add(parent
.getClass());
190 if (!parentName
.contains("$")) {
195 // Better error description in case there is no empty
199 for (Class
<?
> parent
= clazz
; parent
!= null
200 && !parent
.equals(Object
.class); parent
= parent
202 if (!desc
.isEmpty()) {
212 ctor
= clazz
.getDeclaredConstructor(classes
213 .toArray(new Class
[] {}));
214 } catch (NoSuchMethodException nsme
) {
215 // TODO: it seems e do not always need a parameter for each
216 // level, so we currently try "ALL" levels or "FIRST" level
217 // only -> we should check the actual rule and use it
218 ctor
= clazz
.getDeclaredConstructor(classes
.get(0));
219 Object firstParent
= args
.get(0);
221 args
.add(firstParent
);
225 ctor
= clazz
.getDeclaredConstructor();
228 ctor
.setAccessible(true);
229 return ctor
.newInstance(args
.toArray());
230 } catch (ClassNotFoundException e
) {
232 } catch (NoSuchMethodException e
) {
234 throw new NoSuchMethodException("Empty constructor not found: "
238 } catch (Exception e
) {
239 throw new NoSuchMethodException("Cannot instantiate: " + type
);
244 * Insert a custom serialiser that will take precedence over the default one
245 * or the target class.
248 * the custom serialiser
250 static public void addCustomSerializer(CustomSerializer serializer
) {
251 customTypes
.put(serializer
.getType(), serializer
);
255 * Serialise the given object into this {@link StringBuilder}.
257 * <b>Important: </b>If the operation fails (with a
258 * {@link NotSerializableException}), the {@link StringBuilder} will be
259 * corrupted (will contain bad, most probably not importable data).
262 * the output {@link StringBuilder} to serialise to
264 * the object to serialise
266 * the map of already serialised objects (if the given object or
267 * one of its descendant is already present in it, only an ID
268 * will be serialised)
270 * @throws NotSerializableException
271 * if the object cannot be serialised (in this case, the
272 * {@link StringBuilder} can contain bad, most probably not
275 static void append(StringBuilder builder
, Object o
, Map
<Integer
, Object
> map
)
276 throws NotSerializableException
{
278 Field
[] fields
= new Field
[] {};
283 int hash
= System
.identityHashCode(o
);
284 fields
= o
.getClass().getDeclaredFields();
285 type
= o
.getClass().getCanonicalName();
287 // Anonymous inner classes support
288 type
= o
.getClass().getName();
290 id
= Integer
.toString(hash
);
291 if (map
.containsKey(hash
)) {
292 fields
= new Field
[] {};
298 builder
.append("{\nREF ").append(type
).append("@").append(id
)
300 if (!encode(builder
, o
)) { // check if direct value
302 for (Field field
: fields
) {
303 field
.setAccessible(true);
305 if (field
.getName().startsWith("this$")
306 || field
.isSynthetic()
307 || (field
.getModifiers() & Modifier
.STATIC
) == Modifier
.STATIC
) {
308 // Do not keep this links of nested classes
309 // Do not keep synthetic fields
310 // Do not keep final fields
314 builder
.append("\n");
315 builder
.append(field
.getName());
319 value
= field
.get(o
);
321 if (!encode(builder
, value
)) {
322 builder
.append("\n");
323 append(builder
, value
, map
);
326 } catch (IllegalArgumentException e
) {
327 e
.printStackTrace(); // should not happen (see
329 } catch (IllegalAccessException e
) {
330 e
.printStackTrace(); // should not happen (see
334 builder
.append("\n}");
338 * Encode the object into the given builder if possible and if supported.
340 * A supported object in this context means an object we can directly
341 * encode, like an Integer or a String. Custom objects and arrays are also
342 * considered supported, but <b>compound objects are not supported here</b>.
344 * For compound objects, you should use {@link Exporter}.
347 * the builder to append to
349 * the object to encode (can be NULL, which will be encoded)
351 * @return TRUE if success, FALSE if not (the content of the builder won't
352 * be changed in case of failure)
354 static boolean encode(StringBuilder builder
, Object value
) {
356 builder
.append("NULL");
357 } else if (value
.getClass().getSimpleName().endsWith("[]")) {
358 // Simple name does support [] suffix and do not return NULL for
359 // inner anonymous classes
360 return customTypes
.get("[]").encode(builder
, value
);
361 } else if (customTypes
.containsKey(value
.getClass().getCanonicalName())) {
362 return customTypes
.get(value
.getClass().getCanonicalName())//
363 .encode(builder
, value
);
364 } else if (value
instanceof String
) {
365 encodeString(builder
, (String
) value
);
366 } else if (value
instanceof Boolean
) {
367 builder
.append(value
);
368 } else if (value
instanceof Byte
) {
369 builder
.append(value
).append('b');
370 } else if (value
instanceof Character
) {
371 encodeString(builder
, "" + value
);
373 } else if (value
instanceof Short
) {
374 builder
.append(value
).append('s');
375 } else if (value
instanceof Integer
) {
376 builder
.append(value
);
377 } else if (value
instanceof Long
) {
378 builder
.append(value
).append('L');
379 } else if (value
instanceof Float
) {
380 builder
.append(value
).append('F');
381 } else if (value
instanceof Double
) {
382 builder
.append(value
).append('d');
383 } else if (value
instanceof Enum
) {
384 String type
= value
.getClass().getCanonicalName();
385 builder
.append(type
).append(".").append(((Enum
<?
>) value
).name())
395 * Decode the data into an equivalent supported source object.
397 * A supported object in this context means an object we can directly
398 * encode, like an Integer or a String. Custom objects and arrays are also
399 * considered supported, but <b>compound objects are not supported here</b>.
401 * For compound objects, you should use {@link Importer}.
403 * @param encodedValue
404 * the encoded data, cannot be NULL
406 * @return the object (can be NULL for NULL encoded values)
408 * @throws IOException
409 * if the content cannot be converted
411 static Object
decode(String encodedValue
) throws IOException
{
414 if (encodedValue
.length() > 1) {
415 cut
= encodedValue
.substring(0, encodedValue
.length() - 1);
418 if (CustomSerializer
.isCustom(encodedValue
)) {
419 // custom:TYPE_NAME:"content is String-encoded"
420 String type
= CustomSerializer
.typeOf(encodedValue
);
421 if (customTypes
.containsKey(type
)) {
422 return customTypes
.get(type
).decode(encodedValue
);
424 throw new IOException("Unknown custom type: " + type
);
425 } else if (encodedValue
.equals("NULL")
426 || encodedValue
.equals("null")) {
428 } else if (encodedValue
.endsWith("\"")) {
429 return decodeString(encodedValue
);
430 } else if (encodedValue
.equals("true")) {
432 } else if (encodedValue
.equals("false")) {
434 } else if (encodedValue
.endsWith("b")) {
435 return Byte
.parseByte(cut
);
436 } else if (encodedValue
.endsWith("c")) {
437 return decodeString(cut
).charAt(0);
438 } else if (encodedValue
.endsWith("s")) {
439 return Short
.parseShort(cut
);
440 } else if (encodedValue
.endsWith("L")) {
441 return Long
.parseLong(cut
);
442 } else if (encodedValue
.endsWith("F")) {
443 return Float
.parseFloat(cut
);
444 } else if (encodedValue
.endsWith("d")) {
445 return Double
.parseDouble(cut
);
446 } else if (encodedValue
.endsWith(";")) {
447 return decodeEnum(encodedValue
);
449 return Integer
.parseInt(encodedValue
);
451 } catch (Exception e
) {
452 if (e
instanceof IOException
) {
453 throw (IOException
) e
;
455 throw new IOException(e
.getMessage());
460 * Return the corresponding class or throw an {@link Exception} if it
464 * the class name to look for
466 * @return the class (will never be NULL)
468 * @throws ClassNotFoundException
469 * if the class cannot be found
470 * @throws NoSuchMethodException
471 * if the class cannot be created (usually because it or its
472 * enclosing class doesn't have an empty constructor)
474 static private Class
<?
> getClass(String type
)
475 throws ClassNotFoundException
, NoSuchMethodException
{
476 Class
<?
> clazz
= null;
478 clazz
= Class
.forName(type
);
479 } catch (ClassNotFoundException e
) {
480 int pos
= type
.length();
481 pos
= type
.lastIndexOf(".", pos
);
483 String parentType
= type
.substring(0, pos
);
484 String nestedType
= type
.substring(pos
+ 1);
485 Class
<?
> javaParent
= null;
487 javaParent
= getClass(parentType
);
488 parentType
= javaParent
.getName();
489 clazz
= Class
.forName(parentType
+ "$" + nestedType
);
490 } catch (Exception ee
) {
493 if (javaParent
== null) {
494 throw new NoSuchMethodException(
497 + " (the enclosing class cannot be created: maybe it doesn't have an empty constructor?)");
503 throw new ClassNotFoundException("Class not found: " + type
);
509 @SuppressWarnings({ "unchecked", "rawtypes" })
510 private static Enum
<?
> decodeEnum(String escaped
) {
511 // escaped: be.xxx.EnumType.VALUE;
512 int pos
= escaped
.lastIndexOf(".");
513 String type
= escaped
.substring(0, pos
);
514 String name
= escaped
.substring(pos
+ 1, escaped
.length() - 1);
517 return Enum
.valueOf((Class
<Enum
>) getClass(type
), name
);
518 } catch (Exception e
) {
519 throw new UnknownFormatConversionException("Unknown enum: <" + type
525 private static void encodeString(StringBuilder builder
, String raw
) {
526 builder
.append('\"');
527 for (char car
: raw
.toCharArray()) {
530 builder
.append("\\\\");
533 builder
.append("\\r");
536 builder
.append("\\n");
539 builder
.append("\\\"");
546 builder
.append('\"');
550 private static String
decodeString(String escaped
) {
551 StringBuilder builder
= new StringBuilder();
553 boolean escaping
= false;
554 for (char car
: escaped
.toCharArray()) {
564 builder
.append('\\');
567 builder
.append('\r');
570 builder
.append('\n');
580 return builder
.substring(1, builder
.length() - 1);