First ACR search experiments
authorMathieu Baudier <mbaudier@argeo.org>
Wed, 26 Apr 2023 16:39:55 +0000 (18:39 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Wed, 26 Apr 2023 16:39:55 +0000 (18:39 +0200)
12 files changed:
org.argeo.api.acr/src/org/argeo/api/acr/ContentSession.java
org.argeo.api.acr/src/org/argeo/api/acr/search/AndFilter.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/BasicSearch.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/Composition.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/Constraint.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/ContentFilter.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/Intersection.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/OrFilter.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/search/Union.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/spi/ContentProvider.java
org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java
org.argeo.cms/src/org/argeo/cms/acr/MountManager.java

index b7d37dc10a876d38a14bad895c9e0e0a4d3280fc..b8ecd98da97ae29b548fffef0cb44f01ad668d32 100644 (file)
@@ -3,18 +3,25 @@ package org.argeo.api.acr;
 import java.util.Locale;
 import java.util.concurrent.CompletionStage;
 import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
 
 import javax.security.auth.Subject;
 import javax.xml.namespace.NamespaceContext;
 
+import org.argeo.api.acr.search.BasicSearch;
+
+/** An authenticated session to a repository. */
 public interface ContentSession extends NamespaceContext {
        Subject getSubject();
 
        Locale getLocale();
 
        Content get(String path);
-       
+
        boolean exists(String path);
 
        CompletionStage<ContentSession> edit(Consumer<ContentSession> work);
+
+       Stream<Content> search(Consumer<BasicSearch> search);
 }
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/AndFilter.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/AndFilter.java
new file mode 100644 (file)
index 0000000..e58b212
--- /dev/null
@@ -0,0 +1,9 @@
+package org.argeo.api.acr.search;
+
+public class AndFilter extends ContentFilter<Intersection> {
+
+       public AndFilter() {
+               super(Intersection.class);
+       }
+
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/BasicSearch.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/BasicSearch.java
new file mode 100644 (file)
index 0000000..8028f5d
--- /dev/null
@@ -0,0 +1,96 @@
+package org.argeo.api.acr.search;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import javax.xml.namespace.QName;
+
+import org.argeo.api.acr.DName;
+import org.argeo.api.acr.QNamed;
+
+public class BasicSearch {
+
+       private List<QName> select = new ArrayList<>();
+       private List<Scope> from = new ArrayList<>();
+
+       private ContentFilter<? extends Composition> where;
+
+       public BasicSearch select(QNamed... attr) {
+               for (QNamed q : attr)
+                       select.add(q.qName());
+               return this;
+       }
+
+       public BasicSearch select(QName... attr) {
+               select.addAll(Arrays.asList(attr));
+               return this;
+       }
+
+       public BasicSearch from(URI uri) {
+               return from(uri, Depth.INFINITTY);
+       }
+
+       public BasicSearch from(URI uri, Depth depth) {
+               Objects.requireNonNull(uri);
+               Objects.requireNonNull(depth);
+               Scope scope = new Scope(uri, depth);
+               from.add(scope);
+               return this;
+       }
+
+       public BasicSearch where(Consumer<AndFilter> and) {
+               if (where != null)
+                       throw new IllegalStateException("A where clause is already set");
+               AndFilter subFilter = new AndFilter();
+               and.accept(subFilter);
+               where = subFilter;
+               return this;
+       }
+
+       public List<QName> getSelect() {
+               return select;
+       }
+
+       public List<Scope> getFrom() {
+               return from;
+       }
+
+       public ContentFilter<? extends Composition> getWhere() {
+               return where;
+       }
+
+       public static enum Depth {
+               ZERO, ONE, INFINITTY;
+       }
+
+       public static class Scope {
+
+               URI uri;
+               Depth depth;
+
+               public Scope(URI uri, Depth depth) {
+                       this.uri = uri;
+                       this.depth = depth;
+               }
+
+               public URI getUri() {
+                       return uri;
+               }
+
+               public Depth getDepth() {
+                       return depth;
+               }
+
+       }
+
+       static void main(String[] args) {
+               BasicSearch search = new BasicSearch();
+               search.select(DName.creationdate.qName()) //
+                               .from(URI.create("/test")) //
+                               .where((f) -> f.eq(DName.creationdate.qName(), ""));
+       }
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/Composition.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/Composition.java
new file mode 100644 (file)
index 0000000..80786f4
--- /dev/null
@@ -0,0 +1,4 @@
+package org.argeo.api.acr.search;
+interface Composition {
+}
+
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/Constraint.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/Constraint.java
new file mode 100644 (file)
index 0000000..fc4313d
--- /dev/null
@@ -0,0 +1,4 @@
+package org.argeo.api.acr.search;
+
+public interface Constraint {
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/ContentFilter.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/ContentFilter.java
new file mode 100644 (file)
index 0000000..c5f5fc6
--- /dev/null
@@ -0,0 +1,183 @@
+package org.argeo.api.acr.search;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import javax.xml.namespace.QName;
+
+import org.argeo.api.acr.DName;
+import org.argeo.api.acr.QNamed;
+
+public abstract class ContentFilter<COMPOSITION extends Composition> implements Constraint {
+       private Set<Constraint> constraintss = new HashSet<>();
+
+       private COMPOSITION composition;
+
+       boolean negateNextOperator = false;
+
+       @SuppressWarnings("unchecked")
+       ContentFilter(Class<COMPOSITION> clss) {
+               if (clss == null)
+                       this.composition = null;
+               else if (Intersection.class.isAssignableFrom(clss))
+                       this.composition = (COMPOSITION) new Intersection(this);
+               else if (Union.class.isAssignableFrom(clss))
+                       this.composition = (COMPOSITION) new Union(this);
+               else
+                       throw new IllegalArgumentException("Unkown composition " + clss);
+       }
+
+       /*
+        * LOGICAL OPERATORS
+        */
+
+       public COMPOSITION all(Consumer<AndFilter> and) {
+               AndFilter subFilter = new AndFilter();
+               and.accept(subFilter);
+               addConstraint(subFilter);
+               return composition;
+       }
+
+       public COMPOSITION any(Consumer<OrFilter> or) {
+               OrFilter subFilter = new OrFilter();
+               or.accept(subFilter);
+               addConstraint(subFilter);
+               return composition;
+       }
+
+       public ContentFilter<COMPOSITION> not() {
+               negateNextOperator = !negateNextOperator;
+               return this;
+       }
+
+       /*
+        * NON WEBDAV
+        */
+       public COMPOSITION isContentClass(QName... contentClass) {
+               addConstraint(new IsContentClass(contentClass));
+               return composition;
+       }
+
+       public COMPOSITION isContentClass(QNamed... contentClass) {
+               addConstraint(new IsContentClass(contentClass));
+               return composition;
+       }
+
+       /*
+        * COMPARISON OPERATORS
+        */
+
+       public COMPOSITION eq(QName attr, Object value) {
+               addConstraint(new Eq(attr, value));
+               return composition;
+       }
+
+       public COMPOSITION eq(QNamed attr, Object value) {
+               addConstraint(new Eq(attr.qName(), value));
+               return composition;
+       }
+
+       /*
+        * UTILITIES
+        */
+       protected void addConstraint(Constraint operator) {
+               checkAddConstraint();
+               Constraint operatorToAdd;
+               if (negateNextOperator) {
+                       operatorToAdd = new Not(operator);
+                       negateNextOperator = false;
+               } else {
+                       operatorToAdd = operator;
+               }
+               constraintss.add(operatorToAdd);
+       }
+
+       /** Checks that the root operator is not set. */
+       private void checkAddConstraint() {
+               if (composition == null && !constraintss.isEmpty())
+                       throw new IllegalStateException("An operator is already registered (" + constraintss.iterator().next()
+                                       + ") and no composition is defined");
+       }
+
+       /*
+        * ACCESSORs
+        */
+       public Set<Constraint> getConstraints() {
+               return constraintss;
+       }
+
+       public boolean isUnion() {
+               return composition instanceof Union;
+       }
+
+       /*
+        * CLASSES
+        */
+
+       public static class Not implements Constraint {
+               final Constraint negated;
+
+               public Not(Constraint negated) {
+                       this.negated = negated;
+               }
+
+               public Constraint getNegated() {
+                       return negated;
+               }
+
+       }
+
+       public static class Eq implements Constraint {
+               final QName prop;
+               final Object value;
+
+               public Eq(QName prop, Object value) {
+                       super();
+                       this.prop = prop;
+                       this.value = value;
+               }
+
+               public QName getProp() {
+                       return prop;
+               }
+
+               public Object getValue() {
+                       return value;
+               }
+
+       }
+
+       public static class IsContentClass implements Constraint {
+               final QName[] contentClasses;
+
+               public IsContentClass(QName[] contentClasses) {
+                       this.contentClasses = contentClasses;
+               }
+
+               public IsContentClass(QNamed[] contentClasses) {
+                       this.contentClasses = new QName[contentClasses.length];
+                       for (int i = 0; i < contentClasses.length; i++)
+                               this.contentClasses[i] = contentClasses[i].qName();
+               }
+
+               public QName[] getContentClasses() {
+                       return contentClasses;
+               }
+
+       }
+
+       public static void main(String[] args) {
+               AndFilter filter = new AndFilter();
+               filter.eq(new QName("test"), "test").and().not().eq(new QName("type"), "integer");
+
+               OrFilter unionFilter = new OrFilter();
+               unionFilter.all((f) -> {
+                       f.eq(DName.displayname, "").and().eq(DName.creationdate, "");
+               }).or().not().any((f) -> {
+                       f.eq(DName.creationdate, "").or().eq(DName.displayname, "");
+               });
+
+       }
+
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/Intersection.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/Intersection.java
new file mode 100644 (file)
index 0000000..5fff2ae
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.api.acr.search;
+class Intersection implements Composition {
+       ContentFilter<Intersection> filter;
+
+       @SuppressWarnings("unchecked")
+       public Intersection(ContentFilter<?> filter) {
+               this.filter = (ContentFilter<Intersection>) filter;
+       }
+
+       public ContentFilter<Intersection> and() {
+               return filter;
+       }
+
+}
+
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/OrFilter.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/OrFilter.java
new file mode 100644 (file)
index 0000000..40460d4
--- /dev/null
@@ -0,0 +1,9 @@
+package org.argeo.api.acr.search;
+
+public class OrFilter extends ContentFilter<Union> {
+
+       public OrFilter() {
+               super(Union.class);
+       }
+
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/search/Union.java b/org.argeo.api.acr/src/org/argeo/api/acr/search/Union.java
new file mode 100644 (file)
index 0000000..d4b342b
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.api.acr.search;
+
+class Union implements Composition {
+       ContentFilter<Union> filter;
+
+       @SuppressWarnings("unchecked")
+       public Union(ContentFilter<?> filter) {
+               this.filter = (ContentFilter<Union>) filter;
+       }
+
+       public ContentFilter<Union> or() {
+               return filter;
+       }
+
+}
index 72aa162b3b59716af8972c256b6a36d06d476053..25b9be5c2081b924b7f08a0d4f0ce72b8a5e1559 100644 (file)
@@ -1,9 +1,13 @@
 package org.argeo.api.acr.spi;
 
 import java.util.Iterator;
+import java.util.Spliterator;
 
 import javax.xml.namespace.NamespaceContext;
 
+import org.argeo.api.acr.Content;
+import org.argeo.api.acr.search.BasicSearch;
+
 public interface ContentProvider extends NamespaceContext {
 
        ProvidedContent get(ProvidedSession session, String relativePath);
@@ -21,6 +25,10 @@ public interface ContentProvider extends NamespaceContext {
                return prefixes.hasNext() ? prefixes.next() : null;
        }
 
+       default Spliterator<Content> search(ProvidedSession session, BasicSearch search, String relPath) {
+               throw new UnsupportedOperationException();
+       }
+
 //     default ContentName parsePrefixedName(String nameWithPrefix) {
 //             return NamespaceUtils.parsePrefixedName(this, nameWithPrefix);
 //     }
index 17a03fdc501bf9d138998f597c4e1663f0c330f5..af7dca046ab636b014c7c601f0908ff3060661a6 100644 (file)
@@ -2,17 +2,25 @@ package org.argeo.cms.acr;
 
 import java.util.HashSet;
 import java.util.Locale;
+import java.util.Map;
+import java.util.NavigableMap;
 import java.util.Set;
+import java.util.Spliterator;
+import java.util.TreeMap;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 import java.util.function.Consumer;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 
 import javax.security.auth.Subject;
 
 import org.argeo.api.acr.Content;
 import org.argeo.api.acr.ContentSession;
 import org.argeo.api.acr.DName;
+import org.argeo.api.acr.search.BasicSearch;
+import org.argeo.api.acr.search.BasicSearch.Scope;
 import org.argeo.api.acr.spi.ContentProvider;
 import org.argeo.api.acr.spi.ProvidedContent;
 import org.argeo.api.acr.spi.ProvidedRepository;
@@ -170,4 +178,113 @@ class CmsContentSession implements ProvidedSession {
                }
                return sessionRunDir;
        }
+
+       /*
+        * SEARCH
+        */
+       @Override
+       public Stream<Content> search(Consumer<BasicSearch> search) {
+               BasicSearch s = new BasicSearch();
+               search.accept(s);
+               NavigableMap<String, SearchPartition> searchPartitions = new TreeMap<>();
+               for (Scope scope : s.getFrom()) {
+                       String scopePath = scope.getUri().getPath();
+                       NavigableMap<String, ContentProvider> contentProviders = contentRepository.getMountManager()
+                                       .findContentProviders(scopePath);
+                       for (Map.Entry<String, ContentProvider> contentProvider : contentProviders.entrySet()) {
+                               // TODO deal with depth
+                               String relPath;
+                               if (scopePath.startsWith(contentProvider.getKey())) {
+                                       relPath = scopePath.substring(contentProvider.getKey().length());
+                               } else {
+                                       relPath = null;
+                               }
+                               SearchPartition searchPartition = new SearchPartition(s, relPath, contentProvider.getValue());
+                               searchPartitions.put(contentProvider.getKey(), searchPartition);
+                       }
+               }
+               return StreamSupport.stream(new SearchPartitionsSpliterator(searchPartitions), true);
+       }
+
+       class SearchPartition {
+               BasicSearch search;
+               String relPath;
+               ContentProvider contentProvider;
+
+               public SearchPartition(BasicSearch search, String relPath, ContentProvider contentProvider) {
+                       super();
+                       this.search = search;
+                       this.relPath = relPath;
+                       this.contentProvider = contentProvider;
+               }
+
+               public BasicSearch getSearch() {
+                       return search;
+               }
+
+               public String getRelPath() {
+                       return relPath;
+               }
+
+               public ContentProvider getContentProvider() {
+                       return contentProvider;
+               }
+
+       }
+
+       class SearchPartitionsSpliterator implements Spliterator<Content> {
+               NavigableMap<String, SearchPartition> searchPartitions;
+
+               Spliterator<Content> currentSpliterator;
+
+               public SearchPartitionsSpliterator(NavigableMap<String, SearchPartition> searchPartitions) {
+                       super();
+                       this.searchPartitions = searchPartitions;
+                       SearchPartition searchPartition = searchPartitions.pollFirstEntry().getValue();
+                       currentSpliterator = searchPartition.getContentProvider().search(CmsContentSession.this,
+                                       searchPartition.getSearch(), searchPartition.getRelPath());
+               }
+
+               @Override
+               public boolean tryAdvance(Consumer<? super Content> action) {
+                       boolean remaining = currentSpliterator.tryAdvance(action);
+                       if (remaining)
+                               return true;
+                       if (searchPartitions.isEmpty())
+                               return false;
+                       SearchPartition searchPartition = searchPartitions.pollFirstEntry().getValue();
+                       currentSpliterator = searchPartition.getContentProvider().search(CmsContentSession.this,
+                                       searchPartition.getSearch(), searchPartition.getRelPath());
+                       return true;
+               }
+
+               @Override
+               public Spliterator<Content> trySplit() {
+                       if (searchPartitions.isEmpty()) {
+                               return null;
+                       } else if (searchPartitions.size() == 1) {
+                               NavigableMap<String, SearchPartition> newSearchPartitions = new TreeMap<>(searchPartitions);
+                               searchPartitions.clear();
+                               return new SearchPartitionsSpliterator(newSearchPartitions);
+                       } else {
+                               NavigableMap<String, SearchPartition> newSearchPartitions = new TreeMap<>();
+                               for (int i = 0; i < searchPartitions.size() / 2; i++) {
+                                       Map.Entry<String, SearchPartition> searchPartition = searchPartitions.pollLastEntry();
+                                       newSearchPartitions.put(searchPartition.getKey(), searchPartition.getValue());
+                               }
+                               return new SearchPartitionsSpliterator(newSearchPartitions);
+                       }
+               }
+
+               @Override
+               public long estimateSize() {
+                       return Long.MAX_VALUE;
+               }
+
+               @Override
+               public int characteristics() {
+                       return NONNULL;
+               }
+
+       }
 }
\ No newline at end of file
index 36b0cfe5ed051f05a129f40864b2ef4f05380001..75ca427c7e25bdee4d96e7bbd768c65c959b3e90 100644 (file)
@@ -47,6 +47,7 @@ class MountManager {
                return partitions.get(mountPath);
        }
 
+       /** The content provider for this path. */
        synchronized ContentProvider findContentProvider(String path) {
 //             if (ContentUtils.EMPTY.equals(path))
 //                     return partitions.firstEntry().getValue();
@@ -65,4 +66,17 @@ class MountManager {
                assert mountPath.equals(contentProvider.getMountPath());
                return contentProvider;
        }
+
+       /** All content provider under this path. */
+       synchronized NavigableMap<String, ContentProvider> findContentProviders(String path) {
+               NavigableMap<String, ContentProvider> res = new TreeMap<>();
+               tail: for (Map.Entry<String, ContentProvider> provider : partitions.tailMap(path).entrySet()) {
+                       if (!provider.getKey().startsWith(path))
+                               break tail;
+                       res.put(provider.getKey(), provider.getValue());
+               }
+               return res;
+
+       }
+
 }