52bc43aae85c054d03466c2e171f2f1bbfd7c8f1
[nikiroo-utils.git] / src / be / nikiroo / utils / serial / SerialUtils.java
1 package be.nikiroo.utils.serial;
2
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;
13 import java.net.URL;
14 import java.util.ArrayList;
15 import java.util.HashMap;
16 import java.util.List;
17 import java.util.Map;
18 import java.util.UnknownFormatConversionException;
19
20 import be.nikiroo.utils.IOUtils;
21 import be.nikiroo.utils.Image;
22 import be.nikiroo.utils.StringUtils;
23 import be.nikiroo.utils.streams.NextableInputStream;
24 import be.nikiroo.utils.streams.NextableInputStreamStep;
25
26 /**
27 * Small class to help with serialisation.
28 * <p>
29 * Note that we do not support inner classes (but we do support nested classes)
30 * and all objects require an empty constructor to be deserialised.
31 * <p>
32 * It is possible to add support to custom types (both the encoder and the
33 * decoder will require the custom classes) -- see {@link CustomSerializer}.
34 * <p>
35 * Default supported types are:
36 * <ul>
37 * <li>NULL (as a null value)</li>
38 * <li>String</li>
39 * <li>Boolean</li>
40 * <li>Byte</li>
41 * <li>Character</li>
42 * <li>Short</li>
43 * <li>Long</li>
44 * <li>Float</li>
45 * <li>Double</li>
46 * <li>Integer</li>
47 * <li>Enum (any enum whose name and value is known by the caller)</li>
48 * <li>java.awt.image.BufferedImage (as a {@link CustomSerializer})</li>
49 * <li>An array of the above (as a {@link CustomSerializer})</li>
50 * <li>URL</li>
51 * </ul>
52 *
53 * @author niki
54 */
55 public class SerialUtils {
56 private static Map<String, CustomSerializer> customTypes;
57
58 static {
59 customTypes = new HashMap<String, CustomSerializer>();
60
61 // Array types:
62 customTypes.put("[]", new CustomSerializer() {
63 @Override
64 protected void toStream(OutputStream out, Object value)
65 throws IOException {
66
67 // TODO: we use \n to separate, and b64 to un-\n
68 // -- but we could use \\n ?
69 String type = value.getClass().getCanonicalName();
70 type = type.substring(0, type.length() - 2); // remove the []
71
72 write(out, type);
73 try {
74 for (int i = 0; true; i++) {
75 Object item = Array.get(value, i);
76
77 // encode it normally if direct value
78 write(out, "\r");
79 if (!SerialUtils.encode(out, item)) {
80 try {
81 // TODO: bad escaping?
82 write(out, "B64:");
83 OutputStream bout = StringUtils.base64(out,
84 false, false);
85 new Exporter(bout).append(item);
86 } catch (NotSerializableException e) {
87 throw new UnknownFormatConversionException(e
88 .getMessage());
89 }
90 }
91 }
92 } catch (ArrayIndexOutOfBoundsException e) {
93 // Done.
94 }
95 }
96
97 @Override
98 protected Object fromStream(InputStream in) throws IOException {
99 NextableInputStream stream = new NextableInputStream(in,
100 new NextableInputStreamStep('\r'));
101
102 try {
103 List<Object> list = new ArrayList<Object>();
104 stream.next();
105 String type = IOUtils.readSmallStream(stream);
106
107 while (stream.next()) {
108 Object value = new Importer().read(stream).getValue();
109 list.add(value);
110 }
111
112 Object array = Array.newInstance(
113 SerialUtils.getClass(type), list.size());
114 for (int i = 0; i < list.size(); i++) {
115 Array.set(array, i, list.get(i));
116 }
117
118 return array;
119 } catch (Exception e) {
120 if (e instanceof IOException) {
121 throw (IOException) e;
122 }
123 throw new IOException(e.getMessage());
124 }
125 }
126
127 @Override
128 protected String getType() {
129 return "[]";
130 }
131 });
132
133 // URL:
134 customTypes.put("java.net.URL", new CustomSerializer() {
135 @Override
136 protected void toStream(OutputStream out, Object value)
137 throws IOException {
138 String val = "";
139 if (value != null) {
140 val = ((URL) value).toString();
141 }
142
143 out.write(val.getBytes("UTF-8"));
144 }
145
146 @Override
147 protected Object fromStream(InputStream in) throws IOException {
148 String val = IOUtils.readSmallStream(in);
149 if (!val.isEmpty()) {
150 return new URL(val);
151 }
152
153 return null;
154 }
155
156 @Override
157 protected String getType() {
158 return "java.net.URL";
159 }
160 });
161
162 // Images (this is currently the only supported image type by default)
163 customTypes.put("be.nikiroo.utils.Image", new CustomSerializer() {
164 @Override
165 protected void toStream(OutputStream out, Object value)
166 throws IOException {
167 Image img = (Image) value;
168 OutputStream encoded = StringUtils.base64(out, false, false);
169 try {
170 InputStream in = img.newInputStream();
171 try {
172 IOUtils.write(in, encoded);
173 } finally {
174 in.close();
175 }
176 } finally {
177 encoded.close();
178 }
179 }
180
181 @Override
182 protected String getType() {
183 return "be.nikiroo.utils.Image";
184 }
185
186 @Override
187 protected Object fromStream(InputStream in) throws IOException {
188 try {
189 return new Image(in);
190 } catch (IOException e) {
191 throw new UnknownFormatConversionException(e.getMessage());
192 }
193 }
194 });
195 }
196
197 /**
198 * Create an empty object of the given type.
199 *
200 * @param type
201 * the object type (its class name)
202 *
203 * @return the new object
204 *
205 * @throws ClassNotFoundException
206 * if the class cannot be found
207 * @throws NoSuchMethodException
208 * if the given class is not compatible with this code
209 */
210 public static Object createObject(String type)
211 throws ClassNotFoundException, NoSuchMethodException {
212
213 String desc = null;
214 try {
215 Class<?> clazz = getClass(type);
216 String className = clazz.getName();
217 List<Object> args = new ArrayList<Object>();
218 List<Class<?>> classes = new ArrayList<Class<?>>();
219 Constructor<?> ctor = null;
220 if (className.contains("$")) {
221 for (String parentName = className.substring(0,
222 className.lastIndexOf('$'));; parentName = parentName
223 .substring(0, parentName.lastIndexOf('$'))) {
224 Object parent = createObject(parentName);
225 args.add(parent);
226 classes.add(parent.getClass());
227
228 if (!parentName.contains("$")) {
229 break;
230 }
231 }
232
233 // Better error description in case there is no empty
234 // constructor:
235 desc = "";
236 String end = "";
237 for (Class<?> parent = clazz; parent != null
238 && !parent.equals(Object.class); parent = parent
239 .getSuperclass()) {
240 if (!desc.isEmpty()) {
241 desc += " [:";
242 end += "]";
243 }
244 desc += parent;
245 }
246 desc += end;
247 //
248
249 try {
250 ctor = clazz.getDeclaredConstructor(classes
251 .toArray(new Class[] {}));
252 } catch (NoSuchMethodException nsme) {
253 // TODO: it seems we do not always need a parameter for each
254 // level, so we currently try "ALL" levels or "FIRST" level
255 // only -> we should check the actual rule and use it
256 ctor = clazz.getDeclaredConstructor(classes.get(0));
257 Object firstParent = args.get(0);
258 args.clear();
259 args.add(firstParent);
260 }
261 desc = null;
262 } else {
263 ctor = clazz.getDeclaredConstructor();
264 }
265
266 ctor.setAccessible(true);
267 return ctor.newInstance(args.toArray());
268 } catch (ClassNotFoundException e) {
269 throw e;
270 } catch (NoSuchMethodException e) {
271 if (desc != null) {
272 throw new NoSuchMethodException("Empty constructor not found: "
273 + desc);
274 }
275 throw e;
276 } catch (Exception e) {
277 throw new NoSuchMethodException("Cannot instantiate: " + type);
278 }
279 }
280
281 /**
282 * Insert a custom serialiser that will take precedence over the default one
283 * or the target class.
284 *
285 * @param serializer
286 * the custom serialiser
287 */
288 static public void addCustomSerializer(CustomSerializer serializer) {
289 customTypes.put(serializer.getType(), serializer);
290 }
291
292 /**
293 * Serialise the given object into this {@link OutputStream}.
294 * <p>
295 * <b>Important: </b>If the operation fails (with a
296 * {@link NotSerializableException}), the {@link StringBuilder} will be
297 * corrupted (will contain bad, most probably not importable data).
298 *
299 * @param out
300 * the output {@link OutputStream} to serialise to
301 * @param o
302 * the object to serialise
303 * @param map
304 * the map of already serialised objects (if the given object or
305 * one of its descendant is already present in it, only an ID
306 * will be serialised)
307 *
308 * @throws NotSerializableException
309 * if the object cannot be serialised (in this case, the
310 * {@link StringBuilder} can contain bad, most probably not
311 * importable data)
312 * @throws IOException
313 * in case of I/O errors
314 */
315 static void append(OutputStream out, Object o, Map<Integer, Object> map)
316 throws NotSerializableException, IOException {
317
318 Field[] fields = new Field[] {};
319 String type = "";
320 String id = "NULL";
321
322 if (o != null) {
323 int hash = System.identityHashCode(o);
324 fields = o.getClass().getDeclaredFields();
325 type = o.getClass().getCanonicalName();
326 if (type == null) {
327 // Anonymous inner classes support
328 type = o.getClass().getName();
329 }
330 id = Integer.toString(hash);
331 if (map.containsKey(hash)) {
332 fields = new Field[] {};
333 } else {
334 map.put(hash, o);
335 }
336 }
337
338 write(out, "{\nREF ");
339 write(out, type);
340 write(out, "@");
341 write(out, id);
342 write(out, ":");
343
344 if (!encode(out, o)) { // check if direct value
345 try {
346 for (Field field : fields) {
347 field.setAccessible(true);
348
349 if (field.getName().startsWith("this$")
350 || field.isSynthetic()
351 || (field.getModifiers() & Modifier.STATIC) == Modifier.STATIC) {
352 // Do not keep this links of nested classes
353 // Do not keep synthetic fields
354 // Do not keep final fields
355 continue;
356 }
357
358 write(out, "\n");
359 write(out, field.getName());
360 write(out, ":");
361
362 Object value = field.get(o);
363
364 if (!encode(out, value)) {
365 write(out, "\n");
366 append(out, value, map);
367 }
368 }
369 } catch (IllegalArgumentException e) {
370 e.printStackTrace(); // should not happen (see
371 // setAccessible)
372 } catch (IllegalAccessException e) {
373 e.printStackTrace(); // should not happen (see
374 // setAccessible)
375 }
376
377 write(out, "\n}");
378 }
379 }
380
381 /**
382 * Encode the object into the given {@link OutputStream} if possible and if
383 * supported.
384 * <p>
385 * A supported object in this context means an object we can directly
386 * encode, like an Integer or a String. Custom objects and arrays are also
387 * considered supported, but <b>compound objects are not supported here</b>.
388 * <p>
389 * For compound objects, you should use {@link Exporter}.
390 *
391 * @param out
392 * the {@link OutputStream} to append to
393 * @param value
394 * the object to encode (can be NULL, which will be encoded)
395 *
396 * @return TRUE if success, FALSE if not (the content of the
397 * {@link OutputStream} won't be changed in case of failure)
398 *
399 * @throws IOException
400 * in case of I/O error
401 */
402 static boolean encode(OutputStream out, Object value) throws IOException {
403 if (value == null) {
404 write(out, "NULL");
405 } else if (value.getClass().getSimpleName().endsWith("[]")) {
406 // Simple name does support [] suffix and do not return NULL for
407 // inner anonymous classes
408 customTypes.get("[]").encode(out, value);
409 } else if (customTypes.containsKey(value.getClass().getCanonicalName())) {
410 customTypes.get(value.getClass().getCanonicalName())//
411 .encode(out, value);
412 } else if (value instanceof String) {
413 encodeString(out, (String) value);
414 } else if (value instanceof Boolean) {
415 write(out, value);
416 } else if (value instanceof Byte) {
417 write(out, value);
418 write(out, "b");
419 } else if (value instanceof Character) {
420 encodeString(out, "" + value);
421 write(out, "c");
422 } else if (value instanceof Short) {
423 write(out, value);
424 write(out, "s");
425 } else if (value instanceof Integer) {
426 write(out, value);
427 } else if (value instanceof Long) {
428 write(out, value);
429 write(out, "L");
430 } else if (value instanceof Float) {
431 write(out, value);
432 write(out, "F");
433 } else if (value instanceof Double) {
434 write(out, value);
435 write(out, "d");
436 } else if (value instanceof Enum) {
437 String type = value.getClass().getCanonicalName();
438 write(out, type);
439 write(out, ".");
440 write(out, ((Enum<?>) value).name());
441 write(out, ";");
442 } else {
443 return false;
444 }
445
446 return true;
447 }
448
449 /**
450 * Decode the data into an equivalent supported source object.
451 * <p>
452 * A supported object in this context means an object we can directly
453 * encode, like an Integer or a String. Custom objects and arrays are also
454 * considered supported, but <b>compound objects are not supported here</b>.
455 * <p>
456 * For compound objects, you should use {@link Importer}.
457 *
458 * @param encodedValue
459 * the encoded data, cannot be NULL
460 *
461 * @return the object (can be NULL for NULL encoded values)
462 *
463 * @throws IOException
464 * if the content cannot be converted
465 */
466 static Object decode(String encodedValue) throws IOException {
467 try {
468 String cut = "";
469 if (encodedValue.length() > 1) {
470 cut = encodedValue.substring(0, encodedValue.length() - 1);
471 }
472
473 if (CustomSerializer.isCustom(encodedValue)) {
474 // custom:TYPE_NAME:"content is String-encoded"
475 String type = CustomSerializer.typeOf(encodedValue);
476 if (customTypes.containsKey(type)) {
477 // TODO: we should start with a stream
478 InputStream streamEncodedValue = new ByteArrayInputStream(
479 encodedValue.getBytes("UTF-8"));
480 try {
481 return customTypes.get(type).decode(streamEncodedValue);
482 } finally {
483 streamEncodedValue.close();
484 }
485 }
486 throw new IOException("Unknown custom type: " + type);
487 } else if (encodedValue.equals("NULL")
488 || encodedValue.equals("null")) {
489 return null;
490 } else if (encodedValue.endsWith("\"")) {
491 return decodeString(encodedValue);
492 } else if (encodedValue.equals("true")) {
493 return true;
494 } else if (encodedValue.equals("false")) {
495 return false;
496 } else if (encodedValue.endsWith("b")) {
497 return Byte.parseByte(cut);
498 } else if (encodedValue.endsWith("c")) {
499 return decodeString(cut).charAt(0);
500 } else if (encodedValue.endsWith("s")) {
501 return Short.parseShort(cut);
502 } else if (encodedValue.endsWith("L")) {
503 return Long.parseLong(cut);
504 } else if (encodedValue.endsWith("F")) {
505 return Float.parseFloat(cut);
506 } else if (encodedValue.endsWith("d")) {
507 return Double.parseDouble(cut);
508 } else if (encodedValue.endsWith(";")) {
509 return decodeEnum(encodedValue);
510 } else {
511 return Integer.parseInt(encodedValue);
512 }
513 } catch (Exception e) {
514 if (e instanceof IOException) {
515 throw (IOException) e;
516 }
517 throw new IOException(e.getMessage(), e);
518 }
519 }
520
521 /**
522 * Write the given {@link String} into the given {@link OutputStream} in
523 * UTF-8.
524 *
525 * @param out
526 * the {@link OutputStream}
527 * @param data
528 * the data to write, cannot be NULL
529 *
530 * @throws IOException
531 * in case of I/O error
532 */
533 static void write(OutputStream out, Object data) throws IOException {
534 try {
535 out.write(data.toString().getBytes("UTF-8"));
536 } catch (UnsupportedEncodingException e) {
537 // A conforming JVM is required to support UTF-8
538 e.printStackTrace();
539 }
540 }
541
542 /**
543 * Return the corresponding class or throw an {@link Exception} if it
544 * cannot.
545 *
546 * @param type
547 * the class name to look for
548 *
549 * @return the class (will never be NULL)
550 *
551 * @throws ClassNotFoundException
552 * if the class cannot be found
553 * @throws NoSuchMethodException
554 * if the class cannot be created (usually because it or its
555 * enclosing class doesn't have an empty constructor)
556 */
557 static private Class<?> getClass(String type)
558 throws ClassNotFoundException, NoSuchMethodException {
559 Class<?> clazz = null;
560 try {
561 clazz = Class.forName(type);
562 } catch (ClassNotFoundException e) {
563 int pos = type.length();
564 pos = type.lastIndexOf(".", pos);
565 if (pos >= 0) {
566 String parentType = type.substring(0, pos);
567 String nestedType = type.substring(pos + 1);
568 Class<?> javaParent = null;
569 try {
570 javaParent = getClass(parentType);
571 parentType = javaParent.getName();
572 clazz = Class.forName(parentType + "$" + nestedType);
573 } catch (Exception ee) {
574 }
575
576 if (javaParent == null) {
577 throw new NoSuchMethodException(
578 "Class not found: "
579 + type
580 + " (the enclosing class cannot be created: maybe it doesn't have an empty constructor?)");
581 }
582 }
583 }
584
585 if (clazz == null) {
586 throw new ClassNotFoundException("Class not found: " + type);
587 }
588
589 return clazz;
590 }
591
592 @SuppressWarnings({ "unchecked", "rawtypes" })
593 static private Enum<?> decodeEnum(String escaped) {
594 // escaped: be.xxx.EnumType.VALUE;
595 int pos = escaped.lastIndexOf(".");
596 String type = escaped.substring(0, pos);
597 String name = escaped.substring(pos + 1, escaped.length() - 1);
598
599 try {
600 return Enum.valueOf((Class<Enum>) getClass(type), name);
601 } catch (Exception e) {
602 throw new UnknownFormatConversionException("Unknown enum: <" + type
603 + "> " + name);
604 }
605 }
606
607 // aa bb -> "aa\tbb"
608 static void encodeString(OutputStream out, String raw) throws IOException {
609 // TODO: not. efficient.
610 out.write('\"');
611 // TODO !! utf-8 required
612 for (char car : raw.toCharArray()) {
613 encodeString(out, car);
614 }
615 out.write('\"');
616 }
617
618 // aa bb -> "aa\tbb"
619 static void encodeString(OutputStream out, InputStream raw)
620 throws IOException {
621 out.write('\"');
622 byte buffer[] = new byte[4096];
623 for (int len = 0; (len = raw.read(buffer)) > 0;) {
624 for (int i = 0; i < len; i++) {
625 // TODO: not 100% correct, look up howto for UTF-8
626 encodeString(out, (char) buffer[i]);
627 }
628 }
629 out.write('\"');
630 }
631
632 // for encode string, NOT to encode a char by itself!
633 static void encodeString(OutputStream out, char raw) throws IOException {
634 switch (raw) {
635 case '\\':
636 out.write('\\');
637 out.write('\\');
638 break;
639 case '\r':
640 out.write('\\');
641 out.write('r');
642 break;
643 case '\n':
644 out.write('\\');
645 out.write('n');
646 break;
647 case '"':
648 out.write('\\');
649 out.write('\"');
650 break;
651 default:
652 out.write(raw);
653 break;
654 }
655 }
656
657 // "aa\tbb" -> aa bb
658 static String decodeString(String escaped) {
659 StringBuilder builder = new StringBuilder();
660
661 boolean escaping = false;
662 for (char car : escaped.toCharArray()) {
663 if (!escaping) {
664 if (car == '\\') {
665 escaping = true;
666 } else {
667 builder.append(car);
668 }
669 } else {
670 switch (car) {
671 case '\\':
672 builder.append('\\');
673 break;
674 case 'r':
675 builder.append('\r');
676 break;
677 case 'n':
678 builder.append('\n');
679 break;
680 case '"':
681 builder.append('"');
682 break;
683 }
684 escaping = false;
685 }
686 }
687
688 return builder.substring(1, builder.length() - 1);
689 }
690 }