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