1 package be
.nikiroo
.utils
.serial
;
3 import java
.io
.IOException
;
4 import java
.io
.UnsupportedEncodingException
;
5 import java
.lang
.reflect
.Field
;
6 import java
.util
.HashMap
;
9 import be
.nikiroo
.utils
.StringUtils
;
12 * A simple class that can accept the output of {@link Exporter} to recreate
13 * objects as they were sent to said exporter.
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).
21 public class Importer
{
22 static private Integer SIZE_ID
= null;
23 static private byte[] NEWLINE
= null;
27 private Importer child
;
28 private Map
<String
, Object
> map
;
30 private String currentFieldName
;
34 SIZE_ID
= "EXT:".getBytes("UTF-8").length
;
35 NEWLINE
= "\n".getBytes("UTF-8");
36 } catch (UnsupportedEncodingException e
) {
37 // UTF-8 is mandated to exist on confirming jre's
42 * Create a new {@link Importer}.
45 map
= new HashMap
<String
, Object
>();
46 map
.put("NULL", null);
49 private Importer(Map
<String
, Object
> map
) {
54 * Read some data into this {@link Importer}: it can be the full serialised
55 * content, or a number of lines of it (any given line <b>MUST</b> be
56 * complete though) and accumulate it with the already present data.
61 * @return itself so it can be chained
63 * @throws NoSuchFieldException
64 * if the serialised data contains information about a field
65 * which does actually not exist in the class we know of
66 * @throws NoSuchMethodException
67 * if a class described in the serialised data cannot be created
68 * because it is not compatible with this code
69 * @throws ClassNotFoundException
70 * if a class described in the serialised data cannot be found
72 * if the content cannot be read (for instance, corrupt data)
74 public Importer
read(String data
) throws NoSuchFieldException
,
75 NoSuchMethodException
, ClassNotFoundException
, IOException
{
76 return read(data
.getBytes("UTF-8"), 0);
80 * Read some data into this {@link Importer}: it can be the full serialised
81 * content, or a number of lines of it (any given line <b>MUST</b> be
82 * complete though) and accumulate it with the already present data.
87 * the offset at which to start reading the data (we ignore
88 * anything that goes before that offset)
90 * @return itself so it can be chained
92 * @throws NoSuchFieldException
93 * if the serialised data contains information about a field
94 * which does actually not exist in the class we know of
95 * @throws NoSuchMethodException
96 * if a class described in the serialised data cannot be created
97 * because it is not compatible with this code
98 * @throws ClassNotFoundException
99 * if a class described in the serialised data cannot be found
100 * @throws IOException
101 * if the content cannot be read (for instance, corrupt data)
103 private Importer
read(byte[] data
, int offset
) throws NoSuchFieldException
,
104 NoSuchMethodException
, ClassNotFoundException
, IOException
{
106 int dataStart
= offset
;
107 while (dataStart
< data
.length
) {
109 if (data
.length
- dataStart
>= SIZE_ID
) {
110 id
= new String(data
, dataStart
, SIZE_ID
);
113 boolean zip
= id
.equals("ZIP:");
114 boolean b64
= id
.equals("B64:");
116 dataStart
+= SIZE_ID
;
119 int count
= find(data
, dataStart
, NEWLINE
);
122 count
= data
.length
- dataStart
;
126 boolean unpacked
= false;
128 byte[] line
= StringUtils
.unbase64(data
, dataStart
, count
,
132 } catch (IOException e
) {
133 throw new IOException("Internal error when decoding "
134 + (unpacked ?
"unpacked " : "")
135 + (zip ?
"ZIP" : "B64")
136 + " content: input may be corrupt");
139 String line
= new String(data
, dataStart
, count
, "UTF-8");
143 dataStart
+= count
+ NEWLINE
.length
;
150 * Read a single (whole) line of serialised data into this {@link Importer}
151 * and accumulate it with the already present data.
156 * @return TRUE if we are just done with one object or sub-object
158 * @throws NoSuchFieldException
159 * if the serialised data contains information about a field
160 * which does actually not exist in the class we know of
161 * @throws NoSuchMethodException
162 * if a class described in the serialised data cannot be created
163 * because it is not compatible with this code
164 * @throws ClassNotFoundException
165 * if a class described in the serialised data cannot be found
166 * @throws IOException
167 * if the content cannot be read (for instance, corrupt data)
169 private boolean processLine(String line
) throws NoSuchFieldException
,
170 NoSuchMethodException
, ClassNotFoundException
, IOException
{
171 // Defer to latest child if any
173 if (child
.processLine(line
)) {
174 if (currentFieldName
!= null) {
175 setField(currentFieldName
, child
.getValue());
176 currentFieldName
= null;
184 if (line
.equals("{")) { // START: new child if needed
186 child
= new Importer(map
);
188 } else if (line
.equals("}")) { // STOP: report self to parent
190 } else if (line
.startsWith("REF ")) { // REF: create/link self
191 String
[] tab
= line
.substring("REF ".length()).split("@");
192 String type
= tab
[0];
193 tab
= tab
[1].split(":");
196 link
= map
.containsKey(ref
);
200 if (line
.endsWith(":")) {
202 me
= SerialUtils
.createObject(type
);
205 int pos
= line
.indexOf(":");
206 String encodedValue
= line
.substring(pos
+ 1);
207 me
= SerialUtils
.decode(encodedValue
);
211 } else { // FIELD: new field *or* direct simple value
212 if (line
.endsWith(":")) {
213 // field value is compound
214 currentFieldName
= line
.substring(0, line
.length() - 1);
215 } else if (line
.startsWith(":") || !line
.contains(":")
216 || line
.startsWith("\"") || CustomSerializer
.isCustom(line
)) {
217 // not a field value but a direct value
218 me
= SerialUtils
.decode(line
);
220 // field value is direct
221 int pos
= line
.indexOf(":");
222 String fieldName
= line
.substring(0, pos
);
223 String encodedValue
= line
.substring(pos
+ 1);
225 value
= SerialUtils
.decode(encodedValue
);
227 // To support simple types directly:
231 setField(fieldName
, value
);
239 private void setField(String name
, Object value
)
240 throws NoSuchFieldException
{
243 Field field
= me
.getClass().getDeclaredField(name
);
245 field
.setAccessible(true);
246 field
.set(me
, value
);
247 } catch (NoSuchFieldException e
) {
248 throw new NoSuchFieldException(String
.format(
249 "Field \"%s\" was not found in object of type \"%s\".",
250 name
, me
.getClass().getCanonicalName()));
251 } catch (Exception e
) {
252 throw new NoSuchFieldException(String
.format(
253 "Internal error when setting \"%s.%s\": %s", me
.getClass()
254 .getCanonicalName(), name
, e
.getMessage()));
259 * Find the given needle in the data and return its position (or -1 if not
263 * the data to look through
265 * the offset at wich to start searching
269 * @return the position of the needle if found, -1 if not found
271 private int find(byte[] data
, int offset
, byte[] needle
) {
272 for (int i
= offset
; i
+ needle
.length
- 1 < data
.length
; i
++) {
274 for (int j
= 0; j
< needle
.length
; j
++) {
275 if (data
[i
+ j
] != needle
[j
]) {
290 * Return the current deserialised value.
292 * @return the current value
294 public Object
getValue() {