a6a02a8e06205cb5996b0a94c1124056ce2cbf60
[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.flush();
178 // Cannot close!
179 }
180 }
181
182 @Override
183 protected String getType() {
184 return "be.nikiroo.utils.Image";
185 }
186
187 @Override
188 protected Object fromStream(InputStream in) throws IOException {
189 try {
190 // Cannot close it!
191 InputStream decoded = StringUtils.unbase64(in, false);
192 return new Image(decoded);
193 } catch (IOException e) {
194 throw new UnknownFormatConversionException(e.getMessage());
195 }
196 }
197 });
198 }
199
200 /**
201 * Create an empty object of the given type.
202 *
203 * @param type
204 * the object type (its class name)
205 *
206 * @return the new object
207 *
208 * @throws ClassNotFoundException
209 * if the class cannot be found
210 * @throws NoSuchMethodException
211 * if the given class is not compatible with this code
212 */
213 public static Object createObject(String type)
214 throws ClassNotFoundException, NoSuchMethodException {
215
216 String desc = null;
217 try {
218 Class<?> clazz = getClass(type);
219 String className = clazz.getName();
220 List<Object> args = new ArrayList<Object>();
221 List<Class<?>> classes = new ArrayList<Class<?>>();
222 Constructor<?> ctor = null;
223 if (className.contains("$")) {
224 for (String parentName = className.substring(0,
225 className.lastIndexOf('$'));; parentName = parentName
226 .substring(0, parentName.lastIndexOf('$'))) {
227 Object parent = createObject(parentName);
228 args.add(parent);
229 classes.add(parent.getClass());
230
231 if (!parentName.contains("$")) {
232 break;
233 }
234 }
235
236 // Better error description in case there is no empty
237 // constructor:
238 desc = "";
239 String end = "";
240 for (Class<?> parent = clazz; parent != null
241 && !parent.equals(Object.class); parent = parent
242 .getSuperclass()) {
243 if (!desc.isEmpty()) {
244 desc += " [:";
245 end += "]";
246 }
247 desc += parent;
248 }
249 desc += end;
250 //
251
252 try {
253 ctor = clazz.getDeclaredConstructor(classes
254 .toArray(new Class[] {}));
255 } catch (NoSuchMethodException nsme) {
256 // TODO: it seems we do not always need a parameter for each
257 // level, so we currently try "ALL" levels or "FIRST" level
258 // only -> we should check the actual rule and use it
259 ctor = clazz.getDeclaredConstructor(classes.get(0));
260 Object firstParent = args.get(0);
261 args.clear();
262 args.add(firstParent);
263 }
264 desc = null;
265 } else {
266 ctor = clazz.getDeclaredConstructor();
267 }
268
269 ctor.setAccessible(true);
270 return ctor.newInstance(args.toArray());
271 } catch (ClassNotFoundException e) {
272 throw e;
273 } catch (NoSuchMethodException e) {
274 if (desc != null) {
275 throw new NoSuchMethodException("Empty constructor not found: "
276 + desc);
277 }
278 throw e;
279 } catch (Exception e) {
280 throw new NoSuchMethodException("Cannot instantiate: " + type);
281 }
282 }
283
284 /**
285 * Insert a custom serialiser that will take precedence over the default one
286 * or the target class.
287 *
288 * @param serializer
289 * the custom serialiser
290 */
291 static public void addCustomSerializer(CustomSerializer serializer) {
292 customTypes.put(serializer.getType(), serializer);
293 }
294
295 /**
296 * Serialise the given object into this {@link OutputStream}.
297 * <p>
298 * <b>Important: </b>If the operation fails (with a
299 * {@link NotSerializableException}), the {@link StringBuilder} will be
300 * corrupted (will contain bad, most probably not importable data).
301 *
302 * @param out
303 * the output {@link OutputStream} to serialise to
304 * @param o
305 * the object to serialise
306 * @param map
307 * the map of already serialised objects (if the given object or
308 * one of its descendant is already present in it, only an ID
309 * will be serialised)
310 *
311 * @throws NotSerializableException
312 * if the object cannot be serialised (in this case, the
313 * {@link StringBuilder} can contain bad, most probably not
314 * importable data)
315 * @throws IOException
316 * in case of I/O errors
317 */
318 static void append(OutputStream out, Object o, Map<Integer, Object> map)
319 throws NotSerializableException, IOException {
320
321 Field[] fields = new Field[] {};
322 String type = "";
323 String id = "NULL";
324
325 if (o != null) {
326 int hash = System.identityHashCode(o);
327 fields = o.getClass().getDeclaredFields();
328 type = o.getClass().getCanonicalName();
329 if (type == null) {
330 // Anonymous inner classes support
331 type = o.getClass().getName();
332 }
333 id = Integer.toString(hash);
334 if (map.containsKey(hash)) {
335 fields = new Field[] {};
336 } else {
337 map.put(hash, o);
338 }
339 }
340
341 write(out, "{\nREF ");
342 write(out, type);
343 write(out, "@");
344 write(out, id);
345 write(out, ":");
346
347 if (!encode(out, o)) { // check if direct value
348 try {
349 for (Field field : fields) {
350 field.setAccessible(true);
351
352 if (field.getName().startsWith("this$")
353 || field.isSynthetic()
354 || (field.getModifiers() & Modifier.STATIC) == Modifier.STATIC) {
355 // Do not keep this links of nested classes
356 // Do not keep synthetic fields
357 // Do not keep final fields
358 continue;
359 }
360
361 write(out, "\n");
362 write(out, field.getName());
363 write(out, ":");
364
365 Object value = field.get(o);
366
367 if (!encode(out, value)) {
368 write(out, "\n");
369 append(out, value, map);
370 }
371 }
372 } catch (IllegalArgumentException e) {
373 e.printStackTrace(); // should not happen (see
374 // setAccessible)
375 } catch (IllegalAccessException e) {
376 e.printStackTrace(); // should not happen (see
377 // setAccessible)
378 }
379
380 write(out, "\n}");
381 }
382 }
383
384 /**
385 * Encode the object into the given {@link OutputStream} if possible and if
386 * supported.
387 * <p>
388 * A supported object in this context means an object we can directly
389 * encode, like an Integer or a String. Custom objects and arrays are also
390 * considered supported, but <b>compound objects are not supported here</b>.
391 * <p>
392 * For compound objects, you should use {@link Exporter}.
393 *
394 * @param out
395 * the {@link OutputStream} to append to
396 * @param value
397 * the object to encode (can be NULL, which will be encoded)
398 *
399 * @return TRUE if success, FALSE if not (the content of the
400 * {@link OutputStream} won't be changed in case of failure)
401 *
402 * @throws IOException
403 * in case of I/O error
404 */
405 static boolean encode(OutputStream out, Object value) throws IOException {
406 if (value == null) {
407 write(out, "NULL");
408 } else if (value.getClass().getSimpleName().endsWith("[]")) {
409 // Simple name does support [] suffix and do not return NULL for
410 // inner anonymous classes
411 customTypes.get("[]").encode(out, value);
412 } else if (customTypes.containsKey(value.getClass().getCanonicalName())) {
413 customTypes.get(value.getClass().getCanonicalName())//
414 .encode(out, value);
415 } else if (value instanceof String) {
416 encodeString(out, (String) value);
417 } else if (value instanceof Boolean) {
418 write(out, value);
419 } else if (value instanceof Byte) {
420 write(out, value);
421 write(out, "b");
422 } else if (value instanceof Character) {
423 encodeString(out, "" + value);
424 write(out, "c");
425 } else if (value instanceof Short) {
426 write(out, value);
427 write(out, "s");
428 } else if (value instanceof Integer) {
429 write(out, value);
430 } else if (value instanceof Long) {
431 write(out, value);
432 write(out, "L");
433 } else if (value instanceof Float) {
434 write(out, value);
435 write(out, "F");
436 } else if (value instanceof Double) {
437 write(out, value);
438 write(out, "d");
439 } else if (value instanceof Enum) {
440 String type = value.getClass().getCanonicalName();
441 write(out, type);
442 write(out, ".");
443 write(out, ((Enum<?>) value).name());
444 write(out, ";");
445 } else {
446 return false;
447 }
448
449 return true;
450 }
451
452 /**
453 * Decode the data into an equivalent supported source object.
454 * <p>
455 * A supported object in this context means an object we can directly
456 * encode, like an Integer or a String. Custom objects and arrays are also
457 * considered supported, but <b>compound objects are not supported here</b>.
458 * <p>
459 * For compound objects, you should use {@link Importer}.
460 *
461 * @param encodedValue
462 * the encoded data, cannot be NULL
463 *
464 * @return the object (can be NULL for NULL encoded values)
465 *
466 * @throws IOException
467 * if the content cannot be converted
468 */
469 static Object decode(String encodedValue) throws IOException {
470 try {
471 String cut = "";
472 if (encodedValue.length() > 1) {
473 cut = encodedValue.substring(0, encodedValue.length() - 1);
474 }
475
476 if (CustomSerializer.isCustom(encodedValue)) {
477 // custom:TYPE_NAME:"content is String-encoded"
478 String type = CustomSerializer.typeOf(encodedValue);
479 if (customTypes.containsKey(type)) {
480 // TODO: we should start with a stream
481 InputStream streamEncodedValue = new ByteArrayInputStream(
482 encodedValue.getBytes("UTF-8"));
483 try {
484 return customTypes.get(type).decode(streamEncodedValue);
485 } finally {
486 streamEncodedValue.close();
487 }
488 }
489 throw new IOException("Unknown custom type: " + type);
490 } else if (encodedValue.equals("NULL")
491 || encodedValue.equals("null")) {
492 return null;
493 } else if (encodedValue.endsWith("\"")) {
494 return decodeString(encodedValue);
495 } else if (encodedValue.equals("true")) {
496 return true;
497 } else if (encodedValue.equals("false")) {
498 return false;
499 } else if (encodedValue.endsWith("b")) {
500 return Byte.parseByte(cut);
501 } else if (encodedValue.endsWith("c")) {
502 return decodeString(cut).charAt(0);
503 } else if (encodedValue.endsWith("s")) {
504 return Short.parseShort(cut);
505 } else if (encodedValue.endsWith("L")) {
506 return Long.parseLong(cut);
507 } else if (encodedValue.endsWith("F")) {
508 return Float.parseFloat(cut);
509 } else if (encodedValue.endsWith("d")) {
510 return Double.parseDouble(cut);
511 } else if (encodedValue.endsWith(";")) {
512 return decodeEnum(encodedValue);
513 } else {
514 return Integer.parseInt(encodedValue);
515 }
516 } catch (Exception e) {
517 if (e instanceof IOException) {
518 throw (IOException) e;
519 }
520 throw new IOException(e.getMessage(), e);
521 }
522 }
523
524 /**
525 * Write the given {@link String} into the given {@link OutputStream} in
526 * UTF-8.
527 *
528 * @param out
529 * the {@link OutputStream}
530 * @param data
531 * the data to write, cannot be NULL
532 *
533 * @throws IOException
534 * in case of I/O error
535 */
536 static void write(OutputStream out, Object data) throws IOException {
537 try {
538 out.write(data.toString().getBytes("UTF-8"));
539 } catch (UnsupportedEncodingException e) {
540 // A conforming JVM is required to support UTF-8
541 e.printStackTrace();
542 }
543 }
544
545 /**
546 * Return the corresponding class or throw an {@link Exception} if it
547 * cannot.
548 *
549 * @param type
550 * the class name to look for
551 *
552 * @return the class (will never be NULL)
553 *
554 * @throws ClassNotFoundException
555 * if the class cannot be found
556 * @throws NoSuchMethodException
557 * if the class cannot be created (usually because it or its
558 * enclosing class doesn't have an empty constructor)
559 */
560 static private Class<?> getClass(String type)
561 throws ClassNotFoundException, NoSuchMethodException {
562 Class<?> clazz = null;
563 try {
564 clazz = Class.forName(type);
565 } catch (ClassNotFoundException e) {
566 int pos = type.length();
567 pos = type.lastIndexOf(".", pos);
568 if (pos >= 0) {
569 String parentType = type.substring(0, pos);
570 String nestedType = type.substring(pos + 1);
571 Class<?> javaParent = null;
572 try {
573 javaParent = getClass(parentType);
574 parentType = javaParent.getName();
575 clazz = Class.forName(parentType + "$" + nestedType);
576 } catch (Exception ee) {
577 }
578
579 if (javaParent == null) {
580 throw new NoSuchMethodException(
581 "Class not found: "
582 + type
583 + " (the enclosing class cannot be created: maybe it doesn't have an empty constructor?)");
584 }
585 }
586 }
587
588 if (clazz == null) {
589 throw new ClassNotFoundException("Class not found: " + type);
590 }
591
592 return clazz;
593 }
594
595 @SuppressWarnings({ "unchecked", "rawtypes" })
596 static private Enum<?> decodeEnum(String escaped) {
597 // escaped: be.xxx.EnumType.VALUE;
598 int pos = escaped.lastIndexOf(".");
599 String type = escaped.substring(0, pos);
600 String name = escaped.substring(pos + 1, escaped.length() - 1);
601
602 try {
603 return Enum.valueOf((Class<Enum>) getClass(type), name);
604 } catch (Exception e) {
605 throw new UnknownFormatConversionException("Unknown enum: <" + type
606 + "> " + name);
607 }
608 }
609
610 // aa bb -> "aa\tbb"
611 static void encodeString(OutputStream out, String raw) throws IOException {
612 // TODO: not. efficient.
613 out.write('\"');
614 // TODO !! utf-8 required
615 for (char car : raw.toCharArray()) {
616 encodeString(out, car);
617 }
618 out.write('\"');
619 }
620
621 // aa bb -> "aa\tbb"
622 static void encodeString(OutputStream out, InputStream raw)
623 throws IOException {
624 out.write('\"');
625 byte buffer[] = new byte[4096];
626 for (int len = 0; (len = raw.read(buffer)) > 0;) {
627 for (int i = 0; i < len; i++) {
628 // TODO: not 100% correct, look up howto for UTF-8
629 encodeString(out, (char) buffer[i]);
630 }
631 }
632 out.write('\"');
633 }
634
635 // for encode string, NOT to encode a char by itself!
636 static void encodeString(OutputStream out, char raw) throws IOException {
637 switch (raw) {
638 case '\\':
639 out.write('\\');
640 out.write('\\');
641 break;
642 case '\r':
643 out.write('\\');
644 out.write('r');
645 break;
646 case '\n':
647 out.write('\\');
648 out.write('n');
649 break;
650 case '"':
651 out.write('\\');
652 out.write('\"');
653 break;
654 default:
655 out.write(raw);
656 break;
657 }
658 }
659
660 // "aa\tbb" -> aa bb
661 static String decodeString(String escaped) {
662 StringBuilder builder = new StringBuilder();
663
664 boolean escaping = false;
665 for (char car : escaped.toCharArray()) {
666 if (!escaping) {
667 if (car == '\\') {
668 escaping = true;
669 } else {
670 builder.append(car);
671 }
672 } else {
673 switch (car) {
674 case '\\':
675 builder.append('\\');
676 break;
677 case 'r':
678 builder.append('\r');
679 break;
680 case 'n':
681 builder.append('\n');
682 break;
683 case '"':
684 builder.append('"');
685 break;
686 }
687 escaping = false;
688 }
689 }
690
691 return builder.substring(1, builder.length() - 1);
692 }
693 }