da16c843b8793174c52a9698bb1bc97900893b13
[yangtools.git] / model / yang-model-api / src / main / java / org / opendaylight / yangtools / yang / model / api / stmt / IfFeatureExpr.java
1 /*
2  * Copyright (c) 2019 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.yangtools.yang.model.api.stmt;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Verify.verify;
12 import static com.google.common.base.Verify.verifyNotNull;
13 import static java.util.Objects.requireNonNull;
14
15 import com.google.common.annotations.Beta;
16 import com.google.common.collect.ImmutableSet;
17 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
18 import java.util.Arrays;
19 import java.util.HashSet;
20 import java.util.Set;
21 import java.util.function.Predicate;
22 import org.eclipse.jdt.annotation.NonNull;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.opendaylight.yangtools.concepts.Immutable;
25 import org.opendaylight.yangtools.yang.common.QName;
26
27 /**
28  * A resolved {@code if-feature} expression, implementing a {@link Predicate}. Internal representation is that of
29  * a tree of expressions, optimized for memory usage. {@link #negate()} performs an efficient logical negation without
30  * relying on default predicate methods. Other Predicate methods, like {@link #and(Predicate)} are not optimized in
31  * this implementation.
32  *
33  * <p>
34  * The set of features referenced in this expression is available through {@link #getReferencedFeatures()}.
35  *
36  * @author Robert Varga
37  */
38 @Beta
39 public abstract sealed class IfFeatureExpr implements Immutable, Predicate<Set<QName>> {
40     private abstract static sealed class Single extends IfFeatureExpr {
41         final QName qname;
42
43         Single(final QName qname) {
44             this.qname = requireNonNull(qname);
45         }
46
47         @Override
48         public final ImmutableSet<QName> getReferencedFeatures() {
49             return ImmutableSet.of(qname);
50         }
51
52         @Override
53         public final int hashCode() {
54             return qname.hashCode();
55         }
56
57         @Override
58         final void addQNames(final Set<QName> set) {
59             set.add(qname);
60         }
61
62         @Override
63         public final boolean equals(final Object obj) {
64             return this == obj || getClass().isInstance(obj) && qname.equals(((Single) obj).qname);
65         }
66     }
67
68     // We are using arrays to hold our components to save a wee bit of space. The arrays originate from Sets retaining
69     // insertion order of Lists, each component is guaranteed to be unique, in definition order, not appearing multiple
70     // times
71     private abstract static sealed class AbstractArray<T> extends IfFeatureExpr {
72         final T[] array;
73
74         AbstractArray(final T[] array) {
75             this.array = requireNonNull(array);
76             verify(array.length > 1);
77         }
78
79         @Override
80         public final int hashCode() {
81             return Arrays.hashCode(array);
82         }
83
84         @Override
85         public final boolean equals(final Object obj) {
86             return this == obj || getClass().isInstance(obj)
87                     && Arrays.deepEquals(array, ((AbstractArray<?>) obj).array);
88         }
89
90         abstract String infix();
91     }
92
93     private abstract static sealed class Complex extends AbstractArray<IfFeatureExpr> {
94         Complex(final IfFeatureExpr[] array) {
95             super(array);
96         }
97
98         @Override
99         public final Set<QName> getReferencedFeatures() {
100             final Set<QName> ret = new HashSet<>();
101             addQNames(ret);
102             return ret;
103         }
104
105         @Override
106         final void addQNames(final Set<QName> set) {
107             for (IfFeatureExpr expr : array) {
108                 expr.addQNames(set);
109             }
110         }
111
112         final IfFeatureExpr[] negateExprs() {
113             final IfFeatureExpr[] ret = new IfFeatureExpr[array.length];
114             for (int i = 0; i < array.length; i++) {
115                 ret[i] = verifyNotNull(array[i].negate());
116             }
117             return ret;
118         }
119
120         @Override
121         public final String toString() {
122             final StringBuilder sb = new StringBuilder("(");
123             sb.append(array[0]);
124             final String sep = infix();
125             for (int i = 1; i < array.length; ++i) {
126                 sb.append(sep).append(array[i]);
127             }
128             return sb.append(')').toString();
129         }
130     }
131
132     private abstract static sealed class Compound extends AbstractArray<QName> {
133         Compound(final QName[] qnames) {
134             super(qnames);
135         }
136
137         @Override
138         public final ImmutableSet<QName> getReferencedFeatures() {
139             return ImmutableSet.copyOf(array);
140         }
141
142         @Override
143         final void addQNames(final Set<QName> set) {
144             set.addAll(Arrays.asList(array));
145         }
146
147         @Override
148         public final String toString() {
149             final StringBuilder sb = new StringBuilder();
150             if (negated()) {
151                 sb.append("not ");
152             }
153
154             sb.append("(\"").append(array[0]).append('"');
155             final String sep = infix();
156             for (int i = 1; i < array.length; ++i) {
157                 sb.append(sep).append('"').append(array[i]).append('"');
158             }
159             return sb.append(')').toString();
160         }
161
162         abstract boolean negated();
163     }
164
165     private static final class Absent extends Single {
166         Absent(final QName qname) {
167             super(qname);
168         }
169
170         @Override
171         public IfFeatureExpr negate() {
172             return isPresent(qname);
173         }
174
175         @Override
176         public boolean test(final Set<QName> supportedFeatures) {
177             return !supportedFeatures.contains(qname);
178         }
179
180         @Override
181         public String toString() {
182             return "not \"" + qname + '"';
183         }
184     }
185
186     private static final class Present extends Single {
187         Present(final QName qname) {
188             super(qname);
189         }
190
191         @Override
192         public IfFeatureExpr negate() {
193             return new Absent(qname);
194         }
195
196         @Override
197         public boolean test(final Set<QName> supportedFeatures) {
198             return supportedFeatures.contains(qname);
199         }
200
201         @Override
202         public String toString() {
203             return "\"" + qname + '"';
204         }
205     }
206
207     private static final class AllExprs extends Complex {
208         AllExprs(final IfFeatureExpr[] exprs) {
209             super(exprs);
210         }
211
212         @Override
213         public IfFeatureExpr negate() {
214             return new AnyExpr(negateExprs());
215         }
216
217         @Override
218         public boolean test(final Set<QName> supportedFeatures) {
219             for (IfFeatureExpr expr : array) {
220                 if (!expr.test(supportedFeatures)) {
221                     return false;
222                 }
223             }
224             return true;
225         }
226
227         @Override
228         String infix() {
229             return " and ";
230         }
231     }
232
233     private static final class AnyExpr extends Complex {
234         AnyExpr(final IfFeatureExpr[] exprs) {
235             super(exprs);
236         }
237
238         @Override
239         public IfFeatureExpr negate() {
240             return new AllExprs(negateExprs());
241         }
242
243         @Override
244         public boolean test(final Set<QName> supportedFeatures) {
245             for (IfFeatureExpr expr : array) {
246                 if (expr.test(supportedFeatures)) {
247                     return true;
248                 }
249             }
250             return false;
251         }
252
253         @Override
254         String infix() {
255             return " or ";
256         }
257     }
258
259     private abstract static sealed class AbstractAll extends Compound {
260         AbstractAll(final QName[] qnames) {
261             super(qnames);
262         }
263
264         @Override
265         public final boolean test(final Set<QName> supportedFeatures) {
266             final boolean neg = negated();
267             for (QName qname : array) {
268                 if (supportedFeatures.contains(qname) == neg) {
269                     return false;
270                 }
271             }
272             return true;
273         }
274
275         @Override
276         final String infix() {
277             return " and ";
278         }
279     }
280
281     private static final class All extends AbstractAll {
282         All(final QName[] qnames) {
283             super(qnames);
284         }
285
286         @Override
287         public IfFeatureExpr negate() {
288             return new NotAll(array);
289         }
290
291         @Override
292         boolean negated() {
293             return false;
294         }
295     }
296
297     private static final class NotAll extends AbstractAll {
298         NotAll(final QName[] qnames) {
299             super(qnames);
300         }
301
302         @Override
303         public IfFeatureExpr negate() {
304             return new All(array);
305         }
306
307         @Override
308         boolean negated() {
309             return true;
310         }
311     }
312
313     private abstract static sealed class AbstractAny extends Compound {
314         AbstractAny(final QName[] qnames) {
315             super(qnames);
316         }
317
318         @Override
319         public final boolean test(final Set<QName> supportedFeatures) {
320             for (QName qname : array) {
321                 if (supportedFeatures.contains(qname)) {
322                     return !negated();
323                 }
324             }
325             return negated();
326         }
327
328         @Override
329         final String infix() {
330             return " or ";
331         }
332     }
333
334     private static final class Any extends AbstractAny {
335         Any(final QName[] array) {
336             super(array);
337         }
338
339         @Override
340         public IfFeatureExpr negate() {
341             return new NotAny(array);
342         }
343
344         @Override
345         boolean negated() {
346             return false;
347         }
348     }
349
350     private static final class NotAny extends AbstractAny {
351         NotAny(final QName[] qnames) {
352             super(qnames);
353         }
354
355         @Override
356         public IfFeatureExpr negate() {
357             return new Any(array);
358         }
359
360         @Override
361         boolean negated() {
362             return true;
363         }
364     }
365
366     /**
367      * Construct an assertion that a feature is present in the set passed to {@link #test(Set)}.
368      *
369      * @param qname Feature QName
370      * @return An expression
371      * @throws NullPointerException if {@code qname} is null
372      */
373     public static final @NonNull IfFeatureExpr isPresent(final QName qname) {
374         return new Present(qname);
375     }
376
377     /**
378      * Construct a intersection (logical {@code AND}) expression of specified expressions.
379      *
380      * @param exprs Constituent expressions
381      * @return An expression
382      * @throws NullPointerException if {@code exprs} or any of its members is null
383      * @throws IllegalArgumentException if {@code exprs} is empty
384      */
385     public static final @NonNull IfFeatureExpr and(final Set<IfFeatureExpr> exprs) {
386         checkArgument(!exprs.isEmpty(), "Expressions may not be empty");
387         if (exprs.size() == 1) {
388             return exprs.iterator().next();
389         }
390         final Boolean composition = composition(exprs);
391         if (composition == null) {
392             return new AllExprs(exprs.toArray(new IfFeatureExpr[0]));
393         }
394
395         final QName[] qnames = extractQNames(exprs);
396         return composition ? new All(qnames) : new NotAny(qnames);
397     }
398
399     /**
400      * Construct a union (logical {@code OR}) expression of specified expressions.
401      *
402      * @param exprs Constituent expressions
403      * @return An expression
404      * @throws NullPointerException if {@code exprs} or any of its members is null
405      * @throws IllegalArgumentException if {@code exprs} is empty
406      */
407     public static final @NonNull IfFeatureExpr or(final Set<IfFeatureExpr> exprs) {
408         checkArgument(!exprs.isEmpty(), "Expressions may not be empty");
409         if (exprs.size() == 1) {
410             return exprs.iterator().next();
411         }
412         final Boolean composition = composition(exprs);
413         if (composition == null) {
414             return new AnyExpr(exprs.toArray(new IfFeatureExpr[0]));
415         }
416
417         final QName[] qnames = extractQNames(exprs);
418         return composition ? new Any(qnames) : new NotAll(qnames);
419     }
420
421     /**
422      * Returns the set of all {@code feature}s referenced by this expression. Each feature is identified by its QName.
423      *
424      * @return The set of referenced features. Mutability of the returned Set and order of features is undefined.
425      */
426     public abstract @NonNull Set<QName> getReferencedFeatures();
427
428     @Override
429     public abstract @NonNull IfFeatureExpr negate();
430
431     @Override
432     public abstract boolean test(Set<QName> supportedFeatures);
433
434     @Override
435     public abstract int hashCode();
436
437     @Override
438     public abstract boolean equals(Object obj);
439
440     @Override
441     public abstract String toString();
442
443     /**
444      * Add QNames referenced by this expression into a target set.
445      *
446      * @param set The set to fill
447      * @throws NullPointerException if {@code set} is null
448      */
449     abstract void addQNames(@NonNull Set<QName> set);
450
451     @SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL",
452             justification = "We need a tri-state value, this is the simplest")
453     private static @Nullable Boolean composition(final Set<IfFeatureExpr> exprs) {
454         boolean negative = false;
455         boolean positive = false;
456         for (IfFeatureExpr expr : exprs) {
457             if (expr instanceof Present) {
458                 positive = true;
459             } else if (expr instanceof Absent) {
460                 negative = true;
461             } else {
462                 return null;
463             }
464         }
465
466         verify(negative || positive, "Unresolved expressions %s", exprs);
467         return positive == negative ? null : positive;
468     }
469
470     private static QName[] extractQNames(final Set<IfFeatureExpr> exprs) {
471         return exprs.stream().map(expr -> {
472             verify(expr instanceof Single, "Unexpected expression %s", expr);
473             return ((Single) expr).qname;
474         }).collect(ImmutableSet.toImmutableSet()).toArray(new QName[0]);
475     }
476 }