Ranides Atterwim hace 4 años
padre
commit
e9a27b4c3c

+ 0 - 4
assira.commons/src/main/resources/META-INF/services/net.ranides.assira.io.uri.URIHandler

@@ -1,8 +1,4 @@
 net.ranides.assira.io.uri.impl.CFileHandler
 net.ranides.assira.io.uri.impl.CFTPHandler
 net.ranides.assira.io.uri.impl.CHTTPHandler
-net.ranides.assira.io.uri.impl.CJarHandler
-net.ranides.assira.io.uri.impl.CClassPathHandler
-net.ranides.assira.io.uri.impl.CDirHandler$CTestHandler
-net.ranides.assira.io.uri.impl.CDirHandler$CHomeHandler
 

+ 10 - 0
assira.core/src/main/java/net/ranides/assira/collection/query/CQuery.java

@@ -7,6 +7,7 @@
 package net.ranides.assira.collection.query;
 
 import java.io.IOException;
+import java.net.URL;
 import java.nio.file.Path;
 import java.util.*;
 import java.util.function.*;
@@ -15,6 +16,7 @@ import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
 import net.ranides.assira.collection.HashFunction;
+import net.ranides.assira.collection.iterators.EnumerationUtils;
 import net.ranides.assira.collection.iterators.IteratorUtils;
 import net.ranides.assira.functional.Consumers.EachConsumer;
 import net.ranides.assira.functional.Functions.EachFunction;
@@ -220,6 +222,14 @@ public interface CQuery<T> extends Iterable<T> {
             return iterable(() -> source.apply(param));
         }
 
+        public final <T extends F> CQuery<T> enumeration(CheckedSupplier<Enumeration<T>, ?> source) {
+            return iterable(() -> EnumerationUtils.iterator(source.$get()));
+        }
+
+        public final <T extends F,P> CQuery<T> enumeration(P param, CheckedFunction<? super P, Enumeration<T>, ?> source) {
+            return enumeration(() -> source.apply(param));
+        }
+
         public final <T extends F> CQuery<T> spliterator(CheckedSupplier<Spliterator<T>,?> source) {
             return stream(() -> StreamSupport.stream(source.$get(), false));
         }

+ 3 - 7
assira.core/src/main/java/net/ranides/assira/collection/query/CQueryAbstract.java

@@ -293,21 +293,17 @@ public abstract class CQueryAbstract<T> implements CQuery<T> {
 
     @Override
     public final CQuery<T> distinct() {
-        return new CQCollection<>(() -> new OpenSet<>(iterator()));
+        return filter(new OpenSet<T>()::add);
     }
 
     @Override
     public final CQuery<T> distinct(HashFunction<T> eq) {
-        return new CQCollection<>(() -> new CustomSet<>(iterator(), eq));
+        return filter(new CustomSet<>(eq)::add);
     }
 
     @Override
     public final CQuery<T> distinct(Comparator<? super T> cmp) {
-        return new CQCollection<>(() -> {
-            TreeSet<T> tree = new TreeSet<>(cmp);
-            into(tree);
-            return tree;
-        });
+        return filter(new TreeSet<>(cmp)::add);
     }
 
     @Override

+ 5 - 0
assira.core/src/main/java/net/ranides/assira/reflection/util/ClassUtils.java

@@ -51,6 +51,11 @@ public final class ClassUtils {
     public static <T> Optional<Class<? extends T>> asSubclass(Class<T> target, Class<?> source) {
         return (Optional)Optional.ofNullable(source).filter(target::isAssignableFrom);
     }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    public static <T> Optional<IClass<? extends T>> asSubclass(IClass<T> target, IClass<?> source) {
+        return (Optional)Optional.ofNullable(source).filter(target::isSuper);
+    }
     
     public static IClass<?> box(IClass<?> ic) {
         return ic.attributes().has(IAttribute.PRIMITIVE) ? IClass.typeinfo(box(ic.raw())) : ic;

+ 142 - 0
assira.core/src/main/java/net/ranides/assira/reflection/util/ServiceScanner.java

@@ -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> { }
+
+}

+ 14 - 1
assira.core/src/main/java/net/ranides/assira/text/StringTraits.java

@@ -64,5 +64,18 @@ public final class StringTraits {
     public static boolean contains(String text, String fragment) {
 		return null != text && text.contains(fragment);
 	}
-    
+
+    public static boolean isJavaIdentifier(String line) {
+        int cp = line.codePointAt(0);
+        if (!Character.isJavaIdentifierStart(cp)) {
+            return false;
+        }
+        for (int i = Character.charCount(cp), n = line.length(); i < n; i += Character.charCount(cp)) {
+            cp = line.codePointAt(i);
+            if (!Character.isJavaIdentifierPart(cp) && (cp != '.')) {
+                return false;
+            }
+        }
+        return true;
+    }
 }

+ 7 - 15
assira.core/src/main/java/net/ranides/assira/trace/TraceUtils.java

@@ -7,6 +7,7 @@
 package net.ranides.assira.trace;
 
 import net.ranides.assira.reflection.*;
+import net.ranides.assira.reflection.util.ServiceScanner;
 
 import java.util.List;
 import java.util.Optional;
@@ -44,21 +45,12 @@ public final class TraceUtils {
 
     }
 
-    private static final IStackWalker WALKER = init(
-        "net.ranides.assira.trace.FStackWalker11",
-        "net.ranides.assira.trace.FStackWalker8"
-    );
-
-    private static IStackWalker init(String... names) {
-        for(String name : names) {
-            try {
-                return (IStackWalker)Class.forName(name).newInstance();
-            } catch (ReflectiveOperationException ex) {
-                // continue
-            }
-        }
-        return new net.ranides.assira.trace.FStackWalker6();
-    }
+    private static final IStackWalker WALKER = ServiceScanner
+        .scanner(IStackWalker.class)
+        .resolver(name -> Optional.of(IStackWalker.class.cast(Class.forName(name).newInstance())))
+        .services()
+        .first()
+        .orElse(null);
 
     private TraceUtils() {
         /* utility class */

+ 3 - 0
assira.core/src/main/resources/META-INF/services/net.ranides.assira.trace.TraceUtils$IStackWalker

@@ -0,0 +1,3 @@
+net.ranides.assira.trace.FStackWalker11
+net.ranides.assira.trace.FStackWalker8
+net.ranides.assira.trace.FStackWalker6

+ 38 - 0
assira.core/src/test/java/net/ranides/assira/collection/query/CQueryTest.java

@@ -12,12 +12,16 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Comparator;
 import java.util.List;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.stream.Stream;
 
 import org.junit.Test;
+
+import net.ranides.assira.collection.HashFunction;
+
 import static net.ranides.assira.junit.NewAssert.*;
 
 /**
@@ -124,4 +128,38 @@ public class CQueryTest {
 
         assertEquals(Arrays.asList(1,2,20,200,3,4,40,400,5,6,60,600), value2);
     }
+
+    @Test
+    public void testDistinct() {
+        HashFunction<Integer> hash = new HashFunction<Integer>() {
+            @Override
+            public int hashCode(Integer object) {
+                return object % 40;
+            }
+
+            @Override
+            public boolean equals(Integer a, Integer b) {
+                return a % 10 == b % 40;
+            }
+        };
+
+        List<Integer> value1 = CQuery.from()
+            .values(1, 2, 3, 4, 1, 5, 2, 6, 41, 42, 73)
+            .distinct()
+            .list();
+
+        List<Integer> value2 = CQuery.from()
+            .values(1, 2, 3, 4, 1, 5, 2, 6, 41, 42, 73)
+            .distinct(hash)
+            .list();
+
+        List<Integer> value3 = CQuery.from()
+            .values(1, 2, 3, 4, 1, 5, 2, 6, 41, 42, 73)
+            .distinct(Comparator.comparingInt(a -> a % 10))
+            .list();
+
+        assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6, 41, 42, 73), value1);
+        assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6, 73), value2);
+        assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6), value3);
+    }
 }

+ 82 - 0
assira.core/src/test/java/net/ranides/assira/reflection/util/ServiceScannerTest.java

@@ -0,0 +1,82 @@
+package net.ranides.assira.reflection.util;
+
+import org.junit.Test;
+
+import net.ranides.assira.collection.query.CQuery;
+import net.ranides.assira.io.uri.URIHandler;
+import net.ranides.assira.text.Charsets;
+
+import java.nio.charset.Charset;
+import java.nio.charset.spi.CharsetProvider;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.*;
+
+public class ServiceScannerTest {
+
+    @Test
+    public void simple() {
+        ServiceScanner<URIHandler> handlers = ServiceScanner.scanner(URIHandler.class);
+
+        assertTrue( handlers.names().size() > 0 );
+        assertNotNull(handlers.services().first().orElse(null) );
+    }
+
+    @Test
+    public void merge() {
+        ServiceScanner<CharsetProvider> charsets = ServiceScanner.scanner(CharsetProvider.class);
+
+        assertTrue( charsets.names().size() > 1 );
+        assertNotNull(charsets.services().first().orElse(null) );
+
+        assertNotNull(charsets.services().filter(v -> v instanceof SpecialCharset).first().orElse(null) );
+    }
+
+    @Test
+    public void lazy() {
+        List<String> names = new ArrayList<>();
+
+        CQuery<URIHandler> query = ServiceScanner.scanner(URIHandler.class)
+            .resolver(name -> {
+                names.add(name);
+                return Optional.empty();
+            })
+            .services()
+            .limit(2);
+
+        List<?> list = query.list();
+
+        assertEquals(2,list.size());
+
+        // at this moment CQuery iterates a bit further than necessary
+        // it is acceptable in that sense, that it returns correct result...
+        // possibly that is because some iterators move forward inside `#hasNext` instead of `#next`?
+        // bug report is already created:
+        // https://git.ranides.net/projects/assira/issues/47
+        assertTrue(names.size() <= 3);
+    }
+
+    public static class SpecialCharset extends CharsetProvider {
+
+        public SpecialCharset() {
+            // do nothing
+        }
+
+        @Override
+        public Iterator<Charset> charsets() {
+            return Arrays.asList(Charsets.UTF8).iterator();
+        }
+
+        @Override
+        public Charset charsetForName(String charsetName) {
+            return Charsets.UTF8;
+        }
+    }
+}

+ 1 - 0
assira.core/src/test/resources/META-INF/services/java.nio.charset.spi.CharsetProvider

@@ -0,0 +1 @@
+net.ranides.assira.reflection.util.ServiceScannerTest$SpecialCharset