f7fece064d6028d8fca8ff5a742cf997f52f52b2
[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 me = SerialUtils.createObject(type);
175 } else {
176 // direct value
177 int pos = line.indexOf(":");
178 String encodedValue = line.substring(pos + 1);
179 me = SerialUtils.decode(encodedValue);
180 }
181 map.put(ref, me);
182 }
183 } else { // FIELD: new field *or* direct simple value
184 if (line.endsWith(":")) {
185 // field value is compound
186 currentFieldName = line.substring(0, line.length() - 1);
187 } else if (line.startsWith(":") || !line.contains(":")
188 || line.startsWith("\"") || CustomSerializer.isCustom(line)) {
189 // not a field value but a direct value
190 me = SerialUtils.decode(line);
191 } else {
192 // field value is direct
193 int pos = line.indexOf(":");
194 String fieldName = line.substring(0, pos);
195 String encodedValue = line.substring(pos + 1);
196 Object value = null;
197 value = SerialUtils.decode(encodedValue);
198
199 // To support simple types directly:
200 if (me == null) {
201 me = value;
202 } else {
203 setField(fieldName, value);
204 }
205 }
206 }
207
208 return false;
209 }
210
211 private void setField(String name, Object value)
212 throws NoSuchFieldException {
213
214 try {
215 Field field = me.getClass().getDeclaredField(name);
216
217 field.setAccessible(true);
218 field.set(me, value);
219 } catch (NoSuchFieldException e) {
220 throw new NoSuchFieldException(String.format(
221 "Field \"%s\" was not found in object of type \"%s\".",
222 name, me.getClass().getCanonicalName()));
223 } catch (Exception e) {
224 throw new NoSuchFieldException(String.format(
225 "Internal error when setting \"%s.%s\": %s", me.getClass()
226 .getCanonicalName(), name, e.getMessage()));
227 }
228 }
229
230 /**
231 * Return the current deserialised value.
232 *
233 * @return the current value
234 */
235 public Object getValue() {
236 return me;
237 }
238 }