/*
* 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 static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.hawkular.inventory.api.Relationships.WellKnown.contains;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.hawkular.inventory.api.filters.Filter;
import org.hawkular.inventory.api.filters.Related;
import org.hawkular.inventory.api.filters.RelationWith;
import org.hawkular.inventory.api.filters.SwitchElementType;
import org.hawkular.inventory.api.filters.With;
import org.hawkular.inventory.api.model.AbstractElement;
import org.hawkular.inventory.paths.CanonicalPath;
import org.hawkular.inventory.paths.Path;
import org.hawkular.inventory.paths.SegmentType;
/**
* @author Lukas Krejci
* @since 0.6.0
*/
final class QueryOptimizer {
private QueryOptimizer() {
}
public static void appendOptimized(List<QueryFragment> fragments, QueryFragment... filters) {
//remove duplicates
List<QueryFragment> applicableNewFilters;
if (fragments.isEmpty()) {
applicableNewFilters = new ArrayList<>(Arrays.asList(filters));
} else {
applicableNewFilters = removeDuplicates(fragments.get(fragments.size() - 1).getFilter(),
new ArrayList<>(Arrays.asList(filters)));
}
cleanUnnecessaryNoops(applicableNewFilters);
collapseSimpleTypeSwitches(fragments, applicableNewFilters);
QueryFragment cpFilter = findLastCanonicalFilterAndPrepareNewFilters(fragments, applicableNewFilters);
List<CanonicalPath.Extender> checkers;
if (cpFilter == null) {
checkers = Collections.singletonList(CanonicalPath.empty());
} else {
With.CanonicalPaths cps = (With.CanonicalPaths) cpFilter.getFilter();
checkers = Arrays.asList(cps.getPaths()).stream().map(CanonicalPath::modified).collect(toList());
}
while (!applicableNewFilters.isEmpty()) {
boolean isPath = applicableNewFilters.get(0) instanceof PathFragment;
QueryFragment first, second, third;
if ((first = removeOrReadd(applicableNewFilters, null, null)) == null) {
break;
}
if ((second = removeOrReadd(applicableNewFilters, first, null)) == null) {
break;
}
third = applicableNewFilters.isEmpty() ? null : applicableNewFilters.remove(0);
//check that the query is not transitioning from a path to a filter or vice versa
//we do this check only on the positions of the contains filter, because only that can actually influence
//the results
if (cpFilter != null && first.getClass() != cpFilter.getClass()) {
//we were trying to continue a classpath filter. move the contains to the processed fragments
//and try with the type/id as if this was a new start of the cp filter.
reAddNonNull(false, fragments, first);
reAddNonNull(true, applicableNewFilters, second, third);
checkers = Collections.singletonList(CanonicalPath.empty());
cpFilter = null;
continue;
}
With.Types typeFilter;
With.Ids idFilter;
if (cpFilter == null) {
//there is no prior canonical path filter, so we're starting a new one potentially
//this means that first and second are type+id
//additionally, the contains query fragment, if it is present, must be of the same type as the type/id
//filters.
//this is so that they all are either path segs or filter segs - we must not mix them together.
typeFilter = choose(With.Types.class, first, second);
idFilter = choose(With.Ids.class, first, second);
if (third != null && (!isOutgoingContains(third.getFilter()) || second.getClass() != third
.getClass())) {
reAddNonNull(true, applicableNewFilters, third);
third = null;
}
} else {
//there is a prior canonical path filter, so we're trying to continue it.
//this means that first must be a contains filter, and second and third are type+id
//again we must check for the change in the query fragment type
if (!isOutgoingContains(first.getFilter())) {
reAddNonNull(false, fragments, first);
reAddNonNull(true, applicableNewFilters, second, third);
checkers = Collections.singletonList(CanonicalPath.empty());
cpFilter = null;
continue;
}
typeFilter = choose(With.Types.class, second, third);
idFilter = choose(With.Ids.class, second, third);
}
if (typeFilter == null || idFilter == null) {
//hmm... not sure what to do with filters invalid for cp extension.. so just add them back to
//fragments and try to continue
reAddNonNull(false, fragments, first, second, third);
checkers = Collections.singletonList(CanonicalPath.empty());
cpFilter = null;
continue;
}
if (typeFilter.getTypes().length != idFilter.getIds().length) {
//the canonical path filter wouldn't be able to completely match the type+id filters, so
//bail
reAddNonNull(false, fragments, first, second, third);
checkers = Collections.singletonList(CanonicalPath.empty());
cpFilter = null;
continue;
}
// if we should be able to use the canonical path filter, everything must match - all the
// checkers must be able to extend to all types
boolean checkFailed = false;
CHECK:
for (CanonicalPath.Extender checker : checkers) {
org.hawkular.inventory.paths.SegmentType[] types = typeFilter.getSegmentTypes();
for (int n = 0; n < typeFilter.getTypes().length; ++n) {
Path.Segment seg = new Path.Segment(types[n], idFilter.getIds()[n]);
if (!checker.canExtendTo(seg)) {
if (cpFilter == null) {
//k we tried to start a new cp filter, but failed
reAddNonNull(false, fragments, first, second, third);
} else {
//move the contains to "processed" fragments and try again as if this was a new path
reAddNonNull(false, fragments, first);
reAddNonNull(true, applicableNewFilters, second, third);
checkers = Collections.singletonList(CanonicalPath.empty());
cpFilter = null;
}
checkFailed = true;
break CHECK;
} else {
checker.extend(seg);
}
}
}
if (!checkFailed) {
//k, so now we can transform the checkers into a new canonical path filter, because they contain
//our new canonical positions
if (!fragments.isEmpty()
&& fragments.get(fragments.size() - 1).getFilter() instanceof With.CanonicalPaths) {
//remove the last filter from our fragments
fragments.remove(fragments.size() - 1);
}
CanonicalPath[] newPaths = checkers.stream().map(CanonicalPath.Extender::get)
.toArray(CanonicalPath[]::new);
QueryFragment related = cpFilter == null ? third : null;
cpFilter = isPath ? new PathFragment(With.paths(newPaths))
: new FilterFragment(With.paths(newPaths));
fragments.add(cpFilter);
if (related != null) {
reAddNonNull(true, applicableNewFilters, related);
}
}
}
//add the rest of the filters from the input
fragments.addAll(applicableNewFilters);
}
private static void cleanUnnecessaryNoops(List<QueryFragment> filters) {
}
private static <T> void reAddNonNull(boolean atBeginning, List<T> list, T... objs) {
int pos = atBeginning ? 0 : list.size();
for (int i = objs.length - 1; i >= 0; --i) {
T o = objs[i];
if (o != null) {
list.add(pos, o);
}
}
}
private static QueryFragment removeOrReadd(List<QueryFragment> fragments, QueryFragment first,
QueryFragment second) {
if (fragments.isEmpty()) {
addIfNotNull(fragments, first);
addIfNotNull(fragments, second);
return null;
} else {
return fragments.remove(0);
}
}
private static <T> void addIfNotNull(Collection<T> col, T el) {
if (el != null) {
col.add(el);
}
}
private static <T extends Filter> T choose(Class<T> type, QueryFragment... objects) {
for (QueryFragment o : objects) {
if (o != null && type.equals(o.getFilter().getClass())) {
return type.cast(o.getFilter());
}
}
return null;
}
private static boolean isOutgoingContains(Filter f) {
if (!(f instanceof Related)) {
return false;
}
Related rel = (Related) f;
return rel.getEntityRole() == Related.EntityRole.SOURCE &&
contains.name().equals(rel.getRelationshipName());
}
private static QueryFragment findLastCanonicalFilterAndPrepareNewFilters(List<QueryFragment> fragments,
List<QueryFragment> newFilters) {
if (fragments.isEmpty()) {
return null;
}
Filter last = fragments.get(fragments.size() - 1).getFilter();
QueryFragment ret = null;
if (last instanceof With.CanonicalPaths) {
ret = fragments.get(fragments.size() - 1);
CanonicalPath[] sources = ((With.CanonicalPaths) last).getPaths();
//remove anything from newFilters that is also matched by the canonical path filter
Set<Class<?>> expectedClasses = Arrays.asList(sources).stream()
.map((s) -> AbstractElement.toElementClass(s.getSegment().getElementType())).collect(toSet());
Set<String> expectedIds = Arrays.asList(sources).stream().map((s) -> s.getSegment().getElementId())
.collect(toSet());
for (Iterator<QueryFragment> it = newFilters.iterator(); it.hasNext(); ) {
QueryFragment f = it.next();
if (f.getFilter() instanceof With.Types) {
Set<SegmentType> filterTypes =
new HashSet<>(Arrays.asList(((With.Types) f.getFilter()).getSegmentTypes()));
if (!expectedClasses.equals(filterTypes)) {
break;
}
} else if (f.getFilter() instanceof With.Ids) {
Set<String> filterIds = new HashSet<>(Arrays.asList(((With.Ids) f.getFilter()).getIds()));
if (!expectedIds.equals(filterIds)) {
break;
}
} else {
break;
}
it.remove();
}
} else if (fragments.size() > 1) {
//check if the second last isn't a canonical filter and the last + the current filter cannot be
//made into continuation of that canonical path filter
Filter secondLast = fragments.get(fragments.size() - 2).getFilter();
Filter thirdLast = fragments.size() > 2 ? fragments.get(fragments.size() - 3).getFilter() : null;
if (secondLast instanceof With.CanonicalPaths && last instanceof Related) {
Related rel = (Related) last;
if (contains.name().equals(rel.getRelationshipName())) {
ret = fragments.get(fragments.size() - 2);
//prepend the filters with the last
newFilters.add(0, fragments.remove(fragments.size() - 1));
}
} else if (thirdLast instanceof With.CanonicalPaths && secondLast instanceof Related
&& (last instanceof With.Types || last instanceof With.Ids)) {
Related rel = (Related) secondLast;
if (contains.name().equals(rel.getRelationshipName())) {
ret = fragments.get(fragments.size() - 3);
//prepend the filters with the last and second last
newFilters.add(0, fragments.remove(fragments.size() - 1));
newFilters.add(0, fragments.remove(fragments.size() - 1));
} else {
//k, so the Related filter is not what we want... but the last element still might offer something
//to base the cp filter on, so let's provide it
newFilters.add(0, fragments.remove(fragments.size() - 1));
}
} else if (!newFilters.isEmpty() && ((last instanceof With.Types
&& newFilters.get(0).getFilter() instanceof With.Ids)
|| (last instanceof With.Ids
&& newFilters.get(0).getFilter() instanceof With.Types))) {
//we've not seen any canonical path filter, but we might be starting a new one because the end
//of the fragments list lends itself to it
newFilters.add(0, fragments.remove(fragments.size() - 1));
}
} else if (!newFilters.isEmpty() && ((last instanceof With.Types
&& newFilters.get(0).getFilter() instanceof With.Ids)
|| (last instanceof With.Ids
&& newFilters.get(0).getFilter() instanceof With.Types))) {
//we've not seen any canonical path filter, but we might be starting a new one because the end
//of the fragments list lends itself to it
newFilters.add(0, fragments.remove(fragments.size() - 1));
}
return ret;
}
private static List<QueryFragment> removeDuplicates(Filter original, List<QueryFragment> filters) {
List<QueryFragment> ret = filters;
int i = 0;
while (i < filters.size() && original.equals(filters.get(i).getFilter())) {
i++;
}
if (i > 0) {
ret = ret.subList(i, ret.size());
}
return ret;
}
private static void collapseSimpleTypeSwitches(List<QueryFragment> fragments, List<QueryFragment> newFilters) {
int fragmentsToRemove = -2;
//-2 means 2 last elements from the fragments list
//-1 means last element from the fragments list
//0 means the first element from new filters
//1 means first 2 elements from new filters
//2 means first 3 elements from new filters
String label = null;
while (label == null && fragmentsToRemove <= 0) {
label = collapseSimpleTypeSwitchesHelper(fragmentsToRemove, fragments, newFilters);
fragmentsToRemove++;
}
if (label != null) {
Class<?> fragmentType = newFilters.get(0).getClass();
//fragmentsToRemove is 1 past what we actually need
removeLast(fragments, -fragmentsToRemove + 1);
removeFirst(newFilters, fragmentsToRemove + 2);
QueryFragment newFragment = PathFragment.class.equals(fragmentType)
? new PathFragment(Related.by(label))
: new FilterFragment(Related.by(label));
newFilters.add(0, newFragment);
}
}
private static String collapseSimpleTypeSwitchesHelper(int start, List<QueryFragment> fragments,
List<QueryFragment> newFilters) {
QueryFragment firstFragment = getElementFromEitherCollection(start, fragments, newFilters);
QueryFragment secondFragment = getElementFromEitherCollection(start + 1, fragments, newFilters);
QueryFragment thirdFragment = getElementFromEitherCollection(start + 2, fragments, newFilters);
if (firstFragment == null || secondFragment == null || thirdFragment == null) {
return null;
}
if (!firstFragment.getClass().equals(secondFragment.getClass())
|| !firstFragment.getClass().equals(thirdFragment.getClass())) {
return null;
}
Filter firstFilter = firstFragment.getFilter();
Filter secondFilter = secondFragment.getFilter();
Filter thirdFilter = thirdFragment.getFilter();
if (firstFilter instanceof SwitchElementType && secondFilter instanceof RelationWith.PropertyValues
&& thirdFilter instanceof SwitchElementType) {
SwitchElementType toEdge = (SwitchElementType) firstFilter;
RelationWith.PropertyValues labelCheck = (RelationWith.PropertyValues) secondFilter;
SwitchElementType toEntity = (SwitchElementType) thirdFilter;
boolean isLabelCheck = labelCheck.getProperty().equals("label")
&& labelCheck.getValues() != null && labelCheck.getValues().length == 1;
return !toEdge.isFromEdge() && isLabelCheck && toEntity.isFromEdge()
? (String) labelCheck.getValues()[0]
: null;
} else {
return null;
}
}
private static <T> T getElementFromEitherCollection(int index, List<T> negativelyIndexed,
List<T> normallyIndexed) {
if (index < 0) {
return negativelyIndexed.size() > Math.abs(index) - 1
? negativelyIndexed.get(negativelyIndexed.size() + index)
: null;
} else {
return index < normallyIndexed.size() ? normallyIndexed.get(index) : null;
}
}
private static void removeLast(List<?> list, int itemsToRemove) {
for (int i = 0; i < itemsToRemove; ++i) {
list.remove(list.size() - 1);
}
}
private static void removeFirst(List<?> list, int itemsToRemove) {
for (int i = 0; i < itemsToRemove; ++i) {
list.remove(0);
}
}
}