3

I have an @Audit annotation, it has many optional attributes, I need to enforce the use of one boolean attribute useAccount = true for certain packages.

I am trying to use archunit to accomplish this validation, that way whenever a developer commits code that breaks the rule the CI will break and inform the team.

This would break the build:

@Audit
public myMethod(...) {
...
}

This is the right way:

@Audit(useAccount = true)
public myMethod(...) {
...
}

The problem is that Archunit doesn't currently support asserting over methods. I was expecting to do something like:

methods().that().resideInAnyPackage("..controllers..", "..service..").and().areAnnotatedWith(Audit.class).should(attributeCheckCondition)

Then my custom condition attributeCheckCondition would take care of looking into the attribute value.

Is there a way of retrieving methods as we retrieve classes? Without having to write a more complicated predicate and condition?

raspacorp
  • 5,037
  • 11
  • 39
  • 51

3 Answers3

2

Update

Since ArchUnit 0.10.0 it is possible to create rules for members.

methods().that()
  .areDeclaredInClassesThat()
  .resideInAnyPackage("..controllers..", "..service..")
  .and()
  .areAnnotatedWith(Audit.class)
  .should(attributeCheckCondition)

See also Composing Member Rules in the User Guide.

Original Answer

Since there are currently no basic rule definitions available for methods, an intermediate step is necessary. ArchUnit has a ClassesTransformer to transform JavaClasses into a collection of other types.

ClassesTransformer<JavaMethod> methods = new AbstractClassesTransformer<JavaMethod>("methods") {
    @Override
    public Iterable<JavaMethod> doTransform(JavaClasses javaClasses) {
        Set<JavaMethod> allMethods = new HashSet<>();
        for (JavaClass javaClass : javaClasses) {
            allMethods.addAll(javaClass.getMethods());
        }
        return allMethods;
    }
};

This ClassesTransformer can then be used as a base for custom rule definitions.

ArchRule rule = ArchRuleDefinition.all(methods)
    .that(owner(resideInAnyPackage("..controllers..", "..service..")))
    .and(annotatedWith(Audit.class))
    .should(haveAttributeValue());
rule.check(javaClasses);

See also Rules with Custom Concepts in the User Guide and this issue.

Simulant
  • 19,190
  • 8
  • 63
  • 98
Roland Weisleder
  • 9,668
  • 7
  • 37
  • 59
2

I found a way of doing it with custom predicate and condition over classes, when I did that I was not aware of Roland's response which seems to be better, as it provides a way to express the rule assertion from the methods perspective which is why I was asking for.

However I wanted to post the solution here so it can be useful for others.

DescribedPredicate<JavaClass> HAVE_A_METHOD_ANNOTATED_WITH_AUDIT =
    new DescribedPredicate<JavaClass>("have a method annotated with @Audit")
    {
        @Override
        public boolean apply(JavaClass input)
        {
            return input.getMethods().stream().anyMatch(method -> method.isAnnotatedWith(Audit.class));
        }
    };

ArchCondition<JavaClass> ONLY_SET_ATTRIBUTE_USE_ACCOUNT_SET_TO_TRUE =
    new ArchCondition<JavaClass>("only set useAccount attribute to true")
    {
        @Override
        public void check(JavaClass item, ConditionEvents events)
        {
            item.getMethods().stream().filter(method ->
           method.isAnnotatedWith(Audit.class) && !method.getAnnotationOfType(Audit.class)
                                                            .useAccount()
            )
                .forEach(method -> {
                    String message = String.format(
                        "Method %s is annotated with @Audit but useAccount is not set to true",
                        method.getFullName());
                    events.add(SimpleConditionEvent.violated(method, message));
                });
        }
    };

Then the rule is expressed as:

ArchRule ANNOTATION_RULE = classes()
    .that()
    .resideInAnyPackage("..controller..", "..service..")
    .and(HAVE_A_METHOD_ANNOTATED_WITH_AUDIT)
    .should(ONLY_SET_ATTRIBUTE_USE_ACCOUNT_SET_TO_TRUE);
raspacorp
  • 5,037
  • 11
  • 39
  • 51
1

Here is another custom example in addition to @raspacorp (who inspired me!).

To check @Secured(ROLE) method annotation, I've implemented the following rule:

public static class SecuredByRoleArchCondition extends ArchCondition<JavaMethod> {
    private final String[] expectedRoles;

    public SecuredByRoleArchCondition(String[] expectedRoles) {
        super(String.format("accessed by @Secured methods with roles %s", Arrays.toString(expectedRoles)));
        this.expectedRoles = expectedRoles;
    }

    public static SecuredByRoleArchCondition haveSecuredAnnotationWithRoles(String... expectedRoles) {
        return new SecuredByRoleArchCondition(expectedRoles);
    }

    @Override
    public void check(JavaMethod javaMethod, ConditionEvents events) {
        if (!javaMethod.isAnnotatedWith(Secured.class)) {
            String message = String.format("Method %s annotation @Secured(%s) is missing",
                    javaMethod.getFullName(), Arrays.toString(expectedRoles));
            events.add(SimpleConditionEvent.violated(javaMethod, message));
            return;
        }
        String[] annotationRoleValues = javaMethod.getAnnotationOfType(Secured.class).value();
        if (!Arrays.equals(annotationRoleValues, expectedRoles)) {
            String message = String.format("Method %s @Secured with %s has wrong roles, expected %s instead",
                    javaMethod.getFullName(), Arrays.toString(annotationRoleValues), Arrays.toString(expectedRoles));
            events.add(SimpleConditionEvent.violated(javaMethod, message));
        }
    }
}

Here is a sample usage of this archCondition:

@ArchTest
static ArchRule admin_actions_with_post_mapping_should_be_secured_by_ADMIN_WRITE_role =
        methods()
                .that().areDeclaredInClassesThat().resideInAnyPackage(ADMIN_PACKAGES)
                .and().areAnnotatedWith(PostMapping.class)
                .should(haveSecuredAnnotationWithRoles("ADMIN_WRITE"));
boly38
  • 1,806
  • 24
  • 29