Define IfFeature via a specialized class 49/80749/2
authorRobert Varga <robert.varga@pantheon.tech>
Fri, 8 Mar 2019 14:04:34 +0000 (15:04 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Sat, 9 Mar 2019 21:07:02 +0000 (22:07 +0100)
This reworks the grammar to use less recursion, as we can use
wildcards to eagerly combine expressions like "foo || bar || baz"
into any(foo, bar, baz) rather than or(foo, or(bar, baz)).

In order to properly support this contract, we define IfFeatureExpr,
which we specialize, so that we do not need to lug an opaque
predicate tree.

JIRA: YANGTOOLS-964
Change-Id: I048c3ced9c074e340031ac53c1117ce881b0a78b
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
(cherry picked from commit 5874f5da6ff34f70c5754f5a98dbd0a32820fba1)
(cherry picked from commit 024e83fdfef852a995ba5d43e3f753cd391afd69)

yang/yang-model-api/src/main/java/org/opendaylight/yangtools/yang/model/api/stmt/IfFeatureExpr.java [new file with mode: 0644]
yang/yang-parser-rfc7950/src/main/antlr/IfFeatureExpressionParser.g4
yang/yang-parser-rfc7950/src/main/java/org/opendaylight/yangtools/yang/parser/rfc7950/stmt/if_feature/IfFeaturePredicateVisitor.java
yang/yang-parser-rfc7950/src/main/java/org/opendaylight/yangtools/yang/parser/rfc7950/stmt/if_feature/IfFeatureStatementSupport.java

diff --git a/yang/yang-model-api/src/main/java/org/opendaylight/yangtools/yang/model/api/stmt/IfFeatureExpr.java b/yang/yang-model-api/src/main/java/org/opendaylight/yangtools/yang/model/api/stmt/IfFeatureExpr.java
new file mode 100644 (file)
index 0000000..37c1d73
--- /dev/null
@@ -0,0 +1,476 @@
+/*
+ * Copyright (c) 2019 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.model.api.stmt;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Verify.verify;
+import static com.google.common.base.Verify.verifyNotNull;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.Beta;
+import com.google.common.collect.ImmutableSet;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Predicate;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.concepts.Immutable;
+import org.opendaylight.yangtools.yang.common.QName;
+
+/**
+ * A resolved {@code if-feature} expression, implementing a {@link Predicate}. Internal representation is that of
+ * a tree of expressions, optimized for memory usage. {@link #negate()} performs an efficient logical negation without
+ * relying on default predicate methods. Other Predicate methods, like {@link #and(Predicate)} are not optimized in
+ * this implementation.
+ *
+ * <p>
+ * The set of features referenced in this expression is available through {@link #getReferencedFeatures()}.
+ *
+ * @author Robert Varga
+ */
+@Beta
+public abstract class IfFeatureExpr implements Immutable, Predicate<Set<QName>> {
+    private abstract static class Single extends IfFeatureExpr {
+        final QName qname;
+
+        Single(final QName qname) {
+            this.qname = requireNonNull(qname);
+        }
+
+        @Override
+        public final ImmutableSet<QName> getReferencedFeatures() {
+            return ImmutableSet.of(qname);
+        }
+
+        @Override
+        public final int hashCode() {
+            return qname.hashCode();
+        }
+
+        @Override
+        final void addQNames(final Set<QName> set) {
+            set.add(qname);
+        }
+
+        @Override
+        public final boolean equals(final Object obj) {
+            return this == obj || getClass().isInstance(obj) && qname.equals(((Single) obj).qname);
+        }
+    }
+
+    // We are using arrays to hold our components to save a wee bit of space. The arrays originate from Sets retaining
+    // insertion order of Lists, each component is guaranteed to be unique, in definition order, not appearing multiple
+    // times
+    private abstract static class AbstractArray<T> extends IfFeatureExpr {
+        final T[] array;
+
+        AbstractArray(final T[] array) {
+            this.array = requireNonNull(array);
+            verify(array.length > 1);
+        }
+
+        @Override
+        public final int hashCode() {
+            return Arrays.hashCode(array);
+        }
+
+        @Override
+        public final boolean equals(final Object obj) {
+            return this == obj || getClass().isInstance(obj)
+                    && Arrays.deepEquals(array, ((AbstractArray<?>) obj).array);
+        }
+
+        abstract String infix();
+    }
+
+    private abstract static class Complex extends AbstractArray<IfFeatureExpr> {
+        Complex(final IfFeatureExpr[] array) {
+            super(array);
+        }
+
+        @Override
+        public final Set<QName> getReferencedFeatures() {
+            final Set<QName> ret = new HashSet<>();
+            addQNames(ret);
+            return ret;
+        }
+
+        @Override
+        final void addQNames(final Set<QName> set) {
+            for (IfFeatureExpr expr : array) {
+                expr.addQNames(set);
+            }
+        }
+
+        final IfFeatureExpr[] negateExprs() {
+            final IfFeatureExpr[] ret = new IfFeatureExpr[array.length];
+            for (int i = 0; i < array.length; i++) {
+                ret[i] = verifyNotNull(array[i].negate());
+            }
+            return ret;
+        }
+
+        @Override
+        public final String toString() {
+            final StringBuilder sb = new StringBuilder("(");
+            sb.append(array[0]);
+            final String sep = infix();
+            for (int i = 1; i < array.length; ++i) {
+                sb.append(sep).append(array[i]);
+            }
+            return sb.append(')').toString();
+        }
+    }
+
+    private abstract static class Compound extends AbstractArray<QName> {
+        Compound(final QName[] qnames) {
+            super(qnames);
+        }
+
+        @Override
+        public final ImmutableSet<QName> getReferencedFeatures() {
+            return ImmutableSet.copyOf(array);
+        }
+
+        @Override
+        final void addQNames(final Set<QName> set) {
+            set.addAll(Arrays.asList(array));
+        }
+
+        @Override
+        public final String toString() {
+            final StringBuilder sb = new StringBuilder();
+            if (negated()) {
+                sb.append("not ");
+            }
+
+            sb.append("(\"").append(array[0]).append('"');
+            final String sep = infix();
+            for (int i = 1; i < array.length; ++i) {
+                sb.append(sep).append('"').append(array[i]).append('"');
+            }
+            return sb.append(')').toString();
+        }
+
+        abstract boolean negated();
+    }
+
+    private static final class Absent extends Single {
+        Absent(final QName qname) {
+            super(qname);
+        }
+
+        @Override
+        public IfFeatureExpr negate() {
+            return isPresent(qname);
+        }
+
+        @Override
+        public boolean test(final Set<QName> supportedFeatures) {
+            return !supportedFeatures.contains(qname);
+        }
+
+        @Override
+        public String toString() {
+            return "not \"" + qname + '"';
+        }
+    }
+
+    private static final class Present extends Single {
+        Present(final QName qname) {
+            super(qname);
+        }
+
+        @Override
+        public IfFeatureExpr negate() {
+            return new Absent(qname);
+        }
+
+        @Override
+        public boolean test(final Set<QName> supportedFeatures) {
+            return supportedFeatures.contains(qname);
+        }
+
+        @Override
+        public String toString() {
+            return "\"" + qname + '"';
+        }
+    }
+
+    private static final class AllExprs extends Complex {
+        AllExprs(final IfFeatureExpr[] exprs) {
+            super(exprs);
+        }
+
+        @Override
+        public IfFeatureExpr negate() {
+            return new AnyExpr(negateExprs());
+        }
+
+        @Override
+        public boolean test(final Set<QName> supportedFeatures) {
+            for (IfFeatureExpr expr : array) {
+                if (!expr.test(supportedFeatures)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        @Override
+        String infix() {
+            return " and ";
+        }
+    }
+
+    private static final class AnyExpr extends Complex {
+        AnyExpr(final IfFeatureExpr[] exprs) {
+            super(exprs);
+        }
+
+        @Override
+        public IfFeatureExpr negate() {
+            return new AllExprs(negateExprs());
+        }
+
+        @Override
+        public boolean test(final Set<QName> supportedFeatures) {
+            for (IfFeatureExpr expr : array) {
+                if (expr.test(supportedFeatures)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        String infix() {
+            return " or ";
+        }
+    }
+
+    private abstract static class AbstractAll extends Compound {
+        AbstractAll(final QName[] qnames) {
+            super(qnames);
+        }
+
+        @Override
+        public final boolean test(final Set<QName> supportedFeatures) {
+            final boolean neg = negated();
+            for (QName qname : array) {
+                if (supportedFeatures.contains(qname) == neg) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        @Override
+        final String infix() {
+            return " and ";
+        }
+    }
+
+    private static final class All extends AbstractAll {
+        All(final QName[] qnames) {
+            super(qnames);
+        }
+
+        @Override
+        public IfFeatureExpr negate() {
+            return new NotAll(array);
+        }
+
+        @Override
+        boolean negated() {
+            return false;
+        }
+    }
+
+    private static final class NotAll extends AbstractAll {
+        NotAll(final QName[] qnames) {
+            super(qnames);
+        }
+
+        @Override
+        public IfFeatureExpr negate() {
+            return new All(array);
+        }
+
+        @Override
+        boolean negated() {
+            return true;
+        }
+    }
+
+    private abstract static class AbstractAny extends Compound {
+        AbstractAny(final QName[] qnames) {
+            super(qnames);
+        }
+
+        @Override
+        public final boolean test(final Set<QName> supportedFeatures) {
+            for (QName qname : array) {
+                if (supportedFeatures.contains(qname)) {
+                    return !negated();
+                }
+            }
+            return negated();
+        }
+
+        @Override
+        final String infix() {
+            return " or ";
+        }
+    }
+
+    private static final class Any extends AbstractAny {
+        Any(final QName[] array) {
+            super(array);
+        }
+
+        @Override
+        public IfFeatureExpr negate() {
+            return new NotAny(array);
+        }
+
+        @Override
+        boolean negated() {
+            return false;
+        }
+    }
+
+    private static final class NotAny extends AbstractAny {
+        NotAny(final QName[] qnames) {
+            super(qnames);
+        }
+
+        @Override
+        public IfFeatureExpr negate() {
+            return new Any(array);
+        }
+
+        @Override
+        boolean negated() {
+            return true;
+        }
+    }
+
+    /**
+     * Construct an assertion that a feature is present in the set passed to {@link #test(Set)}.
+     *
+     * @param qname Feature QName
+     * @return An expression
+     * @throws NullPointerException if {@code qname} is null
+     */
+    public static final @NonNull IfFeatureExpr isPresent(final QName qname) {
+        return new Present(qname);
+    }
+
+    /**
+     * Construct a intersection (logical {@code AND}) expression of specified expressions.
+     *
+     * @param exprs Constituent expressions
+     * @return An expression
+     * @throws NullPointerException if {@code exprs} or any of its members is null
+     * @throws IllegalArgumentException if {@code exprs} is empty
+     */
+    public static final @NonNull IfFeatureExpr and(final Set<IfFeatureExpr> exprs) {
+        checkArgument(!exprs.isEmpty(), "Expressions may not be empty");
+        if (exprs.size() == 1) {
+            return exprs.iterator().next();
+        }
+        final Boolean composition = composition(exprs);
+        if (composition == null) {
+            return new AllExprs(exprs.toArray(new IfFeatureExpr[0]));
+        }
+
+        final QName[] qnames = extractQNames(exprs);
+        return composition ? new All(qnames) : new NotAny(qnames);
+    }
+
+    /**
+     * Construct a union (logical {@code OR}) expression of specified expressions.
+     *
+     * @param exprs Constituent expressions
+     * @return An expression
+     * @throws NullPointerException if {@code exprs} or any of its members is null
+     * @throws IllegalArgumentException if {@code exprs} is empty
+     */
+    public static final @NonNull IfFeatureExpr or(final Set<IfFeatureExpr> exprs) {
+        checkArgument(!exprs.isEmpty(), "Expressions may not be empty");
+        if (exprs.size() == 1) {
+            return exprs.iterator().next();
+        }
+        final Boolean composition = composition(exprs);
+        if (composition == null) {
+            return new AnyExpr(exprs.toArray(new IfFeatureExpr[0]));
+        }
+
+        final QName[] qnames = extractQNames(exprs);
+        return composition ? new Any(qnames) : new NotAll(qnames);
+    }
+
+    /**
+     * Returns the set of all {@code feature}s referenced by this expression. Each feature is identified by its QName.
+     *
+     * @return The set of referenced features. Mutability of the returned Set and order of features is undefined.
+     */
+    public abstract @NonNull Set<QName> getReferencedFeatures();
+
+    @Override
+    public abstract @NonNull IfFeatureExpr negate();
+
+    @Override
+    public abstract boolean test(Set<QName> supportedFeatures);
+
+    @Override
+    public abstract int hashCode();
+
+    @Override
+    public abstract boolean equals(Object obj);
+
+    @Override
+    public abstract String toString();
+
+    /**
+     * Add QNames referenced by this expression into a target set.
+     *
+     * @param set The set to fill
+     * @throws NullPointerException if {@code set} is null
+     */
+    abstract void addQNames(@NonNull Set<QName> set);
+
+    @SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL",
+            justification = "We need a tri-state value, this is the simplest")
+    private static @Nullable Boolean composition(final Set<IfFeatureExpr> exprs) {
+        boolean negative = false;
+        boolean positive = false;
+        for (IfFeatureExpr expr : exprs) {
+            if (expr instanceof Present) {
+                positive = true;
+            } else if (expr instanceof Absent) {
+                negative = true;
+            } else {
+                return null;
+            }
+        }
+
+        verify(negative || positive, "Unresolved expressions %s", exprs);
+        return positive == negative ? null : positive;
+    }
+
+    private static QName[] extractQNames(final Set<IfFeatureExpr> exprs) {
+        return exprs.stream().map(expr -> {
+            verify(expr instanceof Single, "Unexpected expression %s", expr);
+            return ((Single) expr).qname;
+        }).collect(ImmutableSet.toImmutableSet()).toArray(new QName[0]);
+    }
+}
index 14c58d955516fdd5028315e0d7d13655d0c63d81..33daa34f4bef42ffb1b9f12c56ed2ca034a386db 100644 (file)
@@ -16,10 +16,10 @@ options{
 }
 
 
-if_feature_expr: if_feature_term (SEP OR SEP if_feature_expr)?;
-if_feature_term: if_feature_factor (SEP AND SEP if_feature_term)?;
+if_feature_expr: if_feature_term (SEP OR SEP if_feature_term)*;
+if_feature_term: if_feature_factor (SEP AND SEP if_feature_term)*;
 if_feature_factor: NOT SEP if_feature_factor
                  | LP SEP? if_feature_expr SEP? RP
                  | identifier_ref_arg;
 
-identifier_ref_arg : (IDENTIFIER COLON)? IDENTIFIER;
\ No newline at end of file
+identifier_ref_arg : (IDENTIFIER COLON)? IDENTIFIER;
index eee214d319388d46902118ed1a6915ee673c0243..117101620dde8d5996cc93a96699355d0f11763a 100644 (file)
@@ -9,8 +9,9 @@ package org.opendaylight.yangtools.yang.parser.rfc7950.stmt.if_feature;
 
 import static java.util.Objects.requireNonNull;
 
-import java.util.Set;
-import java.util.function.Predicate;
+import com.google.common.collect.ImmutableSet;
+import java.util.ArrayList;
+import java.util.List;
 import org.antlr.v4.runtime.CharStreams;
 import org.antlr.v4.runtime.CommonTokenStream;
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -22,43 +23,53 @@ import org.opendaylight.yangtools.antlrv4.code.gen.IfFeatureExpressionParser.If_
 import org.opendaylight.yangtools.antlrv4.code.gen.IfFeatureExpressionParser.If_feature_factorContext;
 import org.opendaylight.yangtools.antlrv4.code.gen.IfFeatureExpressionParser.If_feature_termContext;
 import org.opendaylight.yangtools.antlrv4.code.gen.IfFeatureExpressionParserBaseVisitor;
-import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.model.api.stmt.IfFeatureExpr;
 import org.opendaylight.yangtools.yang.parser.rfc7950.antlr.SourceExceptionParser;
 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContext;
 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContextUtils;
 import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
 
 @NonNullByDefault
-final class IfFeaturePredicateVisitor extends IfFeatureExpressionParserBaseVisitor<Predicate<Set<QName>>> {
+final class IfFeaturePredicateVisitor extends IfFeatureExpressionParserBaseVisitor<IfFeatureExpr> {
     private final StmtContext<?, ?, ?> stmtCtx;
 
     private IfFeaturePredicateVisitor(final StmtContext<?, ?, ?> ctx) {
         this.stmtCtx = requireNonNull(ctx);
     }
 
-    static Predicate<Set<QName>> parseIfFeatureExpression(final StmtContext<?, ?, ?> ctx, final String value) {
+    static IfFeatureExpr parseIfFeatureExpression(final StmtContext<?, ?, ?> ctx, final String value) {
         final IfFeatureExpressionLexer lexer = new IfFeatureExpressionLexer(CharStreams.fromString(value));
         final IfFeatureExpressionParser parser = new IfFeatureExpressionParser(new CommonTokenStream(lexer));
-        return new IfFeaturePredicateVisitor(ctx).visit(SourceExceptionParser.parse(lexer, parser,
+        final IfFeatureExpr ret = new IfFeaturePredicateVisitor(ctx).visit(SourceExceptionParser.parse(lexer, parser,
             parser::if_feature_expr, ctx.getStatementSourceReference()));
+
+        return ret;
     }
 
     @Override
-    public Predicate<Set<QName>> visitIf_feature_expr(final @Nullable If_feature_exprContext ctx) {
-        final Predicate<Set<QName>> term = visitIf_feature_term(ctx.if_feature_term());
-        final If_feature_exprContext expr = ctx.if_feature_expr();
-        return expr == null ? term : term.or(visitIf_feature_expr(expr));
+    public IfFeatureExpr visitIf_feature_expr(final @Nullable If_feature_exprContext ctx) {
+        return IfFeatureExpr.or(ctx.if_feature_term().stream()
+            .map(this::visitIf_feature_term)
+            .collect(ImmutableSet.toImmutableSet()));
     }
 
     @Override
-    public Predicate<Set<QName>> visitIf_feature_term(final @Nullable If_feature_termContext ctx) {
-        final Predicate<Set<QName>> factor = visitIf_feature_factor(ctx.if_feature_factor());
-        final If_feature_termContext term = ctx.if_feature_term();
-        return term == null ? factor : factor.and(visitIf_feature_term(term));
+    public IfFeatureExpr visitIf_feature_term(final @Nullable If_feature_termContext ctx) {
+        final IfFeatureExpr factor = visitIf_feature_factor(ctx.if_feature_factor());
+        final List<If_feature_termContext> terms = ctx.if_feature_term();
+        if (terms == null || terms.isEmpty()) {
+            return factor;
+        }
+        final List<IfFeatureExpr> factors = new ArrayList<>(terms.size() + 1);
+        factors.add(factor);
+        for (If_feature_termContext term : terms) {
+            factors.add(visitIf_feature_term(term));
+        }
+        return IfFeatureExpr.and(ImmutableSet.copyOf(factors));
     }
 
     @Override
-    public Predicate<Set<QName>> visitIf_feature_factor(final @Nullable If_feature_factorContext ctx) {
+    public IfFeatureExpr visitIf_feature_factor(final @Nullable If_feature_factorContext ctx) {
         if (ctx.if_feature_expr() != null) {
             return visitIf_feature_expr(ctx.if_feature_expr());
         } else if (ctx.if_feature_factor() != null) {
@@ -72,8 +83,7 @@ final class IfFeaturePredicateVisitor extends IfFeatureExpressionParserBaseVisit
     }
 
     @Override
-    public Predicate<Set<QName>> visitIdentifier_ref_arg(final @Nullable Identifier_ref_argContext ctx) {
-        final QName featureQName = StmtContextUtils.parseNodeIdentifier(stmtCtx, ctx.getText());
-        return setQNames -> setQNames.contains(featureQName);
+    public IfFeatureExpr visitIdentifier_ref_arg(final @Nullable Identifier_ref_argContext ctx) {
+        return IfFeatureExpr.isPresent(StmtContextUtils.parseNodeIdentifier(stmtCtx, ctx.getText()));
     }
 }
\ No newline at end of file
index b5311cf3d2b32a7584638bed480aba6f98836136..4ca9a88b750c7e117e86e4ffc1009008ba5bdcb6 100644 (file)
@@ -13,6 +13,7 @@ import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.common.YangVersion;
 import org.opendaylight.yangtools.yang.model.api.YangStmtMapping;
 import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement;
+import org.opendaylight.yangtools.yang.model.api.stmt.IfFeatureExpr;
 import org.opendaylight.yangtools.yang.model.api.stmt.IfFeatureStatement;
 import org.opendaylight.yangtools.yang.parser.spi.meta.AbstractStatementSupport;
 import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContext;
@@ -39,9 +40,7 @@ public final class IfFeatureStatementSupport extends AbstractStatementSupport<Pr
         if (YangVersion.VERSION_1_1.equals(ctx.getRootVersion())) {
             return IfFeaturePredicateVisitor.parseIfFeatureExpression(ctx, value);
         }
-
-        final QName qname = StmtContextUtils.parseNodeIdentifier(ctx, value);
-        return setQNames -> setQNames.contains(qname);
+        return IfFeatureExpr.isPresent(StmtContextUtils.parseNodeIdentifier(ctx, value));
     }
 
     @Override