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