/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache 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://www.apache.org/licenses/LICENSE-2.0 * * 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.jooby.scanner; import static javaslang.API.$; import static javaslang.API.Case; import static javaslang.API.Match; import static javaslang.Predicates.is; import static javaslang.Predicates.noneOf; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Named; import javax.inject.Provider; import javax.inject.Singleton; import org.jooby.Env; import org.jooby.Jooby; import org.jooby.Router; import org.jooby.mvc.Path; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Service; import com.google.common.util.concurrent.ServiceManager; import com.google.inject.Binder; import com.google.inject.Module; import com.typesafe.config.Config; import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner; import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult; import javaslang.control.Try; /** * <h1>scanner</h1> * <p> * Classpath scanning services via * <a href="https://github.com/lukehutch/fast-classpath-scanner">FastClasspathScanner</a>. * FastClasspathScanner is an uber-fast, ultra-lightweight classpath scanner for Java, Scala and * other JVM languages. * </p> * * <h2>usage</h2> * * <pre>{@code * { * use(new Scanner()); * } * }</pre> * * <p> * This modules scan the application class-path and automatically discover and register * <code>MVC routes/controllers</code>, {@link org.jooby.Jooby.Module} and {@link Jooby} * applications. * </p> * * <p> * It scans the application package. That's the package where your bootstrap application belong to. * Multi-package scanning is available too: * </p> * * <pre>{@code * { * use(new Scanner("foo", "bar")); * } * }</pre> * * <h2>services</h2> * * <p> * The next example scans and initialize any class in the application package annotated with * <code>Named</code>: * </p> * * <pre>{@code * { * use(new Scanner() * .scan(Named.class) * ); * } * }</pre> * * <p> * The next example scans and initialize any class in the application package that implements * <code>MyService</code>: * </p> * * <pre>{@code * { * use(new Scanner() * .scan(MyService.class) * ); * } * }</pre> * * <p> * Guava {@link Service} are also supported: * </p> * * <pre>{@code * { * use(new Scanner() * .scan(com.google.common.util.concurrent.Service.class) * ); * * get("/guava", req -> { * ServiceManager sm = req.require(ServiceManager.class); * ... * }); * } * }</pre> * * <p> * They are added to {@link ServiceManager} and started and stopped automatically. * </p> * * <p> * Raw/plain Guice {@link Module} are supported too * </p> * : * * <pre>{@code * { * use(new Scanner() * .scan(Module.class) * ); * } * }</pre> * * <p> * Of course, you can combine two or more strategies: * </p> * * <pre>{@code * { * use(new Scanner() * .scan(MyService.class) * .scan(Named.class) * .scan(Singleton.class) * .scan(MyAnnotation.class) * ); * } * }</pre> * * <p> * In all cases, services are created as <code>singleton</code> and started/stopped automatically * when {@link PostConstruct} and {@link PreDestroy} annotations are present. * </p> * * @author edgar * @since 1.0.0 */ public class Scanner implements Jooby.Module { @SuppressWarnings("rawtypes") private static final Predicate<Class> A = Class::isAnnotation; @SuppressWarnings("rawtypes") private static final Predicate<Class> C = k -> !Modifier.isAbstract(k.getModifiers()); @SuppressWarnings("rawtypes") private static final Predicate<Class> I = A.negate().and(Class::isInterface); @SuppressWarnings("rawtypes") private static final Predicate<Class> S = noneOf(I, A); private List<String> packages; @SuppressWarnings("rawtypes") private Set<Class> serviceTypes = new LinkedHashSet<>(); /** * Creates a new {@link Scanner} and uses the provided scan spec or packages. * * @param scanSpec Scan spec or packages. See <a href= * "https://github.com/lukehutch/fast-classpath-scanner/wiki/2.-Constructor#specifying-more-complex-scanning-criteria">Scan * spec</a>. */ public Scanner(final String... scanSpec) { this.packages = Lists.newArrayList(scanSpec); } /** * Creates a new {@link Scanner} and use the application package (a.k.a as namespace). */ public Scanner() { } @SuppressWarnings({"unchecked", "rawtypes" }) @Override public void configure(final Env env, final Config conf, final Binder binder) { List<String> packages = Optional.ofNullable(this.packages) .orElseGet(() -> ImmutableList.of(conf.getString("application.ns"))); Set<String> spec = Sets.newLinkedHashSet(packages); serviceTypes.forEach(it -> spec.add(it.getPackage().getName())); FastClasspathScanner scanner = new FastClasspathScanner(spec.toArray(new String[spec.size()])); Router routes = env.router(); ClassLoader loader = getClass().getClassLoader(); Function<String, Class> loadClass = name -> Try.of(() -> loader.loadClass(name)).get(); // bind once as singleton + post/pre callbacks Set<Object> bindings = new HashSet<>(); Predicate<Object> once = bindings::add; Consumer<Class> bind = klass -> { binder.bind(klass).asEagerSingleton(); env.lifeCycle(klass); }; ScanResult result = scanner.scan(conf.getInt("runtime.processors") + 1); Predicate<String> inPackage = name -> packages.stream() .filter(name::startsWith) .findFirst() .isPresent(); /** Controllers: */ result.getNamesOfClassesWithAnnotation(Path.class) .stream() .filter(once) .map(loadClass) .filter(C) .forEach(routes::use); /** Modules: */ result.getNamesOfClassesImplementing(Jooby.Module.class) .stream() .filter(once) .map(loadClass) .filter(C) .forEach(klass -> Try .run(() -> ((Jooby.Module) newObject(klass)).configure(env, conf, binder)).get()); /** Apps: */ result.getNamesOfSubclassesOf(Jooby.class) .stream() .filter(once) .filter(is(conf.getString("application.class")).negate()) .map(loadClass) .filter(C) .forEach(klass -> routes.use(((Jooby) newObject(klass)))); /** Annotated with: */ serviceTypes.stream() .filter(A) .forEach(a -> { result.getNamesOfClassesWithAnnotation(a) .stream() .filter(once) .map(loadClass) .filter(C) .forEach(bind); }); /** Implements: */ serviceTypes.stream() .filter(I) .filter(noneOf(type(Jooby.Module.class), type(Module.class), type(Service.class))) .forEach(i -> { result.getNamesOfClassesImplementing(i) .stream() .filter(inPackage) .filter(once) .map(loadClass) .filter(C) .forEach(bind); }); /** SubclassOf: */ serviceTypes.stream() .filter(S) .forEach(k -> { result.getNamesOfSubclassesOf(k) .stream() .filter(inPackage) .filter(once) .map(loadClass) .filter(C) .forEach(bind); }); /** Guice modules: */ if (serviceTypes.contains(Module.class)) { result.getNamesOfClassesImplementing(Module.class) .stream() .filter(inPackage) .filter(once) .map(loadClass) .filter(C) .forEach(klass -> ((Module) newObject(klass)).configure(binder)); } /** Guava services: */ if (serviceTypes.contains(Service.class)) { Set<Class<Service>> guavaServices = new HashSet<>(); result.getNamesOfClassesImplementing(Service.class) .stream() .filter(inPackage) .filter(once) .map(loadClass) .filter(C) .forEach(guavaServices::add); if (guavaServices.size() > 0) { guavaServices(env, binder, guavaServices); } } } /** * Add a scan criteria like an annotation, interface or class. * * @param type A scan criteria/type. * @return This module. */ public Scanner scan(final Class<?> type) { // standard vs guice annotations Match(type).of( Case(is(Named.class), Arrays.asList(Named.class, com.google.inject.name.Named.class)), Case(is(com.google.inject.name.Named.class), Arrays.asList(Named.class, com.google.inject.name.Named.class)), Case(is(Singleton.class), Arrays.asList(Singleton.class, com.google.inject.Singleton.class)), Case(is(com.google.inject.Singleton.class), Arrays.asList(Singleton.class, com.google.inject.Singleton.class)), Case($(), Arrays.asList(type))) .forEach(serviceTypes::add); return this; } private static <T> T newObject(final Class<T> klass) { return Try.of(() -> klass.newInstance()).get(); } @SuppressWarnings({"unchecked", "rawtypes" }) private static void guavaServices(final Env env, final Binder binder, final Set<Class<Service>> serviceTypes) { Consumer<Class> guavaService = klass -> { binder.bind(klass).asEagerSingleton(); serviceTypes.add(klass); }; serviceTypes.forEach(guavaService); // lazy service manager AtomicReference<ServiceManager> sm = new AtomicReference<>(); Provider<ServiceManager> smProvider = () -> sm.get(); binder.bind(ServiceManager.class).toProvider(smProvider); // ask Guice for services, create ServiceManager and start services env.onStart(r -> { List<Service> services = serviceTypes.stream() .map(r::require) .collect(Collectors.toList()); sm.set(new ServiceManager(services)); sm.get().startAsync().awaitHealthy(); }); // stop services env.onStop(() -> { sm.get().stopAsync().awaitStopped(); }); } @SuppressWarnings({"unchecked", "rawtypes" }) private static Predicate<Class> type(final Class type) { return klass -> klass.isAssignableFrom(type); } }