1 package be
.nikiroo
.utils
.serial
;
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
;
11 import java
.util
.HashMap
;
13 import java
.util
.UnknownFormatConversionException
;
15 import be
.nikiroo
.utils
.ImageUtils
;
18 * Small class to help with serialisation.
20 * Note that we do not support inner classes (but we do support nested classes)
21 * and all objects require an empty constructor to be deserialised.
23 * It is possible to add support to custom types (both the encoder and the
24 * decoder will require the custom classes) -- see {@link CustomSerializer}.
26 * Default supported types are:
28 * <li>NULL (as a null value)</li>
38 * <li>Enum (any enum whose name and value is known by the caller)</li>
39 * <li>java.awt.image.BufferedImage (as a {@link CustomSerializer})</li>
40 * <li>An array of the above (as a {@link CustomSerializer})</li>
46 public class SerialUtils
{
47 private static Map
<String
, CustomSerializer
> customTypes
;
50 customTypes
= new HashMap
<String
, CustomSerializer
>();
53 customTypes
.put("[]", new CustomSerializer() {
55 protected String
toString(Object value
) {
56 String type
= value
.getClass().getCanonicalName();
57 type
= type
.substring(0, type
.length() - 2); // remove the []
59 StringBuilder builder
= new StringBuilder();
60 builder
.append(type
).append("\n");
62 for (int i
= 0; true; i
++) {
63 Object item
= Array
.get(value
, i
);
64 // encode it normally if direct value
65 if (!SerialUtils
.encode(builder
, item
)) {
68 builder
.append(new Exporter().append(item
)
70 } catch (NotSerializableException e
) {
71 throw new UnknownFormatConversionException(e
77 } catch (ArrayIndexOutOfBoundsException e
) {
81 return builder
.toString();
85 protected String
getType() {
90 protected Object
fromString(String content
) throws IOException
{
91 String
[] tab
= content
.split("\n");
94 Object array
= Array
.newInstance(
95 SerialUtils
.getClass(tab
[0]), tab
.length
- 1);
96 for (int i
= 1; i
< tab
.length
; i
++) {
97 Object value
= new Importer().read(tab
[i
]).getValue();
98 Array
.set(array
, i
- 1, value
);
102 } catch (Exception e
) {
103 if (e
instanceof IOException
) {
104 throw (IOException
) e
;
106 throw new IOException(e
.getMessage());
112 customTypes
.put("java.net.URL", new CustomSerializer() {
114 protected String
toString(Object value
) {
116 return ((URL
) value
).toString();
122 protected Object
fromString(String content
) throws IOException
{
123 if (content
!= null) {
124 return new URL(content
);
130 protected String
getType() {
131 return "java.net.URL";
135 // Images (this is currently the only supported image type by default)
136 customTypes
.put("java.awt.image.BufferedImage", new CustomSerializer() {
138 protected String
toString(Object value
) {
140 return ImageUtils
.toBase64((BufferedImage
) value
);
141 } catch (IOException e
) {
142 throw new UnknownFormatConversionException(e
.getMessage());
147 protected String
getType() {
148 return "java.awt.image.BufferedImage";
152 protected Object
fromString(String content
) {
154 return ImageUtils
.fromBase64(content
);
155 } catch (IOException e
) {
156 throw new UnknownFormatConversionException(e
.getMessage());
163 * Create an empty object of the given type.
166 * the object type (its class name)
168 * @return the new object
170 * @throws ClassNotFoundException
171 * if the class cannot be found
172 * @throws NoSuchMethodException
173 * if the given class is not compatible with this code
175 public static Object
createObject(String type
)
176 throws ClassNotFoundException
, NoSuchMethodException
{
179 Class
<?
> clazz
= getClass(type
);
180 String className
= clazz
.getName();
181 Object
[] args
= null;
182 Constructor
<?
> ctor
= null;
183 if (className
.contains("$")) {
184 Object javaParent
= createObject(className
.substring(0,
185 className
.lastIndexOf('$')));
186 args
= new Object
[] { javaParent
};
187 ctor
= clazz
.getDeclaredConstructor(new Class
[] { javaParent
190 args
= new Object
[] {};
191 ctor
= clazz
.getDeclaredConstructor();
194 ctor
.setAccessible(true);
195 return ctor
.newInstance(args
);
196 } catch (ClassNotFoundException e
) {
198 } catch (NoSuchMethodException e
) {
200 } catch (Exception e
) {
201 throw new NoSuchMethodException("Cannot instantiate: " + type
);
206 * Insert a custom serialiser that will take precedence over the default one
207 * or the target class.
210 * the custom serialiser
212 static public void addCustomSerializer(CustomSerializer serializer
) {
213 customTypes
.put(serializer
.getType(), serializer
);
217 * Serialise the given object into this {@link StringBuilder}.
219 * <b>Important: </b>If the operation fails (with a
220 * {@link NotSerializableException}), the {@link StringBuilder} will be
221 * corrupted (will contain bad, most probably not importable data).
224 * the output {@link StringBuilder} to serialise to
226 * the object to serialise
228 * the map of already serialised objects (if the given object or
229 * one of its descendant is already present in it, only an ID
230 * will be serialised)
232 * @throws NotSerializableException
233 * if the object cannot be serialised (in this case, the
234 * {@link StringBuilder} can contain bad, most probably not
237 static void append(StringBuilder builder
, Object o
, Map
<Integer
, Object
> map
)
238 throws NotSerializableException
{
240 Field
[] fields
= new Field
[] {};
245 int hash
= System
.identityHashCode(o
);
246 fields
= o
.getClass().getDeclaredFields();
247 type
= o
.getClass().getCanonicalName();
249 throw new NotSerializableException(
251 "Cannot find the class for this object: %s (it could be an inner class, which is not supported)",
254 id
= Integer
.toString(hash
);
255 if (map
.containsKey(hash
)) {
256 fields
= new Field
[] {};
262 builder
.append("{\nREF ").append(type
).append("@").append(id
)
264 if (!encode(builder
, o
)) { // check if direct value
266 for (Field field
: fields
) {
267 field
.setAccessible(true);
269 if (field
.getName().startsWith("this$")
270 || field
.isSynthetic()
271 || (field
.getModifiers() & Modifier
.STATIC
) == Modifier
.STATIC
) {
272 // Do not keep this links of nested classes
273 // Do not keep synthetic fields
274 // Do not keep final fields
278 builder
.append("\n");
279 builder
.append(field
.getName());
283 value
= field
.get(o
);
285 if (!encode(builder
, value
)) {
286 builder
.append("\n");
287 append(builder
, value
, map
);
290 } catch (IllegalArgumentException e
) {
291 e
.printStackTrace(); // should not happen (see
293 } catch (IllegalAccessException e
) {
294 e
.printStackTrace(); // should not happen (see
298 builder
.append("\n}");
302 * Encode the object into the given builder if possible (if supported).
305 * the builder to append to
307 * the object to encode (can be NULL, which will be encoded)
309 * @return TRUE if success, FALSE if not (the content of the builder won't
310 * be changed in case of failure)
312 static boolean encode(StringBuilder builder
, Object value
) {
314 builder
.append("NULL");
315 } else if (value
.getClass().getCanonicalName().endsWith("[]")) {
316 return customTypes
.get("[]").encode(builder
, value
);
317 } else if (customTypes
.containsKey(value
.getClass().getCanonicalName())) {
318 return customTypes
.get(value
.getClass().getCanonicalName())//
319 .encode(builder
, value
);
320 } else if (value
instanceof String
) {
321 encodeString(builder
, (String
) value
);
322 } else if (value
instanceof Boolean
) {
323 builder
.append(value
);
324 } else if (value
instanceof Byte
) {
325 builder
.append(value
).append('b');
326 } else if (value
instanceof Character
) {
327 encodeString(builder
, "" + value
);
329 } else if (value
instanceof Short
) {
330 builder
.append(value
).append('s');
331 } else if (value
instanceof Integer
) {
332 builder
.append(value
);
333 } else if (value
instanceof Long
) {
334 builder
.append(value
).append('L');
335 } else if (value
instanceof Float
) {
336 builder
.append(value
).append('F');
337 } else if (value
instanceof Double
) {
338 builder
.append(value
).append('d');
339 } else if (value
instanceof Enum
) {
340 String type
= value
.getClass().getCanonicalName();
341 builder
.append(type
).append(".").append(((Enum
<?
>) value
).name())
351 * Decode the data into an equivalent source object.
353 * @param encodedValue
354 * the encoded data, cannot be NULL
356 * @return the object (can be NULL for NULL encoded values)
358 * @throws IOException
359 * if the content cannot be converted
361 static Object
decode(String encodedValue
) throws IOException
{
364 if (encodedValue
.length() > 1) {
365 cut
= encodedValue
.substring(0, encodedValue
.length() - 1);
368 if (CustomSerializer
.isCustom(encodedValue
)) {
369 // custom:TYPE_NAME:"content is String-encoded"
370 String type
= CustomSerializer
.typeOf(encodedValue
);
371 if (customTypes
.containsKey(type
)) {
372 return customTypes
.get(type
).decode(encodedValue
);
374 throw new IOException("Unknown custom type: " + type
);
375 } else if (encodedValue
.equals("NULL")
376 || encodedValue
.equals("null")) {
378 } else if (encodedValue
.endsWith("\"")) {
379 return decodeString(encodedValue
);
380 } else if (encodedValue
.equals("true")) {
382 } else if (encodedValue
.equals("false")) {
384 } else if (encodedValue
.endsWith("b")) {
385 return Byte
.parseByte(cut
);
386 } else if (encodedValue
.endsWith("c")) {
387 return decodeString(cut
).charAt(0);
388 } else if (encodedValue
.endsWith("s")) {
389 return Short
.parseShort(cut
);
390 } else if (encodedValue
.endsWith("L")) {
391 return Long
.parseLong(cut
);
392 } else if (encodedValue
.endsWith("F")) {
393 return Float
.parseFloat(cut
);
394 } else if (encodedValue
.endsWith("d")) {
395 return Double
.parseDouble(cut
);
396 } else if (encodedValue
.endsWith(";")) {
397 return decodeEnum(encodedValue
);
399 return Integer
.parseInt(encodedValue
);
401 } catch (Exception e
) {
402 if (e
instanceof IOException
) {
403 throw (IOException
) e
;
405 throw new IOException(e
.getMessage());
410 * Return the corresponding class or throw an {@link Exception} if it
414 * the class name to look for
416 * @return the class (will never be NULL)
418 * @throws ClassNotFoundException
419 * if the class cannot be found
420 * @throws NoSuchMethodException
421 * if the class cannot be created (usually because it or its
422 * enclosing class doesn't have an empty constructor)
424 static private Class
<?
> getClass(String type
)
425 throws ClassNotFoundException
, NoSuchMethodException
{
426 Class
<?
> clazz
= null;
428 clazz
= Class
.forName(type
);
429 } catch (ClassNotFoundException e
) {
430 int pos
= type
.length();
431 pos
= type
.lastIndexOf(".", pos
);
433 String parentType
= type
.substring(0, pos
);
434 String nestedType
= type
.substring(pos
+ 1);
435 Class
<?
> javaParent
= null;
437 javaParent
= getClass(parentType
);
438 parentType
= javaParent
.getName();
439 clazz
= Class
.forName(parentType
+ "$" + nestedType
);
440 } catch (Exception ee
) {
443 if (javaParent
== null) {
444 throw new NoSuchMethodException(
447 + " (the enclosing class cannot be created: maybe it doesn't have an empty constructor?)");
453 throw new ClassNotFoundException("Class not found: " + type
);
459 @SuppressWarnings({ "unchecked", "rawtypes" })
460 private static Enum
<?
> decodeEnum(String escaped
) {
461 // escaped: be.xxx.EnumType.VALUE;
462 int pos
= escaped
.lastIndexOf(".");
463 String type
= escaped
.substring(0, pos
);
464 String name
= escaped
.substring(pos
+ 1, escaped
.length() - 1);
467 return Enum
.valueOf((Class
<Enum
>) getClass(type
), name
);
468 } catch (Exception e
) {
469 throw new UnknownFormatConversionException("Unknown enum: <" + type
475 private static void encodeString(StringBuilder builder
, String raw
) {
476 builder
.append('\"');
477 for (char car
: raw
.toCharArray()) {
480 builder
.append("\\\\");
483 builder
.append("\\r");
486 builder
.append("\\n");
489 builder
.append("\\\"");
496 builder
.append('\"');
500 private static String
decodeString(String escaped
) {
501 StringBuilder builder
= new StringBuilder();
503 boolean escaping
= false;
504 for (char car
: escaped
.toCharArray()) {
514 builder
.append('\\');
517 builder
.append('\r');
520 builder
.append('\n');
530 return builder
.substring(1, builder
.length() - 1).toString();