|
|
@@ -0,0 +1,142 @@
|
|
|
+package net.ranides.assira.reflection.util;
|
|
|
+
|
|
|
+import org.slf4j.Logger;
|
|
|
+
|
|
|
+import net.ranides.assira.collection.query.CQuery;
|
|
|
+import net.ranides.assira.functional.checked.CheckedFunction;
|
|
|
+import net.ranides.assira.text.Charsets;
|
|
|
+import net.ranides.assira.text.IOStrings;
|
|
|
+import net.ranides.assira.text.StringTraits;
|
|
|
+import net.ranides.assira.text.StringUtils;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.net.URL;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Enumeration;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Optional;
|
|
|
+import java.util.function.Consumer;
|
|
|
+
|
|
|
+public class ServiceScanner<S> {
|
|
|
+
|
|
|
+ private static final String PREFIX = "META-INF/services/";
|
|
|
+
|
|
|
+ private final Class<S> service;
|
|
|
+
|
|
|
+ private ServiceResolver<S> resolver;
|
|
|
+
|
|
|
+ private Consumer<Throwable> listener;
|
|
|
+
|
|
|
+ private ClassLoader loader;
|
|
|
+
|
|
|
+ private ServiceScanner(Class<S> service) {
|
|
|
+ this.service = service;
|
|
|
+ this.loader = ClassLoader.getSystemClassLoader();
|
|
|
+ this.resolver = name -> Optional.of(service.cast(Class.forName(name, false, loader).newInstance()));
|
|
|
+ this.listener = error -> { };
|
|
|
+ }
|
|
|
+
|
|
|
+ public static <S> ServiceScanner<S> scanner(Class<S> service) {
|
|
|
+ return new ServiceScanner<>(service);
|
|
|
+ }
|
|
|
+
|
|
|
+ public ServiceScanner<S> loader(ClassLoader loader) {
|
|
|
+ this.loader = loader;
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ public ServiceScanner<S> resolver(ServiceResolver<S> handler) {
|
|
|
+ ServiceResolver<S> prev = this.resolver;
|
|
|
+ this.resolver = name -> {
|
|
|
+ try {
|
|
|
+ Optional<S> resolved = handler.apply(name);
|
|
|
+ if(resolved.isPresent()) {
|
|
|
+ return resolved;
|
|
|
+ }
|
|
|
+ } catch(ReflectiveOperationException cause) {
|
|
|
+ listener.accept(cause);
|
|
|
+ }
|
|
|
+ return prev.apply(name);
|
|
|
+ };
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ public ServiceScanner<S> listener(Consumer<Throwable> handler) {
|
|
|
+ this.listener = listener.andThen(handler);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ public ServiceScanner<S> logger(Logger logger) {
|
|
|
+ this.listener = listener.andThen(error -> logger.error("ServiceScanner error", error));
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ public CQuery<S> services() {
|
|
|
+ return names().mapOptional(type -> makeInstance(type));
|
|
|
+ }
|
|
|
+
|
|
|
+ public CQuery<String> names() {
|
|
|
+ String fqn = PREFIX + service.getName();
|
|
|
+ return CQuery.from()
|
|
|
+ .enumeration(() -> findFiles(fqn))
|
|
|
+ .flat(this::loadLines)
|
|
|
+ .mapOptional(this::parseLine)
|
|
|
+ .distinct();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ private Enumeration<URL> findFiles(String fqn) throws IOException {
|
|
|
+ return loader == null ? ClassLoader.getSystemResources(fqn) : loader.getResources(fqn);
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<String> loadLines(URL url) {
|
|
|
+ try {
|
|
|
+ return IOStrings.readLines(url.openStream(), Charsets.UTF8);
|
|
|
+ } catch (IOException e) {
|
|
|
+ listener.accept(new IllegalArgumentException("Invalid URL " + url.toString(), e));
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Optional<String> parseLine(String line) {
|
|
|
+ int end = StringUtils.indexOf(line, "#");
|
|
|
+ String name = StringUtils.trim(end>0 ? StringUtils.substr(line, end) : line);
|
|
|
+
|
|
|
+ if(StringTraits.isEmpty(name)) {
|
|
|
+ return Optional.empty();
|
|
|
+ }
|
|
|
+
|
|
|
+ if(name.indexOf(' ')>=0 || name.indexOf('\t')>=0) {
|
|
|
+ listener.accept(new IllegalArgumentException("Illegal configuration-file syntax " + line));
|
|
|
+ return Optional.empty();
|
|
|
+ }
|
|
|
+
|
|
|
+ if(!StringTraits.isJavaIdentifier(name)) {
|
|
|
+ listener.accept(new IllegalArgumentException(line + "Illegal provider-class name"));
|
|
|
+ return Optional.empty();
|
|
|
+ }
|
|
|
+
|
|
|
+ return Optional.of(name);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Optional<S> makeInstance(Class<? extends S> type) {
|
|
|
+ try {
|
|
|
+ return Optional.of(type.newInstance());
|
|
|
+ } catch (InstantiationException | IllegalAccessException e) {
|
|
|
+ listener.accept(e);
|
|
|
+ return Optional.empty();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Optional<S> makeInstance(String name) {
|
|
|
+ try {
|
|
|
+ return resolver.apply(name);
|
|
|
+ } catch(ReflectiveOperationException cause) {
|
|
|
+ listener.accept(cause);
|
|
|
+ }
|
|
|
+ return Optional.empty();
|
|
|
+ }
|
|
|
+
|
|
|
+ public interface ServiceResolver<S> extends CheckedFunction<String, Optional<S>, ReflectiveOperationException> { }
|
|
|
+
|
|
|
+}
|