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