fix/perf base64/serial
[nikiroo-utils.git] / src / be / nikiroo / utils / serial / Importer.java
1 package be.nikiroo.utils.serial;
2
3 import java.io.IOException;
4 import java.io.UnsupportedEncodingException;
5 import java.lang.reflect.Field;
6 import java.util.HashMap;
7 import java.util.Map;
8
9 import be.nikiroo.utils.StringUtils;
10
11 /**
12 * A simple class that can accept the output of {@link Exporter} to recreate
13 * objects as they were sent to said exporter.
14 * <p>
15 * This class requires the objects (and their potential enclosing objects) to
16 * have an empty constructor, and does not support inner classes (it does
17 * support nested classes, though).
18 *
19 * @author niki
20 */
21 public class Importer {
22 static private Integer SIZE_ID = null;
23 static private byte[] NEWLINE = null;
24
25 private Boolean link;
26 private Object me;
27 private Importer child;
28 private Map<String, Object> map;
29
30 private String currentFieldName;
31
32 static {
33 try {
34 SIZE_ID = "EXT:".getBytes("UTF-8").length;
35 NEWLINE = "\n".getBytes("UTF-8");
36 } catch (UnsupportedEncodingException e) {
37 // UTF-8 is mandated to exist on confirming jre's
38 }
39 }
40
41 /**
42 * Create a new {@link Importer}.
43 */
44 public Importer() {
45 map = new HashMap<String, Object>();
46 map.put("NULL", null);
47 }
48
49 private Importer(Map<String, Object> map) {
50 this.map = map;
51 }
52
53 /**
54 * Read some data into this {@link Importer}: it can be the full serialised
55 * content, or a number of lines of it (any given line <b>MUST</b> be
56 * complete though) and accumulate it with the already present data.
57 *
58 * @param data
59 * the data to parse
60 *
61 * @return itself so it can be chained
62 *
63 * @throws NoSuchFieldException
64 * if the serialised data contains information about a field
65 * which does actually not exist in the class we know of
66 * @throws NoSuchMethodException
67 * if a class described in the serialised data cannot be created
68 * because it is not compatible with this code
69 * @throws ClassNotFoundException
70 * if a class described in the serialised data cannot be found
71 * @throws IOException
72 * if the content cannot be read (for instance, corrupt data)
73 */
74 public Importer read(String data) throws NoSuchFieldException,
75 NoSuchMethodException, ClassNotFoundException, IOException {
76 return read(data.getBytes("UTF-8"), 0);
77 }
78
79 /**
80 * Read some data into this {@link Importer}: it can be the full serialised
81 * content, or a number of lines of it (any given line <b>MUST</b> be
82 * complete though) and accumulate it with the already present data.
83 *
84 * @param data
85 * the data to parse
86 * @param offset
87 * the offset at which to start reading the data (we ignore
88 * anything that goes before that offset)
89 *
90 * @return itself so it can be chained
91 *
92 * @throws NoSuchFieldException
93 * if the serialised data contains information about a field
94 * which does actually not exist in the class we know of
95 * @throws NoSuchMethodException
96 * if a class described in the serialised data cannot be created
97 * because it is not compatible with this code
98 * @throws ClassNotFoundException
99 * if a class described in the serialised data cannot be found
100 * @throws IOException
101 * if the content cannot be read (for instance, corrupt data)
102 */
103 private Importer read(byte[] data, int offset) throws NoSuchFieldException,
104 NoSuchMethodException, ClassNotFoundException, IOException {
105
106 int dataStart = offset;
107 while (dataStart < data.length) {
108 String id = "";
109 if (data.length - dataStart >= SIZE_ID) {
110 id = new String(data, dataStart, SIZE_ID);
111 }
112
113 boolean zip = id.equals("ZIP:");
114 boolean b64 = id.equals("B64:");
115 if (zip || b64) {
116 dataStart += SIZE_ID;
117 }
118
119 int count = find(data, dataStart, NEWLINE);
120 count -= dataStart;
121 if (count < 0) {
122 count = data.length - dataStart;
123 }
124
125 if (zip || b64) {
126 boolean unpacked = false;
127 try {
128 byte[] line = StringUtils.unbase64(data, dataStart, count,
129 zip);
130 unpacked = true;
131 read(line, 0);
132 } catch (IOException e) {
133 throw new IOException("Internal error when decoding "
134 + (unpacked ? "unpacked " : "")
135 + (zip ? "ZIP" : "B64")
136 + " content: input may be corrupt");
137 }
138 } else {
139 String line = new String(data, dataStart, count, "UTF-8");
140 processLine(line);
141 }
142
143 dataStart += count + NEWLINE.length;
144 }
145
146 return this;
147 }
148
149 /**
150 * Read a single (whole) line of serialised data into this {@link Importer}
151 * and accumulate it with the already present data.
152 *
153 * @param line
154 * the line to parse
155 *
156 * @return TRUE if we are just done with one object or sub-object
157 *
158 * @throws NoSuchFieldException
159 * if the serialised data contains information about a field
160 * which does actually not exist in the class we know of
161 * @throws NoSuchMethodException
162 * if a class described in the serialised data cannot be created
163 * because it is not compatible with this code
164 * @throws ClassNotFoundException
165 * if a class described in the serialised data cannot be found
166 * @throws IOException
167 * if the content cannot be read (for instance, corrupt data)
168 */
169 private boolean processLine(String line) throws NoSuchFieldException,
170 NoSuchMethodException, ClassNotFoundException, IOException {
171 // Defer to latest child if any
172 if (child != null) {
173 if (child.processLine(line)) {
174 if (currentFieldName != null) {
175 setField(currentFieldName, child.getValue());
176 currentFieldName = null;
177 }
178 child = null;
179 }
180
181 return false;
182 }
183
184 if (line.equals("{")) { // START: new child if needed
185 if (link != null) {
186 child = new Importer(map);
187 }
188 } else if (line.equals("}")) { // STOP: report self to parent
189 return true;
190 } else if (line.startsWith("REF ")) { // REF: create/link self
191 String[] tab = line.substring("REF ".length()).split("@");
192 String type = tab[0];
193 tab = tab[1].split(":");
194 String ref = tab[0];
195
196 link = map.containsKey(ref);
197 if (link) {
198 me = map.get(ref);
199 } else {
200 if (line.endsWith(":")) {
201 // construct
202 me = SerialUtils.createObject(type);
203 } else {
204 // direct value
205 int pos = line.indexOf(":");
206 String encodedValue = line.substring(pos + 1);
207 me = SerialUtils.decode(encodedValue);
208 }
209 map.put(ref, me);
210 }
211 } else { // FIELD: new field *or* direct simple value
212 if (line.endsWith(":")) {
213 // field value is compound
214 currentFieldName = line.substring(0, line.length() - 1);
215 } else if (line.startsWith(":") || !line.contains(":")
216 || line.startsWith("\"") || CustomSerializer.isCustom(line)) {
217 // not a field value but a direct value
218 me = SerialUtils.decode(line);
219 } else {
220 // field value is direct
221 int pos = line.indexOf(":");
222 String fieldName = line.substring(0, pos);
223 String encodedValue = line.substring(pos + 1);
224 Object value = null;
225 value = SerialUtils.decode(encodedValue);
226
227 // To support simple types directly:
228 if (me == null) {
229 me = value;
230 } else {
231 setField(fieldName, value);
232 }
233 }
234 }
235
236 return false;
237 }
238
239 private void setField(String name, Object value)
240 throws NoSuchFieldException {
241
242 try {
243 Field field = me.getClass().getDeclaredField(name);
244
245 field.setAccessible(true);
246 field.set(me, value);
247 } catch (NoSuchFieldException e) {
248 throw new NoSuchFieldException(String.format(
249 "Field \"%s\" was not found in object of type \"%s\".",
250 name, me.getClass().getCanonicalName()));
251 } catch (Exception e) {
252 throw new NoSuchFieldException(String.format(
253 "Internal error when setting \"%s.%s\": %s", me.getClass()
254 .getCanonicalName(), name, e.getMessage()));
255 }
256 }
257
258 /**
259 * Find the given needle in the data and return its position (or -1 if not
260 * found).
261 *
262 * @param data
263 * the data to look through
264 * @param offset
265 * the offset at wich to start searching
266 * @param needle
267 * the needle to find
268 *
269 * @return the position of the needle if found, -1 if not found
270 */
271 private int find(byte[] data, int offset, byte[] needle) {
272 for (int i = offset; i + needle.length - 1 < data.length; i++) {
273 boolean same = true;
274 for (int j = 0; j < needle.length; j++) {
275 if (data[i + j] != needle[j]) {
276 same = false;
277 break;
278 }
279 }
280
281 if (same) {
282 return i;
283 }
284 }
285
286 return -1;
287 }
288
289 /**
290 * Return the current deserialised value.
291 *
292 * @return the current value
293 */
294 public Object getValue() {
295 return me;
296 }
297 }