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