code cleanup + BufInputStream.is()
[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 import java.util.zip.GZIPInputStream;
9
10 import be.nikiroo.utils.IOUtils;
11 import be.nikiroo.utils.streams.Base64InputStream;
12 import be.nikiroo.utils.streams.BufferedInputStream;
13 import be.nikiroo.utils.streams.NextableInputStream;
14 import be.nikiroo.utils.streams.NextableInputStreamStep;
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 */
26 public 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
34 /**
35 * Create a new {@link Importer}.
36 */
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
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 *
51 * @param in
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
64 * @throws IOException
65 * if the content cannot be read (for instance, corrupt data)
66 * @throws NullPointerException
67 * if the stream is empty
68 */
69 public Importer read(InputStream in) throws NoSuchFieldException,
70 NoSuchMethodException, ClassNotFoundException, IOException,
71 NullPointerException {
72
73 NextableInputStream stream = new NextableInputStream(in,
74 new NextableInputStreamStep('\n'));
75
76 try {
77 if (in == null) {
78 throw new NullPointerException("InputStream is null");
79 }
80
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;
91
92 boolean zip = stream.startsWiths("ZIP:");
93 boolean b64 = stream.startsWiths("B64:");
94
95 if (zip || b64) {
96 stream.skip("XXX:".length());
97
98 InputStream decoded = stream.open();
99 if (zip) {
100 decoded = new GZIPInputStream(decoded);
101 }
102 decoded = new Base64InputStream(decoded, false);
103
104 try {
105 read(decoded);
106 } finally {
107 decoded.close();
108 }
109 } else {
110 processLine(stream);
111 }
112 }
113 } finally {
114 stream.close(false);
115 }
116
117 return this;
118 }
119
120 /**
121 * Read a single (whole) line of serialised data into this {@link Importer}
122 * and accumulate it with the already present data.
123 *
124 * @param in
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
137 * @throws IOException
138 * if the content cannot be read (for instance, corrupt data)
139 */
140 private boolean processLine(BufferedInputStream in) throws NoSuchFieldException,
141 NoSuchMethodException, ClassNotFoundException, IOException {
142
143 // Defer to latest child if any
144 if (child != null) {
145 if (child.processLine(in)) {
146 if (currentFieldName != null) {
147 setField(currentFieldName, child.getValue());
148 currentFieldName = null;
149 }
150 child = null;
151 }
152
153 return false;
154 }
155
156 // TODO use the stream, Luke
157 String line = IOUtils.readSmallStream(in);
158
159 if (line.equals("{")) { // START: new child if needed
160 if (link != null) {
161 child = new Importer(map);
162 }
163 } else if (line.equals("}")) { // STOP: report self to parent
164 return true;
165 } else if (line.startsWith("REF ")) { // REF: create/link self
166 String[] tab = line.substring("REF ".length()).split("@");
167 String type = tab[0];
168 tab = tab[1].split(":");
169 String ref = tab[0];
170
171 link = map.containsKey(ref);
172 if (link) {
173 me = map.get(ref);
174 } else {
175 if (line.endsWith(":")) {
176 // construct
177 me = SerialUtils.createObject(type);
178 } else {
179 // direct value
180 int pos = line.indexOf(":");
181 String encodedValue = line.substring(pos + 1);
182 me = SerialUtils.decode(encodedValue);
183 }
184 map.put(ref, me);
185 }
186 } else { // FIELD: new field *or* direct simple value
187 if (line.endsWith(":")) {
188 // field value is compound
189 currentFieldName = line.substring(0, line.length() - 1);
190 } else if (line.startsWith(":") || !line.contains(":")
191 || line.startsWith("\"") || CustomSerializer.isCustom(line)) {
192 // not a field value but a direct value
193 me = SerialUtils.decode(line);
194 } else {
195 // field value is direct
196 int pos = line.indexOf(":");
197 String fieldName = line.substring(0, pos);
198 String encodedValue = line.substring(pos + 1);
199 Object value = null;
200 value = SerialUtils.decode(encodedValue);
201
202 // To support simple types directly:
203 if (me == null) {
204 me = value;
205 } else {
206 setField(fieldName, value);
207 }
208 }
209 }
210
211 return false;
212 }
213
214 private void setField(String name, Object value)
215 throws NoSuchFieldException {
216
217 try {
218 Field field = me.getClass().getDeclaredField(name);
219
220 field.setAccessible(true);
221 field.set(me, value);
222 } catch (NoSuchFieldException e) {
223 throw new NoSuchFieldException(String.format(
224 "Field \"%s\" was not found in object of type \"%s\".",
225 name, me.getClass().getCanonicalName()));
226 } catch (Exception e) {
227 throw new NoSuchFieldException(String.format(
228 "Internal error when setting \"%s.%s\": %s", me.getClass()
229 .getCanonicalName(), name, e.getMessage()));
230 }
231 }
232
233 /**
234 * Return the current deserialised value.
235 *
236 * @return the current value
237 */
238 public Object getValue() {
239 return me;
240 }
241 }