/* This file is part of GNUnet. Copyright (C) 2011, 2012 Christian Grothoff (and other contributing authors) GNUnet is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNUnet is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNUnet; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.gnunet.construct; import com.google.common.base.Preconditions; import org.gnunet.construct.parsers.*; import org.grothoff.Runabout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.nio.ByteBuffer; import java.util.*; /* Wanted syntax (not fully implemented yet) - @(U)Int => signed or unsigned fixnum, represented by n bits - @NestedMessage => nested message - @FillWith @(U)Int => fill the rest of the message with the specified fixnum, annotation valid on primitive arrays - @FillWith @NestedMessage => fill the rest of the message with the specified fixnum, annotation valid on message arrays of the wanted type - @VariableSizeArray(lengthField = "") => same syntax as @FillWith - @FixedSizeArray(length = n) => same syntax as @FillWith - @DoubleValue / @Float => floating point number, should also work with the array annotations - @FrameSize => specifies the fixnum that determines the containing frame's size - @ZeroTerminatedString => self-explanatory - @Constructable => annotation on a class that implements the ConstructableMessage interface, providing methods to serialize/unserialize itself. */ /** * Parse and write the binary representation of java classes, as defined by org.gnunet.construct.*-annotations * on their members. * * @author Christian Grothoff * @author Florian Dold */ @SuppressWarnings("unchecked") public class Construct { private static final Logger logger = LoggerFactory .getLogger(Construct.class); private static Map, Parser> parserCache = new HashMap, Parser>(100); /** * The class Construct is not intended to be instantiated, this its constructor is private. */ private Construct() { } /** * Given a byte buffer with a message, parse it into an object of type c. The * fields of the class are expected to be annotated with annotations from * the construct package. * * @param srcBuf buffer with the serialized binary data * @param c desired object type to return * @return instance of the desired object type */ public static T parseAs(ByteBuffer srcBuf, Class c) { try { T m = ReflectUtil.justInstantiate(c); try { getParser(c).parse(srcBuf, 0, m, m, null); } catch (ProtocolViolationException e) { e.augmentPath("on " + c); } return m; } catch (Error e) { System.err.println( String.format("Exception while parsing message for class '%s':", c.getCanonicalName())); throw e; } } /** * Given a byte array with a message, parse it into an object of type c. The * fields of the class are expected to be annotated with annotations from * the construct package. * * @param srcBuf buffer with the serialized binary data * @param c desired object type to return * @return instance of the desired object type */ public static T parseAs(byte[] srcBuf, Class c) { return parseAs(ByteBuffer.wrap(srcBuf), c); } /** * Create a Parser for a sub-class of Message. The result is always cached. * * @param c annotated sub-class of message * @return a parser */ public static Parser getParser(Class c) { if (parserCache.containsKey(c)) { return parserCache.get(c); } Parser p = getParser(c, new ParserGenerator()); parserCache.put(c, p); return p; } private static List getMessageFields(Class c) { LinkedList fields = new LinkedList(Arrays.asList(c.getDeclaredFields())); while ((c = c.getSuperclass()) != null && Message.class.isAssignableFrom(c)) { // fields of the superclass have to be parsed *before* the subclass fields.addAll(0, Arrays.asList(c.getDeclaredFields())); } return fields; } private static Parser getParser(Class c, final ParserGenerator pg) { Preconditions.checkNotNull(c); SequenceParser parser = new SequenceParser(); if (!Modifier.isPublic(c.getModifiers())) { throw new AssertionError(String.format("Construct Message %s not declared public", c)); } for (Field f : getMessageFields(c)) { pg.c = c; Annotation[] as = f.getAnnotations(); if (as.length == 0 || f.isSynthetic() || Modifier.isStatic(f.getModifiers())) { continue; } if (!Modifier.isPublic(f.getModifiers())) { throw new AssertionError(String.format("Field %s of Message %s not declared public", f, c)); } pg.field = f; pg.annotations = as; pg.annotationsIdx = 0; pg.visitAppropriate(as[0]); parser.add(pg.parser); } parser.setFrameSizePath(pg.frameSizePath); return parser; } @SuppressWarnings("UnusedDeclaration") static class ParserGenerator extends Runabout { // the field we are currently generating a parser for Field field; // all annotations on the field Annotation[] annotations; // the index of the annotation we are supposed to process right now int annotationsIdx; // the message class for which the parser is generated Class c; // the parser we are actually generating, used by the caller, set as // return value of // the runabout invocation Parser parser; // where are we currently, seen from the root message object List path = new LinkedList(); // path of the object that has a frame size field List frameSizePath; private ParserGenerator() { } public void visit(Union u) { parser = new UnionParser(u.optional(), (Class) field.getType(), ReflectUtil.getFieldPathFromString(u.tag(), c), field); } public void visit(FrameSize ts) { frameSizePath = new LinkedList(path); frameSizePath.add(field); if (annotationsIdx != 0) { throw new AssertionError( "FrameSize must be the first annotation on a Field"); } annotationsIdx++; if (annotationsIdx >= annotations.length) { throw new AssertionError( "FrameSize must be followed by an numeric parser"); } visitAppropriate(annotations[annotationsIdx]); } public void visit(UInt8 i) { parser = new IntegerParser(1, IntegerParser.UNSIGNED, field); } public void visit(UInt16 i) { parser = new IntegerParser(2, IntegerParser.UNSIGNED, field); } public void visit(UInt32 i) { parser = new IntegerParser(4, IntegerParser.UNSIGNED, field); } public void visit(UInt64 i) { parser = new IntegerParser(8, IntegerParser.UNSIGNED, field); } public void visit(Int8 i) { parser = new IntegerParser(1, IntegerParser.SIGNED, field); } public void visit(Int16 i) { parser = new IntegerParser(2, IntegerParser.SIGNED, field); } public void visit(Int32 i) { parser = new IntegerParser(4, IntegerParser.SIGNED, field); } public void visit(Int64 i) { parser = new IntegerParser(8, IntegerParser.SIGNED, field); } public void visit(ZeroTerminatedString zts) { parser = new StringParser(zts.charset(), zts.optional(), field); } public void visit(IntegerFill i) { parser = new IntegerFillParser(field, i.signed(), i.bitSize() / 8); } public void visit(NestedMessage n) { if (!Message.class.isAssignableFrom(field.getType())) { throw new AssertionError("@NestedMessage only works on messages, " + field.getType() + " is not a message (origin: " + c + ")"); } Field nestedField = field; if (n.newFrame()) { Parser p = getParser((Class) nestedField.getType()); parser = new NestedParser(p, n.optional(), nestedField, true); } else { Field oldField = field; List oldPath = new ArrayList(path); Class oldClass = c; path.add(field); Parser p = getParser((Class) nestedField.getType(), this); path = oldPath; c = oldClass; parser = new NestedParser(p, n.optional(), oldField, false); } } public void visit(FixedSizeArray fsa) { Field f = field; int elemNumber = fsa.length(); getParser((Class) field.getType() .getComponentType(), this); parser = new FixedSizeArrayParser(elemNumber, parser, f); } public void visit(FixedSizeIntegerArray fsa) { Field f = field; int elemNumber = fsa.length(); parser = new FixedSizeIntegerArrayParser(elemNumber, fsa.signed(), fsa.bitSize() / 8, f); } public void visit(DoubleValue d) { if (!field.getType().equals(java.lang.Double.TYPE)) { throw new AssertionError("@DoubleValue target must be a primitive 'double' field"); } parser = new DoubleParser(field); } public void visit(FillWith fw) { annotationsIdx++; // if there's no further annotation, act like there is @Nested if (annotationsIdx >= annotations.length) { Parser p = getParser((Class) field.getType().getComponentType()); parser = new FillParser(p, field); } else { FillWithParserRunabout r = new FillWithParserRunabout(field); r.visitAppropriate(annotations[annotationsIdx]); if (r.p == null) { throw new AssertionError(); } parser = r.p; } } public void visit(VariableSizeArray vsa) { Parser p = getParser((Class) field.getType() .getComponentType()); if (!Message.class.isAssignableFrom(field.getType().getComponentType())) { throw new AssertionError("VariableSizeArray only valid on arrays of messages."); } try { parser = new VariableSizeArrayParser(p, c.getField(vsa .lengthField()), field); } catch (SecurityException e) { throw new AssertionError( String.format( "VariableSizeArray: length field '%s' not declared public", vsa.lengthField())); } catch (NoSuchFieldException e) { throw new AssertionError(String.format( "VariableSizeArray: length field '%s' does not exist in class %s", vsa.lengthField(), c)); } } public void visit(VariableSizeString vss) { try { parser = new VariableSizeStringParser(vss.terminationType(), c.getField(vss .lengthField()), field); } catch (SecurityException e) { throw new AssertionError( String.format( "VariableSizeString: length field '%s' not declared public", vss.lengthField())); } catch (NoSuchFieldException e) { throw new AssertionError(String.format( "VariableSizeString: length field '%s' does not exist in class %s", vss.lengthField(), c)); } } public void visit(VariableSizeIntegerArray a) { try { parser = new VariableSizeIntegerArrayParser(c.getField(a.lengthField()), field, a.signed(), a.bitSize() / 8); } catch (NoSuchFieldException e) { throw new AssertionError(String.format( "VariableSizeIntegerArray: length field '%s' does not exist in class %s", a.lengthField(), c)); } } /* * We override this to improve the error message, otherwise obfuscated by internal java proxy objects */ @Override public void visitDefault(Object obj) { if (obj instanceof Annotation) { Annotation ann = (Annotation) obj; throw new AssertionError("invalid Construct annotation: " + ann.annotationType().getName()); } else { throw new AssertionError(); } } } private static class FillWithParserRunabout extends Runabout { public Parser p; private Field f; public FillWithParserRunabout(Field f) { this.f = f; } public void visit(Int8 x) { p = new IntegerFillParser(f, true, 1); } public void visit(Int16 x) { p = new IntegerFillParser(f, true, 2); } public void visit(Int32 x) { p = new IntegerFillParser(f, true, 4); } public void visit(UInt8 x) { p = new IntegerFillParser(f, false, 1); } public void visit(UInt16 x) { p = new IntegerFillParser(f, false, 2); } public void visit(UInt32 x) { p = new IntegerFillParser(f, false, 4); } public void visit(NestedMessage n) { Parser componentParser = getParser((Class) f.getType().getComponentType()); p = new FillParser(componentParser, f); } } /** * Serialize a given message object to a binary byte array. The fields of * the object are expected to be annotated with annotations from the * construct package. * * @param dstBuf where to write the binary object data * @param msg object to serialize * @return number of bytes written to data, -1 on error */ public static int write(ByteBuffer dstBuf, Message msg) { Parser p = getParser(msg.getClass()); return p.write(dstBuf, msg); } /** * Compute the exact size of a serialized message. * * @param m object to serialize * @return number of bytes required to store the message in binary form */ public static int getSize(Message m) { if (m == null) { return 0; } Parser p = getParser(m.getClass()); return p.getSize(m); } /** * Return the binary representation of the message m * * @param m the message to serialize * @return a byte array containing the serialized message */ public static byte[] toBinary(Message m) { byte[] a = new byte[getSize(m)]; ByteBuffer buf = ByteBuffer.wrap(a); write(buf, m); return a; } /** * Fill in all fields of a message that are inferable from existing information. * * Examples: The size field for variable size arrays, the type of unions, ... * * @param m the message that should be patched */ public static void patch(Message m) { Parser p = getParser(m.getClass()); p.patch(m, p.getSize(m), null, m); } /** * Get the minimum static size for the message, determinable even if the message's members * are not filled in. * * @param m the message of interest * @return the static minimum size of the message */ public static int getStaticSize(Message m) { Parser p = getParser(m.getClass()); return p.getStaticSize(); } }