3

I have a function that provide a map of classes annotated with a given annotation

void <A extends Annotation> ImmutableMap<Class<?>, A> find(Class<A> annotation, String packageBase) {
    final ClassLoader loader = Thread.currentThread().getContextClassLoader();

    return ClassPath.from(loader).getTopLevelClassesRecursive(packageBase).stream()
            .filter(x -> x.load().getAnnotation(annotation) != null)
            .collect(Collectors.collectingAndThen(Collectors
                    .toMap(ClassPath.ClassInfo::load, x -> x.load().getAnnotation(annotation)), ImmutableMap::copyOf));
}

I would like to make a general provider with a cache, like the following example

@Singleton
public class AnnotatedClassProvider {
    private final Map<Class<? extends Annotation>, ImmutableMap<Class<?>, Object>> cache;
    private final String basePackage;

    public AnnotatedClassProvider(String basePackage) {
        this.basePackage = basePackage;
        this.cache = Maps.newHashMap();
    }

    public <A extends Annotation> ImmutableMap<Class<?>, A> get(Class<A> annotation) {
        ImmutableMap<Class<?>, A> cached = cache.get(annotation);
        if (cached != null)
            return cached;

        final ClassLoader loader = Thread.currentThread().getContextClassLoader();
        cached = ClassPath.from(loader).getTopLevelClassesRecursive(basePackage).stream()
                .filter(x -> x.load().getAnnotation(annotation) != null)
                .collect(Collectors.collectingAndThen(Collectors
                        .toMap(ClassPath.ClassInfo::load, x -> x.load().getAnnotation(annotation)), ImmutableMap::copyOf));
        this.cache.put(annotation, cached);
        return (cached);
    }
}

My problem: I don't find the way to replace Object by a generic A like in the get function, for the following map:

private final Map<Class<? extends Annotation>, ImmutableMap<Class<?>, Object>> cache;

EDIT:
It compiles when I don't specify the map generics but I have to cast in the get method. Is there a way to avoid this?

private final Map<Class<? extends Annotation>, ImmutableMap> cache;

I think it should look like this

private final <A extends Annotation> Map<Class<A>, ImmutableMap<Class<?>, A>> cache;
  • You can't bind the generic type parameter that was declared on one member to another member. You have to declare the type parameter at class level (make the whole class generic). – André Stannek Jul 21 '17 at 08:20
  • I could but like as described, it's a single instance of provider. Add a generic to my class will force me to create a Provider for each annotation type. Then my idea becomes useless –  Jul 21 '17 at 08:24
  • what you are trying to do can't be done. If you want to use a generic type parameter on a field, you have to declare it on your class. See https://stackoverflow.com/questions/32008752/java-generic-field-declaration – André Stannek Jul 21 '17 at 08:55
  • "I have to cast in the get method. Is there a way to avoid this?" - Nope. – JimmyB Jul 21 '17 at 08:56
  • "as described, it's a single instance of provider. Add a generic to my class will force me to create a Provider for each annotation type." - And there is a *single* instance of the `cache` map, with a single generic type. How should the map's *declaration* know which annotation you're querying for at runtime? – JimmyB Jul 21 '17 at 08:59
  • I see, then let me cast.. Would appreciate an answer to close this subject –  Jul 21 '17 at 08:59

1 Answers1

0

It seems to be impossible without a cast, so the following lines fixed my problem:

private final Map<Class<? extends Annotation>, ImmutableMap> cache;

public <A extends Annotation> ImmutableMap<Class<?>, A> get(Class<A> annotation) {
    @SuppressWarnings("unchecked")
    ImmutableMap<Class<?>, A> cached = cache.get(annotation);
    ...
}

For interesting people, that's how my provider look now

Interface

public interface AnnotatedClassProvider {
    <A extends Annotation> ImmutableMap<Class<?>, A> get(Class<A> annotation) throws IOException;
}

Abstract class

public abstract class AbstractAnnotatedClassProvider implements AnnotatedClassProvider {
    private final Map<Class<? extends Annotation>, ImmutableMap> cache;

    public AbstractAnnotatedClassProvider() {
        this.cache = Maps.newHashMap();
    }

    protected final <A extends Annotation> ImmutableMap<Class<?>, A> find(Class<A> annotation, @Nullable String basePackage) throws IOException {
        @SuppressWarnings("unchecked")
        ImmutableMap<Class<?>, A> cached = cache.get(annotation);

        if (cached != null)
            return cached;

        ClassPath classPath = ClassPath.from(Thread.currentThread().getContextClassLoader());

        ImmutableSet<ClassPath.ClassInfo> set = basePackage == null
                ? classPath.getAllClasses()
                : classPath.getTopLevelClasses(basePackage);

        cached = set.stream()
                .filter(x -> x.load().getAnnotation(annotation) != null)
                .collect(Collectors.collectingAndThen(Collectors
                        .toMap(ClassPath.ClassInfo::load, x -> x.load().getAnnotation(annotation)), ImmutableMap::copyOf));
        this.cache.put(annotation, cached);
        return (cached);
    }
}

Implementations

public final class Providers {
    public static AnnotatedClassProvider newBased(String basePackage) {
        return new AbstractAnnotatedClassProvider() {
            @Override
            public <A extends Annotation> ImmutableMap<Class<?>, A> get(Class<A> annotation) throws IOException {
                return super.find(annotation, basePackage);
            }
        };
    }

    public static AnnotatedClassProvider newSimple() {
        return new AbstractAnnotatedClassProvider() {
            @Override
            public <A extends Annotation> ImmutableMap<Class<?>, A> get(Class<A> annotation) throws IOException {
                return super.find(annotation, null);
            }
        };
    }
}

Example

@Retention(RUNTIME)
@Target(ElementType.TYPE)
public @interface Controller {
    String value();
}

package com.test

@Controller("mainController")
public class Main {

    public static void main(String[] args) {
        AnnotatedClassProvider provider = Providers.newBased("com.test");
        Map<Class<?>, Controller> classes = provider.get(Controller.class);

        classes.forEach((x, y ) -> System.out.println(String.format("Class: %s annotated with %s with value %s",
                x.getName(), y.getClass().getName(), y.value())));
    }
}

Output: Class: Main.java annotated with Controller.class with value mainController

Thanks for all comments.