server: String -> Stream
[nikiroo-utils.git] / src / be / nikiroo / utils / 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;
db31c358 8
08f80ac5 9import be.nikiroo.utils.IOUtils;
db31c358 10import be.nikiroo.utils.StringUtils;
08f80ac5
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 *
08f80ac5 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)
08f80ac5
NR
64 * @throws NullPointerException
65 * if the stream is empty
8c8da42a 66 */
08f80ac5
NR
67 public Importer read(InputStream in) throws NoSuchFieldException,
68 NoSuchMethodException, ClassNotFoundException, IOException,
69 NullPointerException {
db31c358 70
08f80ac5
NR
71 NextableInputStream stream = new NextableInputStream(in,
72 new NextableInputStreamStep('\n'));
b6200792 73
08f80ac5
NR
74 try {
75 if (in == null) {
76 throw new NullPointerException("InputStream is null");
b6200792
NR
77 }
78
08f80ac5
NR
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;
b6200792 89
08f80ac5
NR
90 boolean zip = stream.startsWiths("ZIP:");
91 boolean b64 = stream.startsWiths("B64:");
b6200792 92
08f80ac5
NR
93 if (zip || b64) {
94 stream.skip("XXX:".length());
95 InputStream decoded = StringUtils.unbase64(stream.open(),
b6200792 96 zip);
08f80ac5
NR
97 try {
98 read(decoded);
99 } finally {
100 decoded.close();
101 }
102 } else {
103 processLine(stream);
8c8da42a 104 }
db31c358 105 }
08f80ac5
NR
106 } finally {
107 stream.close(false);
db31c358 108 }
8c8da42a 109
db31c358
NR
110 return this;
111 }
112
8c8da42a
NR
113 /**
114 * Read a single (whole) line of serialised data into this {@link Importer}
115 * and accumulate it with the already present data.
116 *
08f80ac5 117 * @param in
8c8da42a
NR
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
452f38c8
NR
130 * @throws IOException
131 * if the content cannot be read (for instance, corrupt data)
8c8da42a 132 */
08f80ac5 133 private boolean processLine(InputStream in) throws NoSuchFieldException,
452f38c8 134 NoSuchMethodException, ClassNotFoundException, IOException {
08f80ac5 135
db31c358
NR
136 // Defer to latest child if any
137 if (child != null) {
08f80ac5 138 if (child.processLine(in)) {
db31c358
NR
139 if (currentFieldName != null) {
140 setField(currentFieldName, child.getValue());
141 currentFieldName = null;
142 }
143 child = null;
144 }
145
146 return false;
147 }
148
08f80ac5
NR
149 // TODO use the stream, Luke
150 String line = IOUtils.readSmallStream(in);
151
152 if (line.isEmpty()) {
153 return false;
154 }
155
db31c358
NR
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
ce0974c4
NR
163 String[] tab = line.substring("REF ".length()).split("@");
164 String type = tab[0];
165 tab = tab[1].split(":");
166 String ref = tab[0];
167
db31c358
NR
168 link = map.containsKey(ref);
169 if (link) {
170 me = map.get(ref);
171 } else {
ce0974c4
NR
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 }
db31c358
NR
181 map.put(ref, me);
182 }
ce0974c4 183 } else { // FIELD: new field *or* direct simple value
db31c358
NR
184 if (line.endsWith(":")) {
185 // field value is compound
186 currentFieldName = line.substring(0, line.length() - 1);
f4053377
NR
187 } else if (line.startsWith(":") || !line.contains(":")
188 || line.startsWith("\"") || CustomSerializer.isCustom(line)) {
ce0974c4
NR
189 // not a field value but a direct value
190 me = SerialUtils.decode(line);
db31c358
NR
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
db31c358 211 private void setField(String name, Object value)
8c8da42a 212 throws NoSuchFieldException {
db31c358
NR
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()));
8c8da42a
NR
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()));
db31c358
NR
227 }
228 }
229
8c8da42a
NR
230 /**
231 * Return the current deserialised value.
232 *
233 * @return the current value
234 */
db31c358
NR
235 public Object getValue() {
236 return me;
237 }
238}