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);
}
--- /dev/null
+package org.argeo.api.acr.search;
+
+public class AndFilter extends ContentFilter<Intersection> {
+
+ public AndFilter() {
+ super(Intersection.class);
+ }
+
+}
--- /dev/null
+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(), ""));
+ }
+}
--- /dev/null
+package org.argeo.api.acr.search;
+interface Composition {
+}
+
--- /dev/null
+package org.argeo.api.acr.search;
+
+public interface Constraint {
+}
--- /dev/null
+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, "");
+ });
+
+ }
+
+}
--- /dev/null
+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;
+ }
+
+}
+
--- /dev/null
+package org.argeo.api.acr.search;
+
+public class OrFilter extends ContentFilter<Union> {
+
+ public OrFilter() {
+ super(Union.class);
+ }
+
+}
--- /dev/null
+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;
+ }
+
+}
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);
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);
// }
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;
}
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
return partitions.get(mountPath);
}
+ /** The content provider for this path. */
synchronized ContentProvider findContentProvider(String path) {
// if (ContentUtils.EMPTY.equals(path))
// return partitions.firstEntry().getValue();
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;
+
+ }
+
}