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