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