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