VERSION + fix custom image
[nikiroo-utils.git] / src / be / nikiroo / utils / serial / SerialUtils.java
CommitLineData
db31c358
NR
1package be.nikiroo.utils.serial;
2
564bbbdb 3import java.io.ByteArrayInputStream;
e570f7eb 4import java.io.IOException;
68532958 5import java.io.InputStream;
db31c358 6import java.io.NotSerializableException;
68532958
NR
7import java.io.OutputStream;
8import java.io.UnsupportedEncodingException;
ce0974c4 9import java.lang.reflect.Array;
8c8da42a 10import java.lang.reflect.Constructor;
db31c358 11import java.lang.reflect.Field;
e570f7eb 12import java.lang.reflect.Modifier;
f4053377 13import java.net.URL;
72648e75 14import java.util.ArrayList;
db31c358 15import java.util.HashMap;
72648e75 16import java.util.List;
db31c358 17import java.util.Map;
ce0974c4 18import java.util.UnknownFormatConversionException;
db31c358 19
dc41a952 20import be.nikiroo.utils.IOUtils;
80500544 21import be.nikiroo.utils.Image;
3b4319db 22import be.nikiroo.utils.StringUtils;
52fb9a56
NR
23import be.nikiroo.utils.streams.NextableInputStream;
24import be.nikiroo.utils.streams.NextableInputStreamStep;
e570f7eb 25
db31c358 26/**
8c8da42a 27 * Small class to help with serialisation.
db31c358
NR
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.
5bc55b51
NR
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>
f4053377 50 * <li>URL</li>
5bc55b51 51 * </ul>
db31c358
NR
52 *
53 * @author niki
54 */
aad14586 55public class SerialUtils {
db31c358
NR
56 private static Map<String, CustomSerializer> customTypes;
57
58 static {
59 customTypes = new HashMap<String, CustomSerializer>();
ce0974c4
NR
60
61 // Array types:
62 customTypes.put("[]", new CustomSerializer() {
63 @Override
143a64f4
NR
64 protected void toStream(OutputStream out, Object value)
65 throws IOException {
3b4319db
NR
66
67 // TODO: we use \n to separate, and b64 to un-\n
68 // -- but we could use \\n ?
ce0974c4
NR
69 String type = value.getClass().getCanonicalName();
70 type = type.substring(0, type.length() - 2); // remove the []
71
dc41a952 72 write(out, type);
ce0974c4
NR
73 try {
74 for (int i = 0; true; i++) {
75 Object item = Array.get(value, i);
23cf894d 76
ce0974c4 77 // encode it normally if direct value
23cf894d 78 write(out, "\r");
143a64f4 79 if (!SerialUtils.encode(out, item)) {
ce0974c4 80 try {
4d319565
NR
81 // TODO: bad escaping?
82 write(out, "B64:");
83 OutputStream bout = StringUtils.base64(out,
84 false, false);
85 new Exporter(bout).append(item);
ce0974c4
NR
86 } catch (NotSerializableException e) {
87 throw new UnknownFormatConversionException(e
88 .getMessage());
89 }
90 }
ce0974c4
NR
91 }
92 } catch (ArrayIndexOutOfBoundsException e) {
93 // Done.
94 }
ce0974c4
NR
95 }
96
143a64f4
NR
97 @Override
98 protected Object fromStream(InputStream in) throws IOException {
dc41a952 99 NextableInputStream stream = new NextableInputStream(in,
4d319565 100 new NextableInputStreamStep('\r'));
ce0974c4
NR
101
102 try {
dc41a952
NR
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
ce0974c4 112 Object array = Array.newInstance(
dc41a952
NR
113 SerialUtils.getClass(type), list.size());
114 for (int i = 0; i < list.size(); i++) {
115 Array.set(array, i, list.get(i));
ce0974c4
NR
116 }
117
118 return array;
119 } catch (Exception e) {
452f38c8
NR
120 if (e instanceof IOException) {
121 throw (IOException) e;
122 }
123 throw new IOException(e.getMessage());
ce0974c4
NR
124 }
125 }
dc41a952
NR
126
127 @Override
128 protected String getType() {
129 return "[]";
130 }
ce0974c4 131 });
e570f7eb 132
f4053377
NR
133 // URL:
134 customTypes.put("java.net.URL", new CustomSerializer() {
135 @Override
dc41a952
NR
136 protected void toStream(OutputStream out, Object value)
137 throws IOException {
138 String val = "";
f4053377 139 if (value != null) {
dc41a952 140 val = ((URL) value).toString();
f4053377 141 }
dc41a952
NR
142
143 out.write(val.getBytes("UTF-8"));
f4053377
NR
144 }
145
146 @Override
dc41a952
NR
147 protected Object fromStream(InputStream in) throws IOException {
148 String val = IOUtils.readSmallStream(in);
149 if (!val.isEmpty()) {
150 return new URL(val);
f4053377 151 }
dc41a952 152
f4053377
NR
153 return null;
154 }
155
156 @Override
157 protected String getType() {
158 return "java.net.URL";
159 }
160 });
161
e570f7eb 162 // Images (this is currently the only supported image type by default)
80500544 163 customTypes.put("be.nikiroo.utils.Image", new CustomSerializer() {
e570f7eb 164 @Override
dc41a952
NR
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 {
01e45aa4
NR
177 encoded.flush();
178 // Cannot close!
dc41a952 179 }
e570f7eb
NR
180 }
181
182 @Override
183 protected String getType() {
80500544 184 return "be.nikiroo.utils.Image";
e570f7eb
NR
185 }
186
187 @Override
dc41a952 188 protected Object fromStream(InputStream in) throws IOException {
e570f7eb 189 try {
01e45aa4
NR
190 // Cannot close it!
191 InputStream decoded = StringUtils.unbase64(in, false);
192 return new Image(decoded);
e570f7eb
NR
193 } catch (IOException e) {
194 throw new UnknownFormatConversionException(e.getMessage());
195 }
196 }
197 });
db31c358 198 }
8c8da42a 199
aad14586
NR
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 *
8c8da42a
NR
208 * @throws ClassNotFoundException
209 * if the class cannot be found
210 * @throws NoSuchMethodException
211 * if the given class is not compatible with this code
aad14586 212 */
8c8da42a
NR
213 public static Object createObject(String type)
214 throws ClassNotFoundException, NoSuchMethodException {
aad14586 215
72648e75 216 String desc = null;
aad14586
NR
217 try {
218 Class<?> clazz = getClass(type);
aad14586 219 String className = clazz.getName();
72648e75
NR
220 List<Object> args = new ArrayList<Object>();
221 List<Class<?>> classes = new ArrayList<Class<?>>();
aad14586
NR
222 Constructor<?> ctor = null;
223 if (className.contains("$")) {
72648e75
NR
224 for (String parentName = className.substring(0,
225 className.lastIndexOf('$'));; parentName = parentName
949445ee 226 .substring(0, parentName.lastIndexOf('$'))) {
72648e75
NR
227 Object parent = createObject(parentName);
228 args.add(parent);
229 classes.add(parent.getClass());
949445ee 230
72648e75
NR
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
949445ee 242 .getSuperclass()) {
72648e75
NR
243 if (!desc.isEmpty()) {
244 desc += " [:";
245 end += "]";
246 }
247 desc += parent;
248 }
249 desc += end;
250 //
251
cd26ee07
NR
252 try {
253 ctor = clazz.getDeclaredConstructor(classes
254 .toArray(new Class[] {}));
255 } catch (NoSuchMethodException nsme) {
564bbbdb 256 // TODO: it seems we do not always need a parameter for each
cd26ee07
NR
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 }
72648e75 264 desc = null;
aad14586 265 } else {
aad14586
NR
266 ctor = clazz.getDeclaredConstructor();
267 }
268
269 ctor.setAccessible(true);
72648e75 270 return ctor.newInstance(args.toArray());
8c8da42a
NR
271 } catch (ClassNotFoundException e) {
272 throw e;
aad14586 273 } catch (NoSuchMethodException e) {
72648e75 274 if (desc != null) {
949445ee
NR
275 throw new NoSuchMethodException("Empty constructor not found: "
276 + desc);
72648e75 277 }
8c8da42a
NR
278 throw e;
279 } catch (Exception e) {
280 throw new NoSuchMethodException("Cannot instantiate: " + type);
aad14586 281 }
aad14586 282 }
db31c358 283
8c8da42a
NR
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 */
db31c358
NR
291 static public void addCustomSerializer(CustomSerializer serializer) {
292 customTypes.put(serializer.getType(), serializer);
293 }
294
8c8da42a 295 /**
68532958 296 * Serialise the given object into this {@link OutputStream}.
8c8da42a
NR
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 *
68532958
NR
302 * @param out
303 * the output {@link OutputStream} to serialise to
8c8da42a
NR
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)
68532958
NR
315 * @throws IOException
316 * in case of I/O errors
8c8da42a 317 */
68532958
NR
318 static void append(OutputStream out, Object o, Map<Integer, Object> map)
319 throws NotSerializableException, IOException {
db31c358
NR
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) {
72648e75
NR
330 // Anonymous inner classes support
331 type = o.getClass().getName();
db31c358
NR
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
68532958
NR
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
ce0974c4
NR
348 try {
349 for (Field field : fields) {
350 field.setAccessible(true);
db31c358 351
e570f7eb
NR
352 if (field.getName().startsWith("this$")
353 || field.isSynthetic()
354 || (field.getModifiers() & Modifier.STATIC) == Modifier.STATIC) {
ce0974c4 355 // Do not keep this links of nested classes
e570f7eb
NR
356 // Do not keep synthetic fields
357 // Do not keep final fields
ce0974c4
NR
358 continue;
359 }
db31c358 360
68532958
NR
361 write(out, "\n");
362 write(out, field.getName());
363 write(out, ":");
db31c358 364
68532958 365 Object value = field.get(o);
db31c358 366
68532958
NR
367 if (!encode(out, value)) {
368 write(out, "\n");
369 append(out, value, map);
ce0974c4 370 }
db31c358 371 }
ce0974c4
NR
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)
db31c358 378 }
95c5b4f9
NR
379
380 write(out, "\n}");
db31c358 381 }
db31c358
NR
382 }
383
5bc55b51 384 /**
68532958
NR
385 * Encode the object into the given {@link OutputStream} if possible and if
386 * supported.
949445ee
NR
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}.
5bc55b51 393 *
68532958
NR
394 * @param out
395 * the {@link OutputStream} to append to
5bc55b51
NR
396 * @param value
397 * the object to encode (can be NULL, which will be encoded)
398 *
68532958
NR
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
5bc55b51 404 */
68532958 405 static boolean encode(OutputStream out, Object value) throws IOException {
db31c358 406 if (value == null) {
68532958 407 write(out, "NULL");
72648e75
NR
408 } else if (value.getClass().getSimpleName().endsWith("[]")) {
409 // Simple name does support [] suffix and do not return NULL for
410 // inner anonymous classes
3b4319db 411 customTypes.get("[]").encode(out, value);
db31c358 412 } else if (customTypes.containsKey(value.getClass().getCanonicalName())) {
3b4319db 413 customTypes.get(value.getClass().getCanonicalName())//
68532958 414 .encode(out, value);
db31c358 415 } else if (value instanceof String) {
68532958 416 encodeString(out, (String) value);
db31c358 417 } else if (value instanceof Boolean) {
68532958 418 write(out, value);
db31c358 419 } else if (value instanceof Byte) {
68532958
NR
420 write(out, value);
421 write(out, "b");
db31c358 422 } else if (value instanceof Character) {
68532958
NR
423 encodeString(out, "" + value);
424 write(out, "c");
db31c358 425 } else if (value instanceof Short) {
68532958
NR
426 write(out, value);
427 write(out, "s");
db31c358 428 } else if (value instanceof Integer) {
68532958 429 write(out, value);
db31c358 430 } else if (value instanceof Long) {
68532958
NR
431 write(out, value);
432 write(out, "L");
db31c358 433 } else if (value instanceof Float) {
68532958
NR
434 write(out, value);
435 write(out, "F");
db31c358 436 } else if (value instanceof Double) {
68532958
NR
437 write(out, value);
438 write(out, "d");
e570f7eb
NR
439 } else if (value instanceof Enum) {
440 String type = value.getClass().getCanonicalName();
68532958
NR
441 write(out, type);
442 write(out, ".");
443 write(out, ((Enum<?>) value).name());
444 write(out, ";");
db31c358
NR
445 } else {
446 return false;
447 }
448
449 return true;
450 }
451
5bc55b51 452 /**
949445ee
NR
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}.
5bc55b51
NR
460 *
461 * @param encodedValue
462 * the encoded data, cannot be NULL
463 *
464 * @return the object (can be NULL for NULL encoded values)
465 *
452f38c8 466 * @throws IOException
5bc55b51
NR
467 * if the content cannot be converted
468 */
452f38c8 469 static Object decode(String encodedValue) throws IOException {
5bc55b51 470 try {
452f38c8
NR
471 String cut = "";
472 if (encodedValue.length() > 1) {
473 cut = encodedValue.substring(0, encodedValue.length() - 1);
474 }
475
5bc55b51
NR
476 if (CustomSerializer.isCustom(encodedValue)) {
477 // custom:TYPE_NAME:"content is String-encoded"
478 String type = CustomSerializer.typeOf(encodedValue);
479 if (customTypes.containsKey(type)) {
564bbbdb
NR
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 }
5bc55b51 488 }
452f38c8 489 throw new IOException("Unknown custom type: " + type);
5bc55b51
NR
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);
db31c358 515 }
5bc55b51 516 } catch (Exception e) {
452f38c8
NR
517 if (e instanceof IOException) {
518 throw (IOException) e;
5bc55b51 519 }
23742ced 520 throw new IOException(e.getMessage(), e);
db31c358
NR
521 }
522 }
8c8da42a 523
68532958
NR
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
8c8da42a
NR
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 {
aad14586
NR
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 }
db31c358 578
aad14586
NR
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
8c8da42a
NR
588 if (clazz == null) {
589 throw new ClassNotFoundException("Class not found: " + type);
590 }
591
aad14586
NR
592 return clazz;
593 }
8c8da42a 594
e570f7eb 595 @SuppressWarnings({ "unchecked", "rawtypes" })
68532958 596 static private Enum<?> decodeEnum(String escaped) {
e570f7eb
NR
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) {
e570f7eb
NR
605 throw new UnknownFormatConversionException("Unknown enum: <" + type
606 + "> " + name);
607 }
608 }
609
db31c358 610 // aa bb -> "aa\tbb"
68532958 611 static void encodeString(OutputStream out, String raw) throws IOException {
3b4319db 612 // TODO: not. efficient.
68532958
NR
613 out.write('\"');
614 // TODO !! utf-8 required
db31c358 615 for (char car : raw.toCharArray()) {
68532958
NR
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('\"');
564bbbdb 625 byte buffer[] = new byte[4096];
68532958
NR
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]);
db31c358
NR
630 }
631 }
68532958
NR
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 }
db31c358
NR
658 }
659
660 // "aa\tbb" -> aa bb
68532958 661 static String decodeString(String escaped) {
db31c358
NR
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
0988831f 691 return builder.substring(1, builder.length() - 1);
db31c358
NR
692 }
693}