diff --git a/avro-builder/tests/tests-allavro/src/test/java/com/linkedin/avroutil1/builder/SpecificRecordTest.java b/avro-builder/tests/tests-allavro/src/test/java/com/linkedin/avroutil1/builder/SpecificRecordTest.java index 66f2a0552..e6f74a0ba 100644 --- a/avro-builder/tests/tests-allavro/src/test/java/com/linkedin/avroutil1/builder/SpecificRecordTest.java +++ b/avro-builder/tests/tests-allavro/src/test/java/com/linkedin/avroutil1/builder/SpecificRecordTest.java @@ -10,6 +10,8 @@ import com.linkedin.avroutil1.compatibility.RandomRecordGenerator; import com.linkedin.avroutil1.compatibility.RecordGenerationConfig; import com.linkedin.avroutil1.compatibility.StringConverterUtil; +import com.linkedin.avroutil1.compatibility.exception.AvroUtilException; +import com.linkedin.avroutil1.compatibility.exception.AvroUtilMissingFieldException; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Parameter; @@ -1696,6 +1698,35 @@ public void testConstructorTypeForPrimitive(Class clazz) { } } + public void testInputValidation() throws Exception { + try { + vs14.Amount.newBuilder().setAmount("").build(); + } catch (Exception e) { + Assert.assertTrue(e instanceof AvroUtilException); + Assert.assertTrue(e.getMessage().contains("not set and has no default value")); + } + + try { + new vs14.Amount(null, ""); + } catch (AvroUtilMissingFieldException e) { + Assert.assertTrue(e.getMessage().contains("Field currencyCode not set and has no default value")); + } + + try { + new vs14.Amount("", null); + } catch (AvroUtilMissingFieldException e) { + Assert.assertTrue(e.getMessage().contains("Field amount not set and has no default value")); + } + + try { + new vs14.Amount(null, null); + } catch (AvroUtilMissingFieldException e) { + Assert.assertTrue(e.getMessage().contains("Field currencyCode not set and has no default value")); + Assert.assertTrue(e.getMessage().contains("Field amount not set and has no default value")); + } + + } + @BeforeClass public void setup() { System.setProperty("org.apache.avro.specific.use_custom_coders", "true"); diff --git a/avro-codegen/src/main/java/com/linkedin/avroutil1/codegen/SpecificRecordClassGenerator.java b/avro-codegen/src/main/java/com/linkedin/avroutil1/codegen/SpecificRecordClassGenerator.java index 230239c8d..82a7e9731 100644 --- a/avro-codegen/src/main/java/com/linkedin/avroutil1/codegen/SpecificRecordClassGenerator.java +++ b/avro-codegen/src/main/java/com/linkedin/avroutil1/codegen/SpecificRecordClassGenerator.java @@ -12,6 +12,7 @@ import com.linkedin.avroutil1.compatibility.HelperConsts; import com.linkedin.avroutil1.compatibility.SourceCodeUtils; import com.linkedin.avroutil1.compatibility.StringUtils; +import com.linkedin.avroutil1.compatibility.exception.AvroUtilMissingFieldException; import com.linkedin.avroutil1.model.AvroArraySchema; import com.linkedin.avroutil1.model.AvroEnumSchema; import com.linkedin.avroutil1.model.AvroFixedSchema; @@ -527,7 +528,12 @@ private void addAllArgsConstructor(AvroRecordSchema recordSchema, AvroJavaStringRepresentation defaultFieldStringRepresentation, AvroJavaStringRepresentation defaultMethodStringRepresentation, TypeSpec.Builder classBuilder) { if(recordSchema.getFields().size() < 254) { - MethodSpec.Builder allArgsConstructorBuilder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC); + MethodSpec.Builder allArgsConstructorBuilder = MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addException(AvroUtilMissingFieldException.class); + + allArgsConstructorBuilder.addCode(getNullValidationBlockForNonNullableFields(recordSchema.getFields())); + for (AvroSchemaField field : recordSchema.getFields()) { //if declared schema, use fully qualified class (no import) String escapedFieldName = getFieldNameWithSuffix(field); @@ -719,14 +725,14 @@ private void populateBuilderClassBuilder(TypeSpec.Builder recordBuilder, AvroRec if (config.getMinimumSupportedAvroVersion().laterThan(AvroVersion.AVRO_1_8)) { buildMethodCodeBlockBuilder.beginControlFlow("catch (org.apache.avro.AvroMissingFieldException e)") - .addStatement("com.linkedin.avroutil1.compatibility.exception.AvroUtilMissingFieldException avroUtilException = new com.linkedin.avroutil1.compatibility.exception.AvroUtilMissingFieldException(e)") + .addStatement("com.linkedin.avroutil1.compatibility.exception.AvroUtilMissingFieldException avroUtilException = new com.linkedin.avroutil1.compatibility.exception.AvroUtilMissingFieldException(e.getMessage(), e)") .addStatement("avroUtilException.addParentField(record.getSchema().getField($S))", escapedFieldName) .addStatement("throw avroUtilException") .endControlFlow(); } buildMethodCodeBlockBuilder.beginControlFlow("catch (org.apache.avro.AvroRuntimeException e)") - .addStatement("com.linkedin.avroutil1.compatibility.exception.AvroUtilMissingFieldException avroUtilException = new com.linkedin.avroutil1.compatibility.exception.AvroUtilMissingFieldException(e)") + .addStatement("com.linkedin.avroutil1.compatibility.exception.AvroUtilMissingFieldException avroUtilException = new com.linkedin.avroutil1.compatibility.exception.AvroUtilMissingFieldException(e.getMessage(), e)") .addStatement("avroUtilException.addParentField(record.getSchema().getField($S))", escapedFieldName) .addStatement("throw avroUtilException") .endControlFlow() @@ -828,7 +834,7 @@ private void populateBuilderClassBuilder(TypeSpec.Builder recordBuilder, AvroRec buildMethodCodeBlockBuilder .beginControlFlow("catch ($T e)", Exception.class) - .addStatement("throw new com.linkedin.avroutil1.compatibility.exception.AvroUtilException(e)") + .addStatement("throw new com.linkedin.avroutil1.compatibility.exception.AvroUtilException(e.getMessage(), e)") .endControlFlow(); recordBuilder.addMethod( @@ -840,6 +846,31 @@ private void populateBuilderClassBuilder(TypeSpec.Builder recordBuilder, AvroRec .build()); } + private CodeBlock getNullValidationBlockForNonNullableFields(List fields) { + CodeBlock.Builder codeBuilder = CodeBlock.builder(); + List nonNullableFields = new ArrayList<>(); + for(AvroSchemaField field : fields) { + if(!SpecificRecordGeneratorUtil.nullValueAllowedForSchema(field)) { + nonNullableFields.add(field); + } + } + if(!nonNullableFields.isEmpty()) { + codeBuilder.addStatement("StringBuilder sb = new StringBuilder()"); + for(AvroSchemaField nonNullableField : nonNullableFields) { + String escapedFieldName = getFieldNameWithSuffix(nonNullableField); + codeBuilder.beginControlFlow("if ($L == null)", escapedFieldName) + .addStatement("sb.append($L)", + SpecificRecordGeneratorUtil.getNonNullableFieldErrorMessage(nonNullableField.getName())) + .endControlFlow(); + } + codeBuilder.beginControlFlow("if(sb.length() != 0)") + .addStatement("throw new com.linkedin.avroutil1.compatibility.exception.AvroUtilMissingFieldException(sb.toString())") + .endControlFlow(); + } + + return codeBuilder.build(); + } + private List getNewBuilderMethods(AvroRecordSchema recordSchema) { MethodSpec noArgNewBuilderSpec = MethodSpec.methodBuilder("newBuilder") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) diff --git a/avro-codegen/src/main/java/com/linkedin/avroutil1/codegen/SpecificRecordGeneratorUtil.java b/avro-codegen/src/main/java/com/linkedin/avroutil1/codegen/SpecificRecordGeneratorUtil.java index dfe5491a3..6b1d73868 100644 --- a/avro-codegen/src/main/java/com/linkedin/avroutil1/codegen/SpecificRecordGeneratorUtil.java +++ b/avro-codegen/src/main/java/com/linkedin/avroutil1/codegen/SpecificRecordGeneratorUtil.java @@ -375,4 +375,29 @@ public static boolean recordHasSimpleStringField(AvroRecordSchema schema) { return false; } + public static boolean nullValueAllowedForSchema(AvroSchemaField field) { + if(field.hasDefaultValue()) return true; + + if(isSingleTypeNullableUnionSchema(field.getSchema())) return true; + + // NULL, Boolean, Int, Float, Long, Double, String, Bytes + if(field.getSchema().type().isPrimitive()) return false; + + // Array, Map + if(field.getSchema().type().isCollection()) return true; + + switch (field.getSchema().type()) { + case ENUM: + case FIXED: return false; + case UNION: + case RECORD: return true; + default: + throw new IllegalArgumentException("type : "+ field.getSchema().type()+ "not handled."); + } + } + + public static String getNonNullableFieldErrorMessage(String fieldName) { + return ("\"Field " + fieldName + " not set and has no default value\\n\""); + } + } diff --git a/helper/helper/src/main/java/com/linkedin/avroutil1/compatibility/exception/AvroUtilException.java b/helper/helper/src/main/java/com/linkedin/avroutil1/compatibility/exception/AvroUtilException.java index d7e2f67df..34bccd5c8 100644 --- a/helper/helper/src/main/java/com/linkedin/avroutil1/compatibility/exception/AvroUtilException.java +++ b/helper/helper/src/main/java/com/linkedin/avroutil1/compatibility/exception/AvroUtilException.java @@ -17,4 +17,7 @@ public AvroUtilException(Exception e) { public AvroUtilException(String message) { super(message); } + public AvroUtilException(String message, Exception e) { + super(message, e); + } } diff --git a/helper/helper/src/main/java/com/linkedin/avroutil1/compatibility/exception/AvroUtilMissingFieldException.java b/helper/helper/src/main/java/com/linkedin/avroutil1/compatibility/exception/AvroUtilMissingFieldException.java index 90d949948..f107b7272 100644 --- a/helper/helper/src/main/java/com/linkedin/avroutil1/compatibility/exception/AvroUtilMissingFieldException.java +++ b/helper/helper/src/main/java/com/linkedin/avroutil1/compatibility/exception/AvroUtilMissingFieldException.java @@ -21,6 +21,13 @@ public class AvroUtilMissingFieldException extends AvroUtilException{ public AvroUtilMissingFieldException(AvroRuntimeException e) { super(e); } + public AvroUtilMissingFieldException(String errorMessage, AvroRuntimeException e) { + super(errorMessage, e); + } + + public AvroUtilMissingFieldException(String errorMessage) { + super(errorMessage); + } public AvroUtilMissingFieldException(String message, Schema.Field field) { super(message);