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