Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle Meta-Annotations #57

Closed
rweisleder opened this issue Dec 28, 2017 · 8 comments
Closed

Handle Meta-Annotations #57

rweisleder opened this issue Dec 28, 2017 · 8 comments

Comments

@rweisleder
Copy link
Contributor

The Spring Framework allows (and encourages) to compose annotations, for example composing @Service and @Transactional to @TransactionalService

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Service
@Transactional
public @interface TransactionalService {
}

@TransactionalService
public class DummyBean {    
}

With ArchUnit it should be possible to access the meta-annotations @Service and @Transactional, for example

@Test
void serviceAnnotation() {
    JavaClasses classes = new ClassFileImporter().importPackagesOf(DummyBean.class);

    classes()
            .that().areAssignableTo(DummyBean.class)
            .should().beAnnotatedWith(Service.class)
            .check(classes);
}

With 0.5.0 this test fails with

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that are assignable to example.DummyBean should be annotated with @Service' was violated (1 times):
class example.DummyBean is not annotated with @Service

	at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:92)
	at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:81)
	at com.tngtech.archunit.lang.ArchRule$Factory$SimpleArchRule.check(ArchRule.java:190)
	at com.tngtech.archunit.lang.syntax.ObjectsShouldInternal.check(ObjectsShouldInternal.java:75)
	at example.DummyTest.serviceAnnotation(DummyTest.java:61)

See also the Spring Annotation Programming Model.

@codecholeric
Copy link
Collaborator

You're right, the predefined API doesn't support this case at the moment. However, you can add it yourself:

@Test
void serviceAnnotation() {
  JavaClasses classes = new ClassFileImporter().importPackagesOf(DummyBean.class);

  classes()
      .that().areAssignableTo(DummyBean.class)
      .should(beMetaAnnotatedWith(Service.class))
      .check(classes);
}

private ArchCondition<JavaClass> beMetaAnnotatedWith(
    final Class<? extends Annotation> annotationType) {

  final String annotationName = annotationType.getSimpleName();
  return new ArchCondition<JavaClass>("be meta-annotated with @" + annotationName) {
    @Override
    public void check(JavaClass javaClass, ConditionEvents events) {
      if (!isMetaAnnotated(javaClass)) {
        events.add(SimpleConditionEvent.violated(javaClass,
            String.format("Class %s is not meta-annotated with @%s",
                javaClass.getName(), annotationName)));
      }
    }

    private boolean isMetaAnnotated(JavaClass javaClass) {
      if (javaClass.isAnnotatedWith(annotationType)) {
        return true;
      }

      for (JavaClass metaAnnotationType : getMetaAnnotationTypes(javaClass.getAnnotations())) {
        if (metaAnnotationType.isAnnotatedWith(annotationType)) {
          return true;
        }
      }
      return false;
    }

    private Set<JavaClass> getMetaAnnotationTypes(Set<JavaAnnotation> annotations) {
      Set<JavaClass> types = new HashSet<>();
      for (JavaAnnotation annotation : annotations) {
        types.add(annotation.getType());
        types.addAll(getMetaAnnotationTypes(annotation.getType().getAnnotations()));
      }
      return types;
    }
  };
}

It's a little verbose, but it should do, what you want, if I've correctly understood your issue. Does this help you?

@codecholeric
Copy link
Collaborator

Actually I was just pondering, this can be written a little shorter, by using the syntax should().beAnnotatedWith(predicate) (this time in Java 8):

@Test
void serviceAnnotation() {
  JavaClasses classes = new ClassFileImporter().importPackagesOf(DummyBean.class);

  classes()
      .that().areAssignableTo(DummyBean.class)
      .should().beAnnotatedWith(metaAnnotation(Service.class))
      .check(classes);
}

private DescribedPredicate<JavaAnnotation> metaAnnotation(
    final Class<? extends Annotation> annotationType) {

  return new DescribedPredicate<JavaAnnotation>("meta-annotation @" + annotationType.getSimpleName()) {
    @Override
    public boolean apply(JavaAnnotation annotation) {
      return annotation.getType().isEquivalentTo(annotationType) ||
          getMetaAnnotations(annotation).stream()
              .anyMatch(type -> type.isAnnotatedWith(annotationType));
    }

    private Set<JavaClass> getMetaAnnotations(JavaAnnotation annotation) {
      Set<JavaClass> types = new HashSet<>(singleton(annotation.getType()));
      annotation.getType().getAnnotations().stream()
          .map(this::getMetaAnnotations)
          .forEach(types::addAll);
      return types;
    }
  };
}

Should do the same thing, as the code above, i.e. accept all types that are directly annotated with @Service, or meta-annotated with @Service...

@rweisleder
Copy link
Contributor Author

Yep, that's just right for my requirement.

Should this be part of ArchUnit, either as API or as an "how to extend"-example in the user guide?

@codecholeric
Copy link
Collaborator

Glad to hear that it covers your case 😃
Yes, I think this would be a good addition to the predefined API, so I'll keep this issue open and propose to extend the syntax consistently to annotatedWith:

classes().

  • that().areMetaAnnotatedWith(Class<? extends Annotation> clazz)
  • that().areMetaAnnotatedWith(String className)
  • that().areMetaAnnotatedWith(DescribedPredicate<JavaAnnotation> predicate)
  • should().beMetaAnnotatedWith(Class<? extends Annotation> clazz)
  • should().beMetaAnnotatedWith(String className)
  • should().beMetaAnnotatedWith(DescribedPredicate<JavaAnnotation> predicate)

and the respective negations. I'll see if someone wants to contribute a PR, otherwise I'll do it myself, as soon as I get around to it (which might be a little while though, since there are so many older issues in the pipeline).

@rweisleder
Copy link
Contributor Author

Since I'm able to build the project now (thanks @codecholeric), I will start to work on this issue.

@codecholeric
Copy link
Collaborator

Cool, thanks a lot 😃 Let me know, if you need any support!

@rweisleder
Copy link
Contributor Author

While implementing tests for this one, I found an issue relating to class resolving. If an annotation type is not imported directly, the information about meta-annotations are not present.

Example:

@Deprecated
public class ExampleTest {

  @Test
  public void demo() {
    ArchRule rule = classes()
        .that().haveSimpleName("ExampleTest")
        .should().beAnnotatedWith(metaAnnotation(Retention.class));

    ClassFileImporter importer = new ClassFileImporter();

    // this check succeeds
    JavaClasses allClasses = importer.importClasses(ExampleTest.class, Deprecated.class);
    rule.check(allClasses);

    // this check fails, but should succeed
    JavaClasses myClasses = importer.importClasses(ExampleTest.class);
    rule.check(myClasses);
  }

  private DescribedPredicate<? super JavaAnnotation> metaAnnotation(final Class<? extends Annotation> type) {
    return new DescribedPredicate<JavaAnnotation>("meta-annotation @" + type.getSimpleName()) {
      @Override
      public boolean apply(JavaAnnotation input) {
        return input.getType().isAnnotatedWith(type);
      }
    };
  }

}

@codecholeric any ideas how to proceed?

@codecholeric
Copy link
Collaborator

I don't think this is an issue, but a known limitation. Imagine this structure:

foo
 |-- bar/MyAnnotation.class
 |-- baz/AnnotatedClass.class

If you import via new ClassFileImporter().importPath("/foo/baz"), then ArchUnit will find that AnnotatedClass is annotated with MyAnnotation from the byte code. However, all further information about MyAnnotation (like meta-annotation) is not available. And there is simply no way, to know anything about MyAnnotation, if it was not imported as well.
On the other hand, this is something the user of ArchUnit has to take care of. All classes that are to be considered within tests have to be imported. So in my opinion, this is no obstacle for you, simply import all necessary classes within tests, including meta-annotated types to test.

One thing you could argue about though, is if the ClassResolverFromClasspath should be able to correctly import meta-annotations, since it does not do this at the moment (I would expect, that if resolveMissingDependenciesFromClasspath=true, the above test should work, since ArchUnit should resolve Deprecated including all meta-annotations from the classpath, however, meta-annotations are still missing).

I'll think about this, but I think this is a separate issue, independently of syntax extensions, since those work fine if all necessary classes are imported.

codecholeric added a commit that referenced this issue Apr 20, 2018
Add support for meta-annotations
@codecholeric codecholeric added this to the 0.8.0 milestone May 16, 2018
codecholeric added a commit that referenced this issue Feb 21, 2021
Add support for meta-annotations
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants