/**
* 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);
}
}