/*
* Copyright 2015-2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.hawkular.inventory.api;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.hawkular.inventory.api.filters.Filter;
import org.hawkular.inventory.api.filters.With;
import org.hawkular.inventory.base.spi.NoopFilter;
import org.hawkular.inventory.paths.CanonicalPath;
/**
* Represents a tree of filters.
* The filters that are used to represent an inventory query (so that new query instances can be generated at will)
* form a tree because of the fact that environments can both contain resources or metrics directly or they can contain
* feeds that in turn contain the metrics and resources.
*
* <p>The API contains a method to traverse both of these kinds of "placement" of metrics or resources
* simultaneously. This is nice from a user perspective but creates problems in the impl, because using the
* relationships of the resources or metrics, one can construct queries, that would be able to branch multiple times
* and hence form a tree.
*
* @author Lukas Krejci
* @since 0.1.0
*/
public final class Query {
private QueryFragment[] fragments;
private List<Query> subTrees = new ArrayList<>();
/**
* Converts the list of applicators to the list of filters.
*
* @param fragments the list of applicators
* @return the list of corresponding filters
*/
private static Filter[] filters(QueryFragment... fragments) {
Filter[] ret = new Filter[fragments.length];
for (int i = 0; i < fragments.length; ++i) {
ret[i] = fragments[i].getFilter();
}
return ret;
}
/**
* Converts the provided filter tree to a list of paths (array of filters). Each of the paths in the result
* represent 1 branch from the tree from root to some leaf.
*
* @param query the tree of filters
* @return the list of paths
*/
public static Filter[][] filters(Query query) {
List<List<Filter>> paths = new ArrayList<>();
List<Filter[]> workingPath = new ArrayList<>();
addPathsToLeaves(query, workingPath, paths);
Filter[][] ret = new Filter[paths.size()][];
Arrays.setAll(ret, (i) -> paths.get(i).toArray(new Filter[paths.get(i).size()]));
return ret;
}
private static void addPathsToLeaves(Query tree, List<Filter[]> workingPath,
List<List<Filter>> results) {
if (tree.getSubTrees().isEmpty()) {
//this is the leaf
List<Filter> pathToLeaf = new ArrayList<>();
Consumer<Filter> addOps = (f) -> {
if (!(f instanceof NoopFilter)) {
pathToLeaf.add(f);
}
};
for (Filter[] fs : workingPath) {
Arrays.asList(fs).forEach(addOps);
}
Arrays.asList(filters(tree.getFragments())).forEach(addOps);
results.add(pathToLeaf);
} else {
workingPath.add(filters(tree.getFragments()));
for (Query subTree : tree.getSubTrees()) {
addPathsToLeaves(subTree, workingPath, results);
}
workingPath.remove(workingPath.size() - 1);
}
}
@SuppressWarnings({"ConstantConditions", "unchecked"})
public static Query to(CanonicalPath entity) {
return Query.path().with(With.path(entity)).get();
}
/**
* @return an empty query
*/
public static Query empty() {
return new Builder().build();
}
/**
* @return a symmetric builder that will start appending filter fragments to the query
*/
public static SymmetricExtender filter() {
return empty().extend().filter();
}
/**
* @return a symmetric builder that will start appending path fragments to the query
*/
public static SymmetricExtender path() {
return empty().extend().path();
}
private Query() {
}
/**
* @return the query fragments to compose the query from
*/
public QueryFragment[] getFragments() {
return fragments;
}
/**
* @return the list of query "branches" from the point this query object represents.
*/
public List<Query> getSubTrees() {
return subTrees;
}
/**
* @return a new builder initialized to "contain" this query
*/
public Builder asBuilder() {
Builder b = new Builder();
b.fragments = new ArrayList<>(Arrays.asList(fragments));
for (Query subTree : subTrees) {
Builder childBuilder = subTree.asBuilder();
childBuilder.parent = b;
b.children.add(childBuilder);
}
return b;
}
public static Builder builder() {
return new Query.Builder();
}
/**
* @return a new symmetric builder initialized with this query
*/
public SymmetricExtender extend() {
return new SymmetricExtender(asBuilder());
}
@Override
public String toString() {
StringBuilder bld = new StringBuilder();
addToString(bld, 0);
return bld.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Query that = (Query) o;
return Arrays.equals(this.getFragments(), that.getFragments()) &&
this.getSubTrees().equals(that.getSubTrees());
}
@Override
public int hashCode() {
int fragments = this.fragments != null ? Arrays.hashCode(this.fragments) : 0;
int subTrees = this.subTrees != null ? this.subTrees.hashCode() : 0;
return 31 * (fragments + subTrees) ;
}
private void addToString(StringBuilder bld, int indentation) {
indent(bld, indentation);
bld.append("Query[fragments=").append(Arrays.toString(fragments));
if (!subTrees.isEmpty()) {
bld.append("\n");
subTrees.forEach((s) -> {
s.addToString(bld, indentation + 1);
bld.append("\n");
});
indent(bld, indentation);
}
bld.append("]");
}
private void indent(StringBuilder bld, int indentation) {
for (int i = 0; i < indentation; ++i) {
bld.append(" ");
}
}
/**
* A low-level builder able to create new branches in the query tree.
*/
public static final class Builder {
private List<QueryFragment> fragments = new ArrayList<>();
private Query tree = new Query();
private Builder parent;
private List<Builder> children = new ArrayList<>();
private boolean done;
private Function<Filter[], QueryFragment[]> queryFragmentSupplier = PathFragment::from;
private Function<Filter, QueryFragment> converter = PathFragment::new;
private Boolean isFilter = false;
/**
* @return a new symmetric extender extending the query of this builder
*/
public SymmetricExtender symmetricExtender() {
return new SymmetricExtender(this);
}
/**
* Creates a new branch in the tree and returns a builder of that branch.
* @return a new builder instance for building the child
*/
public Builder branch() {
if (fragments.isEmpty()) {
fragments.add(new PathFragment(new NoopFilter()));
}
Builder child = new Builder();
child.parent = this;
children.add(child);
return child;
}
/**
* Concludes the work on a branch and returns a builder of the parent "node", if any.
* @return the parent builder
*/
public Builder done() {
if (done) {
return parent;
}
this.tree.fragments = fragments.toArray(new QueryFragment[fragments.size()]);
if (parent != null) {
parent.tree.subTrees.add(this.tree);
parent.children.remove(this);
}
//done will remove the child from the children, so we'd get concurrent modification exceptions
//avoid that stupidly by working on a copy of children
new ArrayList<>(children).forEach(Query.Builder::done);
done = true;
return parent;
}
/**
* Sets the filters to be used on the current node in the tree.
* <p>
* This method tries to optimize the query by checking if the provided fragments (appended to the current query)
* form a canonical traversal and if so, replaces the query so far with a simple filter on the canonical path
* instead of a traversal with more steps.
*
* @param filters the list of filters to apply to the query at this position in the tree.
* @return this builder
*/
public Builder with(QueryFragment... filters) {
QueryOptimizer.appendOptimized(fragments, filters);
return this;
}
public Builder with(Function<Filter, QueryFragment> converter, QueryFragment... filters) {
QueryFragment[] converted = Arrays.asList(filters).stream().map(QueryFragment::getFilter).map(converter)
.toArray(QueryFragment[]::new);
QueryOptimizer.appendOptimized(fragments, converted);
return this;
}
public Builder with(Query other) {
with(other.fragments);
for (Query sub : other.getSubTrees()) {
branch().with(sub).done();
}
return this;
}
public Builder with(Query other, Function<Filter, QueryFragment> converter) {
with(converter, other.fragments);
for (Query sub : other.getSubTrees()) {
branch().with(sub, converter).done();
}
return this;
}
public Builder path() {
queryFragmentSupplier = PathFragment::from;
converter = PathFragment::new;
if (isFilter != null && isFilter && !this.fragments.isEmpty()) {
with(NoopFilter.INSTANCE);
}
isFilter = false;
return this;
}
public Builder filter() {
queryFragmentSupplier = FilterFragment::from;
converter = FilterFragment::new;
if (isFilter != null && !isFilter && this.fragments.isEmpty()) {
with(NoopFilter.INSTANCE);
}
isFilter = true;
return this;
}
public Builder with(Filter... filters) {
with(converter, queryFragmentSupplier.apply(filters));
return this;
}
/**
* Builds the <b>whole</b> tree regardless of where in the tree the current builder "operates".
*
* @return the fully built tree
*/
public Query build() {
Query.Builder root = this;
while (true) {
if (root.parent == null) {
break;
}
root = root.parent;
}
root.done();
return root.tree;
}
}
/**
* Constructs a query fragment tree by extending all the leaves with a uniform set of filters at a time.
*/
public static final class SymmetricExtender {
private Query.Builder filters;
private Function<Filter[], QueryFragment[]> queryFragmentSupplier;
private Function<Filter, QueryFragment> converter;
private Boolean isFilter;
private SymmetricExtender(Query.Builder filters) {
this.filters = filters;
onLeaves(filters, (b) -> {
QueryFragment last = b.fragments.isEmpty() ? null : b.fragments.get(b.fragments.size() - 1);
if (last != null) {
isFilter = last instanceof FilterFragment;
queryFragmentSupplier = isFilter ? FilterFragment::from : PathFragment::from;
converter = isFilter ? FilterFragment::new : PathFragment::new;
}
});
}
public Query.Builder rawQueryBuilder() {
return filters;
}
/**
* Modifies this extender to append path fragments with future calls to {@code with()} methods.
* @return this instance
*/
public SymmetricExtender path() {
queryFragmentSupplier = PathFragment::from;
converter = PathFragment::new;
if (isFilter != null && isFilter && !filters.fragments.isEmpty()) {
with(NoopFilter.INSTANCE);
}
isFilter = false;
return this;
}
/**
* Modifies this extender to append filter fragments with future calls to {@code with()} methods.\
*
* @return this instance
*/
public SymmetricExtender filter() {
queryFragmentSupplier = FilterFragment::from;
converter = FilterFragment::new;
if (isFilter != null && !isFilter && filters.fragments.isEmpty()) {
with(NoopFilter.INSTANCE);
}
isFilter = true;
return this;
}
/**
* Appends the provided query to the leaves of the current query tree, converting all its fragments to the
* current fragment type (determined by the last call to {@link #filter()} or {@link #path()}).
*
* @param other the query to append
* @return this instance
*/
public SymmetricExtender with(Query other) {
onLeaves(this.filters, (builder) -> builder.with(other, converter));
return this;
}
/**
* Appends the provided query to the leaves of the current query tree, leaving the type of its fragments as they
* originally were.
*
* @param other the query to append
* @return this instance
*/
public SymmetricExtender withExact(Query other) {
onLeaves(this.filters, (builder) -> builder.with(other));
return this;
}
/**
* Appends the filters as query fragments determined by the last call to {@link #filter()} or {@link #path()}.
*
* <p>The filters is an array of arrays representing a new set of branches to be created at all the leaves of
* the current query tree.
*
* @param filters the filters to append to the leaves of the query tree
* @return this instance
*/
public SymmetricExtender with(Filter[][] filters) {
onLeaves(this.filters, (builder) -> {
for (Filter[] fs : filters) {
builder.branch().with(queryFragmentSupplier.apply(fs));
}
});
return this;
}
/**
* Appends the filters as query fragments determined by the last call to {@link #filter()} or {@link #path()}.
*
* @param filters the filters to append to the leaves of the query tree
* @return this instance
*/
public SymmetricExtender with(Filter... filters) {
onLeaves(this.filters, (t) -> t.with(queryFragmentSupplier.apply(filters)));
return this;
}
/**
* @return the final query.
*/
public Query get() {
return filters.build();
}
private void onLeaves(Query.Builder root, Consumer<Query.Builder> leafMutator) {
if (root.children.isEmpty()) {
leafMutator.accept(root);
} else {
for (Query.Builder c : root.children) {
onLeaves(c, leafMutator);
}
}
}
}
}