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