1 package be
.nikiroo
.utils
.serial
;
3 import java
.io
.ByteArrayInputStream
;
4 import java
.io
.IOException
;
5 import java
.io
.InputStream
;
6 import java
.io
.NotSerializableException
;
7 import java
.io
.OutputStream
;
8 import java
.io
.UnsupportedEncodingException
;
9 import java
.lang
.reflect
.Array
;
10 import java
.lang
.reflect
.Constructor
;
11 import java
.lang
.reflect
.Field
;
12 import java
.lang
.reflect
.Modifier
;
14 import java
.util
.ArrayList
;
15 import java
.util
.HashMap
;
16 import java
.util
.List
;
18 import java
.util
.UnknownFormatConversionException
;
20 import be
.nikiroo
.utils
.IOUtils
;
21 import be
.nikiroo
.utils
.Image
;
22 import be
.nikiroo
.utils
.StringUtils
;
23 import be
.nikiroo
.utils
.streams
.Base64InputStream
;
24 import be
.nikiroo
.utils
.streams
.Base64OutputStream
;
25 import be
.nikiroo
.utils
.streams
.NextableInputStream
;
26 import be
.nikiroo
.utils
.streams
.NextableInputStreamStep
;
29 * Small class to help with serialisation.
31 * Note that we do not support inner classes (but we do support nested classes)
32 * and all objects require an empty constructor to be deserialised.
34 * It is possible to add support to custom types (both the encoder and the
35 * decoder will require the custom classes) -- see {@link CustomSerializer}.
37 * Default supported types are:
39 * <li>NULL (as a null value)</li>
49 * <li>Enum (any enum whose name and value is known by the caller)</li>
50 * <li>java.awt.image.BufferedImage (as a {@link CustomSerializer})</li>
51 * <li>An array of the above (as a {@link CustomSerializer})</li>
57 public class SerialUtils
{
58 private static Map
<String
, CustomSerializer
> customTypes
;
61 customTypes
= new HashMap
<String
, CustomSerializer
>();
64 customTypes
.put("[]", new CustomSerializer() {
66 protected void toStream(OutputStream out
, Object value
)
69 // TODO: we use \n to separate, and b64 to un-\n
70 // -- but we could use \\n ?
71 String type
= value
.getClass().getCanonicalName();
72 type
= type
.substring(0, type
.length() - 2); // remove the []
76 for (int i
= 0; true; i
++) {
77 Object item
= Array
.get(value
, i
);
79 // encode it normally if direct value
81 if (!SerialUtils
.encode(out
, item
)) {
83 // TODO: bad escaping?
85 OutputStream out64
= new Base64OutputStream(
87 new Exporter(out64
).append(item
);
88 } catch (NotSerializableException e
) {
89 throw new UnknownFormatConversionException(e
94 } catch (ArrayIndexOutOfBoundsException e
) {
100 protected Object
fromStream(InputStream in
) throws IOException
{
101 NextableInputStream stream
= new NextableInputStream(in
,
102 new NextableInputStreamStep('\r'));
105 List
<Object
> list
= new ArrayList
<Object
>();
107 String type
= IOUtils
.readSmallStream(stream
);
109 while (stream
.next()) {
110 Object value
= new Importer().read(stream
).getValue();
114 Object array
= Array
.newInstance(
115 SerialUtils
.getClass(type
), list
.size());
116 for (int i
= 0; i
< list
.size(); i
++) {
117 Array
.set(array
, i
, list
.get(i
));
121 } catch (Exception e
) {
122 if (e
instanceof IOException
) {
123 throw (IOException
) e
;
125 throw new IOException(e
.getMessage());
130 protected String
getType() {
136 customTypes
.put("java.net.URL", new CustomSerializer() {
138 protected void toStream(OutputStream out
, Object value
)
142 val
= ((URL
) value
).toString();
145 out
.write(val
.getBytes("UTF-8"));
149 protected Object
fromStream(InputStream in
) throws IOException
{
150 String val
= IOUtils
.readSmallStream(in
);
151 if (!val
.isEmpty()) {
159 protected String
getType() {
160 return "java.net.URL";
164 // Images (this is currently the only supported image type by default)
165 customTypes
.put("be.nikiroo.utils.Image", new CustomSerializer() {
167 protected void toStream(OutputStream out
, Object value
)
169 Image img
= (Image
) value
;
170 OutputStream encoded
= new Base64OutputStream(out
, true);
172 InputStream in
= img
.newInputStream();
174 IOUtils
.write(in
, encoded
);
185 protected String
getType() {
186 return "be.nikiroo.utils.Image";
190 protected Object
fromStream(InputStream in
) throws IOException
{
193 InputStream decoded
= new Base64InputStream(in
, false);
194 return new Image(decoded
);
195 } catch (IOException e
) {
196 throw new UnknownFormatConversionException(e
.getMessage());
203 * Create an empty object of the given type.
206 * the object type (its class name)
208 * @return the new object
210 * @throws ClassNotFoundException
211 * if the class cannot be found
212 * @throws NoSuchMethodException
213 * if the given class is not compatible with this code
215 public static Object
createObject(String type
)
216 throws ClassNotFoundException
, NoSuchMethodException
{
220 Class
<?
> clazz
= getClass(type
);
221 String className
= clazz
.getName();
222 List
<Object
> args
= new ArrayList
<Object
>();
223 List
<Class
<?
>> classes
= new ArrayList
<Class
<?
>>();
224 Constructor
<?
> ctor
= null;
225 if (className
.contains("$")) {
226 for (String parentName
= className
.substring(0,
227 className
.lastIndexOf('$'));; parentName
= parentName
228 .substring(0, parentName
.lastIndexOf('$'))) {
229 Object parent
= createObject(parentName
);
231 classes
.add(parent
.getClass());
233 if (!parentName
.contains("$")) {
238 // Better error description in case there is no empty
242 for (Class
<?
> parent
= clazz
; parent
!= null
243 && !parent
.equals(Object
.class); parent
= parent
245 if (!desc
.isEmpty()) {
255 ctor
= clazz
.getDeclaredConstructor(classes
256 .toArray(new Class
[] {}));
257 } catch (NoSuchMethodException nsme
) {
258 // TODO: it seems we do not always need a parameter for each
259 // level, so we currently try "ALL" levels or "FIRST" level
260 // only -> we should check the actual rule and use it
261 ctor
= clazz
.getDeclaredConstructor(classes
.get(0));
262 Object firstParent
= args
.get(0);
264 args
.add(firstParent
);
268 ctor
= clazz
.getDeclaredConstructor();
271 ctor
.setAccessible(true);
272 return ctor
.newInstance(args
.toArray());
273 } catch (ClassNotFoundException e
) {
275 } catch (NoSuchMethodException e
) {
277 throw new NoSuchMethodException("Empty constructor not found: "
281 } catch (Exception e
) {
282 throw new NoSuchMethodException("Cannot instantiate: " + type
);
287 * Insert a custom serialiser that will take precedence over the default one
288 * or the target class.
291 * the custom serialiser
293 static public void addCustomSerializer(CustomSerializer serializer
) {
294 customTypes
.put(serializer
.getType(), serializer
);
298 * Serialise the given object into this {@link OutputStream}.
300 * <b>Important: </b>If the operation fails (with a
301 * {@link NotSerializableException}), the {@link StringBuilder} will be
302 * corrupted (will contain bad, most probably not importable data).
305 * the output {@link OutputStream} to serialise to
307 * the object to serialise
309 * the map of already serialised objects (if the given object or
310 * one of its descendant is already present in it, only an ID
311 * will be serialised)
313 * @throws NotSerializableException
314 * if the object cannot be serialised (in this case, the
315 * {@link StringBuilder} can contain bad, most probably not
317 * @throws IOException
318 * in case of I/O errors
320 static void append(OutputStream out
, Object o
, Map
<Integer
, Object
> map
)
321 throws NotSerializableException
, IOException
{
323 Field
[] fields
= new Field
[] {};
328 int hash
= System
.identityHashCode(o
);
329 fields
= o
.getClass().getDeclaredFields();
330 type
= o
.getClass().getCanonicalName();
332 // Anonymous inner classes support
333 type
= o
.getClass().getName();
335 id
= Integer
.toString(hash
);
336 if (map
.containsKey(hash
)) {
337 fields
= new Field
[] {};
343 write(out
, "{\nREF ");
349 if (!encode(out
, o
)) { // check if direct value
351 for (Field field
: fields
) {
352 field
.setAccessible(true);
354 if (field
.getName().startsWith("this$")
355 || field
.isSynthetic()
356 || (field
.getModifiers() & Modifier
.STATIC
) == Modifier
.STATIC
) {
357 // Do not keep this links of nested classes
358 // Do not keep synthetic fields
359 // Do not keep final fields
364 write(out
, field
.getName());
367 Object value
= field
.get(o
);
369 if (!encode(out
, value
)) {
371 append(out
, value
, map
);
374 } catch (IllegalArgumentException e
) {
375 e
.printStackTrace(); // should not happen (see
377 } catch (IllegalAccessException e
) {
378 e
.printStackTrace(); // should not happen (see
387 * Encode the object into the given {@link OutputStream} if possible and if
390 * A supported object in this context means an object we can directly
391 * encode, like an Integer or a String. Custom objects and arrays are also
392 * considered supported, but <b>compound objects are not supported here</b>.
394 * For compound objects, you should use {@link Exporter}.
397 * the {@link OutputStream} to append to
399 * the object to encode (can be NULL, which will be encoded)
401 * @return TRUE if success, FALSE if not (the content of the
402 * {@link OutputStream} won't be changed in case of failure)
404 * @throws IOException
405 * in case of I/O error
407 static boolean encode(OutputStream out
, Object value
) throws IOException
{
410 } else if (value
.getClass().getSimpleName().endsWith("[]")) {
411 // Simple name does support [] suffix and do not return NULL for
412 // inner anonymous classes
413 customTypes
.get("[]").encode(out
, value
);
414 } else if (customTypes
.containsKey(value
.getClass().getCanonicalName())) {
415 customTypes
.get(value
.getClass().getCanonicalName())//
417 } else if (value
instanceof String
) {
418 encodeString(out
, (String
) value
);
419 } else if (value
instanceof Boolean
) {
421 } else if (value
instanceof Byte
) {
424 } else if (value
instanceof Character
) {
425 encodeString(out
, "" + value
);
427 } else if (value
instanceof Short
) {
430 } else if (value
instanceof Integer
) {
432 } else if (value
instanceof Long
) {
435 } else if (value
instanceof Float
) {
438 } else if (value
instanceof Double
) {
441 } else if (value
instanceof Enum
) {
442 String type
= value
.getClass().getCanonicalName();
445 write(out
, ((Enum
<?
>) value
).name());
455 * Decode the data into an equivalent supported source object.
457 * A supported object in this context means an object we can directly
458 * encode, like an Integer or a String. Custom objects and arrays are also
459 * considered supported, but <b>compound objects are not supported here</b>.
461 * For compound objects, you should use {@link Importer}.
463 * @param encodedValue
464 * the encoded data, cannot be NULL
466 * @return the object (can be NULL for NULL encoded values)
468 * @throws IOException
469 * if the content cannot be converted
471 static Object
decode(String encodedValue
) throws IOException
{
474 if (encodedValue
.length() > 1) {
475 cut
= encodedValue
.substring(0, encodedValue
.length() - 1);
478 if (CustomSerializer
.isCustom(encodedValue
)) {
479 // custom:TYPE_NAME:"content is String-encoded"
480 String type
= CustomSerializer
.typeOf(encodedValue
);
481 if (customTypes
.containsKey(type
)) {
482 // TODO: we should start with a stream
483 InputStream streamEncodedValue
= new ByteArrayInputStream(
484 encodedValue
.getBytes("UTF-8"));
486 return customTypes
.get(type
).decode(streamEncodedValue
);
488 streamEncodedValue
.close();
491 throw new IOException("Unknown custom type: " + type
);
492 } else if (encodedValue
.equals("NULL")
493 || encodedValue
.equals("null")) {
495 } else if (encodedValue
.endsWith("\"")) {
496 return decodeString(encodedValue
);
497 } else if (encodedValue
.equals("true")) {
499 } else if (encodedValue
.equals("false")) {
501 } else if (encodedValue
.endsWith("b")) {
502 return Byte
.parseByte(cut
);
503 } else if (encodedValue
.endsWith("c")) {
504 return decodeString(cut
).charAt(0);
505 } else if (encodedValue
.endsWith("s")) {
506 return Short
.parseShort(cut
);
507 } else if (encodedValue
.endsWith("L")) {
508 return Long
.parseLong(cut
);
509 } else if (encodedValue
.endsWith("F")) {
510 return Float
.parseFloat(cut
);
511 } else if (encodedValue
.endsWith("d")) {
512 return Double
.parseDouble(cut
);
513 } else if (encodedValue
.endsWith(";")) {
514 return decodeEnum(encodedValue
);
516 return Integer
.parseInt(encodedValue
);
518 } catch (Exception e
) {
519 if (e
instanceof IOException
) {
520 throw (IOException
) e
;
522 throw new IOException(e
.getMessage(), e
);
527 * Write the given {@link String} into the given {@link OutputStream} in
531 * the {@link OutputStream}
533 * the data to write, cannot be NULL
535 * @throws IOException
536 * in case of I/O error
538 static void write(OutputStream out
, Object data
) throws IOException
{
540 out
.write(data
.toString().getBytes("UTF-8"));
541 } catch (UnsupportedEncodingException e
) {
542 // A conforming JVM is required to support UTF-8
548 * Return the corresponding class or throw an {@link Exception} if it
552 * the class name to look for
554 * @return the class (will never be NULL)
556 * @throws ClassNotFoundException
557 * if the class cannot be found
558 * @throws NoSuchMethodException
559 * if the class cannot be created (usually because it or its
560 * enclosing class doesn't have an empty constructor)
562 static private Class
<?
> getClass(String type
)
563 throws ClassNotFoundException
, NoSuchMethodException
{
564 Class
<?
> clazz
= null;
566 clazz
= Class
.forName(type
);
567 } catch (ClassNotFoundException e
) {
568 int pos
= type
.length();
569 pos
= type
.lastIndexOf(".", pos
);
571 String parentType
= type
.substring(0, pos
);
572 String nestedType
= type
.substring(pos
+ 1);
573 Class
<?
> javaParent
= null;
575 javaParent
= getClass(parentType
);
576 parentType
= javaParent
.getName();
577 clazz
= Class
.forName(parentType
+ "$" + nestedType
);
578 } catch (Exception ee
) {
581 if (javaParent
== null) {
582 throw new NoSuchMethodException(
585 + " (the enclosing class cannot be created: maybe it doesn't have an empty constructor?)");
591 throw new ClassNotFoundException("Class not found: " + type
);
597 @SuppressWarnings({ "unchecked", "rawtypes" })
598 static private Enum
<?
> decodeEnum(String escaped
) {
599 // escaped: be.xxx.EnumType.VALUE;
600 int pos
= escaped
.lastIndexOf(".");
601 String type
= escaped
.substring(0, pos
);
602 String name
= escaped
.substring(pos
+ 1, escaped
.length() - 1);
605 return Enum
.valueOf((Class
<Enum
>) getClass(type
), name
);
606 } catch (Exception e
) {
607 throw new UnknownFormatConversionException("Unknown enum: <" + type
613 static void encodeString(OutputStream out
, String raw
) throws IOException
{
614 // TODO: not. efficient.
616 // TODO !! utf-8 required
617 for (char car
: raw
.toCharArray()) {
618 encodeString(out
, car
);
624 static void encodeString(OutputStream out
, InputStream raw
)
627 byte buffer
[] = new byte[4096];
628 for (int len
= 0; (len
= raw
.read(buffer
)) > 0;) {
629 for (int i
= 0; i
< len
; i
++) {
630 // TODO: not 100% correct, look up howto for UTF-8
631 encodeString(out
, (char) buffer
[i
]);
637 // for encode string, NOT to encode a char by itself!
638 static void encodeString(OutputStream out
, char raw
) throws IOException
{
663 static String
decodeString(String escaped
) {
664 StringBuilder builder
= new StringBuilder();
666 boolean escaping
= false;
667 for (char car
: escaped
.toCharArray()) {
677 builder
.append('\\');
680 builder
.append('\r');
683 builder
.append('\n');
693 return builder
.substring(1, builder
.length() - 1);