657dbec45fb069e26b77cc7423c0c606ffdc6a39
[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.Field;
5 import java.util.HashMap;
6 import java.util.Map;
7
8 /**
9 * Small class to help serialise/deserialise objects.
10 * <p>
11 * Note that we do not support inner classes (but we do support nested classes)
12 * and all objects require an empty constructor to be deserialised.
13 *
14 * @author niki
15 */
16 class SerialUtils {
17 private static Map<String, CustomSerializer> customTypes;
18
19 static {
20 customTypes = new HashMap<String, CustomSerializer>();
21 // TODO: add "default" custom serialisers
22 }
23
24 static public void addCustomSerializer(CustomSerializer serializer) {
25 customTypes.put(serializer.getType(), serializer);
26 }
27
28 static void append(StringBuilder builder, Object o, Map<Integer, Object> map)
29 throws NotSerializableException {
30
31 Field[] fields = new Field[] {};
32 String type = "";
33 String id = "NULL";
34
35 if (o != null) {
36 int hash = System.identityHashCode(o);
37 fields = o.getClass().getDeclaredFields();
38 type = o.getClass().getCanonicalName();
39 if (type == null) {
40 throw new NotSerializableException(
41 String.format(
42 "Cannot find the class for this object: %s (it could be an inner class, which is not supported)",
43 o));
44 }
45 id = Integer.toString(hash);
46 if (map.containsKey(hash)) {
47 fields = new Field[] {};
48 } else {
49 map.put(hash, o);
50 }
51 }
52
53 builder.append("{\nREF ").append(type).append("@").append(id);
54 try {
55 for (Field field : fields) {
56 field.setAccessible(true);
57
58 if (field.getName().startsWith("this$")) {
59 // Do not keep this links of nested classes
60 continue;
61 }
62
63 builder.append("\n");
64 builder.append(field.getName());
65 builder.append(":");
66 Object value;
67
68 value = field.get(o);
69
70 if (!encode(builder, value)) {
71 builder.append("\n");
72 append(builder, value, map);
73 }
74 }
75 } catch (IllegalArgumentException e) {
76 e.printStackTrace(); // should not happen (see setAccessible)
77 } catch (IllegalAccessException e) {
78 e.printStackTrace(); // should not happen (see setAccessible)
79 }
80 builder.append("\n}");
81 }
82
83 // return true if encoded (supported)
84 static boolean encode(StringBuilder builder, Object value) {
85 if (value == null) {
86 builder.append("NULL");
87 } else if (customTypes.containsKey(value.getClass().getCanonicalName())) {
88 customTypes.get(value.getClass().getCanonicalName())//
89 .encode(builder, value);
90 } else if (value instanceof String) {
91 encodeString(builder, (String) value);
92 } else if (value instanceof Boolean) {
93 builder.append(value);
94 } else if (value instanceof Byte) {
95 builder.append(value).append('b');
96 } else if (value instanceof Character) {
97 encodeString(builder, (String) value);
98 builder.append('c');
99 } else if (value instanceof Short) {
100 builder.append(value).append('s');
101 } else if (value instanceof Integer) {
102 builder.append(value);
103 } else if (value instanceof Long) {
104 builder.append(value).append('L');
105 } else if (value instanceof Float) {
106 builder.append(value).append('F');
107 } else if (value instanceof Double) {
108 builder.append(value).append('d');
109 } else {
110 return false;
111 }
112
113 return true;
114 }
115
116 static Object decode(String encodedValue) {
117 String cut = "";
118 if (encodedValue.length() > 1) {
119 cut = encodedValue.substring(0, encodedValue.length() - 1);
120 }
121
122 if (CustomSerializer.isCustom(encodedValue)) {
123 // custom:TYPE_NAME:"content is String-encoded"
124 String type = CustomSerializer.typeOf(encodedValue);
125 if (customTypes.containsKey(type)) {
126 return customTypes.get(type).decode(encodedValue);
127 } else {
128 throw new java.util.UnknownFormatConversionException(
129 "Unknown custom type: " + type);
130 }
131 } else if (encodedValue.equals("NULL") || encodedValue.equals("null")) {
132 return null;
133 } else if (encodedValue.endsWith("\"")) {
134 return decodeString(encodedValue);
135 } else if (encodedValue.equals("true")) {
136 return true;
137 } else if (encodedValue.equals("false")) {
138 return false;
139 } else if (encodedValue.endsWith("b")) {
140 return Byte.parseByte(cut);
141 } else if (encodedValue.endsWith("c")) {
142 return decodeString(cut).charAt(0);
143 } else if (encodedValue.endsWith("s")) {
144 return Short.parseShort(cut);
145 } else if (encodedValue.endsWith("L")) {
146 return Long.parseLong(cut);
147 } else if (encodedValue.endsWith("F")) {
148 return Float.parseFloat(cut);
149 } else if (encodedValue.endsWith("d")) {
150 return Double.parseDouble(cut);
151 } else {
152 return Integer.parseInt(encodedValue);
153 }
154 }
155
156 // aa bb -> "aa\tbb"
157 private static void encodeString(StringBuilder builder, String raw) {
158 builder.append('\"');
159 for (char car : raw.toCharArray()) {
160 switch (car) {
161 case '\\':
162 builder.append("\\\\");
163 break;
164 case '\r':
165 builder.append("\\r");
166 break;
167 case '\n':
168 builder.append("\\n");
169 break;
170 case '"':
171 builder.append("\\\"");
172 break;
173 default:
174 builder.append(car);
175 break;
176 }
177 }
178 builder.append('\"');
179 }
180
181 // "aa\tbb" -> aa bb
182 private static String decodeString(String escaped) {
183 StringBuilder builder = new StringBuilder();
184
185 boolean escaping = false;
186 for (char car : escaped.toCharArray()) {
187 if (!escaping) {
188 if (car == '\\') {
189 escaping = true;
190 } else {
191 builder.append(car);
192 }
193 } else {
194 switch (car) {
195 case '\\':
196 builder.append('\\');
197 break;
198 case 'r':
199 builder.append('\r');
200 break;
201 case 'n':
202 builder.append('\n');
203 break;
204 case '"':
205 builder.append('"');
206 break;
207 }
208 escaping = false;
209 }
210 }
211
212 return builder.substring(1, builder.length() - 1).toString();
213 }
214 }