diff --git a/fluapigen-integrationTest/src/main/java/io/toolisticon/fluapigen/integrationtest/ValidatorExample.java b/fluapigen-integrationTest/src/main/java/io/toolisticon/fluapigen/integrationtest/ValidatorExample.java index af67139..24f5e28 100644 --- a/fluapigen-integrationTest/src/main/java/io/toolisticon/fluapigen/integrationtest/ValidatorExample.java +++ b/fluapigen-integrationTest/src/main/java/io/toolisticon/fluapigen/integrationtest/ValidatorExample.java @@ -10,6 +10,7 @@ import io.toolisticon.fluapigen.validation.api.Matches; import io.toolisticon.fluapigen.validation.api.MaxLength; import io.toolisticon.fluapigen.validation.api.NotNull; +import io.toolisticon.fluapigen.validation.api.Nullable; @FluentApi("ValidatorExampleStarter") public class ValidatorExample { @@ -26,10 +27,18 @@ interface MyBackingBean { // Fluent Api interfaces @FluentApiInterface(MyBackingBean.class) @FluentApiRoot + @Nullable public interface MyRootInterface { MyRootInterface setName(@NotNull @MaxLength(8) @Matches("aaa.*") @FluentApiBackingBeanMapping("name") String name); + @NotNull + MyRootInterface setNameWithNullableOnParameter(@Nullable @MaxLength(8) @Matches("aaa.*") @FluentApiBackingBeanMapping("name") String name); + + @Nullable + MyRootInterface setNameWithNullableOnMethod(@NotNull @MaxLength(8) @Matches("aaa.*") @FluentApiBackingBeanMapping("name") String name); + + @FluentApiCommand(MyCommand.class) void myCommand(); diff --git a/fluapigen-integrationTest/src/test/java/io/toolisticon/fluapigen/integrationtest/ValidatorExampleTest.java b/fluapigen-integrationTest/src/test/java/io/toolisticon/fluapigen/integrationtest/ValidatorExampleTest.java index 9c4dd9e..5bf81b8 100644 --- a/fluapigen-integrationTest/src/test/java/io/toolisticon/fluapigen/integrationtest/ValidatorExampleTest.java +++ b/fluapigen-integrationTest/src/test/java/io/toolisticon/fluapigen/integrationtest/ValidatorExampleTest.java @@ -1,5 +1,6 @@ package io.toolisticon.fluapigen.integrationtest; +import io.toolisticon.fluapigen.validation.api.ValidatorException; import org.junit.Test; public class ValidatorExampleTest { @@ -15,7 +16,14 @@ public void testValidator_failingValidation() { public void testValidator() { ValidatorExampleStarter.setName("aaaBDSXS").myCommand(); + ValidatorExampleStarter.setNameWithNullableOnParameter(null).myCommand(); + ValidatorExampleStarter.setNameWithNullableOnMethod("aaaBDSXS").myCommand(); } + @Test(expected = ValidatorException.class) + public void testOverruledNotNullAtParameter() { + ValidatorExampleStarter.setNameWithNullableOnMethod(null).myCommand(); + } + } diff --git a/fluapigen-processor/src/main/java/io/toolisticon/fluapigen/processor/ModelInterfaceMethodParameter.java b/fluapigen-processor/src/main/java/io/toolisticon/fluapigen/processor/ModelInterfaceMethodParameter.java index 9818b4c..e21ffee 100644 --- a/fluapigen-processor/src/main/java/io/toolisticon/fluapigen/processor/ModelInterfaceMethodParameter.java +++ b/fluapigen-processor/src/main/java/io/toolisticon/fluapigen/processor/ModelInterfaceMethodParameter.java @@ -3,12 +3,16 @@ import io.toolisticon.aptk.compilermessage.api.DeclareCompilerMessage; import io.toolisticon.aptk.tools.InterfaceUtils; import io.toolisticon.aptk.tools.TypeMirrorWrapper; +import io.toolisticon.aptk.tools.wrapper.ElementWrapper; import io.toolisticon.aptk.tools.wrapper.VariableElementWrapper; import io.toolisticon.fluapigen.api.FluentApiBackingBeanMapping; import io.toolisticon.fluapigen.api.FluentApiConverter; import io.toolisticon.fluapigen.api.TargetBackingBean; import io.toolisticon.fluapigen.validation.api.FluentApiValidator; +import javax.lang.model.element.Element; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -83,7 +87,29 @@ public boolean hasValidators() { } public List getValidators() { - return this.parameterElement.getAnnotations().stream().filter(e -> e.asElement().hasAnnotation(FluentApiValidator.class)).map(e -> new ModelValidator(this.parameterElement, e)).collect(Collectors.toList()); + + // First get all enclosing elements - order is from element to top level parent + List> enclosingElements = this.parameterElement.getAllEnclosingElements(true); + + // now map it to ModelValidators + List allValidatorsWithoutOverwrites = enclosingElements.stream() + .flatMap( + e -> e.getAnnotations().stream() + .filter(f -> f.asElement().hasAnnotation(FluentApiValidator.class)) + .map(f -> new ModelValidator(this.parameterElement, f)) + ).collect(Collectors.toList()); + + // Must remove overwritten validators - add them one by one from lowest to highest level and check if the element to add is already overruled by thos in list. + List result = new ArrayList(); + + for (ModelValidator nextValidator : allValidatorsWithoutOverwrites) { + if (!nextValidator.isOverruledBy(result)) { + result.add(nextValidator); + } + } + + return result; + } public String getParameterName() { diff --git a/fluapigen-processor/src/main/java/io/toolisticon/fluapigen/processor/ModelValidator.java b/fluapigen-processor/src/main/java/io/toolisticon/fluapigen/processor/ModelValidator.java index 4145fe3..87d5ea6 100644 --- a/fluapigen-processor/src/main/java/io/toolisticon/fluapigen/processor/ModelValidator.java +++ b/fluapigen-processor/src/main/java/io/toolisticon/fluapigen/processor/ModelValidator.java @@ -11,6 +11,7 @@ import javax.lang.model.element.Modifier; import java.util.Arrays; +import java.util.List; import java.util.stream.Collectors; public class ModelValidator { @@ -28,6 +29,23 @@ FluentApiValidatorWrapper getValidatorAnnotation() { return FluentApiValidatorWrapper.wrap(this.validatorAnnotation.asElement().unwrap()); } + // visible for testing + AnnotationMirrorWrapper getAnnotationMirrorWrapper() { + return validatorAnnotation; + } + + public boolean isOverruledBy (List existingLowestLevelValidators) { + for (ModelValidator existinModelValidator : existingLowestLevelValidators) { + + for (String overruledAnnotation : existinModelValidator.getValidatorAnnotation().overwritesAsFqn()) { + if (overruledAnnotation.equals(validatorAnnotation.asTypeMirror().getQualifiedName())) { + return true; + } + } + + } + return false; + } @DeclareCompilerMessage(code = "752", enumValueName = "ERROR_BROKEN_VALIDATOR_ATTRIBUTE_NAME_MISMATCH", message = "The configured validator ${0} seems to have a broken annotation attribute to validator constructor parameter mapping. Attributes names ${1} doesn't exist. Please fix validator implementation or don't use it.", processorClass = FluentApiProcessor.class) @DeclareCompilerMessage(code = "753", enumValueName = "ERROR_BROKEN_VALIDATOR_CONSTRUCTOR_PARAMETER_MAPPING", message = "The configured validator ${0} seems to have a broken annotation attribute to validator constructor parameter mapping. Please fix validator implementation or don't use it.", processorClass = FluentApiProcessor.class) diff --git a/fluapigen-processor/src/test/java/io/toolisticon/fluapigen/processor/ModelInterfaceMethodParameterTest.java b/fluapigen-processor/src/test/java/io/toolisticon/fluapigen/processor/ModelInterfaceMethodParameterTest.java new file mode 100644 index 0000000..81320c0 --- /dev/null +++ b/fluapigen-processor/src/test/java/io/toolisticon/fluapigen/processor/ModelInterfaceMethodParameterTest.java @@ -0,0 +1,120 @@ +package io.toolisticon.fluapigen.processor; + +import io.toolisticon.aptk.cute.APTKUnitTestProcessor; +import io.toolisticon.aptk.tools.wrapper.VariableElementWrapper; +import io.toolisticon.cute.Cute; +import io.toolisticon.cute.PassIn; +import io.toolisticon.fluapigen.validation.api.Matches; +import io.toolisticon.fluapigen.validation.api.NotNull; +import io.toolisticon.fluapigen.validation.api.Nullable; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Test; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.VariableElement; +import java.util.stream.Collectors; + +/** + * Unit test for {@link ModelInterfaceMethodParameter}. + */ +public class ModelInterfaceMethodParameterTest { + + interface TestClassWithoutValidators { + + public void testMethod(@PassIn String parameter); + + } + + + @Test + public void test_getValidators_withoutValidatorsPresent() { + Cute.unitTest().when().passInElement().fromClass(TestClassWithoutValidators.class) + .intoUnitTest(new APTKUnitTestProcessor() { + @Override + public void aptkUnitTest(ProcessingEnvironment processingEnvironment, VariableElement variableElement) { + + ModelInterfaceMethodParameter unit = new ModelInterfaceMethodParameter(VariableElementWrapper.wrap(variableElement), null, null); + + MatcherAssert.assertThat("Expect to find no validators", !unit.hasValidators()); + + } + }).executeTest(); + } + + @NotNull + interface TestClassWithValidators { + + public void testMethod(@PassIn @Matches(".*[@].*") String parameter); + + } + + @Test + public void test_getValidators_withValidatorsPresent() { + Cute.unitTest().when().passInElement().fromClass(TestClassWithValidators.class) + .intoUnitTest(new APTKUnitTestProcessor() { + @Override + public void aptkUnitTest(ProcessingEnvironment processingEnvironment, VariableElement variableElement) { + + ModelInterfaceMethodParameter unit = new ModelInterfaceMethodParameter(VariableElementWrapper.wrap(variableElement), null, null); + + MatcherAssert.assertThat("Expect to find validators", unit.hasValidators()); + MatcherAssert.assertThat("Expect to find 2 validators", unit.getValidators().size() == 2); + MatcherAssert.assertThat(unit.getValidators().stream().map(e -> e.getAnnotationMirrorWrapper().asTypeMirror().getQualifiedName()).collect(Collectors.toList()), Matchers.containsInAnyOrder(NotNull.class.getCanonicalName(), Matches.class.getCanonicalName())); + + + } + }).executeTest(); + } + + @NotNull + interface TestClassWithOverruledValidators { + + void testMethod(@PassIn @Nullable @Matches(".*[@].*") String parameter); + + } + + @Test + public void test_getValidators_withOverruledValidatorsPresent() { + Cute.unitTest().when().passInElement().fromClass(TestClassWithOverruledValidators.class) + .intoUnitTest(new APTKUnitTestProcessor() { + @Override + public void aptkUnitTest(ProcessingEnvironment processingEnvironment, VariableElement variableElement) { + + ModelInterfaceMethodParameter unit = new ModelInterfaceMethodParameter(VariableElementWrapper.wrap(variableElement), null, null); + + MatcherAssert.assertThat("Expect to find validators", unit.hasValidators()); + MatcherAssert.assertThat("Expect to find 2 validators", unit.getValidators().size() == 2); + MatcherAssert.assertThat(unit.getValidators().stream().map(e -> e.getAnnotationMirrorWrapper().asTypeMirror().getQualifiedName()).collect(Collectors.toList()), Matchers.containsInAnyOrder(Nullable.class.getCanonicalName(), Matches.class.getCanonicalName())); + + + } + }).executeTest(); + } + + @Nullable + interface TestClassWithOverruledValidators2 { + + void testMethod(@PassIn @NotNull @Matches(".*[@].*") String parameter); + + } + + @Test + public void test_getValidators_withOverruledValidatorsPresent2() { + Cute.unitTest().when().passInElement().fromClass(TestClassWithOverruledValidators2.class) + .intoUnitTest(new APTKUnitTestProcessor() { + @Override + public void aptkUnitTest(ProcessingEnvironment processingEnvironment, VariableElement variableElement) { + + ModelInterfaceMethodParameter unit = new ModelInterfaceMethodParameter(VariableElementWrapper.wrap(variableElement), null, null); + + MatcherAssert.assertThat("Expect to find validators", unit.hasValidators()); + MatcherAssert.assertThat("Expect to find 2 validators", unit.getValidators().size() == 2); + MatcherAssert.assertThat(unit.getValidators().stream().map(e -> e.getAnnotationMirrorWrapper().asTypeMirror().getQualifiedName()).collect(Collectors.toList()), Matchers.containsInAnyOrder(NotNull.class.getCanonicalName(), Matches.class.getCanonicalName())); + + + } + }).executeTest(); + } + +}