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