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