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