AnnotationScanner.java

/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 *
 * The Apereo Foundation licenses this file to you under the Educational
 * Community License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License
 * at:
 *
 *   http://opensource.org/licenses/ecl2.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 */

package org.opencastproject.graphql.util;

import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * This is a utility class that provides methods for scanning annotations in classes.
 * It provides a public method to find and return a collection of classes that are annotated with a specific annotation.
 * It also provides private methods to recursively scan a given path in a bundle for class files
 * and to load a class given its fully qualified name and a class loader.*
 */
public final class AnnotationScanner {

  private static final Logger logger = LoggerFactory.getLogger(AnnotationScanner.class);

  private AnnotationScanner() {
  }


  /**
   * Finds and returns a collection of classes that are annotated with a specific annotation.
   *
   * @param context The class from which the context class loader is derived. This is typically the class in which
   *                you are working.
   * @param annotation The annotation that the classes must have to be included in the returned collection.
   * @return A collection of classes that are annotated with the specified annotation.
   */
  public static Collection<Class<?>> findAnnotatedClasses(Class<?> context, Class<? extends Annotation> annotation) {
    return FrameworkUtil.getBundle(context.getClassLoader())
        .map(b -> entryPaths(b, context.getPackageName().replace('.', '/')))
        .orElse(Stream.empty())
        .map(clazz -> loadClass(clazz, context.getClassLoader()))
        .filter(Objects::nonNull)
        .filter(c -> c.isAnnotationPresent(annotation))
        .collect(Collectors.toSet());
  }

  /**
   * Recursively scans the given path in the bundle for class files, returning a stream of their fully qualified names.
   *
   * @param bundle The bundle to scan for class files.
   * @param path The path in the bundle to start scanning from.
   * @return A stream of fully qualified class names found in the bundle starting from the given path.
   */
  private static Stream<String> entryPaths(Bundle bundle, String path) {
    Enumeration<String> entries = bundle.getEntryPaths(path);
    if (entries == null) {
      return Stream.empty();
    }
    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(entries.asIterator(),
        Spliterator.IMMUTABLE | Spliterator.NONNULL | Spliterator.DISTINCT), false)
        .flatMap(name ->
            name.endsWith(".class") && !name.contains("$")
                ? Stream.of(name.replace('/', '.').replace(".class", ""))
                : entryPaths(bundle, name)
        );
  }

  /**
   * Attempts to load a class given its fully qualified name and a class loader.
   *
   * @param name The fully qualified name of the class to load.
   * @param classLoader The class loader to use to load the class.
   * @return The loaded class, or null if the class could not be found or loaded.
   */
  private static Class<?> loadClass(String name, ClassLoader classLoader) {
    try {
      return Class.forName(name, true, classLoader);
    } catch (ClassNotFoundException | NoClassDefFoundError e) {
      logger.debug("Class not found: {}", name);
      return null;
    }
  }
}