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