/*
* 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.rest;
import static java.util.stream.Collectors.toList;
import static org.hawkular.inventory.api.Relationships.WellKnown.contains;
import static org.hawkular.inventory.api.filters.With.id;
import static org.hawkular.inventory.api.filters.With.type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.BailErrorStrategy;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.RuleContext;
import org.antlr.v4.runtime.misc.ParseCancellationException;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.hawkular.inventory.api.Query;
import org.hawkular.inventory.api.filters.Defined;
import org.hawkular.inventory.api.filters.Filter;
import org.hawkular.inventory.api.filters.RecurseFilter;
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.Entity;
import org.hawkular.inventory.paths.CanonicalPath;
import org.hawkular.inventory.paths.PathSegmentCodec;
import org.hawkular.inventory.paths.SegmentType;
import org.hawkular.inventory.rest.HawkularInventoryGetUriParser.EntityTypeContext;
import org.hawkular.inventory.rest.HawkularInventoryGetUriParser.FilterSpecContext;
import org.hawkular.inventory.rest.HawkularInventoryGetUriParser.IdContext;
import org.hawkular.inventory.rest.HawkularInventoryGetUriParser.NameContext;
import org.hawkular.inventory.rest.HawkularInventoryGetUriParser.PathContinuationContext;
import org.hawkular.inventory.rest.HawkularInventoryGetUriParser.PathEndContext;
import org.hawkular.inventory.rest.HawkularInventoryGetUriParser.PathLikeContinuationContext;
import org.hawkular.inventory.rest.HawkularInventoryGetUriParser.RecursiveContinuationContext;
import org.hawkular.inventory.rest.HawkularInventoryGetUriParser.RecursiveFilterSpecContext;
import org.hawkular.inventory.rest.HawkularInventoryGetUriParser.RelationshipAsFirstSegmentContext;
import org.hawkular.inventory.rest.HawkularInventoryGetUriParser.RelationshipContinuationContext;
import org.hawkular.inventory.rest.HawkularInventoryGetUriParser.RelationshipDirectionContext;
import org.hawkular.inventory.rest.HawkularInventoryGetUriParser.RelationshipEndContext;
import org.hawkular.inventory.rest.HawkularInventoryGetUriParser.RelationshipFilterSpecContext;
import org.hawkular.inventory.rest.HawkularInventoryGetUriParser.UriContext;
/**
* @author Lukas Krejci
* @since 0.16.0.Final
*/
public final class Traverser {
private final int indexPrefixSize;
private final Query.Builder queryPrefix;
private final Function<String, CanonicalPath> cpParser;
public Traverser(int indexPrefixSize, Query.Builder queryPrefix, Function<String, CanonicalPath> cpParser) {
this.indexPrefixSize = indexPrefixSize;
this.queryPrefix = queryPrefix;
this.cpParser = cpParser;
}
public Query navigate(String traversal) {
HawkularInventoryGetUriLexer lexer = new HawkularInventoryGetUriLexer(new ANTLRInputStream(traversal));
CommonTokenStream tokens = new CommonTokenStream(lexer);
HawkularInventoryGetUriParser parser = new HawkularInventoryGetUriParser(tokens);
parser.setErrorHandler(new BailErrorStrategy());
ParseListener listener = new ParseListener(queryPrefix);
try {
UriContext ctx = parser.uri();
ParseTreeWalker.DEFAULT.walk(listener, ctx);
return listener.getParsedQuery();
} catch (ParseCancellationException e) {
Throwable error = e.getCause();
if (error instanceof RecognitionException) {
RecognitionException re = (RecognitionException) error;
int errorIndex = indexPrefixSize + re.getOffendingToken().getTokenIndex();
String errorToken = re.getOffendingToken().getText();
String expectedAlternatives = re.getExpectedTokens() == null ? "none" : re.getExpectedTokens()
.toString(HawkularInventoryGetUriLexer.VOCABULARY);
throw new IllegalArgumentException("Illegal inventory traversal URL. Token '" + errorToken
+ "' on index " + errorIndex + " is not legal. Expected " + expectedAlternatives);
} else {
throw new IllegalArgumentException("Illegal inventory traversal URL. Error message: "
+ e.getCause().getMessage(), e.getCause());
}
}
}
private class ParseListener extends HawkularInventoryGetUriBaseListener {
private Query.Builder query;
private Query.Builder recurseBuilder;
private boolean first = true;
private final boolean prefixEmpty;
private ParseListener(Query.Builder query) {
Query q = query.build();
this.query = q.asBuilder();
this.prefixEmpty = q.getFragments().length == 0;
}
Query getParsedQuery() {
if (recurseBuilder != null) {
Filter[][] recurseCondition = Query.filters(recurseBuilder.build());
query.filter().with(new RecurseFilter(recurseCondition));
}
return query.build();
}
@Override public void exitEveryRule(ParserRuleContext ctx) {
super.exitEveryRule(ctx);
first = false;
}
@Override
public void enterRelationshipAsFirstSegment(RelationshipAsFirstSegmentContext ctx) {
//we override whatever implicit query we were given, because relationship ids are global
query = Query.builder();
IdContext id = ctx.id();
RelationshipDirectionContext directionContext = ctx.relationshipDirection();
List<RelationshipFilterSpecContext> filterCtxs = ctx.relationshipFilterSpec();
RelationshipEndContext endCtx = ctx.relationshipEnd();
processRelationship(true, RelationWith.id(id.getText()), directionContext, filterCtxs, endCtx);
}
@SuppressWarnings("Duplicates")
@Override public void enterPathContinuation(PathContinuationContext ctx) {
//the query prefix ends on an entity and we're continuing with another entity or filter. We should never
//directly filter the prefix entity, so implicitly add a contains to filter on the direct children of the
//query prefix.
if (first && !prefixEmpty) {
query.with(Related.by(contains));
}
EntityTypeContext entityTypeCtx = ctx.entityType();
IdContext idCtx = ctx.id();
List<FilterSpecContext> filterSpecCtxs = ctx.filterSpec();
PathLikeContinuationContext pathLikeCtx = ctx.pathLikeContinuation();
boolean appendContains = pathLikeCtx != null && pathLikeCtx.pathContinuation() != null;
processEntityOrFilter(entityTypeCtx, idCtx, filterSpecCtxs, appendContains);
}
@Override public void enterRecursiveContinuation(RecursiveContinuationContext ctx) {
RecursiveFilterSpecContext overCtx = ctx.recursiveFilterSpec();
String overRelationship = overCtx == null ? contains.name() : overCtx.value().getText();
recurseBuilder = Query.builder().filter().with(Related.by(overRelationship));
processEntityOrFilter(null, null, ctx.filterSpec(), false);
Query q = recurseBuilder.build();
recurseBuilder = null;
getQueryBuilder().path().with(RecurseFilter.builder().addChain(Query.filters(q)[0]).build());
}
@Override
public void enterRelationshipContinuation(RelationshipContinuationContext ctx) {
NameContext nameCtx = ctx.name();
RelationshipDirectionContext directionContext = ctx.relationshipDirection();
List<RelationshipFilterSpecContext> filterCtxs = ctx.relationshipFilterSpec();
RelationshipEndContext endCtx = ctx.relationshipEnd();
processRelationship(false, RelationWith.name(nameCtx.getText()), directionContext, filterCtxs, endCtx);
}
@Override
public void enterIdenticalContinuation(HawkularInventoryGetUriParser.IdenticalContinuationContext ctx) {
new FilterApplicator.OnEntity().identical(query);
processEntityOrFilter(null, null, Collections.emptyList(), false);
}
@Override public void enterPathEnd(PathEndContext ctx) {
if ("relationships".equals(ctx.getChild(1).getText())) {
RelationshipDirectionContext directionCtx = ctx.relationshipDirection();
List<RelationshipFilterSpecContext> filterCtxs = ctx.relationshipFilterSpec();
if (directionCtx != null && "in".equals(directionCtx.getText())) {
getQueryBuilder().path().with(SwitchElementType.incomingRelationships());
} else {
getQueryBuilder().path().with(SwitchElementType.outgoingRelationships());
}
if (filterCtxs != null && !filterCtxs.isEmpty()) {
Map<String, List<ValueAndPos>> filters = extractFilters(filterCtxs,
RelationshipFilterSpecContext::relationshipFilterName,
RelationshipFilterSpecContext::value);
processFilters(filters, new FilterApplicator.OnRelationship());
}
} else if ("entities".equals(ctx.getChild(1).getText())) {
List<FilterSpecContext> filterCtxs = ctx.filterSpec();
processEntityOrFilter(null, null, filterCtxs, false);
}
}
private void processEntityOrFilter(EntityTypeContext entityTypeCtx, IdContext idCtx,
List<FilterSpecContext> filterSpecCtxs,
boolean appendContains) {
if (entityTypeCtx != null) {
SegmentType segmentType = SegmentType.valueOf(entityTypeCtx.getText());
//path is important because this can be a continuation of a canonical path...
//this helps the query optimizer do its magic
getQueryBuilder().path().with(type(segmentType), id(PathSegmentCodec.decode(idCtx.getText())));
}
if (filterSpecCtxs != null && !filterSpecCtxs.isEmpty()) {
Map<String, List<ValueAndPos>> filters = extractFilters(filterSpecCtxs, FilterSpecContext::filterName,
FilterSpecContext::value);
processFilters(filters, new FilterApplicator.OnEntity());
}
if (appendContains) {
getQueryBuilder().path().with(Related.by(contains));
}
}
private void processRelationship(boolean isFirst, Filter idOrNameFilter,
RelationshipDirectionContext directionContext,
List<RelationshipFilterSpecContext> filterCtxs,
RelationshipEndContext endCtx) {
boolean outgoing;
if (directionContext != null && "in".equals(directionContext.getText())) {
if (!isFirst) {
getQueryBuilder().path().with(SwitchElementType.incomingRelationships());
}
outgoing = false;
} else {
if (!isFirst) {
getQueryBuilder().path().with(SwitchElementType.outgoingRelationships());
}
outgoing = true;
}
getQueryBuilder().path().with(idOrNameFilter);
if (filterCtxs != null && !filterCtxs.isEmpty()) {
Map<String, List<ValueAndPos>> filters = extractFilters(filterCtxs,
RelationshipFilterSpecContext::relationshipFilterName,
RelationshipFilterSpecContext::value);
processFilters(filters, new FilterApplicator.OnRelationship());
}
if (endCtx == null || endCtx.getChild(1).getText().equals("entities")) {
if (outgoing) {
getQueryBuilder().path().with(SwitchElementType.targetEntities());
} else {
getQueryBuilder().path().with(SwitchElementType.sourceEntities());
}
}
}
/**
* Assumes the query builder has been set up to either filter or path according to the current "situation"
* in the query.
*
* @param filters the filters extracted from the URL at the current URL path segment
* @param filterApplicator the applicator to use to apply the filters to the resulting inventory query
*/
private void processFilters(Map<String, List<ValueAndPos>> filters,
FilterApplicator filterApplicator) {
//first handle propertyName/propertyValue pairs
List<ValueAndPos> propertyNames = filters.get("propertyName");
List<ValueAndPos> propertyValues = filters.get("propertyValue");
if (propertyValues != null) {
if (propertyNames == null || propertyNames.size() < propertyValues.size()) {
throw new IllegalArgumentException("Unmatched propertyValue filters. For each propertyValue " +
"filter there must be a matching propertyName filter.");
}
//user might use propertyName=a;propertyValue=b;propertyName=a;propertyValue=c which we understand as a
// logical or.. to achieve that in our filters, we first need to "collapse" the values of a single
// prop into a list
Map<String, List<String>> nameAndValues = new LinkedHashMap<>(propertyNames.size());
int len = propertyValues.size();
for (int i = 0; i < len; ++i) {
String name = propertyNames.get(i).value;
String value = propertyValues.get(i).value;
List<String> values = nameAndValues.get(name);
if (values == null) {
values = new ArrayList<>(2);
nameAndValues.put(name, values);
}
values.add(value);
}
for (Map.Entry<String, List<String>> e : nameAndValues.entrySet()) {
filterApplicator.propertyValue(getQueryBuilder(), PathSegmentCodec.decode(e.getKey()),
e.getValue().stream().map(PathSegmentCodec::decode).toArray(String[]::new));
}
propertyNames.removeIf(p -> nameAndValues.keySet().contains(p.value));
}
if (propertyNames != null) {
propertyNames.stream().map(p -> PathSegmentCodec.decode(p.value))
.forEach(p -> filterApplicator.propertyName(getQueryBuilder(), p));
}
filters.remove("propertyName");
filters.remove("propertyValue");
//now process the relatedBy+relatedTo pairs
List<ValueAndPos> relatedBys = filters.getOrDefault("relatedBy", Collections.emptyList());
List<ValueAndPos> relatedTos = filters.getOrDefault("relatedTo", Collections.emptyList());
List<ValueAndPos> relatedWiths = filters.getOrDefault("relatedWith", Collections.emptyList());
if (relatedBys.size() != relatedTos.size() + relatedWiths.size()) {
throw new IllegalArgumentException("Each 'relatedBy' must correspond to 1 'relatedTo' or" +
" 'relatedWith'.");
}
for (int bys = 0, tos = 0, withs = 0; bys < relatedBys.size(); ++bys) {
String relatedBy = relatedBys.get(bys).value;
ValueAndPos relatedTo = tos < relatedTos.size() ? relatedTos.get(tos) : null;
ValueAndPos relatedWith = withs < relatedWiths.size() ? relatedWiths.get(withs) : null;
relatedBy = PathSegmentCodec.decode(relatedBy);
if (relatedTo != null && (relatedWith == null || relatedTo.pos < relatedWith.pos)) {
String value = PathSegmentCodec.decode(relatedTo.value);
CanonicalPath target = cpParser.apply(value);
filterApplicator.relatedBy(getQueryBuilder(), target, relatedBy);
tos++;
} else if (relatedWith != null) {
String value = PathSegmentCodec.decode(relatedWith.value);
CanonicalPath source = cpParser.apply(value);
filterApplicator.relatedWith(getQueryBuilder(), source, relatedBy);
withs++;
}
}
for (Map.Entry<String, List<ValueAndPos>> e : filters.entrySet()) {
String filterName = e.getKey();
String[] filterValues = e.getValue().stream().map(p -> PathSegmentCodec.decode(p.value))
.toArray(String[]::new);
switch (filterName) {
case "name":
filterApplicator.name(getQueryBuilder(), filterValues);
break;
case "sourceType":
Class<? extends Entity<?, ?>>[] types = getTypes(filterValues);
filterApplicator.sourceType(getQueryBuilder(), types);
break;
case "targetType":
types = getTypes(filterValues);
filterApplicator.targetType(getQueryBuilder(), types);
break;
case "id":
filterApplicator.id(getQueryBuilder(), filterValues);
break;
case "type":
types = getTypes(filterValues);
filterApplicator.type(getQueryBuilder(), types);
break;
case "cp":
CanonicalPath[] paths = Stream.of(filterValues).map(PathSegmentCodec::decode).map(cpParser)
.toArray(CanonicalPath[]::new);
filterApplicator.canonicalPath(getQueryBuilder(), paths);
break;
case "identical":
if (filterValues.length > 0) {
throw new IllegalArgumentException("The 'identical' filter doesn't accept any values.");
}
filterApplicator.identical(getQueryBuilder());
break;
case "definedBy":
CanonicalPath path = Stream.of(filterValues).map(PathSegmentCodec::decode).map(cpParser)
.findFirst().orElse(null);
filterApplicator.definedBy(getQueryBuilder(), path);
break;
}
}
}
private Query.Builder getQueryBuilder() {
return recurseBuilder == null ? query : recurseBuilder;
}
}
private <C extends RuleContext> Map<String, List<ValueAndPos>>
extractFilters(List<C> filterContexts, Function<C, RuleContext> filterName, Function<C, RuleContext> filterValue) {
Map<String, List<ValueAndPos>> ret = new LinkedHashMap<>();
int pos = 0;
for (C ctx : filterContexts) {
RuleContext nameCtx = filterName.apply(ctx);
RuleContext valueCtx = filterValue.apply(ctx);
String name = nameCtx.getText();
String value = valueCtx.getText();
List<ValueAndPos> values = ret.get(name);
if (values == null) {
values = new ArrayList<>();
ret.put(name, values);
}
values.add(new ValueAndPos(value, pos++));
}
return ret;
}
@SuppressWarnings("unchecked")
private Class<? extends Entity<?, ?>>[] getTypes(String[] typeFilterValues) {
List<Class<? extends Entity<?, ?>>> typeList = Stream.of(typeFilterValues).map(v -> {
SegmentType t = Utils.getSegmentTypeFromSimpleName(v);
return Entity.entityTypeFromSegmentType(t);
}).collect(toList());
return typeList.toArray(new Class[typeList.size()]);
}
private static final class ValueAndPos {
final String value;
final int pos;
private ValueAndPos(String value, int pos) {
this.value = value;
this.pos = pos;
}
}
private interface FilterApplicator {
default void propertyName(Query.Builder query, String name) {}
default void propertyValue(Query.Builder query, String propertyName, String[] propertyValues) {}
default void name(Query.Builder query, String[] names) {}
default void id(Query.Builder query, String[] id) {}
default void type(Query.Builder query, Class<? extends Entity<?, ?>>[] types) {}
default void sourceType(Query.Builder query, Class<? extends Entity<?, ?>>[] types) {}
default void targetType(Query.Builder query, Class<? extends Entity<?, ?>>[] types) {}
default void canonicalPath(Query.Builder query, CanonicalPath[] cps) {}
default void identical(Query.Builder query) {}
default void definedBy(Query.Builder query, CanonicalPath target) {}
default void relatedBy(Query.Builder query, CanonicalPath target, String relationship) {}
default void relatedWith(Query.Builder query, CanonicalPath source, String relationship) {}
class OnEntity implements FilterApplicator {
@Override public void canonicalPath(Query.Builder query, CanonicalPath[] cps) {
query.path().with(With.paths(cps));
}
@Override public void id(Query.Builder query, String[] ids) {
query.path().with(With.ids(ids));
}
@Override public void identical(Query.Builder query) {
query.path().with(With.sameIdentityHash());
}
@Override public void name(Query.Builder query, String[] names) {
query.filter().with(With.names(names));
}
@Override public void propertyName(Query.Builder query, String name) {
query.filter().with(With.property(name));
}
@Override public void propertyValue(Query.Builder query, String propertyName, String[] propertyValues) {
query.filter().with(With.propertyValues(propertyName, (Object[]) propertyValues));
}
@Override public void type(Query.Builder query, Class<? extends Entity<?, ?>>[] types) {
//path, because this can be part of the canonical path progression
query.path().with(With.types(types));
}
@Override public void definedBy(Query.Builder query, CanonicalPath target) {
query.filter().with(Defined.by(target));
}
@Override public void relatedBy(Query.Builder query, CanonicalPath target, String relationship) {
query.filter().with(Related.with(target, relationship));
}
@Override public void relatedWith(Query.Builder query, CanonicalPath source, String relationship) {
query.filter().with(Related.asTargetWith(source, relationship));
}
}
class OnRelationship implements FilterApplicator {
@Override public void id(Query.Builder query, String[] ids) {
query.path().with(RelationWith.ids(ids));
}
@Override public void name(Query.Builder query, String[] names) {
query.path().with(RelationWith.names(names));
}
@Override public void propertyName(Query.Builder query, String name) {
query.path().with(RelationWith.property(name));
}
@Override public void propertyValue(Query.Builder query, String propertyName, String[] propertyValues) {
query.path().with(RelationWith.propertyValues(propertyName, (Object[]) propertyValues));
}
@Override public void sourceType(Query.Builder query, Class<? extends Entity<?, ?>>[] types) {
query.path().with(RelationWith.sourcesOfTypes(types));
}
@Override public void targetType(Query.Builder query, Class<? extends Entity<?, ?>>[] types) {
query.path().with(RelationWith.targetsOfTypes(types));
}
}
}
}