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
.lang
.reflect
.Array
;
8 import java
.lang
.reflect
.Constructor
;
9 import java
.lang
.reflect
.Field
;
10 import java
.lang
.reflect
.Modifier
;
12 import java
.util
.ArrayList
;
13 import java
.util
.HashMap
;
14 import java
.util
.List
;
16 import java
.util
.UnknownFormatConversionException
;
18 import be
.nikiroo
.utils
.IOUtils
;
19 import be
.nikiroo
.utils
.Image
;
20 import be
.nikiroo
.utils
.StringUtils
;
21 import be
.nikiroo
.utils
.streams
.Base64InputStream
;
22 import be
.nikiroo
.utils
.streams
.Base64OutputStream
;
23 import be
.nikiroo
.utils
.streams
.BufferedInputStream
;
24 import be
.nikiroo
.utils
.streams
.NextableInputStream
;
25 import be
.nikiroo
.utils
.streams
.NextableInputStreamStep
;
28 * Small class to help with serialisation.
30 * Note that we do not support inner classes (but we do support nested classes)
31 * and all objects require an empty constructor to be deserialised.
33 * It is possible to add support to custom types (both the encoder and the
34 * decoder will require the custom classes) -- see {@link CustomSerializer}.
36 * Default supported types are:
38 * <li>NULL (as a null value)</li>
48 * <li>Enum (any enum whose name and value is known by the caller)</li>
49 * <li>java.awt.image.BufferedImage (as a {@link CustomSerializer})</li>
50 * <li>An array of the above (as a {@link CustomSerializer})</li>
56 public class SerialUtils
{
57 private static Map
<String
, CustomSerializer
> customTypes
;
60 customTypes
= new HashMap
<String
, CustomSerializer
>();
63 customTypes
.put("[]", new CustomSerializer() {
65 protected void toStream(OutputStream out
, Object value
)
68 // TODO: we use \n to separate, and b64 to un-\n
69 // -- but we could use \\n ?
70 String type
= value
.getClass().getCanonicalName();
71 type
= type
.substring(0, type
.length() - 2); // remove the []
75 for (int i
= 0; true; i
++) {
76 Object item
= Array
.get(value
, i
);
78 // encode it normally if direct value
80 if (!SerialUtils
.encode(out
, item
)) {
82 // TODO: bad escaping?
84 OutputStream out64
= new Base64OutputStream(
86 new Exporter(out64
).append(item
);
87 } catch (NotSerializableException e
) {
88 throw new UnknownFormatConversionException(e
93 } catch (ArrayIndexOutOfBoundsException e
) {
99 protected Object
fromStream(InputStream in
) throws IOException
{
100 NextableInputStream stream
= new NextableInputStream(in
,
101 new NextableInputStreamStep('\r'));
104 List
<Object
> list
= new ArrayList
<Object
>();
106 String type
= IOUtils
.readSmallStream(stream
);
108 while (stream
.next()) {
109 Object value
= new Importer().read(stream
).getValue();
113 Object array
= Array
.newInstance(
114 SerialUtils
.getClass(type
), list
.size());
115 for (int i
= 0; i
< list
.size(); i
++) {
116 Array
.set(array
, i
, list
.get(i
));
120 } catch (Exception e
) {
121 if (e
instanceof IOException
) {
122 throw (IOException
) e
;
124 throw new IOException(e
.getMessage());
129 protected String
getType() {
135 customTypes
.put("java.net.URL", new CustomSerializer() {
137 protected void toStream(OutputStream out
, Object value
)
141 val
= ((URL
) value
).toString();
144 out
.write(StringUtils
.getBytes(val
));
148 protected Object
fromStream(InputStream in
) throws IOException
{
149 String val
= IOUtils
.readSmallStream(in
);
150 if (!val
.isEmpty()) {
158 protected String
getType() {
159 return "java.net.URL";
163 // Images (this is currently the only supported image type by default)
164 customTypes
.put("be.nikiroo.utils.Image", new CustomSerializer() {
166 protected void toStream(OutputStream out
, Object value
)
168 Image img
= (Image
) value
;
169 OutputStream encoded
= new Base64OutputStream(out
, true);
171 InputStream in
= img
.newInputStream();
173 IOUtils
.write(in
, encoded
);
184 protected String
getType() {
185 return "be.nikiroo.utils.Image";
189 protected Object
fromStream(InputStream in
) throws IOException
{
192 InputStream decoded
= new Base64InputStream(in
, false);
193 return new Image(decoded
);
194 } catch (IOException e
) {
195 throw new UnknownFormatConversionException(e
.getMessage());
202 * Create an empty object of the given type.
205 * the object type (its class name)
207 * @return the new object
209 * @throws ClassNotFoundException
210 * if the class cannot be found
211 * @throws NoSuchMethodException
212 * if the given class is not compatible with this code
214 public static Object
createObject(String type
)
215 throws ClassNotFoundException
, NoSuchMethodException
{
219 Class
<?
> clazz
= getClass(type
);
220 String className
= clazz
.getName();
221 List
<Object
> args
= new ArrayList
<Object
>();
222 List
<Class
<?
>> classes
= new ArrayList
<Class
<?
>>();
223 Constructor
<?
> ctor
= null;
224 if (className
.contains("$")) {
225 for (String parentName
= className
.substring(0,
226 className
.lastIndexOf('$'));; parentName
= parentName
227 .substring(0, parentName
.lastIndexOf('$'))) {
228 Object parent
= createObject(parentName
);
230 classes
.add(parent
.getClass());
232 if (!parentName
.contains("$")) {
237 // Better error description in case there is no empty
241 for (Class
<?
> parent
= clazz
; parent
!= null
242 && !parent
.equals(Object
.class); parent
= parent
244 if (!desc
.isEmpty()) {
254 ctor
= clazz
.getDeclaredConstructor(classes
255 .toArray(new Class
[] {}));
256 } catch (NoSuchMethodException nsme
) {
257 // TODO: it seems we do not always need a parameter for each
258 // level, so we currently try "ALL" levels or "FIRST" level
259 // only -> we should check the actual rule and use it
260 ctor
= clazz
.getDeclaredConstructor(classes
.get(0));
261 Object firstParent
= args
.get(0);
263 args
.add(firstParent
);
267 ctor
= clazz
.getDeclaredConstructor();
270 ctor
.setAccessible(true);
271 return ctor
.newInstance(args
.toArray());
272 } catch (ClassNotFoundException e
) {
274 } catch (NoSuchMethodException e
) {
276 throw new NoSuchMethodException("Empty constructor not found: "
280 } catch (Exception e
) {
281 throw new NoSuchMethodException("Cannot instantiate: " + type
);
286 * Insert a custom serialiser that will take precedence over the default one
287 * or the target class.
290 * the custom serialiser
292 static public void addCustomSerializer(CustomSerializer serializer
) {
293 customTypes
.put(serializer
.getType(), serializer
);
297 * Serialise the given object into this {@link OutputStream}.
299 * <b>Important: </b>If the operation fails (with a
300 * {@link NotSerializableException}), the {@link StringBuilder} will be
301 * corrupted (will contain bad, most probably not importable data).
304 * the output {@link OutputStream} to serialise to
306 * the object to serialise
308 * the map of already serialised objects (if the given object or
309 * one of its descendant is already present in it, only an ID
310 * will be serialised)
312 * @throws NotSerializableException
313 * if the object cannot be serialised (in this case, the
314 * {@link StringBuilder} can contain bad, most probably not
316 * @throws IOException
317 * in case of I/O errors
319 static void append(OutputStream out
, Object o
, Map
<Integer
, Object
> map
)
320 throws NotSerializableException
, IOException
{
322 Field
[] fields
= new Field
[] {};
327 int hash
= System
.identityHashCode(o
);
328 fields
= o
.getClass().getDeclaredFields();
329 type
= o
.getClass().getCanonicalName();
331 // Anonymous inner classes support
332 type
= o
.getClass().getName();
334 id
= Integer
.toString(hash
);
335 if (map
.containsKey(hash
)) {
336 fields
= new Field
[] {};
342 write(out
, "{\nREF ");
348 if (!encode(out
, o
)) { // check if direct value
350 for (Field field
: fields
) {
351 field
.setAccessible(true);
353 if (field
.getName().startsWith("this$")
354 || field
.isSynthetic()
355 || (field
.getModifiers() & Modifier
.STATIC
) == Modifier
.STATIC
) {
356 // Do not keep this links of nested classes
357 // Do not keep synthetic fields
358 // Do not keep final fields
363 write(out
, field
.getName());
366 Object value
= field
.get(o
);
368 if (!encode(out
, value
)) {
370 append(out
, value
, map
);
373 } catch (IllegalArgumentException e
) {
374 e
.printStackTrace(); // should not happen (see
376 } catch (IllegalAccessException e
) {
377 e
.printStackTrace(); // should not happen (see
386 * Encode the object into the given {@link OutputStream} if possible and if
389 * A supported object in this context means an object we can directly
390 * encode, like an Integer or a String. Custom objects and arrays are also
391 * considered supported, but <b>compound objects are not supported here</b>.
393 * For compound objects, you should use {@link Exporter}.
396 * the {@link OutputStream} to append to
398 * the object to encode (can be NULL, which will be encoded)
400 * @return TRUE if success, FALSE if not (the content of the
401 * {@link OutputStream} won't be changed in case of failure)
403 * @throws IOException
404 * in case of I/O error
406 static boolean encode(OutputStream out
, Object value
) throws IOException
{
409 } else if (value
.getClass().getSimpleName().endsWith("[]")) {
410 // Simple name does support [] suffix and do not return NULL for
411 // inner anonymous classes
412 customTypes
.get("[]").encode(out
, value
);
413 } else if (customTypes
.containsKey(value
.getClass().getCanonicalName())) {
414 customTypes
.get(value
.getClass().getCanonicalName())//
416 } else if (value
instanceof String
) {
417 encodeString(out
, (String
) value
);
418 } else if (value
instanceof Boolean
) {
420 } else if (value
instanceof Byte
) {
423 } else if (value
instanceof Character
) {
425 encodeString(out
, "" + value
);
426 } else if (value
instanceof Short
) {
429 } 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
) {
443 String type
= value
.getClass().getCanonicalName();
446 write(out
, ((Enum
<?
>) value
).name());
455 static boolean isDirectValue(BufferedInputStream encodedValue
)
457 if (CustomSerializer
.isCustom(encodedValue
)) {
461 for (String fullValue
: new String
[] { "NULL", "null", "true", "false" }) {
462 if (encodedValue
.is(fullValue
)) {
467 // TODO: Not efficient
468 for (String prefix
: new String
[] { "c\"", "\"", "b", "s", "i", "l",
470 if (encodedValue
.startsWith(prefix
)) {
479 * Decode the data into an equivalent supported source object.
481 * A supported object in this context means an object we can directly
482 * encode, like an Integer or a String (see
483 * {@link SerialUtils#decode(String)}.
485 * Custom objects and arrays are also considered supported here, but
486 * <b>compound objects are not</b>.
488 * For compound objects, you should use {@link Importer}.
490 * @param encodedValue
491 * the encoded data, cannot be NULL
493 * @return the object (can be NULL for NULL encoded values)
495 * @throws IOException
496 * if the content cannot be converted
498 static Object
decode(BufferedInputStream encodedValue
) throws IOException
{
499 if (CustomSerializer
.isCustom(encodedValue
)) {
500 // custom^TYPE^ENCODED_VALUE
501 NextableInputStream content
= new NextableInputStream(encodedValue
,
502 new NextableInputStreamStep('^'));
505 @SuppressWarnings("unused")
506 String custom
= IOUtils
.readSmallStream(content
);
508 String type
= IOUtils
.readSmallStream(content
);
510 if (customTypes
.containsKey(type
)) {
511 return customTypes
.get(type
).decode(content
);
514 throw new IOException("Unknown custom type: " + type
);
516 content
.close(false);
517 // TODO: check what happens with thrown Exception in finally
522 String encodedString
= IOUtils
.readSmallStream(encodedValue
);
523 return decode(encodedString
);
527 * Decode the data into an equivalent supported source object.
529 * A supported object in this context means an object we can directly
530 * encode, like an Integer or a String.
532 * For custom objects and arrays, you should use
533 * {@link SerialUtils#decode(InputStream)} or directly {@link Importer}.
535 * For compound objects, you should use {@link Importer}.
537 * @param encodedValue
538 * the encoded data, cannot be NULL
540 * @return the object (can be NULL for NULL encoded values)
542 * @throws IOException
543 * if the content cannot be converted
545 static Object
decode(String encodedValue
) throws IOException
{
548 if (encodedValue
.length() > 1) {
549 cut
= encodedValue
.substring(1);
552 if (encodedValue
.equals("NULL") || encodedValue
.equals("null")) {
554 } else if (encodedValue
.startsWith("\"")) {
555 return decodeString(encodedValue
);
556 } else if (encodedValue
.equals("true")) {
558 } else if (encodedValue
.equals("false")) {
560 } else if (encodedValue
.startsWith("b")) {
561 return Byte
.parseByte(cut
);
562 } else if (encodedValue
.startsWith("c")) {
563 return decodeString(cut
).charAt(0);
564 } else if (encodedValue
.startsWith("s")) {
565 return Short
.parseShort(cut
);
566 } else if (encodedValue
.startsWith("l")) {
567 return Long
.parseLong(cut
);
568 } else if (encodedValue
.startsWith("f")) {
569 return Float
.parseFloat(cut
);
570 } else if (encodedValue
.startsWith("d")) {
571 return Double
.parseDouble(cut
);
572 } else if (encodedValue
.startsWith("i")) {
573 return Integer
.parseInt(cut
);
574 } else if (encodedValue
.startsWith("E:")) {
575 cut
= cut
.substring(1);
576 return decodeEnum(cut
);
578 throw new IOException("Unrecognized value: " + encodedValue
);
580 } catch (Exception e
) {
581 if (e
instanceof IOException
) {
582 throw (IOException
) e
;
584 throw new IOException(e
.getMessage(), e
);
589 * Write the given {@link String} into the given {@link OutputStream} in
593 * the {@link OutputStream}
595 * the data to write, cannot be NULL
597 * @throws IOException
598 * in case of I/O error
600 static void write(OutputStream out
, Object data
) throws IOException
{
601 out
.write(StringUtils
.getBytes(data
.toString()));
605 * Return the corresponding class or throw an {@link Exception} if it
609 * the class name to look for
611 * @return the class (will never be NULL)
613 * @throws ClassNotFoundException
614 * if the class cannot be found
615 * @throws NoSuchMethodException
616 * if the class cannot be created (usually because it or its
617 * enclosing class doesn't have an empty constructor)
619 static private Class
<?
> getClass(String type
)
620 throws ClassNotFoundException
, NoSuchMethodException
{
621 Class
<?
> clazz
= null;
623 clazz
= Class
.forName(type
);
624 } catch (ClassNotFoundException e
) {
625 int pos
= type
.length();
626 pos
= type
.lastIndexOf(".", pos
);
628 String parentType
= type
.substring(0, pos
);
629 String nestedType
= type
.substring(pos
+ 1);
630 Class
<?
> javaParent
= null;
632 javaParent
= getClass(parentType
);
633 parentType
= javaParent
.getName();
634 clazz
= Class
.forName(parentType
+ "$" + nestedType
);
635 } catch (Exception ee
) {
638 if (javaParent
== null) {
639 throw new NoSuchMethodException(
642 + " (the enclosing class cannot be created: maybe it doesn't have an empty constructor?)");
648 throw new ClassNotFoundException("Class not found: " + type
);
654 @SuppressWarnings({ "unchecked", "rawtypes" })
655 static private Enum
<?
> decodeEnum(String escaped
) {
656 // escaped: be.xxx.EnumType.VALUE;
657 int pos
= escaped
.lastIndexOf(".");
658 String type
= escaped
.substring(0, pos
);
659 String name
= escaped
.substring(pos
+ 1, escaped
.length() - 1);
662 return Enum
.valueOf((Class
<Enum
>) getClass(type
), name
);
663 } catch (Exception e
) {
664 throw new UnknownFormatConversionException("Unknown enum: <" + type
670 static void encodeString(OutputStream out
, String raw
) throws IOException
{
671 // TODO: not. efficient.
673 for (char car
: raw
.toCharArray()) {
674 encodeString(out
, car
);
680 static void encodeString(OutputStream out
, InputStream raw
)
683 byte buffer
[] = new byte[4096];
684 for (int len
= 0; (len
= raw
.read(buffer
)) > 0;) {
685 for (int i
= 0; i
< len
; i
++) {
686 // TODO: not 100% correct, look up howto for UTF-8
687 encodeString(out
, (char) buffer
[i
]);
693 // for encoding string, NOT to encode a char by itself!
694 static void encodeString(OutputStream out
, char raw
) throws IOException
{
719 static String
decodeString(String escaped
) {
720 StringBuilder builder
= new StringBuilder();
722 boolean escaping
= false;
723 for (char car
: escaped
.toCharArray()) {
733 builder
.append('\\');
736 builder
.append('\r');
739 builder
.append('\n');
749 return builder
.substring(1, builder
.length() - 1);