diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaGenerator.java index 6f2c3df1d1..4fba35afb5 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaGenerator.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaGenerator.java @@ -1054,12 +1054,13 @@ private void generateComposite(final List tokens) throws IOException final StringBuilder sb = new StringBuilder(); generateEncodingOffsetMethod(sb, propertyName, encodingToken.offset(), BASE_INDENT); generateEncodingLengthMethod(sb, propertyName, encodingToken.encodedLength(), BASE_INDENT); + generateFieldSinceVersionMethod(sb, encodingToken, BASE_INDENT); switch (encodingToken.signal()) { case ENCODING: out.append(sb).append(generatePrimitiveDecoder( - true, encodingToken.name(), encodingToken, BASE_INDENT)); + true, encodingToken.name(), encodingToken, encodingToken, BASE_INDENT)); break; case BEGIN_ENUM: @@ -1477,19 +1478,24 @@ private static CharSequence generateEnumDeclaration(final String name, final Tok } private CharSequence generatePrimitiveDecoder( - final boolean inComposite, final String propertyName, final Token token, final String indent) + final boolean inComposite, + final String propertyName, + final Token propertyToken, + final Token encodingToken, + final String indent) { final StringBuilder sb = new StringBuilder(); - sb.append(generatePrimitiveFieldMetaData(propertyName, token, indent)); + sb.append(generatePrimitiveFieldMetaData(propertyName, encodingToken, indent)); - if (token.isConstantEncoding()) + if (encodingToken.isConstantEncoding()) { - sb.append(generateConstPropertyMethods(propertyName, token, indent)); + sb.append(generateConstPropertyMethods(propertyName, encodingToken, indent)); } else { - sb.append(generatePrimitivePropertyDecodeMethods(inComposite, propertyName, token, indent)); + sb.append(generatePrimitivePropertyDecodeMethods( + inComposite, propertyName, propertyToken, encodingToken, indent)); } return sb; @@ -1515,11 +1521,16 @@ private CharSequence generatePrimitiveEncoder( } private CharSequence generatePrimitivePropertyDecodeMethods( - final boolean inComposite, final String propertyName, final Token token, final String indent) + final boolean inComposite, + final String propertyName, + final Token propertyToken, + final Token encodingToken, + final String indent) { - return token.matchOnLength( - () -> generatePrimitivePropertyDecode(inComposite, propertyName, token, indent), - () -> generatePrimitiveArrayPropertyDecode(inComposite, propertyName, token, indent)); + return encodingToken.matchOnLength( + () -> generatePrimitivePropertyDecode(inComposite, propertyName, propertyToken, encodingToken, indent), + () -> generatePrimitiveArrayPropertyDecode( + inComposite, propertyName, propertyToken, encodingToken, indent)); } private CharSequence generatePrimitivePropertyEncodeMethods( @@ -1570,12 +1581,16 @@ private CharSequence generatePrimitiveFieldMetaData( } private CharSequence generatePrimitivePropertyDecode( - final boolean inComposite, final String propertyName, final Token token, final String indent) + final boolean inComposite, + final String propertyName, + final Token propertyToken, + final Token encodingToken, + final String indent) { - final Encoding encoding = token.encoding(); + final Encoding encoding = encodingToken.encoding(); final String javaTypeName = javaTypeName(encoding.primitiveType()); - final int offset = token.offset(); + final int offset = encodingToken.offset(); final String byteOrderStr = byteOrderString(encoding); return String.format( @@ -1587,7 +1602,7 @@ private CharSequence generatePrimitivePropertyDecode( indent + " }\n\n", javaTypeName, propertyName, - generateFieldNotPresentCondition(inComposite, token.version(), encoding, indent), + generateFieldNotPresentCondition(inComposite, propertyToken.version(), encoding, indent), generateGet(encoding.primitiveType(), "offset + " + offset, byteOrderStr)); } @@ -1676,13 +1691,17 @@ private static CharSequence generatePropertyNotPresentCondition( } private CharSequence generatePrimitiveArrayPropertyDecode( - final boolean inComposite, final String propertyName, final Token token, final String indent) + final boolean inComposite, + final String propertyName, + final Token propertyToken, + final Token encodingToken, + final String indent) { - final Encoding encoding = token.encoding(); + final Encoding encoding = encodingToken.encoding(); final String javaTypeName = javaTypeName(encoding.primitiveType()); - final int offset = token.offset(); + final int offset = encodingToken.offset(); final String byteOrderStr = byteOrderString(encoding); - final int fieldLength = token.arrayLength(); + final int fieldLength = encodingToken.arrayLength(); final int typeSize = sizeOfPrimitive(encoding); final StringBuilder sb = new StringBuilder(); @@ -1703,7 +1722,7 @@ private CharSequence generatePrimitiveArrayPropertyDecode( javaTypeName, propertyName, fieldLength, - generateFieldNotPresentCondition(inComposite, token.version(), encoding, indent), + generateFieldNotPresentCondition(inComposite, propertyToken.version(), encoding, indent), offset, typeSize, generateGet(encoding.primitiveType(), "pos", byteOrderStr))); @@ -1727,7 +1746,7 @@ private CharSequence generatePrimitiveArrayPropertyDecode( indent + " }\n", Generators.toUpperFirstChar(propertyName), fieldLength, - generateArrayFieldNotPresentCondition(token.version(), indent), + generateArrayFieldNotPresentCondition(propertyToken.version(), indent), offset)); sb.append(String.format("\n" + @@ -1741,7 +1760,7 @@ private CharSequence generatePrimitiveArrayPropertyDecode( indent + " return new String(dst, 0, end, %s);\n" + indent + " }\n\n", formatPropertyName(propertyName), - generateStringNotPresentCondition(token.version(), indent), + generateStringNotPresentCondition(propertyToken.version(), indent), fieldLength, offset, fieldLength, fieldLength, charset(encoding.characterEncoding()))); @@ -2259,7 +2278,7 @@ private CharSequence generateDecoderFields(final List tokens, final Strin switch (typeToken.signal()) { case ENCODING: - sb.append(generatePrimitiveDecoder(false, propertyName, typeToken, indent)); + sb.append(generatePrimitiveDecoder(false, propertyName, fieldToken, typeToken, indent)); break; case BEGIN_ENUM: @@ -2391,7 +2410,7 @@ private CharSequence generateEnumDecoder( indent + " }\n\n", enumName, propertyName, - generatePropertyNotPresentCondition(inComposite, DECODER, token.version(), indent), + generatePropertyNotPresentCondition(inComposite, DECODER, signalToken.version(), indent), enumName, generateGet(encoding.primitiveType(), "offset + " + token.offset(), byteOrderString(encoding))); } @@ -2449,7 +2468,7 @@ private CharSequence generateBitSetProperty( generateFlyweightPropertyJavadoc(indent + INDENT, propertyToken, bitSetName), bitSetName, propertyName, - generatePropertyNotPresentCondition(inComposite, codecType, bitsetToken.version(), indent), + generatePropertyNotPresentCondition(inComposite, codecType, propertyToken.version(), indent), propertyName, bitsetToken.offset(), propertyName)); @@ -2485,7 +2504,7 @@ private CharSequence generateCompositeProperty( generateFlyweightPropertyJavadoc(indent + INDENT, propertyToken, compositeName), compositeName, propertyName, - generatePropertyNotPresentCondition(inComposite, codecType, compositeToken.version(), indent), + generatePropertyNotPresentCondition(inComposite, codecType, propertyToken.version(), indent), propertyName, compositeToken.offset(), propertyName)); diff --git a/sbe-tool/src/test/java/uk/co/real_logic/sbe/generation/java/SchemaExtensionTest.java b/sbe-tool/src/test/java/uk/co/real_logic/sbe/generation/java/SchemaExtensionTest.java new file mode 100644 index 0000000000..4a1127ca03 --- /dev/null +++ b/sbe-tool/src/test/java/uk/co/real_logic/sbe/generation/java/SchemaExtensionTest.java @@ -0,0 +1,314 @@ +/* + * Copyright 2013-2018 Real Logic Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package uk.co.real_logic.sbe.generation.java; + +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; +import org.agrona.generation.CompilerUtil; +import org.agrona.generation.StringWriterOutputManager; +import org.junit.Before; +import org.junit.Test; +import uk.co.real_logic.sbe.TestUtil; +import uk.co.real_logic.sbe.ir.Ir; +import uk.co.real_logic.sbe.xml.IrGenerator; +import uk.co.real_logic.sbe.xml.MessageSchema; +import uk.co.real_logic.sbe.xml.ParserOptions; + +import java.util.Map; + +import static org.junit.Assert.*; +import static uk.co.real_logic.sbe.generation.java.ReflectionUtil.get; +import static uk.co.real_logic.sbe.generation.java.ReflectionUtil.set; +import static uk.co.real_logic.sbe.xml.XmlSchemaParser.parse; + +public class SchemaExtensionTest +{ + private static final Class BUFFER_CLASS = MutableDirectBuffer.class; + private static final String BUFFER_NAME = BUFFER_CLASS.getName(); + private static final Class READ_ONLY_BUFFER_CLASS = DirectBuffer.class; + private static final String READ_ONLY_BUFFER_NAME = READ_ONLY_BUFFER_CLASS.getName(); + + private final StringWriterOutputManager outputManager = new StringWriterOutputManager(); + + private Ir ir; + + @Before + public void setup() throws Exception + { + final ParserOptions options = ParserOptions.builder().stopOnError(true).build(); + final MessageSchema schema = parse(TestUtil.getLocalResource("extension-schema.xml"), options); + final IrGenerator irg = new IrGenerator(); + ir = irg.generate(schema); + + outputManager.clear(); + outputManager.setPackageName(ir.applicableNamespace()); + + generator().generate(); + } + + @Test + public void testMessage1() throws Exception + { + final UnsafeBuffer buffer = new UnsafeBuffer(new byte[4096]); + + { // Encode + final Object encoder = wrap(buffer, compile("TestMessage1Encoder").newInstance()); + + set(encoder, "tag1", int.class, 100); + set(encoder, "tag2", int.class, 200); + + final Object compositeEncoder = encoder.getClass().getMethod("tag3").invoke(encoder); + set(compositeEncoder, "value", int.class, 300); + + final Object enumConstant = getAEnumConstant(encoder, "AEnum", 1); + set(encoder, "tag4", enumConstant.getClass(), enumConstant); + + final Object setEncoder = encoder.getClass().getMethod("tag5").invoke(encoder); + set(setEncoder, "firstChoice", boolean.class, false); + set(setEncoder, "secondChoice", boolean.class, true); + } + + { // Decode version 0 + final Object decoderVersion0 = getMessage1Decoder(buffer, 4, 0); + assertEquals(100, get(decoderVersion0, "tag1")); + assertEquals(Integer.MIN_VALUE, get(decoderVersion0, "tag2")); + assertNull(get(decoderVersion0, "tag3")); + assertNull(get(decoderVersion0, "tag4")); + assertNull(get(decoderVersion0, "tag5")); + + assertEquals(0, decoderVersion0.getClass().getMethod("tag1SinceVersion").invoke(null)); + assertEquals(1, decoderVersion0.getClass().getMethod("tag2SinceVersion").invoke(null)); + assertEquals(2, decoderVersion0.getClass().getMethod("tag3SinceVersion").invoke(null)); + assertEquals(3, decoderVersion0.getClass().getMethod("tag4SinceVersion").invoke(null)); + assertEquals(4, decoderVersion0.getClass().getMethod("tag5SinceVersion").invoke(null)); + } + + { // Decode version 1 + final Object decoderVersion1 = getMessage1Decoder(buffer, 8, 1); + assertEquals(100, get(decoderVersion1, "tag1")); + assertEquals(200, get(decoderVersion1, "tag2")); + assertNull(get(decoderVersion1, "tag3")); + assertNull(get(decoderVersion1, "tag4")); + assertNull(get(decoderVersion1, "tag5")); + } + + { // Decode version 2 + final Object decoderVersion2 = getMessage1Decoder(buffer, 8, 2); + assertEquals(100, get(decoderVersion2, "tag1")); + assertEquals(200, get(decoderVersion2, "tag2")); + final Object compositeDecoder2 = get(decoderVersion2, "tag3"); + assertNotNull(compositeDecoder2); + assertEquals(300, get(compositeDecoder2, "value")); + assertNull(get(decoderVersion2, "tag4")); + assertNull(get(decoderVersion2, "tag5")); + } + + { // Decode version 3 + final Object decoderVersion3 = getMessage1Decoder(buffer, 12, 3); + assertEquals(100, get(decoderVersion3, "tag1")); + assertEquals(200, get(decoderVersion3, "tag2")); + final Object compositeDecoder3 = get(decoderVersion3, "tag3"); + assertNotNull(compositeDecoder3); + assertEquals(300, get(compositeDecoder3, "value")); + final Object enumConstant = getAEnumConstant(decoderVersion3, "AEnum", 1); + assertEquals(enumConstant, get(decoderVersion3, "tag4")); + assertNull(get(decoderVersion3, "tag5")); + } + + { // Decode version 4 + final Object decoderVersion4 = getMessage1Decoder(buffer, 12, 4); + assertEquals(100, get(decoderVersion4, "tag1")); + assertEquals(200, get(decoderVersion4, "tag2")); + final Object compositeDecoder4 = get(decoderVersion4, "tag3"); + assertNotNull(compositeDecoder4); + assertEquals(300, get(compositeDecoder4, "value")); + final Object enumConstant = getAEnumConstant(decoderVersion4, "AEnum", 1); + assertEquals(enumConstant, get(decoderVersion4, "tag4")); + final Object setDecoder = get(decoderVersion4, "tag5"); + assertNotNull(setDecoder); + assertEquals(false, get(setDecoder, "firstChoice")); + assertEquals(true, get(setDecoder, "secondChoice")); + } + } + + @Test + public void testMessage2() throws Exception + { + final UnsafeBuffer buffer = new UnsafeBuffer(new byte[4096]); + + { // Encode + final Object encoder = wrap(buffer, compile("TestMessage2Encoder").newInstance()); + + set(encoder, "tag1", int.class, 100); + set(encoder, "tag2", int.class, 200); + + final Object compositeEncoder = encoder.getClass().getMethod("tag3").invoke(encoder); + set(compositeEncoder, "value", int.class, 300); + + final Object enumConstant = getAEnumConstant(encoder, "AEnum", 1); + set(encoder, "tag4", enumConstant.getClass(), enumConstant); + + final Object setEncoder = encoder.getClass().getMethod("tag5").invoke(encoder); + set(setEncoder, "firstChoice", boolean.class, false); + set(setEncoder, "secondChoice", boolean.class, true); + } + + { // Decode version 0 + final Object decoderVersion0 = getMessage2Decoder(buffer, 4, 0); + assertEquals(100, get(decoderVersion0, "tag1")); + assertEquals(Integer.MIN_VALUE, get(decoderVersion0, "tag2")); + assertNull(get(decoderVersion0, "tag3")); + assertNull(get(decoderVersion0, "tag4")); + assertNull(get(decoderVersion0, "tag5")); + + assertEquals(0, decoderVersion0.getClass().getMethod("tag1SinceVersion").invoke(null)); + assertEquals(2, decoderVersion0.getClass().getMethod("tag2SinceVersion").invoke(null)); + assertEquals(1, decoderVersion0.getClass().getMethod("tag3SinceVersion").invoke(null)); + assertEquals(4, decoderVersion0.getClass().getMethod("tag4SinceVersion").invoke(null)); + assertEquals(3, decoderVersion0.getClass().getMethod("tag5SinceVersion").invoke(null)); + } + + { // Decode version 1 + final Object decoderVersion1 = getMessage2Decoder(buffer, 8, 1); + assertEquals(100, get(decoderVersion1, "tag1")); + assertEquals(Integer.MIN_VALUE, get(decoderVersion1, "tag2")); + final Object compositeDecoder2 = get(decoderVersion1, "tag3"); + assertNotNull(compositeDecoder2); + assertEquals(300, get(compositeDecoder2, "value")); + assertNull(get(decoderVersion1, "tag4")); + assertNull(get(decoderVersion1, "tag5")); + } + + { // Decode version 2 + final Object decoderVersion2 = getMessage2Decoder(buffer, 8, 2); + assertEquals(100, get(decoderVersion2, "tag1")); + assertEquals(200, get(decoderVersion2, "tag2")); + final Object compositeDecoder2 = get(decoderVersion2, "tag3"); + assertNotNull(compositeDecoder2); + assertEquals(300, get(compositeDecoder2, "value")); + assertNull(get(decoderVersion2, "tag4")); + assertNull(get(decoderVersion2, "tag5")); + } + + { // Decode version 3 + final Object decoderVersion3 = getMessage2Decoder(buffer, 12, 3); + assertEquals(100, get(decoderVersion3, "tag1")); + assertEquals(200, get(decoderVersion3, "tag2")); + final Object compositeDecoder3 = get(decoderVersion3, "tag3"); + assertNotNull(compositeDecoder3); + assertEquals(300, get(compositeDecoder3, "value")); + assertNull(get(decoderVersion3, "tag4")); + final Object setDecoder = get(decoderVersion3, "tag5"); + assertNotNull(setDecoder); + assertEquals(false, get(setDecoder, "firstChoice")); + assertEquals(true, get(setDecoder, "secondChoice")); + } + + { // Decode version 4 + final Object decoderVersion4 = getMessage2Decoder(buffer, 12, 4); + assertEquals(100, get(decoderVersion4, "tag1")); + assertEquals(200, get(decoderVersion4, "tag2")); + final Object compositeDecoder4 = get(decoderVersion4, "tag3"); + assertNotNull(compositeDecoder4); + assertEquals(300, get(compositeDecoder4, "value")); + final Object enumConstant = getAEnumConstant(decoderVersion4, "AEnum", 1); + assertEquals(enumConstant, get(decoderVersion4, "tag4")); + final Object setDecoder = get(decoderVersion4, "tag5"); + assertNotNull(setDecoder); + assertEquals(false, get(setDecoder, "firstChoice")); + assertEquals(true, get(setDecoder, "secondChoice")); + } + } + + private JavaGenerator generator() + { + return new JavaGenerator(ir, BUFFER_NAME, READ_ONLY_BUFFER_NAME, false, false, false, outputManager); + } + + private Object getMessage1Decoder(final UnsafeBuffer buffer, final int blockLength, final int version) + throws Exception + { + final Object decoder = compile("TestMessage1Decoder").newInstance(); + return wrap(buffer, decoder, blockLength, version); + } + + private Object getMessage2Decoder(final UnsafeBuffer buffer, final int blockLength, final int version) + throws Exception + { + final Object decoder = compile("TestMessage2Decoder").newInstance(); + return wrap(buffer, decoder, blockLength, version); + } + + private Object getAEnumConstant( + final Object flyweight, final String enumClassName, final int constantIndex) throws Exception + { + final String fqClassName = ir.applicableNamespace() + "." + enumClassName; + return flyweight.getClass().getClassLoader().loadClass(fqClassName).getEnumConstants()[constantIndex]; + } + + private Class compile(final String className) throws Exception + { + final String fqClassName = ir.applicableNamespace() + "." + className; + final Map sources = outputManager.getSources(); + final Class aClass = CompilerUtil.compileInMemory(fqClassName, sources); + if (aClass == null) + { + System.out.println(sources); + } + assertNotNull(aClass); + + return aClass; + } + + private static Object wrap( + final UnsafeBuffer buffer, final Object decoder, final int blockLength, final int version) throws Exception + { + return wrap(buffer, decoder, blockLength, version, READ_ONLY_BUFFER_CLASS); + } + + private static Object wrap( + final UnsafeBuffer buffer, + final Object decoder, + final int blockLength, + final int version, + final Class bufferClass) throws Exception + { + decoder + .getClass() + .getMethod("wrap", bufferClass, int.class, int.class, int.class) + .invoke(decoder, buffer, 0, blockLength, version); + + return decoder; + } + + private static void wrap( + final int bufferOffset, final Object flyweight, final MutableDirectBuffer buffer, final Class bufferClass) + throws Exception + { + flyweight + .getClass() + .getDeclaredMethod("wrap", bufferClass, int.class) + .invoke(flyweight, buffer, bufferOffset); + } + + private static Object wrap(final UnsafeBuffer buffer, final Object encoder) throws Exception + { + wrap(0, encoder, buffer, BUFFER_CLASS); + + return encoder; + } +} diff --git a/sbe-tool/src/test/resources/extension-schema.xml b/sbe-tool/src/test/resources/extension-schema.xml new file mode 100644 index 0000000000..713e67515f --- /dev/null +++ b/sbe-tool/src/test/resources/extension-schema.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + 0 + 1 + + + 0 + 1 + + + + + + + + + + + + + + + + + +