Commit | Line | Data |
---|---|---|
db31c358 NR |
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 | } |