Serial: enums and BufferedImages
[nikiroo-utils.git] / src / be / nikiroo / utils / serial / SerialUtils.java
CommitLineData
db31c358
NR
1package be.nikiroo.utils.serial;
2
e570f7eb
NR
3import java.awt.image.BufferedImage;
4import java.io.IOException;
db31c358 5import java.io.NotSerializableException;
ce0974c4 6import java.lang.reflect.Array;
8c8da42a 7import java.lang.reflect.Constructor;
db31c358 8import java.lang.reflect.Field;
e570f7eb 9import java.lang.reflect.Modifier;
db31c358
NR
10import java.util.HashMap;
11import java.util.Map;
ce0974c4 12import java.util.UnknownFormatConversionException;
db31c358 13
e570f7eb
NR
14import be.nikiroo.utils.StringUtils;
15
db31c358 16/**
8c8da42a 17 * Small class to help with serialisation.
db31c358
NR
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 */
aad14586 24public class SerialUtils {
db31c358
NR
25 private static Map<String, CustomSerializer> customTypes;
26
27 static {
28 customTypes = new HashMap<String, CustomSerializer>();
ce0974c4
NR
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 });
e570f7eb
NR
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 StringUtils.fromImage((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 StringUtils.toImage(content);
106 } catch (IOException e) {
107 throw new UnknownFormatConversionException(e.getMessage());
108 }
109 }
110 });
db31c358 111 }
8c8da42a 112
aad14586
NR
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 *
8c8da42a
NR
121 * @throws ClassNotFoundException
122 * if the class cannot be found
123 * @throws NoSuchMethodException
124 * if the given class is not compatible with this code
aad14586 125 */
8c8da42a
NR
126 public static Object createObject(String type)
127 throws ClassNotFoundException, NoSuchMethodException {
aad14586
NR
128
129 try {
130 Class<?> clazz = getClass(type);
aad14586
NR
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);
8c8da42a
NR
147 } catch (ClassNotFoundException e) {
148 throw e;
aad14586 149 } catch (NoSuchMethodException e) {
8c8da42a
NR
150 throw e;
151 } catch (Exception e) {
152 throw new NoSuchMethodException("Cannot instantiate: " + type);
aad14586 153 }
aad14586 154 }
db31c358 155
8c8da42a
NR
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 */
db31c358
NR
163 static public void addCustomSerializer(CustomSerializer serializer) {
164 customTypes.put(serializer.getType(), serializer);
165 }
166
8c8da42a
NR
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 */
db31c358
NR
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
ce0974c4
NR
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);
db31c358 219
e570f7eb
NR
220 if (field.getName().startsWith("this$")
221 || field.isSynthetic()
222 || (field.getModifiers() & Modifier.STATIC) == Modifier.STATIC) {
ce0974c4 223 // Do not keep this links of nested classes
e570f7eb
NR
224 // Do not keep synthetic fields
225 // Do not keep final fields
ce0974c4
NR
226 continue;
227 }
db31c358 228
ce0974c4
NR
229 builder.append("\n");
230 builder.append(field.getName());
231 builder.append(":");
232 Object value;
db31c358 233
ce0974c4 234 value = field.get(o);
db31c358 235
ce0974c4
NR
236 if (!encode(builder, value)) {
237 builder.append("\n");
238 append(builder, value, map);
239 }
db31c358 240 }
ce0974c4
NR
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)
db31c358 247 }
db31c358
NR
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");
ce0974c4
NR
256 } else if (value.getClass().getCanonicalName().endsWith("[]")) {
257 customTypes.get("[]").encode(builder, value);
db31c358
NR
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) {
ce0974c4 268 encodeString(builder, "" + value);
db31c358
NR
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');
e570f7eb
NR
280 } else if (value instanceof Enum) {
281 String type = value.getClass().getCanonicalName();
282 builder.append(type).append(".").append(((Enum<?>) value).name())
283 .append(";");
db31c358
NR
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 } else {
ce0974c4 303 throw new UnknownFormatConversionException(
db31c358
NR
304 "Unknown custom type: " + type);
305 }
306 } else if (encodedValue.equals("NULL") || encodedValue.equals("null")) {
307 return null;
308 } else if (encodedValue.endsWith("\"")) {
309 return decodeString(encodedValue);
310 } else if (encodedValue.equals("true")) {
311 return true;
312 } else if (encodedValue.equals("false")) {
313 return false;
314 } else if (encodedValue.endsWith("b")) {
315 return Byte.parseByte(cut);
316 } else if (encodedValue.endsWith("c")) {
317 return decodeString(cut).charAt(0);
318 } else if (encodedValue.endsWith("s")) {
319 return Short.parseShort(cut);
320 } else if (encodedValue.endsWith("L")) {
321 return Long.parseLong(cut);
322 } else if (encodedValue.endsWith("F")) {
323 return Float.parseFloat(cut);
324 } else if (encodedValue.endsWith("d")) {
325 return Double.parseDouble(cut);
e570f7eb
NR
326 } else if (encodedValue.endsWith(";")) {
327 return decodeEnum(encodedValue);
db31c358
NR
328 } else {
329 return Integer.parseInt(encodedValue);
330 }
331 }
8c8da42a
NR
332
333 /**
334 * Return the corresponding class or throw an {@link Exception} if it
335 * cannot.
336 *
337 * @param type
338 * the class name to look for
339 *
340 * @return the class (will never be NULL)
341 *
342 * @throws ClassNotFoundException
343 * if the class cannot be found
344 * @throws NoSuchMethodException
345 * if the class cannot be created (usually because it or its
346 * enclosing class doesn't have an empty constructor)
347 */
348 static private Class<?> getClass(String type)
349 throws ClassNotFoundException, NoSuchMethodException {
aad14586
NR
350 Class<?> clazz = null;
351 try {
352 clazz = Class.forName(type);
353 } catch (ClassNotFoundException e) {
354 int pos = type.length();
355 pos = type.lastIndexOf(".", pos);
356 if (pos >= 0) {
357 String parentType = type.substring(0, pos);
358 String nestedType = type.substring(pos + 1);
359 Class<?> javaParent = null;
360 try {
361 javaParent = getClass(parentType);
362 parentType = javaParent.getName();
363 clazz = Class.forName(parentType + "$" + nestedType);
364 } catch (Exception ee) {
365 }
db31c358 366
aad14586
NR
367 if (javaParent == null) {
368 throw new NoSuchMethodException(
369 "Class not found: "
370 + type
371 + " (the enclosing class cannot be created: maybe it doesn't have an empty constructor?)");
372 }
373 }
374 }
375
8c8da42a
NR
376 if (clazz == null) {
377 throw new ClassNotFoundException("Class not found: " + type);
378 }
379
aad14586
NR
380 return clazz;
381 }
8c8da42a 382
e570f7eb
NR
383 @SuppressWarnings({ "unchecked", "rawtypes" })
384 private static Enum<?> decodeEnum(String escaped) {
385 // escaped: be.xxx.EnumType.VALUE;
386 int pos = escaped.lastIndexOf(".");
387 String type = escaped.substring(0, pos);
388 String name = escaped.substring(pos + 1, escaped.length() - 1);
389
390 try {
391 return Enum.valueOf((Class<Enum>) getClass(type), name);
392 } catch (Exception e) {
393 e.printStackTrace();
394 throw new UnknownFormatConversionException("Unknown enum: <" + type
395 + "> " + name);
396 }
397 }
398
db31c358
NR
399 // aa bb -> "aa\tbb"
400 private static void encodeString(StringBuilder builder, String raw) {
401 builder.append('\"');
402 for (char car : raw.toCharArray()) {
403 switch (car) {
404 case '\\':
405 builder.append("\\\\");
406 break;
407 case '\r':
408 builder.append("\\r");
409 break;
410 case '\n':
411 builder.append("\\n");
412 break;
413 case '"':
414 builder.append("\\\"");
415 break;
416 default:
417 builder.append(car);
418 break;
419 }
420 }
421 builder.append('\"');
422 }
423
424 // "aa\tbb" -> aa bb
425 private static String decodeString(String escaped) {
426 StringBuilder builder = new StringBuilder();
427
428 boolean escaping = false;
429 for (char car : escaped.toCharArray()) {
430 if (!escaping) {
431 if (car == '\\') {
432 escaping = true;
433 } else {
434 builder.append(car);
435 }
436 } else {
437 switch (car) {
438 case '\\':
439 builder.append('\\');
440 break;
441 case 'r':
442 builder.append('\r');
443 break;
444 case 'n':
445 builder.append('\n');
446 break;
447 case '"':
448 builder.append('"');
449 break;
450 }
451 escaping = false;
452 }
453 }
454
455 return builder.substring(1, builder.length() - 1).toString();
456 }
457}