/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.avro.compiler.specific; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertThat; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.equalTo; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.apache.avro.AvroTestUtil; import org.apache.avro.LogicalTypes; import org.apache.avro.Schema; import org.apache.avro.SchemaBuilder; import org.apache.avro.generic.GenericData.StringType; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import javax.tools.JavaCompiler; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; @RunWith(JUnit4.class) public class TestSpecificCompiler { private final String schemaSrcPath = "src/test/resources/simple_record.avsc"; private final String velocityTemplateDir = "src/main/velocity/org/apache/avro/compiler/specific/templates/java/classic/"; private File src; private File outputDir; private File outputFile; @Before public void setUp() { this.src = new File(this.schemaSrcPath); this.outputDir = AvroTestUtil.tempDirectory(getClass(), "specific-output"); this.outputFile = new File(this.outputDir, "SimpleRecord.java"); if (outputFile.exists() && !outputFile.delete()) { throw new IllegalStateException("unable to delete " + outputFile); } } @After public void tearDow() { if (this.outputFile != null) { this.outputFile.delete(); } } /** Uses the system's java compiler to actually compile the generated code. */ static void assertCompilesWithJavaCompiler(Collection<SpecificCompiler.OutputFile> outputs) throws IOException { if (outputs.isEmpty()) return; // Nothing to compile! JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); File dstDir = AvroTestUtil.tempFile(TestSpecificCompiler.class, "realCompiler"); List<File> javaFiles = new ArrayList<File>(); for (SpecificCompiler.OutputFile o : outputs) { javaFiles.add(o.writeToDestination(null, dstDir)); } JavaCompiler.CompilationTask cTask = compiler.getTask(null, fileManager, null, null, null, fileManager.getJavaFileObjects( javaFiles.toArray(new File[javaFiles.size()]))); boolean compilesWithoutError = cTask.call(); assertTrue(compilesWithoutError); } private static Schema createSampleRecordSchema(int numStringFields, int numDoubleFields) { SchemaBuilder.FieldAssembler<Schema> sb = SchemaBuilder.record("sample.record").fields(); for (int i = 0; i < numStringFields; i++) { sb.name("sf_" + i).type().stringType().noDefault(); } for (int i = 0; i < numDoubleFields; i++) { sb.name("df_" + i).type().doubleType().noDefault(); } return sb.endRecord(); } private SpecificCompiler createCompiler() throws IOException { Schema.Parser parser = new Schema.Parser(); Schema schema = parser.parse(this.src); SpecificCompiler compiler = new SpecificCompiler(schema); compiler.setTemplateDir(this.velocityTemplateDir); compiler.setStringType(StringType.CharSequence); return compiler; } @Test public void testCanReadTemplateFilesOnTheFilesystem() throws IOException, URISyntaxException{ SpecificCompiler compiler = createCompiler(); compiler.compileToDestination(this.src, this.outputDir); assertTrue(this.outputFile.exists()); } @Test public void testPublicFieldVisibility() throws IOException { SpecificCompiler compiler = createCompiler(); compiler.setFieldVisibility(SpecificCompiler.FieldVisibility.PUBLIC); assertFalse(compiler.deprecatedFields()); assertTrue(compiler.publicFields()); assertFalse(compiler.privateFields()); compiler.compileToDestination(this.src, this.outputDir); assertTrue(this.outputFile.exists()); BufferedReader reader = new BufferedReader(new FileReader(this.outputFile)); String line = null; while ((line = reader.readLine()) != null) { // No line, once trimmed, should start with a deprecated field declaration // nor a private field declaration. Since the nested builder uses private // fields, we cannot do the second check. line = line.trim(); assertFalse("Line started with a deprecated field declaration: " + line, line.startsWith("@Deprecated public int value")); } reader.close(); } @Test public void testCreateAllArgsConstructor() throws Exception { SpecificCompiler compiler = createCompiler(); compiler.compileToDestination(this.src, this.outputDir); assertTrue(this.outputFile.exists()); BufferedReader reader = new BufferedReader(new FileReader(this.outputFile)); String line = null; boolean foundAllArgsConstructor = false; while (!foundAllArgsConstructor && (line = reader.readLine()) != null) { foundAllArgsConstructor = line.contains("All-args constructor"); } reader.close(); assertTrue(foundAllArgsConstructor); } @Test public void testMaxValidParameterCounts() throws Exception { Schema validSchema1 = createSampleRecordSchema(SpecificCompiler.MAX_FIELD_PARAMETER_UNIT_COUNT, 0); assertCompilesWithJavaCompiler(new SpecificCompiler(validSchema1).compile()); Schema validSchema2 = createSampleRecordSchema(SpecificCompiler.MAX_FIELD_PARAMETER_UNIT_COUNT - 2, 1); assertCompilesWithJavaCompiler(new SpecificCompiler(validSchema1).compile()); } @Test public void testInvalidParameterCounts() throws Exception { Schema invalidSchema1 = createSampleRecordSchema(SpecificCompiler.MAX_FIELD_PARAMETER_UNIT_COUNT + 1, 0); SpecificCompiler compiler = new SpecificCompiler(invalidSchema1); assertCompilesWithJavaCompiler(compiler.compile()); Schema invalidSchema2 = createSampleRecordSchema(SpecificCompiler.MAX_FIELD_PARAMETER_UNIT_COUNT, 10); compiler = new SpecificCompiler(invalidSchema2); assertCompilesWithJavaCompiler(compiler.compile()); } @Test public void testMaxParameterCounts() throws Exception { Schema validSchema1 = createSampleRecordSchema(SpecificCompiler.MAX_FIELD_PARAMETER_UNIT_COUNT, 0); assertTrue(new SpecificCompiler(validSchema1).compile().size() > 0); Schema validSchema2 = createSampleRecordSchema(SpecificCompiler.MAX_FIELD_PARAMETER_UNIT_COUNT - 2, 1); assertTrue(new SpecificCompiler(validSchema2).compile().size() > 0); Schema validSchema3 = createSampleRecordSchema(SpecificCompiler.MAX_FIELD_PARAMETER_UNIT_COUNT - 1, 1); assertTrue(new SpecificCompiler(validSchema3).compile().size() > 0); Schema validSchema4 = createSampleRecordSchema(SpecificCompiler.MAX_FIELD_PARAMETER_UNIT_COUNT + 1, 0); assertTrue(new SpecificCompiler(validSchema4).compile().size() > 0); } @Test(expected=RuntimeException.class) public void testCalcAllArgConstructorParameterUnitsFailure() { Schema nonRecordSchema = SchemaBuilder.array().items().booleanType(); new SpecificCompiler().calcAllArgConstructorParameterUnits(nonRecordSchema); } @Test public void testPublicDeprecatedFieldVisibility() throws IOException { SpecificCompiler compiler = createCompiler(); assertTrue(compiler.deprecatedFields()); assertTrue(compiler.publicFields()); assertFalse(compiler.privateFields()); compiler.compileToDestination(this.src, this.outputDir); assertTrue(this.outputFile.exists()); BufferedReader reader = new BufferedReader(new FileReader(this.outputFile)); String line = null; while ((line = reader.readLine()) != null) { // No line, once trimmed, should start with a public field declaration line = line.trim(); assertFalse("Line started with a public field declaration: " + line, line.startsWith("public int value")); } reader.close(); } @Test public void testPrivateFieldVisibility() throws IOException { SpecificCompiler compiler = createCompiler(); compiler.setFieldVisibility(SpecificCompiler.FieldVisibility.PRIVATE); assertFalse(compiler.deprecatedFields()); assertFalse(compiler.publicFields()); assertTrue(compiler.privateFields()); compiler.compileToDestination(this.src, this.outputDir); assertTrue(this.outputFile.exists()); BufferedReader reader = new BufferedReader(new FileReader(this.outputFile)); String line = null; while ((line = reader.readLine()) != null) { // No line, once trimmed, should start with a public field declaration // or with a deprecated public field declaration line = line.trim(); assertFalse("Line started with a public field declaration: " + line, line.startsWith("public int value")); assertFalse("Line started with a deprecated field declaration: " + line, line.startsWith("@Deprecated public int value")); } reader.close(); } @Test public void testSettersCreatedByDefault() throws IOException { SpecificCompiler compiler = createCompiler(); assertTrue(compiler.isCreateSetters()); compiler.compileToDestination(this.src, this.outputDir); assertTrue(this.outputFile.exists()); BufferedReader reader = new BufferedReader(new FileReader(this.outputFile)); int foundSetters = 0; String line = null; while ((line = reader.readLine()) != null) { // We should find the setter in the main class line = line.trim(); if (line.startsWith("public void setValue(")) { foundSetters++; } } reader.close(); assertEquals("Found the wrong number of setters", 1, foundSetters); } @Test public void testSettersNotCreatedWhenOptionTurnedOff() throws IOException { SpecificCompiler compiler = createCompiler(); compiler.setCreateSetters(false); assertFalse(compiler.isCreateSetters()); compiler.compileToDestination(this.src, this.outputDir); assertTrue(this.outputFile.exists()); BufferedReader reader = new BufferedReader(new FileReader(this.outputFile)); String line = null; while ((line = reader.readLine()) != null) { // No setter should be found line = line.trim(); assertFalse("No line should include the setter: " + line, line.startsWith("public void setValue(")); } reader.close(); } @Test public void testSettingOutputCharacterEncoding() throws Exception { SpecificCompiler compiler = createCompiler(); // Generated file in default encoding compiler.compileToDestination(this.src, this.outputDir); byte[] fileInDefaultEncoding = new byte[(int) this.outputFile.length()]; FileInputStream is = new FileInputStream(this.outputFile); is.read(fileInDefaultEncoding); is.close(); //close input stream otherwise delete might fail if (!this.outputFile.delete()) { throw new IllegalStateException("unable to delete " + this.outputFile); //delete otherwise compiler might not overwrite because src timestamp hasnt changed. } // Generate file in another encoding (make sure it has different number of bytes per character) String differentEncoding = Charset.defaultCharset().equals(Charset.forName("UTF-16")) ? "UTF-32" : "UTF-16"; compiler.setOutputCharacterEncoding(differentEncoding); compiler.compileToDestination(this.src, this.outputDir); byte[] fileInDifferentEncoding = new byte[(int) this.outputFile.length()]; is = new FileInputStream(this.outputFile); is.read(fileInDifferentEncoding); is.close(); // Compare as bytes assertThat("Generated file should contain different bytes after setting non-default encoding", fileInDefaultEncoding, not(equalTo(fileInDifferentEncoding))); // Compare as strings assertThat("Generated files should contain the same characters in the proper encodings", new String(fileInDefaultEncoding), equalTo(new String(fileInDifferentEncoding, differentEncoding))); } @Test public void testJavaTypeWithDecimalLogicalTypeEnabled() throws Exception { SpecificCompiler compiler = createCompiler(); compiler.setEnableDecimalLogicalType(true); Schema dateSchema = LogicalTypes.date() .addToSchema(Schema.create(Schema.Type.INT)); Schema timeSchema = LogicalTypes.timeMillis() .addToSchema(Schema.create(Schema.Type.INT)); Schema timestampSchema = LogicalTypes.timestampMillis() .addToSchema(Schema.create(Schema.Type.LONG)); Schema decimalSchema = LogicalTypes.decimal(9,2) .addToSchema(Schema.create(Schema.Type.BYTES)); Schema uuidSchema = LogicalTypes.uuid() .addToSchema(Schema.create(Schema.Type.STRING)); // Date/time types should always use upper level java classes // Decimal type target class depends on configuration // UUID should always be CharSequence since we haven't added its // support in SpecificRecord Assert.assertEquals("Should use Joda LocalDate for date type", "org.joda.time.LocalDate", compiler.javaType(dateSchema)); Assert.assertEquals("Should use Joda LocalTime for time-millis type", "org.joda.time.LocalTime", compiler.javaType(timeSchema)); Assert.assertEquals("Should use Joda DateTime for timestamp-millis type", "org.joda.time.DateTime", compiler.javaType(timestampSchema)); Assert.assertEquals("Should use Java BigDecimal type", "java.math.BigDecimal", compiler.javaType(decimalSchema)); Assert.assertEquals("Should use Java CharSequence type", "java.lang.CharSequence", compiler.javaType(uuidSchema)); } @Test public void testJavaTypeWithDecimalLogicalTypeDisabled() throws Exception { SpecificCompiler compiler = createCompiler(); compiler.setEnableDecimalLogicalType(false); Schema dateSchema = LogicalTypes.date() .addToSchema(Schema.create(Schema.Type.INT)); Schema timeSchema = LogicalTypes.timeMillis() .addToSchema(Schema.create(Schema.Type.INT)); Schema timestampSchema = LogicalTypes.timestampMillis() .addToSchema(Schema.create(Schema.Type.LONG)); Schema decimalSchema = LogicalTypes.decimal(9,2) .addToSchema(Schema.create(Schema.Type.BYTES)); Schema uuidSchema = LogicalTypes.uuid() .addToSchema(Schema.create(Schema.Type.STRING)); // Date/time types should always use upper level java classes // Decimal type target class depends on configuration // UUID should always be CharSequence since we haven't added its // support in SpecificRecord Assert.assertEquals("Should use Joda LocalDate for date type", "org.joda.time.LocalDate", compiler.javaType(dateSchema)); Assert.assertEquals("Should use Joda LocalTime for time-millis type", "org.joda.time.LocalTime", compiler.javaType(timeSchema)); Assert.assertEquals("Should use Joda DateTime for timestamp-millis type", "org.joda.time.DateTime", compiler.javaType(timestampSchema)); Assert.assertEquals("Should use ByteBuffer type", "java.nio.ByteBuffer", compiler.javaType(decimalSchema)); Assert.assertEquals("Should use Java CharSequence type", "java.lang.CharSequence", compiler.javaType(uuidSchema)); } @Test public void testJavaUnbox() throws Exception { SpecificCompiler compiler = createCompiler(); compiler.setEnableDecimalLogicalType(false); Schema intSchema = Schema.create(Schema.Type.INT); Schema longSchema = Schema.create(Schema.Type.LONG); Schema floatSchema = Schema.create(Schema.Type.FLOAT); Schema doubleSchema = Schema.create(Schema.Type.DOUBLE); Schema boolSchema = Schema.create(Schema.Type.BOOLEAN); Assert.assertEquals("Should use int for Type.INT", "int", compiler.javaUnbox(intSchema)); Assert.assertEquals("Should use long for Type.LONG", "long", compiler.javaUnbox(longSchema)); Assert.assertEquals("Should use float for Type.FLOAT", "float", compiler.javaUnbox(floatSchema)); Assert.assertEquals("Should use double for Type.DOUBLE", "double", compiler.javaUnbox(doubleSchema)); Assert.assertEquals("Should use boolean for Type.BOOLEAN", "boolean", compiler.javaUnbox(boolSchema)); Schema dateSchema = LogicalTypes.date() .addToSchema(Schema.create(Schema.Type.INT)); Schema timeSchema = LogicalTypes.timeMillis() .addToSchema(Schema.create(Schema.Type.INT)); Schema timestampSchema = LogicalTypes.timestampMillis() .addToSchema(Schema.create(Schema.Type.LONG)); // Date/time types should always use upper level java classes, even though // their underlying representations are primitive types Assert.assertEquals("Should use Joda LocalDate for date type", "org.joda.time.LocalDate", compiler.javaUnbox(dateSchema)); Assert.assertEquals("Should use Joda LocalTime for time-millis type", "org.joda.time.LocalTime", compiler.javaUnbox(timeSchema)); Assert.assertEquals("Should use Joda DateTime for timestamp-millis type", "org.joda.time.DateTime", compiler.javaUnbox(timestampSchema)); } @Test public void testNullableTypesJavaUnbox() throws Exception { SpecificCompiler compiler = createCompiler(); compiler.setEnableDecimalLogicalType(false); // Nullable types should return boxed types instead of primitive types Schema nullableIntSchema1 = Schema.createUnion( Schema.create(Schema.Type.NULL), Schema.create(Schema.Type.INT)); Schema nullableIntSchema2 = Schema.createUnion( Schema.create(Schema.Type.INT), Schema.create(Schema.Type.NULL)); Assert.assertEquals("Should return boxed type", compiler.javaUnbox(nullableIntSchema1), "java.lang.Integer"); Assert.assertEquals("Should return boxed type", compiler.javaUnbox(nullableIntSchema2), "java.lang.Integer"); Schema nullableLongSchema1 = Schema.createUnion( Schema.create(Schema.Type.NULL), Schema.create(Schema.Type.LONG)); Schema nullableLongSchema2 = Schema.createUnion( Schema.create(Schema.Type.LONG), Schema.create(Schema.Type.NULL)); Assert.assertEquals("Should return boxed type", compiler.javaUnbox(nullableLongSchema1), "java.lang.Long"); Assert.assertEquals("Should return boxed type", compiler.javaUnbox(nullableLongSchema2), "java.lang.Long"); Schema nullableFloatSchema1 = Schema.createUnion( Schema.create(Schema.Type.NULL), Schema.create(Schema.Type.FLOAT)); Schema nullableFloatSchema2 = Schema.createUnion( Schema.create(Schema.Type.FLOAT), Schema.create(Schema.Type.NULL)); Assert.assertEquals("Should return boxed type", compiler.javaUnbox(nullableFloatSchema1), "java.lang.Float"); Assert.assertEquals("Should return boxed type", compiler.javaUnbox(nullableFloatSchema2), "java.lang.Float"); Schema nullableDoubleSchema1 = Schema.createUnion( Schema.create(Schema.Type.NULL), Schema.create(Schema.Type.DOUBLE)); Schema nullableDoubleSchema2 = Schema.createUnion( Schema.create(Schema.Type.DOUBLE), Schema.create(Schema.Type.NULL)); Assert.assertEquals("Should return boxed type", compiler.javaUnbox(nullableDoubleSchema1), "java.lang.Double"); Assert.assertEquals("Should return boxed type", compiler.javaUnbox(nullableDoubleSchema2), "java.lang.Double"); Schema nullableBooleanSchema1 = Schema.createUnion( Schema.create(Schema.Type.NULL), Schema.create(Schema.Type.BOOLEAN)); Schema nullableBooleanSchema2 = Schema.createUnion( Schema.create(Schema.Type.BOOLEAN), Schema.create(Schema.Type.NULL)); Assert.assertEquals("Should return boxed type", compiler.javaUnbox(nullableBooleanSchema1), "java.lang.Boolean"); Assert.assertEquals("Should return boxed type", compiler.javaUnbox(nullableBooleanSchema2), "java.lang.Boolean"); } @Test public void testLogicalTypesWithMultipleFields() throws Exception { Schema logicalTypesWithMultipleFields = new Schema.Parser().parse( new File("src/test/resources/simple_record.avsc")); assertCompilesWithJavaCompiler( new SpecificCompiler(logicalTypesWithMultipleFields).compile()); } @Test public void testConversionInstanceWithDecimalLogicalTypeDisabled() throws Exception { SpecificCompiler compiler = createCompiler(); compiler.setEnableDecimalLogicalType(false); Schema dateSchema = LogicalTypes.date() .addToSchema(Schema.create(Schema.Type.INT)); Schema timeSchema = LogicalTypes.timeMillis() .addToSchema(Schema.create(Schema.Type.INT)); Schema timestampSchema = LogicalTypes.timestampMillis() .addToSchema(Schema.create(Schema.Type.LONG)); Schema decimalSchema = LogicalTypes.decimal(9,2) .addToSchema(Schema.create(Schema.Type.BYTES)); Schema uuidSchema = LogicalTypes.uuid() .addToSchema(Schema.create(Schema.Type.STRING)); Assert.assertEquals("Should use DATE_CONVERSION for date type", "DATE_CONVERSION", compiler.conversionInstance(dateSchema)); Assert.assertEquals("Should use TIME_CONVERSION for time type", "TIME_CONVERSION", compiler.conversionInstance(timeSchema)); Assert.assertEquals("Should use TIMESTAMP_CONVERSION for date type", "TIMESTAMP_CONVERSION", compiler.conversionInstance(timestampSchema)); Assert.assertEquals("Should use null for decimal if the flag is off", "null", compiler.conversionInstance(decimalSchema)); Assert.assertEquals("Should use null for decimal if the flag is off", "null", compiler.conversionInstance(uuidSchema)); } @Test public void testConversionInstanceWithDecimalLogicalTypeEnabled() throws Exception { SpecificCompiler compiler = createCompiler(); compiler.setEnableDecimalLogicalType(true); Schema dateSchema = LogicalTypes.date() .addToSchema(Schema.create(Schema.Type.INT)); Schema timeSchema = LogicalTypes.timeMillis() .addToSchema(Schema.create(Schema.Type.INT)); Schema timestampSchema = LogicalTypes.timestampMillis() .addToSchema(Schema.create(Schema.Type.LONG)); Schema decimalSchema = LogicalTypes.decimal(9,2) .addToSchema(Schema.create(Schema.Type.BYTES)); Schema uuidSchema = LogicalTypes.uuid() .addToSchema(Schema.create(Schema.Type.STRING)); Assert.assertEquals("Should use DATE_CONVERSION for date type", "DATE_CONVERSION", compiler.conversionInstance(dateSchema)); Assert.assertEquals("Should use TIME_CONVERSION for time type", "TIME_CONVERSION", compiler.conversionInstance(timeSchema)); Assert.assertEquals("Should use TIMESTAMP_CONVERSION for date type", "TIMESTAMP_CONVERSION", compiler.conversionInstance(timestampSchema)); Assert.assertEquals("Should use null for decimal if the flag is off", "DECIMAL_CONVERSION", compiler.conversionInstance(decimalSchema)); Assert.assertEquals("Should use null for decimal if the flag is off", "null", compiler.conversionInstance(uuidSchema)); } public void testToFromByteBuffer() { } }