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