X-Git-Url: http://git.nikiroo.be/?p=fanfix.git;a=blobdiff_plain;f=src%2Fbe%2Fnikiroo%2Futils%2Fserial%2FImporter.java;fp=src%2Fbe%2Fnikiroo%2Futils%2Fserial%2FImporter.java;h=81814dfdbadf6a71ec2224d14be5926314bbd105;hp=0000000000000000000000000000000000000000;hb=d46b7b96f94e88a776bcd2dfd756549ffb300cc9;hpb=c9994f27667bc421bcd448d39e55774fddf5c431 diff --git a/src/be/nikiroo/utils/serial/Importer.java b/src/be/nikiroo/utils/serial/Importer.java new file mode 100644 index 0000000..81814df --- /dev/null +++ b/src/be/nikiroo/utils/serial/Importer.java @@ -0,0 +1,288 @@ +package be.nikiroo.utils.serial; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.GZIPInputStream; + +import be.nikiroo.utils.IOUtils; +import be.nikiroo.utils.streams.Base64InputStream; +import be.nikiroo.utils.streams.BufferedInputStream; +import be.nikiroo.utils.streams.NextableInputStream; +import be.nikiroo.utils.streams.NextableInputStreamStep; + +/** + * A simple class that can accept the output of {@link Exporter} to recreate + * objects as they were sent to said exporter. + *

+ * This class requires the objects (and their potential enclosing objects) to + * have an empty constructor, and does not support inner classes (it does + * support nested classes, though). + * + * @author niki + */ +public class Importer { + private Boolean link; + private Object me; + private Importer child; + private Map map; + + private String currentFieldName; + + /** + * Create a new {@link Importer}. + */ + public Importer() { + map = new HashMap(); + map.put("NULL", null); + } + + private Importer(Map map) { + this.map = map; + } + + /** + * Read some data into this {@link Importer}: it can be the full serialised + * content, or a number of lines of it (any given line MUST be + * complete though) and accumulate it with the already present data. + * + * @param in + * the data to parse + * + * @return itself so it can be chained + * + * @throws NoSuchFieldException + * if the serialised data contains information about a field + * which does actually not exist in the class we know of + * @throws NoSuchMethodException + * if a class described in the serialised data cannot be created + * because it is not compatible with this code + * @throws ClassNotFoundException + * if a class described in the serialised data cannot be found + * @throws IOException + * if the content cannot be read (for instance, corrupt data) + * @throws NullPointerException + * if the stream is empty + */ + public Importer read(InputStream in) throws NoSuchFieldException, + NoSuchMethodException, ClassNotFoundException, IOException, + NullPointerException { + + NextableInputStream stream = new NextableInputStream(in, + new NextableInputStreamStep('\n')); + + try { + if (in == null) { + throw new NullPointerException("InputStream is null"); + } + + boolean first = true; + while (stream.next()) { + if (stream.eof()) { + if (first) { + throw new NullPointerException( + "InputStream empty, normal termination"); + } + return this; + } + first = false; + + boolean zip = stream.startsWith("ZIP:"); + boolean b64 = stream.startsWith("B64:"); + + if (zip || b64) { + stream.skip("XXX:".length()); + + InputStream decoded = stream.open(); + if (zip) { + decoded = new GZIPInputStream(decoded); + } + decoded = new Base64InputStream(decoded, false); + + try { + read(decoded); + } finally { + decoded.close(); + } + } else { + processLine(stream); + } + } + } finally { + stream.close(false); + } + + return this; + } + + /** + * Read a single (whole) line of serialised data into this {@link Importer} + * and accumulate it with the already present data. + * + * @param in + * the line to parse + * + * @return TRUE if we are just done with one object or sub-object + * + * @throws NoSuchFieldException + * if the serialised data contains information about a field + * which does actually not exist in the class we know of + * @throws NoSuchMethodException + * if a class described in the serialised data cannot be created + * because it is not compatible with this code + * @throws ClassNotFoundException + * if a class described in the serialised data cannot be found + * @throws IOException + * if the content cannot be read (for instance, corrupt data) + */ + private boolean processLine(BufferedInputStream in) + throws NoSuchFieldException, NoSuchMethodException, + ClassNotFoundException, IOException { + + // Defer to latest child if any + if (child != null) { + if (child.processLine(in)) { + if (currentFieldName != null) { + setField(currentFieldName, child.getValue()); + currentFieldName = null; + } + child = null; + } + + return false; + } + + // Start/Stop object + if (in.is("{")) { // START: new child if needed + if (link != null) { + child = new Importer(map); + } + in.end(); + return false; + } else if (in.is("}")) { // STOP: report self to parent + in.end(); + return true; + } + + // Custom objects + if (CustomSerializer.isCustom(in)) { + // not a field value but a direct value + me = SerialUtils.decode(in); + return false; + } + + // REF: (object) + if (in.startsWith("REF ")) { // REF: create/link self + // here, line is REF type@999:xxx + // xxx is optional + + NextableInputStream stream = new NextableInputStream(in, + new NextableInputStreamStep(':')); + try { + stream.next(); + + stream.skip("REF ".length()); + String header = IOUtils.readSmallStream(stream); + + String[] tab = header.split("@"); + if (tab.length != 2) { + throw new IOException("Bad import header line: " + header); + } + String type = tab[0]; + String ref = tab[1]; + + stream.nextAll(); + + link = map.containsKey(ref); + if (link) { + me = map.get(ref); + stream.end(); + } else { + if (stream.eof()) { + // construct + me = SerialUtils.createObject(type); + } else { + // direct value + me = SerialUtils.decode(stream); + } + map.put(ref, me); + } + } finally { + stream.close(false); + } + + return false; + } + + if (SerialUtils.isDirectValue(in)) { + // not a field value but a direct value + me = SerialUtils.decode(in); + return false; + } + + if (in.startsWith("^")) { + in.skip(1); + + NextableInputStream nameThenContent = new NextableInputStream(in, + new NextableInputStreamStep(':')); + + try { + nameThenContent.next(); + String fieldName = IOUtils.readSmallStream(nameThenContent); + + if (nameThenContent.nextAll() && !nameThenContent.eof()) { + // field value is direct or custom + Object value = null; + value = SerialUtils.decode(nameThenContent); + + // To support simple types directly: + if (me == null) { + me = value; + } else { + setField(fieldName, value); + } + } else { + // field value is compound + currentFieldName = fieldName; + } + } finally { + nameThenContent.close(false); + } + + return false; + } + + String line = IOUtils.readSmallStream(in); + throw new IOException("Line cannot be processed: <" + line + ">"); + } + + private void setField(String name, Object value) + throws NoSuchFieldException { + + try { + Field field = me.getClass().getDeclaredField(name); + + field.setAccessible(true); + field.set(me, value); + } catch (NoSuchFieldException e) { + throw new NoSuchFieldException(String.format( + "Field \"%s\" was not found in object of type \"%s\".", + name, me.getClass().getCanonicalName())); + } catch (Exception e) { + throw new NoSuchFieldException(String.format( + "Internal error when setting \"%s.%s\": %s", me.getClass() + .getCanonicalName(), name, e.getMessage())); + } + } + + /** + * Return the current deserialised value. + * + * @return the current value + */ + public Object getValue() { + return me; + } +} \ No newline at end of file