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