1 package be
.nikiroo
.utils
.serial
;
3 import java
.io
.IOException
;
4 import java
.io
.InputStream
;
5 import java
.io
.NotSerializableException
;
6 import java
.io
.OutputStream
;
7 import java
.io
.UnsupportedEncodingException
;
8 import java
.lang
.reflect
.Array
;
9 import java
.lang
.reflect
.Constructor
;
10 import java
.lang
.reflect
.Field
;
11 import java
.lang
.reflect
.Modifier
;
13 import java
.util
.ArrayList
;
14 import java
.util
.HashMap
;
15 import java
.util
.List
;
17 import java
.util
.UnknownFormatConversionException
;
19 import be
.nikiroo
.utils
.Image
;
22 * Small class to help with serialisation.
24 * Note that we do not support inner classes (but we do support nested classes)
25 * and all objects require an empty constructor to be deserialised.
27 * It is possible to add support to custom types (both the encoder and the
28 * decoder will require the custom classes) -- see {@link CustomSerializer}.
30 * Default supported types are:
32 * <li>NULL (as a null value)</li>
42 * <li>Enum (any enum whose name and value is known by the caller)</li>
43 * <li>java.awt.image.BufferedImage (as a {@link CustomSerializer})</li>
44 * <li>An array of the above (as a {@link CustomSerializer})</li>
50 public class SerialUtils
{
51 private static Map
<String
, CustomSerializer
> customTypes
;
54 customTypes
= new HashMap
<String
, CustomSerializer
>();
57 customTypes
.put("[]", new CustomSerializer() {
59 protected String
toString(Object value
) {
60 String type
= value
.getClass().getCanonicalName();
61 type
= type
.substring(0, type
.length() - 2); // remove the []
63 StringBuilder builder
= new StringBuilder();
64 builder
.append(type
).append("\n");
66 for (int i
= 0; true; i
++) {
67 Object item
= Array
.get(value
, i
);
68 // encode it normally if direct value
69 if (!SerialUtils
.encode(builder
, item
)) {
72 new Exporter().append(item
).appendTo(builder
,
74 } catch (NotSerializableException e
) {
75 throw new UnknownFormatConversionException(e
81 } catch (ArrayIndexOutOfBoundsException e
) {
85 return builder
.toString();
89 protected String
getType() {
94 protected Object
fromString(String content
) throws IOException
{
95 String
[] tab
= content
.split("\n");
98 Object array
= Array
.newInstance(
99 SerialUtils
.getClass(tab
[0]), tab
.length
- 1);
100 for (int i
= 1; i
< tab
.length
; i
++) {
101 Object value
= new Importer().read(tab
[i
]).getValue();
102 Array
.set(array
, i
- 1, value
);
106 } catch (Exception e
) {
107 if (e
instanceof IOException
) {
108 throw (IOException
) e
;
110 throw new IOException(e
.getMessage());
116 customTypes
.put("java.net.URL", new CustomSerializer() {
118 protected String
toString(Object value
) {
120 return ((URL
) value
).toString();
126 protected Object
fromString(String content
) throws IOException
{
127 if (content
!= null) {
128 return new URL(content
);
134 protected String
getType() {
135 return "java.net.URL";
139 // Images (this is currently the only supported image type by default)
140 customTypes
.put("be.nikiroo.utils.Image", new CustomSerializer() {
142 protected String
toString(Object value
) {
143 return ((Image
) value
).toBase64();
147 protected String
getType() {
148 return "be.nikiroo.utils.Image";
152 protected Object
fromString(String content
) {
154 return new Image(content
);
155 } catch (IOException e
) {
156 throw new UnknownFormatConversionException(e
.getMessage());
163 * Create an empty object of the given type.
166 * the object type (its class name)
168 * @return the new object
170 * @throws ClassNotFoundException
171 * if the class cannot be found
172 * @throws NoSuchMethodException
173 * if the given class is not compatible with this code
175 public static Object
createObject(String type
)
176 throws ClassNotFoundException
, NoSuchMethodException
{
180 Class
<?
> clazz
= getClass(type
);
181 String className
= clazz
.getName();
182 List
<Object
> args
= new ArrayList
<Object
>();
183 List
<Class
<?
>> classes
= new ArrayList
<Class
<?
>>();
184 Constructor
<?
> ctor
= null;
185 if (className
.contains("$")) {
186 for (String parentName
= className
.substring(0,
187 className
.lastIndexOf('$'));; parentName
= parentName
188 .substring(0, parentName
.lastIndexOf('$'))) {
189 Object parent
= createObject(parentName
);
191 classes
.add(parent
.getClass());
193 if (!parentName
.contains("$")) {
198 // Better error description in case there is no empty
202 for (Class
<?
> parent
= clazz
; parent
!= null
203 && !parent
.equals(Object
.class); parent
= parent
205 if (!desc
.isEmpty()) {
215 ctor
= clazz
.getDeclaredConstructor(classes
216 .toArray(new Class
[] {}));
217 } catch (NoSuchMethodException nsme
) {
218 // TODO: it seems e do not always need a parameter for each
219 // level, so we currently try "ALL" levels or "FIRST" level
220 // only -> we should check the actual rule and use it
221 ctor
= clazz
.getDeclaredConstructor(classes
.get(0));
222 Object firstParent
= args
.get(0);
224 args
.add(firstParent
);
228 ctor
= clazz
.getDeclaredConstructor();
231 ctor
.setAccessible(true);
232 return ctor
.newInstance(args
.toArray());
233 } catch (ClassNotFoundException e
) {
235 } catch (NoSuchMethodException e
) {
237 throw new NoSuchMethodException("Empty constructor not found: "
241 } catch (Exception e
) {
242 throw new NoSuchMethodException("Cannot instantiate: " + type
);
247 * Insert a custom serialiser that will take precedence over the default one
248 * or the target class.
251 * the custom serialiser
253 static public void addCustomSerializer(CustomSerializer serializer
) {
254 customTypes
.put(serializer
.getType(), serializer
);
258 * Serialise the given object into this {@link OutputStream}.
260 * <b>Important: </b>If the operation fails (with a
261 * {@link NotSerializableException}), the {@link StringBuilder} will be
262 * corrupted (will contain bad, most probably not importable data).
265 * the output {@link OutputStream} to serialise to
267 * the object to serialise
269 * the map of already serialised objects (if the given object or
270 * one of its descendant is already present in it, only an ID
271 * will be serialised)
273 * @throws NotSerializableException
274 * if the object cannot be serialised (in this case, the
275 * {@link StringBuilder} can contain bad, most probably not
277 * @throws IOException
278 * in case of I/O errors
280 static void append(OutputStream out
, Object o
, Map
<Integer
, Object
> map
)
281 throws NotSerializableException
, IOException
{
283 Field
[] fields
= new Field
[] {};
288 int hash
= System
.identityHashCode(o
);
289 fields
= o
.getClass().getDeclaredFields();
290 type
= o
.getClass().getCanonicalName();
292 // Anonymous inner classes support
293 type
= o
.getClass().getName();
295 id
= Integer
.toString(hash
);
296 if (map
.containsKey(hash
)) {
297 fields
= new Field
[] {};
303 write(out
, "{\nREF ");
309 if (!encode(out
, o
)) { // check if direct value
311 for (Field field
: fields
) {
312 field
.setAccessible(true);
314 if (field
.getName().startsWith("this$")
315 || field
.isSynthetic()
316 || (field
.getModifiers() & Modifier
.STATIC
) == Modifier
.STATIC
) {
317 // Do not keep this links of nested classes
318 // Do not keep synthetic fields
319 // Do not keep final fields
324 write(out
, field
.getName());
327 Object value
= field
.get(o
);
329 if (!encode(out
, value
)) {
331 append(out
, value
, map
);
334 } catch (IllegalArgumentException e
) {
335 e
.printStackTrace(); // should not happen (see
337 } catch (IllegalAccessException e
) {
338 e
.printStackTrace(); // should not happen (see
346 * Encode the object into the given {@link OutputStream} if possible and if
349 * A supported object in this context means an object we can directly
350 * encode, like an Integer or a String. Custom objects and arrays are also
351 * considered supported, but <b>compound objects are not supported here</b>.
353 * For compound objects, you should use {@link Exporter}.
356 * the {@link OutputStream} to append to
358 * the object to encode (can be NULL, which will be encoded)
360 * @return TRUE if success, FALSE if not (the content of the
361 * {@link OutputStream} won't be changed in case of failure)
363 * @throws IOException
364 * in case of I/O error
366 static boolean encode(OutputStream out
, Object value
) throws IOException
{
369 } else if (value
.getClass().getSimpleName().endsWith("[]")) {
370 // Simple name does support [] suffix and do not return NULL for
371 // inner anonymous classes
372 return customTypes
.get("[]").encode(out
, value
);
373 } else if (customTypes
.containsKey(value
.getClass().getCanonicalName())) {
374 return customTypes
.get(value
.getClass().getCanonicalName())//
376 } else if (value
instanceof String
) {
377 encodeString(out
, (String
) value
);
378 } else if (value
instanceof Boolean
) {
380 } else if (value
instanceof Byte
) {
383 } else if (value
instanceof Character
) {
384 encodeString(out
, "" + value
);
386 } else if (value
instanceof Short
) {
389 } else if (value
instanceof Integer
) {
391 } else if (value
instanceof Long
) {
394 } else if (value
instanceof Float
) {
397 } else if (value
instanceof Double
) {
400 } else if (value
instanceof Enum
) {
401 String type
= value
.getClass().getCanonicalName();
404 write(out
, ((Enum
<?
>) value
).name());
414 * Decode the data into an equivalent supported source object.
416 * A supported object in this context means an object we can directly
417 * encode, like an Integer or a String. Custom objects and arrays are also
418 * considered supported, but <b>compound objects are not supported here</b>.
420 * For compound objects, you should use {@link Importer}.
422 * @param encodedValue
423 * the encoded data, cannot be NULL
425 * @return the object (can be NULL for NULL encoded values)
427 * @throws IOException
428 * if the content cannot be converted
430 static Object
decode(String encodedValue
) throws IOException
{
433 if (encodedValue
.length() > 1) {
434 cut
= encodedValue
.substring(0, encodedValue
.length() - 1);
437 if (CustomSerializer
.isCustom(encodedValue
)) {
438 // custom:TYPE_NAME:"content is String-encoded"
439 String type
= CustomSerializer
.typeOf(encodedValue
);
440 if (customTypes
.containsKey(type
)) {
441 return customTypes
.get(type
).decode(encodedValue
);
443 throw new IOException("Unknown custom type: " + type
);
444 } else if (encodedValue
.equals("NULL")
445 || encodedValue
.equals("null")) {
447 } else if (encodedValue
.endsWith("\"")) {
448 return decodeString(encodedValue
);
449 } else if (encodedValue
.equals("true")) {
451 } else if (encodedValue
.equals("false")) {
453 } else if (encodedValue
.endsWith("b")) {
454 return Byte
.parseByte(cut
);
455 } else if (encodedValue
.endsWith("c")) {
456 return decodeString(cut
).charAt(0);
457 } else if (encodedValue
.endsWith("s")) {
458 return Short
.parseShort(cut
);
459 } else if (encodedValue
.endsWith("L")) {
460 return Long
.parseLong(cut
);
461 } else if (encodedValue
.endsWith("F")) {
462 return Float
.parseFloat(cut
);
463 } else if (encodedValue
.endsWith("d")) {
464 return Double
.parseDouble(cut
);
465 } else if (encodedValue
.endsWith(";")) {
466 return decodeEnum(encodedValue
);
468 return Integer
.parseInt(encodedValue
);
470 } catch (Exception e
) {
471 if (e
instanceof IOException
) {
472 throw (IOException
) e
;
474 throw new IOException(e
.getMessage());
479 * Write the given {@link String} into the given {@link OutputStream} in
483 * the {@link OutputStream}
485 * the data to write, cannot be NULL
487 * @throws IOException
488 * in case of I/O error
490 static void write(OutputStream out
, Object data
) throws IOException
{
492 out
.write(data
.toString().getBytes("UTF-8"));
493 } catch (UnsupportedEncodingException e
) {
494 // A conforming JVM is required to support UTF-8
500 * Return the corresponding class or throw an {@link Exception} if it
504 * the class name to look for
506 * @return the class (will never be NULL)
508 * @throws ClassNotFoundException
509 * if the class cannot be found
510 * @throws NoSuchMethodException
511 * if the class cannot be created (usually because it or its
512 * enclosing class doesn't have an empty constructor)
514 static private Class
<?
> getClass(String type
)
515 throws ClassNotFoundException
, NoSuchMethodException
{
516 Class
<?
> clazz
= null;
518 clazz
= Class
.forName(type
);
519 } catch (ClassNotFoundException e
) {
520 int pos
= type
.length();
521 pos
= type
.lastIndexOf(".", pos
);
523 String parentType
= type
.substring(0, pos
);
524 String nestedType
= type
.substring(pos
+ 1);
525 Class
<?
> javaParent
= null;
527 javaParent
= getClass(parentType
);
528 parentType
= javaParent
.getName();
529 clazz
= Class
.forName(parentType
+ "$" + nestedType
);
530 } catch (Exception ee
) {
533 if (javaParent
== null) {
534 throw new NoSuchMethodException(
537 + " (the enclosing class cannot be created: maybe it doesn't have an empty constructor?)");
543 throw new ClassNotFoundException("Class not found: " + type
);
549 @SuppressWarnings({ "unchecked", "rawtypes" })
550 static private Enum
<?
> decodeEnum(String escaped
) {
551 // escaped: be.xxx.EnumType.VALUE;
552 int pos
= escaped
.lastIndexOf(".");
553 String type
= escaped
.substring(0, pos
);
554 String name
= escaped
.substring(pos
+ 1, escaped
.length() - 1);
557 return Enum
.valueOf((Class
<Enum
>) getClass(type
), name
);
558 } catch (Exception e
) {
559 throw new UnknownFormatConversionException("Unknown enum: <" + type
565 static void encodeString(OutputStream out
, String raw
) throws IOException
{
567 // TODO !! utf-8 required
568 for (char car
: raw
.toCharArray()) {
569 encodeString(out
, car
);
575 static void encodeString(OutputStream out
, InputStream raw
)
578 byte buffer
[] = new byte[4069];
579 for (int len
= 0; (len
= raw
.read(buffer
)) > 0;) {
580 for (int i
= 0; i
< len
; i
++) {
581 // TODO: not 100% correct, look up howto for UTF-8
582 encodeString(out
, (char) buffer
[i
]);
588 // for encode string, NOT to encode a char by itself!
589 static void encodeString(OutputStream out
, char raw
) throws IOException
{
614 static String
decodeString(String escaped
) {
615 StringBuilder builder
= new StringBuilder();
617 boolean escaping
= false;
618 for (char car
: escaped
.toCharArray()) {
628 builder
.append('\\');
631 builder
.append('\r');
634 builder
.append('\n');
644 return builder
.substring(1, builder
.length() - 1);