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