/* * 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.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 java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import org.eclipse.jdt.annotation.NonNull; 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. * *

* The set of features referenced in this expression is available through {@link #getReferencedFeatures()}. * * @author Robert Varga */ @Beta public abstract sealed class IfFeatureExpr implements Immutable, Predicate> { private abstract static sealed class Single extends IfFeatureExpr { final QName qname; Single(final QName qname) { this.qname = requireNonNull(qname); } @Override public final ImmutableSet getReferencedFeatures() { return ImmutableSet.of(qname); } @Override public final int hashCode() { return qname.hashCode(); } @Override final void addQNames(final Set 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 sealed class AbstractArray 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 sealed class Complex extends AbstractArray { Complex(final IfFeatureExpr[] array) { super(array); } @Override public final Set getReferencedFeatures() { final Set ret = new HashSet<>(); addQNames(ret); return ret; } @Override final void addQNames(final Set 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 sealed class Compound extends AbstractArray { Compound(final QName[] qnames) { super(qnames); } @Override public final ImmutableSet getReferencedFeatures() { return ImmutableSet.copyOf(array); } @Override final void addQNames(final Set 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 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 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 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 supportedFeatures) { for (IfFeatureExpr expr : array) { if (expr.test(supportedFeatures)) { return true; } } return false; } @Override String infix() { return " or "; } } private abstract static sealed class AbstractAll extends Compound { AbstractAll(final QName[] qnames) { super(qnames); } @Override public final boolean test(final Set 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 sealed class AbstractAny extends Compound { AbstractAny(final QName[] qnames) { super(qnames); } @Override public final boolean test(final Set 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 exprs) { return compose(exprs, All::new, NotAny::new, AllExprs::new); } /** * 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 exprs) { return compose(exprs, Any::new, NotAll::new, AnyExpr::new); } /** * 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 getReferencedFeatures(); @Override public abstract @NonNull IfFeatureExpr negate(); @Override public abstract boolean test(Set 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 set); private static @NonNull IfFeatureExpr compose(final Set exprs, final Function allPresent, final Function allAbsent, final Function mixed) { switch (exprs.size()) { case 0: throw new IllegalArgumentException("Expressions may not be empty"); case 1: return requireNonNull(exprs.iterator().next()); default: // Heavy lifting is needed } 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 mixed.apply(exprs.toArray(new IfFeatureExpr[0])); } } verify(negative || positive, "Unresolved expressions %s", exprs); if (positive == negative) { return mixed.apply(exprs.toArray(new IfFeatureExpr[0])); } final var qnames = exprs.stream() .map(expr -> { verify(expr instanceof Single, "Unexpected expression %s", expr); return ((Single) expr).qname; }) .collect(ImmutableSet.toImmutableSet()) .toArray(new QName[0]); return positive ? allPresent.apply(qnames) : allAbsent.apply(qnames); } }