1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath.cst.items;
7   
8   import java.util.ArrayList;
9   import java.util.Collection;
10  import java.util.Collections;
11  import java.util.Iterator;
12  import java.util.LinkedHashMap;
13  import java.util.List;
14  import java.util.Map;
15  import java.util.NoSuchElementException;
16  import java.util.stream.Collectors;
17  import java.util.stream.Stream;
18  
19  import dev.metaschema.core.metapath.DynamicContext;
20  import dev.metaschema.core.metapath.IExpression;
21  import dev.metaschema.core.metapath.cst.AbstractExpression;
22  import dev.metaschema.core.metapath.cst.IExpressionVisitor;
23  import dev.metaschema.core.metapath.function.library.FnBoolean;
24  import dev.metaschema.core.metapath.item.IItem;
25  import dev.metaschema.core.metapath.item.ISequence;
26  import dev.metaschema.core.metapath.item.atomic.IBooleanItem;
27  import dev.metaschema.core.qname.IEnhancedQName;
28  import dev.metaschema.core.util.ObjectUtils;
29  import edu.umd.cs.findbugs.annotations.NonNull;
30  
31  /**
32   * An XPath 3.1 <a href=
33   * "https://www.w3.org/TR/xpath-31/#id-quantified-expressions">quantified
34   * expression</a>.
35   */
36  public class Quantified
37      extends AbstractExpression {
38    /**
39     * The quantified expression qualifier.
40     */
41    public enum Quantifier {
42      /**
43       * The quantified expression is {@code true} if at least one evaluation of the
44       * test expression has the
45       * <a href="https://www.w3.org/TR/xpath-31/#dt-ebv">effective boolean value</a>
46       * {@code true}; otherwise the quantified expression is {@code false}.
47       * <p>
48       * This rule implies that, if the in-clauses generate zero binding tuples, the
49       * value of the quantified expression is {@code false}.
50       */
51      SOME,
52      /**
53       * the quantified expression is {@code true} if every evaluation of the test
54       * expression has the <a href="https://www.w3.org/TR/xpath-31/#dt-ebv">effective
55       * boolean value</a> {@code true}; otherwise the quantified expression is
56       * {@code false}.
57       * <p>
58       * This rule implies that, if the in-clauses generate zero binding tuples, the
59       * value of the quantified expression is {@code true}.
60       */
61      EVERY;
62    }
63  
64    @NonNull
65    private final Quantifier quantifier;
66    @NonNull
67    private final Map<IEnhancedQName, IExpression> inClauses;
68    @NonNull
69    private final IExpression satisfies;
70  
71    /**
72     * Construct a new quantified expression.
73     *
74     * @param text
75     *          the parsed text of the expression
76     * @param quantifier
77     *          the quantifier operation
78     * @param inClauses
79     *          the set of expressions that define the variables to use for
80     *          determining the Cartesian product for evaluation
81     * @param satisfies
82     *          the expression used for evaluation using the Cartesian product of
83     *          the variables
84     */
85    public Quantified(
86        @NonNull String text,
87        @NonNull Quantifier quantifier,
88        @NonNull Map<IEnhancedQName, IExpression> inClauses,
89        @NonNull IExpression satisfies) {
90      super(text);
91      this.quantifier = quantifier;
92      this.inClauses = inClauses;
93      this.satisfies = satisfies;
94    }
95  
96    /**
97     * Get the quantifier operation.
98     *
99     * @return the quantifier operations
100    */
101   @NonNull
102   public Quantifier getQuantifier() {
103     return quantifier;
104   }
105 
106   /**
107    * Get the set of expressions that define the variables to use for determining
108    * the Cartesian product for evaluation.
109    *
110    * @return the variable names mapped to the associated Metapath expression
111    */
112   @NonNull
113   public Map<IEnhancedQName, IExpression> getInClauses() {
114     return inClauses;
115   }
116 
117   /**
118    * Get the expression used for evaluation using the Cartesian product of the
119    * variables.
120    *
121    * @return the evaluation expression
122    */
123   @NonNull
124   public IExpression getSatisfies() {
125     return satisfies;
126   }
127 
128   @Override
129   public List<? extends IExpression> getChildren() {
130     return ObjectUtils.notNull(Stream.concat(inClauses.values().stream(), Stream.of(satisfies))
131         .collect(Collectors.toList()));
132   }
133 
134   @Override
135   protected ISequence<IBooleanItem> evaluate(DynamicContext dynamicContext, ISequence<?> focus) {
136     Map<IEnhancedQName, ISequence<? extends IItem>> clauses = getInClauses().entrySet().stream()
137         .map(entry -> Map.entry(
138             entry.getKey(),
139             entry.getValue().accept(dynamicContext, focus)))
140         .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
141 
142     List<IEnhancedQName> clauseKeys = new ArrayList<>(clauses.keySet());
143     List<? extends Collection<? extends IItem>> clauseValues = new ArrayList<>(clauses.values());
144 
145     boolean retval = true;
146     for (List<IItem> product : new CartesianProduct<>(clauseValues)) {
147       DynamicContext subDynamicContext = dynamicContext.subContext();
148       for (int idx = 0; idx < product.size(); idx++) {
149         IEnhancedQName var = clauseKeys.get(idx);
150         IItem item = product.get(idx);
151 
152         assert var != null;
153 
154         subDynamicContext.bindVariableValue(var, ISequence.of(item));
155       }
156       boolean result = FnBoolean.fnBooleanAsPrimitive(getSatisfies().accept(subDynamicContext, focus));
157       if (quantifier == Quantifier.EVERY && !result) {
158         // fail on first false
159         retval = false;
160         break;
161       }
162       if (quantifier == Quantifier.SOME) {
163         if (result) {
164           // pass on first true
165           retval = true;
166           break;
167         }
168         // store (false) result
169         retval = false;
170       }
171     }
172 
173     return ISequence.of(IBooleanItem.valueOf(retval));
174   }
175 
176   @Override
177   public <RESULT, CONTEXT> RESULT accept(IExpressionVisitor<RESULT, CONTEXT> visitor, CONTEXT context) {
178     return visitor.visitQuantified(this, context);
179   }
180 
181   /**
182    * Get the Cartesian product of the provided lists of value axis.
183    *
184    * @param <T>
185    *          the Java type of value item
186    * @param axes
187    *          the values to compute the Cartesian product of
188    * @return an iterator of lists contain the Cartesian product of the axis values
189    */
190   public static <T extends IItem> Iterable<List<T>> cartesianProduct(
191       @NonNull List<? extends Collection<? extends T>> axes) {
192     return new CartesianProduct<>(axes);
193   }
194 
195   // based on https://gist.github.com/jhorstmann/a7aba9947bc4926a75f6de8f69560c6e
196   private static class CartesianProductIterator<T extends IItem> implements Iterator<List<T>> {
197     private final Object[][] dimensions;
198     private final int length;
199     private final int[] indizes;
200     private boolean reachedMax;
201 
202     @SuppressWarnings({
203         "PMD.UseVarargs",
204         "PMD.ArrayIsStoredDirectly" // ok for internal use
205     })
206     CartesianProductIterator(final Object[][] dimensions) {
207       this.dimensions = dimensions;
208       this.length = dimensions.length;
209       this.indizes = new int[length];
210     }
211 
212     private void increment(final int index) {
213       if (index >= length) {
214         reachedMax = true;
215       } else {
216         indizes[index]++;
217         if (indizes[index] == dimensions[index].length) {
218           indizes[index] = 0;
219           increment(index + 1);
220         }
221       }
222     }
223 
224     private void increment() {
225       increment(0);
226     }
227 
228     @SuppressWarnings("unchecked")
229     @Override
230     public List<T> next() {
231       if (reachedMax) {
232         throw new NoSuchElementException();
233       }
234 
235       List<T> list = new ArrayList<>();
236       for (int i = 0; i < length; i++) {
237         list.add((T) dimensions[i][indizes[i]]);
238       }
239 
240       increment();
241 
242       return Collections.unmodifiableList(list);
243     }
244 
245     @Override
246     public boolean hasNext() {
247       return !reachedMax;
248     }
249 
250     @Override
251     public void remove() {
252       throw new UnsupportedOperationException("remove not supported");
253     }
254   }
255 
256   // based on https://gist.github.com/jhorstmann/a7aba9947bc4926a75f6de8f69560c6e
257   private static final class CartesianProduct<T extends IItem> implements Iterable<List<T>> {
258     private final Object[][] dimensions;
259     private final long size;
260 
261     private CartesianProduct(final List<? extends Collection<? extends T>> axes) {
262       Object[][] dimensions = new Object[axes.size()][];
263       long size = dimensions.length == 0 ? 0 : 1;
264       for (int i = 0; i < axes.size(); i++) {
265         dimensions[i] = axes.get(i).toArray();
266         size *= dimensions[i].length;
267       }
268       this.dimensions = dimensions;
269       this.size = size;
270     }
271 
272     @Override
273     public Iterator<List<T>> iterator() {
274       if (size == 0) {
275         return Collections.emptyListIterator();
276       }
277       return new CartesianProductIterator<>(dimensions);
278     }
279 
280     // /**
281     // * Get a stream of list items, representing each Cartesian product, based on
282     // * this iterator.
283     // *
284     // * @return a stream of list items representing each Cartesian product
285     // */
286     // @NonNull
287     // public Stream<List<T>> stream() {
288     // int characteristics = Spliterator.ORDERED | Spliterator.SIZED |
289     // Spliterator.IMMUTABLE;
290     // return StreamSupport.stream(Spliterators.spliterator(iterator(), size,
291     // characteristics), false);
292     // }
293   }
294 }