org.hawkular.inventory.rest.Traverser.java Source code

Java tutorial

Introduction

Here is the source code for org.hawkular.inventory.rest.Traverser.java

Source

/*
 * 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));
            }
        }
    }
}