49817b250f4f170f3547e62355fa3a7b7b4023c3
[nikiroo-utils.git] / src / be / nikiroo / utils / serial / SerialUtils.java
1 package be.nikiroo.utils.serial;
2
3 import java.io.NotSerializableException;
4 import java.lang.reflect.Constructor;
5 import java.lang.reflect.Field;
6 import java.util.HashMap;
7 import java.util.Map;
8
9 /**
10 * Small class to help with serialisation.
11 * <p>
12 * Note that we do not support inner classes (but we do support nested classes)
13 * and all objects require an empty constructor to be deserialised.
14 *
15 * @author niki
16 */
17 public class SerialUtils {
18 private static Map<String, CustomSerializer> customTypes;
19
20 static {
21 customTypes = new HashMap<String, CustomSerializer>();
22 // TODO: add "default" custom serialisers if any (Bitmap?)
23 }
24
25 /**
26 * Create an empty object of the given type.
27 *
28 * @param type
29 * the object type (its class name)
30 *
31 * @return the new object
32 *
33 * @throws ClassNotFoundException
34 * if the class cannot be found
35 * @throws NoSuchMethodException
36 * if the given class is not compatible with this code
37 */
38 public static Object createObject(String type)
39 throws ClassNotFoundException, NoSuchMethodException {
40
41 try {
42 Class<?> clazz = getClass(type);
43 String className = clazz.getName();
44 Object[] args = null;
45 Constructor<?> ctor = null;
46 if (className.contains("$")) {
47 Object javaParent = createObject(className.substring(0,
48 className.lastIndexOf('$')));
49 args = new Object[] { javaParent };
50 ctor = clazz.getDeclaredConstructor(new Class[] { javaParent
51 .getClass() });
52 } else {
53 args = new Object[] {};
54 ctor = clazz.getDeclaredConstructor();
55 }
56
57 ctor.setAccessible(true);
58 return ctor.newInstance(args);
59 } catch (ClassNotFoundException e) {
60 throw e;
61 } catch (NoSuchMethodException e) {
62 throw e;
63 } catch (Exception e) {
64 throw new NoSuchMethodException("Cannot instantiate: " + type);
65 }
66 }
67
68 /**
69 * Insert a custom serialiser that will take precedence over the default one
70 * or the target class.
71 *
72 * @param serializer
73 * the custom serialiser
74 */
75 static public void addCustomSerializer(CustomSerializer serializer) {
76 customTypes.put(serializer.getType(), serializer);
77 }
78
79 /**
80 * Serialise the given object into this {@link StringBuilder}.
81 * <p>
82 * <b>Important: </b>If the operation fails (with a
83 * {@link NotSerializableException}), the {@link StringBuilder} will be
84 * corrupted (will contain bad, most probably not importable data).
85 *
86 * @param builder
87 * the output {@link StringBuilder} to serialise to
88 * @param o
89 * the object to serialise
90 * @param map
91 * the map of already serialised objects (if the given object or
92 * one of its descendant is already present in it, only an ID
93 * will be serialised)
94 *
95 * @throws NotSerializableException
96 * if the object cannot be serialised (in this case, the
97 * {@link StringBuilder} can contain bad, most probably not
98 * importable data)
99 */
100 static void append(StringBuilder builder, Object o, Map<Integer, Object> map)
101 throws NotSerializableException {
102
103 Field[] fields = new Field[] {};
104 String type = "";
105 String id = "NULL";
106
107 if (o != null) {
108 int hash = System.identityHashCode(o);
109 fields = o.getClass().getDeclaredFields();
110 type = o.getClass().getCanonicalName();
111 if (type == null) {
112 throw new NotSerializableException(
113 String.format(
114 "Cannot find the class for this object: %s (it could be an inner class, which is not supported)",
115 o));
116 }
117 id = Integer.toString(hash);
118 if (map.containsKey(hash)) {
119 fields = new Field[] {};
120 } else {
121 map.put(hash, o);
122 }
123 }
124
125 builder.append("{\nREF ").append(type).append("@").append(id);
126 try {
127 for (Field field : fields) {
128 field.setAccessible(true);
129
130 if (field.getName().startsWith("this$")) {
131 // Do not keep this links of nested classes
132 continue;
133 }
134
135 builder.append("\n");
136 builder.append(field.getName());
137 builder.append(":");
138 Object value;
139
140 value = field.get(o);
141
142 if (!encode(builder, value)) {
143 builder.append("\n");
144 append(builder, value, map);
145 }
146 }
147 } catch (IllegalArgumentException e) {
148 e.printStackTrace(); // should not happen (see setAccessible)
149 } catch (IllegalAccessException e) {
150 e.printStackTrace(); // should not happen (see setAccessible)
151 }
152 builder.append("\n}");
153 }
154
155 // return true if encoded (supported)
156 static boolean encode(StringBuilder builder, Object value) {
157 if (value == null) {
158 builder.append("NULL");
159 } else if (customTypes.containsKey(value.getClass().getCanonicalName())) {
160 customTypes.get(value.getClass().getCanonicalName())//
161 .encode(builder, value);
162 } else if (value instanceof String) {
163 encodeString(builder, (String) value);
164 } else if (value instanceof Boolean) {
165 builder.append(value);
166 } else if (value instanceof Byte) {
167 builder.append(value).append('b');
168 } else if (value instanceof Character) {
169 encodeString(builder, (String) value);
170 builder.append('c');
171 } else if (value instanceof Short) {
172 builder.append(value).append('s');
173 } else if (value instanceof Integer) {
174 builder.append(value);
175 } else if (value instanceof Long) {
176 builder.append(value).append('L');
177 } else if (value instanceof Float) {
178 builder.append(value).append('F');
179 } else if (value instanceof Double) {
180 builder.append(value).append('d');
181 } else {
182 return false;
183 }
184
185 return true;
186 }
187
188 static Object decode(String encodedValue) {
189 String cut = "";
190 if (encodedValue.length() > 1) {
191 cut = encodedValue.substring(0, encodedValue.length() - 1);
192 }
193
194 if (CustomSerializer.isCustom(encodedValue)) {
195 // custom:TYPE_NAME:"content is String-encoded"
196 String type = CustomSerializer.typeOf(encodedValue);
197 if (customTypes.containsKey(type)) {
198 return customTypes.get(type).decode(encodedValue);
199 } else {
200 throw new java.util.UnknownFormatConversionException(
201 "Unknown custom type: " + type);
202 }
203 } else if (encodedValue.equals("NULL") || encodedValue.equals("null")) {
204 return null;
205 } else if (encodedValue.endsWith("\"")) {
206 return decodeString(encodedValue);
207 } else if (encodedValue.equals("true")) {
208 return true;
209 } else if (encodedValue.equals("false")) {
210 return false;
211 } else if (encodedValue.endsWith("b")) {
212 return Byte.parseByte(cut);
213 } else if (encodedValue.endsWith("c")) {
214 return decodeString(cut).charAt(0);
215 } else if (encodedValue.endsWith("s")) {
216 return Short.parseShort(cut);
217 } else if (encodedValue.endsWith("L")) {
218 return Long.parseLong(cut);
219 } else if (encodedValue.endsWith("F")) {
220 return Float.parseFloat(cut);
221 } else if (encodedValue.endsWith("d")) {
222 return Double.parseDouble(cut);
223 } else {
224 return Integer.parseInt(encodedValue);
225 }
226 }
227
228 /**
229 * Return the corresponding class or throw an {@link Exception} if it
230 * cannot.
231 *
232 * @param type
233 * the class name to look for
234 *
235 * @return the class (will never be NULL)
236 *
237 * @throws ClassNotFoundException
238 * if the class cannot be found
239 * @throws NoSuchMethodException
240 * if the class cannot be created (usually because it or its
241 * enclosing class doesn't have an empty constructor)
242 */
243 static private Class<?> getClass(String type)
244 throws ClassNotFoundException, NoSuchMethodException {
245 Class<?> clazz = null;
246 try {
247 clazz = Class.forName(type);
248 } catch (ClassNotFoundException e) {
249 int pos = type.length();
250 pos = type.lastIndexOf(".", pos);
251 if (pos >= 0) {
252 String parentType = type.substring(0, pos);
253 String nestedType = type.substring(pos + 1);
254 Class<?> javaParent = null;
255 try {
256 javaParent = getClass(parentType);
257 parentType = javaParent.getName();
258 clazz = Class.forName(parentType + "$" + nestedType);
259 } catch (Exception ee) {
260 }
261
262 if (javaParent == null) {
263 throw new NoSuchMethodException(
264 "Class not found: "
265 + type
266 + " (the enclosing class cannot be created: maybe it doesn't have an empty constructor?)");
267 }
268 }
269 }
270
271 if (clazz == null) {
272 throw new ClassNotFoundException("Class not found: " + type);
273 }
274
275 return clazz;
276 }
277
278 // aa bb -> "aa\tbb"
279 private static void encodeString(StringBuilder builder, String raw) {
280 builder.append('\"');
281 for (char car : raw.toCharArray()) {
282 switch (car) {
283 case '\\':
284 builder.append("\\\\");
285 break;
286 case '\r':
287 builder.append("\\r");
288 break;
289 case '\n':
290 builder.append("\\n");
291 break;
292 case '"':
293 builder.append("\\\"");
294 break;
295 default:
296 builder.append(car);
297 break;
298 }
299 }
300 builder.append('\"');
301 }
302
303 // "aa\tbb" -> aa bb
304 private static String decodeString(String escaped) {
305 StringBuilder builder = new StringBuilder();
306
307 boolean escaping = false;
308 for (char car : escaped.toCharArray()) {
309 if (!escaping) {
310 if (car == '\\') {
311 escaping = true;
312 } else {
313 builder.append(car);
314 }
315 } else {
316 switch (car) {
317 case '\\':
318 builder.append('\\');
319 break;
320 case 'r':
321 builder.append('\r');
322 break;
323 case 'n':
324 builder.append('\n');
325 break;
326 case '"':
327 builder.append('"');
328 break;
329 }
330 escaping = false;
331 }
332 }
333
334 return builder.substring(1, builder.length() - 1).toString();
335 }
336 }