From 6d463a65d04d641c94a493579a75f8015c82097d Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Wed, 26 Apr 2023 18:39:55 +0200 Subject: [PATCH] First ACR search experiments --- .../src/org/argeo/api/acr/ContentSession.java | 9 +- .../org/argeo/api/acr/search/AndFilter.java | 9 + .../org/argeo/api/acr/search/BasicSearch.java | 96 +++++++++ .../org/argeo/api/acr/search/Composition.java | 4 + .../org/argeo/api/acr/search/Constraint.java | 4 + .../argeo/api/acr/search/ContentFilter.java | 183 ++++++++++++++++++ .../argeo/api/acr/search/Intersection.java | 15 ++ .../org/argeo/api/acr/search/OrFilter.java | 9 + .../src/org/argeo/api/acr/search/Union.java | 15 ++ .../argeo/api/acr/spi/ContentProvider.java | 8 + .../org/argeo/cms/acr/CmsContentSession.java | 117 +++++++++++ .../src/org/argeo/cms/acr/MountManager.java | 14 ++ 12 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 org.argeo.api.acr/src/org/argeo/api/acr/search/AndFilter.java create mode 100644 org.argeo.api.acr/src/org/argeo/api/acr/search/BasicSearch.java create mode 100644 org.argeo.api.acr/src/org/argeo/api/acr/search/Composition.java create mode 100644 org.argeo.api.acr/src/org/argeo/api/acr/search/Constraint.java create mode 100644 org.argeo.api.acr/src/org/argeo/api/acr/search/ContentFilter.java create mode 100644 org.argeo.api.acr/src/org/argeo/api/acr/search/Intersection.java create mode 100644 org.argeo.api.acr/src/org/argeo/api/acr/search/OrFilter.java create mode 100644 org.argeo.api.acr/src/org/argeo/api/acr/search/Union.java diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ContentSession.java b/org.argeo.api.acr/src/org/argeo/api/acr/ContentSession.java index b7d37dc10..b8ecd98da 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/ContentSession.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/ContentSession.java @@ -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 edit(Consumer work); + + Stream search(Consumer 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 index 000000000..e58b21236 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/AndFilter.java @@ -0,0 +1,9 @@ +package org.argeo.api.acr.search; + +public class AndFilter extends ContentFilter { + + 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 index 000000000..8028f5d20 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/BasicSearch.java @@ -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 select = new ArrayList<>(); + private List from = new ArrayList<>(); + + private ContentFilter 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 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 getSelect() { + return select; + } + + public List getFrom() { + return from; + } + + public ContentFilter 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 index 000000000..80786f462 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/Composition.java @@ -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 index 000000000..fc4313d7a --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/Constraint.java @@ -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 index 000000000..c5f5fc607 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/ContentFilter.java @@ -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 implements Constraint { + private Set constraintss = new HashSet<>(); + + private COMPOSITION composition; + + boolean negateNextOperator = false; + + @SuppressWarnings("unchecked") + ContentFilter(Class 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 and) { + AndFilter subFilter = new AndFilter(); + and.accept(subFilter); + addConstraint(subFilter); + return composition; + } + + public COMPOSITION any(Consumer or) { + OrFilter subFilter = new OrFilter(); + or.accept(subFilter); + addConstraint(subFilter); + return composition; + } + + public ContentFilter 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 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 index 000000000..5fff2ae88 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/Intersection.java @@ -0,0 +1,15 @@ +package org.argeo.api.acr.search; +class Intersection implements Composition { + ContentFilter filter; + + @SuppressWarnings("unchecked") + public Intersection(ContentFilter filter) { + this.filter = (ContentFilter) filter; + } + + public ContentFilter 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 index 000000000..40460d499 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/OrFilter.java @@ -0,0 +1,9 @@ +package org.argeo.api.acr.search; + +public class OrFilter extends ContentFilter { + + 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 index 000000000..d4b342b5f --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/search/Union.java @@ -0,0 +1,15 @@ +package org.argeo.api.acr.search; + +class Union implements Composition { + ContentFilter filter; + + @SuppressWarnings("unchecked") + public Union(ContentFilter filter) { + this.filter = (ContentFilter) filter; + } + + public ContentFilter or() { + return filter; + } + +} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ContentProvider.java b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ContentProvider.java index 72aa162b3..25b9be5c2 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ContentProvider.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ContentProvider.java @@ -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 search(ProvidedSession session, BasicSearch search, String relPath) { + throw new UnsupportedOperationException(); + } + // default ContentName parsePrefixedName(String nameWithPrefix) { // return NamespaceUtils.parsePrefixedName(this, nameWithPrefix); // } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java index 17a03fdc5..af7dca046 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java @@ -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 search(Consumer search) { + BasicSearch s = new BasicSearch(); + search.accept(s); + NavigableMap searchPartitions = new TreeMap<>(); + for (Scope scope : s.getFrom()) { + String scopePath = scope.getUri().getPath(); + NavigableMap contentProviders = contentRepository.getMountManager() + .findContentProviders(scopePath); + for (Map.Entry 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 { + NavigableMap searchPartitions; + + Spliterator currentSpliterator; + + public SearchPartitionsSpliterator(NavigableMap 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 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 trySplit() { + if (searchPartitions.isEmpty()) { + return null; + } else if (searchPartitions.size() == 1) { + NavigableMap newSearchPartitions = new TreeMap<>(searchPartitions); + searchPartitions.clear(); + return new SearchPartitionsSpliterator(newSearchPartitions); + } else { + NavigableMap newSearchPartitions = new TreeMap<>(); + for (int i = 0; i < searchPartitions.size() / 2; i++) { + Map.Entry 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 diff --git a/org.argeo.cms/src/org/argeo/cms/acr/MountManager.java b/org.argeo.cms/src/org/argeo/cms/acr/MountManager.java index 36b0cfe5e..75ca427c7 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/MountManager.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/MountManager.java @@ -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 findContentProviders(String path) { + NavigableMap res = new TreeMap<>(); + tail: for (Map.Entry provider : partitions.tailMap(path).entrySet()) { + if (!provider.getKey().startsWith(path)) + break tail; + res.put(provider.getKey(), provider.getValue()); + } + return res; + + } + } -- 2.30.2