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