...
[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.StringUtils;
11 import be.nikiroo.utils.streams.NextableInputStream;
12 import be.nikiroo.utils.streams.NextableInputStreamStep;
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 in
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 NextableInputStream stream = new NextableInputStream(in,
72 new NextableInputStreamStep('\n'));
73
74 try {
75 if (in == null) {
76 throw new NullPointerException("InputStream is null");
77 }
78
79 boolean first = true;
80 while (stream.next()) {
81 if (stream.eof()) {
82 if (first) {
83 throw new NullPointerException(
84 "InputStream empty, normal termination");
85 }
86 return this;
87 }
88 first = false;
89
90 boolean zip = stream.startsWiths("ZIP:");
91 boolean b64 = stream.startsWiths("B64:");
92
93 if (zip || b64) {
94 stream.skip("XXX:".length());
95 InputStream decoded = StringUtils.unbase64(stream.open(),
96 zip);
97 try {
98 read(decoded);
99 } finally {
100 decoded.close();
101 }
102 } else {
103 processLine(stream);
104 }
105 }
106 } finally {
107 stream.close(false);
108 }
109
110 return this;
111 }
112
113 /**
114 * Read a single (whole) line of serialised data into this {@link Importer}
115 * and accumulate it with the already present data.
116 *
117 * @param in
118 * the line to parse
119 *
120 * @return TRUE if we are just done with one object or sub-object
121 *
122 * @throws NoSuchFieldException
123 * if the serialised data contains information about a field
124 * which does actually not exist in the class we know of
125 * @throws NoSuchMethodException
126 * if a class described in the serialised data cannot be created
127 * because it is not compatible with this code
128 * @throws ClassNotFoundException
129 * if a class described in the serialised data cannot be found
130 * @throws IOException
131 * if the content cannot be read (for instance, corrupt data)
132 */
133 private boolean processLine(InputStream in) throws NoSuchFieldException,
134 NoSuchMethodException, ClassNotFoundException, IOException {
135
136 // Defer to latest child if any
137 if (child != null) {
138 if (child.processLine(in)) {
139 if (currentFieldName != null) {
140 setField(currentFieldName, child.getValue());
141 currentFieldName = null;
142 }
143 child = null;
144 }
145
146 return false;
147 }
148
149 // TODO use the stream, Luke
150 String line = IOUtils.readSmallStream(in);
151
152 if (line.isEmpty()) {
153 return false;
154 }
155
156 if (line.equals("{")) { // START: new child if needed
157 if (link != null) {
158 child = new Importer(map);
159 }
160 } else if (line.equals("}")) { // STOP: report self to parent
161 return true;
162 } else if (line.startsWith("REF ")) { // REF: create/link self
163 String[] tab = line.substring("REF ".length()).split("@");
164 String type = tab[0];
165 tab = tab[1].split(":");
166 String ref = tab[0];
167
168 link = map.containsKey(ref);
169 if (link) {
170 me = map.get(ref);
171 } else {
172 if (line.endsWith(":")) {
173 // construct
174 try {
175 me = SerialUtils.createObject(type);
176 } catch (NoSuchMethodException e) {
177 System.out.println("LINE: <" + line + ">");
178 throw e;
179 }
180 } else {
181 // direct value
182 int pos = line.indexOf(":");
183 String encodedValue = line.substring(pos + 1);
184 me = SerialUtils.decode(encodedValue);
185 }
186 map.put(ref, me);
187 }
188 } else { // FIELD: new field *or* direct simple value
189 if (line.endsWith(":")) {
190 // field value is compound
191 currentFieldName = line.substring(0, line.length() - 1);
192 } else if (line.startsWith(":") || !line.contains(":")
193 || line.startsWith("\"") || CustomSerializer.isCustom(line)) {
194 // not a field value but a direct value
195 me = SerialUtils.decode(line);
196 } else {
197 // field value is direct
198 int pos = line.indexOf(":");
199 String fieldName = line.substring(0, pos);
200 String encodedValue = line.substring(pos + 1);
201 Object value = null;
202 value = SerialUtils.decode(encodedValue);
203
204 // To support simple types directly:
205 if (me == null) {
206 me = value;
207 } else {
208 setField(fieldName, value);
209 }
210 }
211 }
212
213 return false;
214 }
215
216 private void setField(String name, Object value)
217 throws NoSuchFieldException {
218
219 try {
220 Field field = me.getClass().getDeclaredField(name);
221
222 field.setAccessible(true);
223 field.set(me, value);
224 } catch (NoSuchFieldException e) {
225 throw new NoSuchFieldException(String.format(
226 "Field \"%s\" was not found in object of type \"%s\".",
227 name, me.getClass().getCanonicalName()));
228 } catch (Exception e) {
229 throw new NoSuchFieldException(String.format(
230 "Internal error when setting \"%s.%s\": %s", me.getClass()
231 .getCanonicalName(), name, e.getMessage()));
232 }
233 }
234
235 /**
236 * Return the current deserialised value.
237 *
238 * @return the current value
239 */
240 public Object getValue() {
241 return me;
242 }
243 }