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