/**
* Copyright (c) 2016, All Contributors (see CONTRIBUTORS file)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.eventsourcing.queries;
import com.eventsourcing.Model;
import com.eventsourcing.Repository;
import java.util.*;
import java.util.stream.BaseStream;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* A toolkit for composing model collections queries. An implementation of this interface
* can be passed to {@link #query(Repository, ModelCollectionQuery)} to retrieve a collection
* of matching model instances.
*
* Following logical operator functions can be used to compose a query:
*
* <ul>
* <li>{@link LogicalOperators#or(Collection)}</li>
* <li>{@link LogicalOperators#and(Collection)}</li>
* </ul>
*
* @param <T>
*/
public interface ModelCollectionQuery<T extends Model> {
Stream<T> getCollectionStream(Repository repository);
static <T extends Model> Collection<T> query(Repository repository, ModelCollectionQuery<T> query) {
try (Stream<T> collectionStream = query.getCollectionStream(repository)) {
return collectionStream.collect(Collectors.toList());
}
}
final class LogicalOperators {
public static <T extends Model> ModelCollectionQuery<T> or(Collection<ModelCollectionQuery<T>> queries) {
return new Or<>(queries);
}
public static <T extends Model> ModelCollectionQuery<T> or(ModelCollectionQuery<T>... queries) {
return new Or<>(queries);
}
public static class Or<T extends Model> implements ModelCollectionQuery<T> {
private final Collection<ModelCollectionQuery<T>> queries;
public Or(Collection<ModelCollectionQuery<T>> queries) {
this.queries = queries;
}
public Or(ModelCollectionQuery<T>... queries) {
this.queries = Arrays.asList(queries);
}
@Override public Stream<T> getCollectionStream(Repository repository) {
Stream<T> stream = Stream.empty();
List<Stream<T>> streams = new ArrayList<>();
Set<UUID> seen = new HashSet<>();
for (ModelCollectionQuery<T> query : queries) {
Stream<T> queryStream = query.getCollectionStream(repository)
.filter(m -> !seen.contains(m.getId()))
.map(m -> {
seen.add(m.getId());
return m;
});
streams.add(queryStream);
stream = Stream.concat(stream, queryStream);
}
return stream.onClose(() -> {
streams.forEach(BaseStream::close);
});
}
}
public static <T extends Model> ModelCollectionQuery<T> and(Collection<ModelCollectionQuery<T>> queries) {
return new And<>(queries);
}
public static <T extends Model> ModelCollectionQuery<T> and(ModelCollectionQuery<T>... queries) {
return new And<>(queries);
}
public static class And<T extends Model> implements ModelCollectionQuery<T> {
private final Collection<ModelCollectionQuery<T>> queries;
public And(Collection<ModelCollectionQuery<T>> queries) {
this.queries = queries;
}
public And(ModelCollectionQuery<T>... queries) {
this.queries = Arrays.asList(queries);
}
@Override public Stream<T> getCollectionStream(Repository repository) {
Iterator<ModelCollectionQuery<T>> iterator = queries.iterator();
if (iterator.hasNext()) {
ModelCollectionQuery<T> first = iterator.next();
Stream<T> firstStream = first.getCollectionStream(repository);
List<T> realizedStream = firstStream.collect(Collectors.toList());
Set<UUID> set = realizedStream.stream()
.map(Model::getId).collect(Collectors.toSet());
Set<UUID> seen = new HashSet<>();
firstStream.close();
List<ModelCollectionQuery<T>> allQueries = new ArrayList<>();
iterator.forEachRemaining(allQueries::add);
Stream<T> stream = Stream.empty();
List<Stream<T>> streams = new ArrayList<>();
for (ModelCollectionQuery<T> query : allQueries) {
Stream<T> queryStream =
query.getCollectionStream(repository)
.filter(m -> set.contains(m.getId()) && !seen.contains(m.getId()))
.map(m -> {
seen.add(m.getId());
return m;
});
streams.add(queryStream);
stream = Stream.concat(stream, queryStream);
}
stream = Stream.concat(realizedStream.stream().filter(m -> seen.contains(m.getId())),
stream);
return stream.onClose(() -> streams.forEach(BaseStream::close));
} else {
return Stream.empty();
}
}
}
}
}