1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.core.metapath.function;
7   
8   import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
9   import gov.nist.secauto.metaschema.core.metapath.function.impl.OperationFunctions;
10  import gov.nist.secauto.metaschema.core.metapath.function.library.FnNot;
11  import gov.nist.secauto.metaschema.core.metapath.item.ISequence;
12  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
13  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IBase64BinaryItem;
14  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IBooleanItem;
15  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IDateItem;
16  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IDateTimeItem;
17  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IDayTimeDurationItem;
18  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IDecimalItem;
19  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IDurationItem;
20  import gov.nist.secauto.metaschema.core.metapath.item.atomic.INumericItem;
21  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem;
22  import gov.nist.secauto.metaschema.core.metapath.item.atomic.ITimeItem;
23  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IUntypedAtomicItem;
24  import gov.nist.secauto.metaschema.core.metapath.item.atomic.IYearMonthDurationItem;
25  import gov.nist.secauto.metaschema.core.metapath.type.InvalidTypeMetapathException;
26  
27  import java.util.Locale;
28  
29  import edu.umd.cs.findbugs.annotations.NonNull;
30  import edu.umd.cs.findbugs.annotations.Nullable;
31  
32  /**
33   * A collection of comparison functions supporting value and general
34   * comparisons.
35   * <p>
36   * Based on the XPath 3.1
37   * <a href="https://www.w3.org/TR/xpath-31/#id-comparisons">comparison
38   * expressions</a> syntax.
39   */
40  // FIXME: Add unit tests
41  @SuppressWarnings({ "PMD.GodClass", "PMD.CyclomaticComplexity" })
42  public final class ComparisonFunctions {
43    /**
44     * Comparison operators.
45     */
46    public enum Operator {
47      /**
48       * An equal comparison.
49       */
50      EQ,
51      /**
52       * A not equal comparison.
53       */
54      NE,
55      /**
56       * A less than comparison.
57       */
58      LT,
59      /**
60       * A less than or equal comparison.
61       */
62      LE,
63      /**
64       * A greater than comparison.
65       */
66      GT,
67      /**
68       * A greater than or equal comparison.
69       */
70      GE;
71    }
72  
73    private ComparisonFunctions() {
74      // disable construction
75    }
76  
77    /**
78     * Compare the two items using the provided {@code operator}.
79     *
80     * @param leftItem
81     *          the first item to compare
82     * @param operator
83     *          the comparison operator
84     * @param rightItem
85     *          the second item to compare
86     * @param dynamicContext
87     *          used to get the implicit timezone from the evaluation context
88     * @return the result of the comparison
89     */
90    @NonNull
91    public static IBooleanItem valueCompairison(
92        @NonNull IAnyAtomicItem leftItem,
93        @NonNull Operator operator,
94        @NonNull IAnyAtomicItem rightItem,
95        @Nullable DynamicContext dynamicContext) {
96      return compare(leftItem, operator, rightItem, dynamicContext);
97    }
98  
99    /**
100    * Compare the sets of atomic items.
101    *
102    * @param leftItems
103    *          the first set of items to compare
104    * @param operator
105    *          the comparison operator
106    * @param rightItems
107    *          the second set of items to compare
108    * @param dynamicContext
109    *          used to get the implicit timezone from the evaluation context
110    * @return a or an empty {@link ISequence} if either item is {@code null}
111    */
112   @NonNull
113   public static IBooleanItem generalComparison( // NOPMD - acceptable complexity
114       @NonNull ISequence<? extends IAnyAtomicItem> leftItems,
115       @NonNull Operator operator,
116       @NonNull ISequence<? extends IAnyAtomicItem> rightItems,
117       @NonNull DynamicContext dynamicContext) {
118 
119     IBooleanItem retval = IBooleanItem.FALSE;
120     for (IAnyAtomicItem left : leftItems) {
121       assert left != null;
122       for (IAnyAtomicItem right : rightItems) {
123         assert right != null;
124         IAnyAtomicItem leftCast;
125         IAnyAtomicItem rightCast;
126         if (left instanceof IUntypedAtomicItem) {
127           if (right instanceof IUntypedAtomicItem) {
128             leftCast = IStringItem.cast(left);
129             rightCast = IStringItem.cast(right);
130           } else {
131             leftCast = applyGeneralComparisonCast(right, left);
132             rightCast = right;
133           }
134         } else if (right instanceof IUntypedAtomicItem) {
135           leftCast = left;
136           rightCast = applyGeneralComparisonCast(left, right);
137         } else {
138           leftCast = left;
139           rightCast = right;
140         }
141 
142         assert leftCast != null;
143         IBooleanItem result = compare(leftCast, operator, rightCast, dynamicContext);
144         if (IBooleanItem.TRUE.equals(result)) {
145           retval = IBooleanItem.TRUE;
146         }
147       }
148     }
149     return retval;
150   }
151 
152   /**
153    * Attempts to cast the provided {@code other} item to the type of the
154    * {@code item}.
155    *
156    * @param item
157    *          the item whose type the other item is to be cast to
158    * @param other
159    *          the item to cast
160    * @return the casted item
161    */
162   @NonNull
163   private static IAnyAtomicItem applyGeneralComparisonCast(
164       @NonNull IAnyAtomicItem item,
165       @NonNull IAnyAtomicItem other) {
166     IAnyAtomicItem retval;
167     if (item instanceof INumericItem) {
168       retval = IDecimalItem.cast(other);
169     } else if (item instanceof IDayTimeDurationItem) {
170       retval = IDayTimeDurationItem.cast(other);
171     } else if (item instanceof IYearMonthDurationItem) {
172       retval = IYearMonthDurationItem.cast(other);
173     } else {
174       retval = item.castAsType(other);
175     }
176     return retval;
177   }
178 
179   /**
180    * Compare the {@code right} item with the {@code left} item using the specified
181    * {@code operator}.
182    *
183    * @param left
184    *          the value to compare against
185    * @param operator
186    *          the comparison operator
187    * @param right
188    *          the value to compare with
189    * @param dynamicContext
190    *          used to get the implicit timezone from the evaluation context
191    * @return the comparison result
192    */
193   @NonNull
194   public static IBooleanItem compare( // NOPMD - unavoidable
195       @NonNull IAnyAtomicItem left,
196       @NonNull Operator operator,
197       @NonNull IAnyAtomicItem right,
198       @Nullable DynamicContext dynamicContext) {
199     @NonNull
200     IBooleanItem retval;
201     if (left instanceof IStringItem || right instanceof IStringItem) {
202       retval = stringCompare(IStringItem.cast(left), operator, IStringItem.cast(right));
203     } else if (left instanceof INumericItem && right instanceof INumericItem) {
204       retval = numericCompare((INumericItem) left, operator, (INumericItem) right);
205     } else if (left instanceof IBooleanItem && right instanceof IBooleanItem) {
206       retval = booleanCompare((IBooleanItem) left, operator, (IBooleanItem) right);
207     } else if (left instanceof IDateTimeItem && right instanceof IDateTimeItem) {
208       retval = dateTimeCompare((IDateTimeItem) left, operator, (IDateTimeItem) right, dynamicContext);
209     } else if (left instanceof IDateItem && right instanceof IDateItem) {
210       retval = dateTimeCompare(
211           ((IDateItem) left).asDateTime(),
212           operator,
213           ((IDateItem) right).asDateTime(),
214           dynamicContext);
215     } else if (left instanceof ITimeItem && right instanceof ITimeItem) {
216       retval = dateTimeCompare(
217           IDateTimeItem.valueOf(OperationFunctions.referenceDate(), (ITimeItem) left),
218           operator,
219           IDateTimeItem.valueOf(OperationFunctions.referenceDate(), (ITimeItem) right),
220           dynamicContext);
221     } else if (left instanceof IDurationItem && right instanceof IDurationItem) {
222       retval = durationCompare((IDurationItem) left, operator, (IDurationItem) right);
223     } else if (left instanceof IBase64BinaryItem && right instanceof IBase64BinaryItem) {
224       retval = binaryCompare((IBase64BinaryItem) left, operator, (IBase64BinaryItem) right);
225     } else {
226       throw new InvalidTypeMetapathException(
227           null,
228           String.format("invalid types for comparison: %s %s %s", left.getClass().getName(),
229               operator.name().toLowerCase(Locale.ROOT), right.getClass().getName()));
230     }
231     return retval;
232   }
233 
234   /**
235    * Perform a string-based comparison of the {@code right} item against the
236    * {@code left} item using the specified {@code operator}.
237    *
238    * @param left
239    *          the value to compare against
240    * @param operator
241    *          the comparison operator
242    * @param right
243    *          the value to compare with
244    * @return the comparison result
245    */
246   @NonNull
247   public static IBooleanItem stringCompare(
248       @NonNull IStringItem left,
249       @NonNull Operator operator,
250       @NonNull IStringItem right) {
251     int result = left.compareTo(right);
252     Boolean retval = null;
253     switch (operator) {
254     case EQ:
255       retval = result == 0;
256       break;
257     case GE:
258       retval = result >= 0;
259       break;
260     case GT:
261       retval = result > 0;
262       break;
263     case LE:
264       retval = result <= 0;
265       break;
266     case LT:
267       retval = result < 0;
268       break;
269     case NE:
270       retval = result != 0;
271       break;
272     }
273 
274     assert retval != null : String.format("Unsupported operator '%s'", operator.name());
275 
276     return IBooleanItem.valueOf(retval);
277   }
278 
279   /**
280    * Perform a number-based comparison of the {@code right} item against the
281    * {@code left} item using the specified {@code operator}.
282    *
283    * @param left
284    *          the value to compare against
285    * @param operator
286    *          the comparison operator
287    * @param right
288    *          the value to compare with
289    * @return the comparison result
290    */
291   @NonNull
292   public static IBooleanItem numericCompare(@NonNull INumericItem left, @NonNull Operator operator,
293       @NonNull INumericItem right) {
294     IBooleanItem retval = null;
295     switch (operator) {
296     case EQ:
297       retval = OperationFunctions.opNumericEqual(left, right);
298       break;
299     case GE: {
300       IBooleanItem gt = OperationFunctions.opNumericGreaterThan(left, right);
301       IBooleanItem eq = OperationFunctions.opNumericEqual(left, right);
302       retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
303       break;
304     }
305     case GT:
306       retval = OperationFunctions.opNumericGreaterThan(left, right);
307       break;
308     case LE: {
309       IBooleanItem lt = OperationFunctions.opNumericLessThan(left, right);
310       IBooleanItem eq = OperationFunctions.opNumericEqual(left, right);
311       retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
312       break;
313     }
314     case LT:
315       retval = OperationFunctions.opNumericLessThan(left, right);
316       break;
317     case NE:
318       retval = FnNot.fnNot(OperationFunctions.opNumericEqual(left, right));
319       break;
320     }
321     assert retval != null : String.format("Unsupported operator '%s'", operator.name());
322 
323     return retval;
324   }
325 
326   /**
327    * Perform a boolean-based comparison of the {@code right} item against the
328    * {@code left} item using the specified {@code operator}.
329    *
330    * @param left
331    *          the value to compare against
332    * @param operator
333    *          the comparison operator
334    * @param right
335    *          the value to compare with
336    * @return the comparison result
337    */
338   @NonNull
339   public static IBooleanItem booleanCompare(
340       @NonNull IBooleanItem left,
341       @NonNull Operator operator,
342       @NonNull IBooleanItem right) {
343     IBooleanItem retval = null;
344     switch (operator) {
345     case EQ:
346       retval = OperationFunctions.opBooleanEqual(left, right);
347       break;
348     case GE: {
349       IBooleanItem gt = OperationFunctions.opBooleanGreaterThan(left, right);
350       IBooleanItem eq = OperationFunctions.opBooleanEqual(left, right);
351       retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
352       break;
353     }
354     case GT:
355       retval = OperationFunctions.opBooleanGreaterThan(left, right);
356       break;
357     case LE: {
358       IBooleanItem lt = OperationFunctions.opBooleanLessThan(left, right);
359       IBooleanItem eq = OperationFunctions.opBooleanEqual(left, right);
360       retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
361       break;
362     }
363     case LT:
364       retval = OperationFunctions.opBooleanLessThan(left, right);
365       break;
366     case NE:
367       retval = FnNot.fnNot(OperationFunctions.opBooleanEqual(left, right));
368       break;
369     }
370     assert retval != null : String.format("Unsupported operator '%s'", operator.name());
371 
372     return retval;
373   }
374 
375   /**
376    * Perform a date and time-based comparison of the {@code right} item against
377    * the {@code left} item using the specified {@code operator}.
378    *
379    * @param left
380    *          the value to compare against
381    * @param operator
382    *          the comparison operator
383    * @param right
384    *          the value to compare with
385    * @param dynamicContext
386    *          used to get the implicit timezone from the evaluation context
387    * @return the comparison result
388    */
389   @NonNull
390   public static IBooleanItem dateTimeCompare(
391       @NonNull IDateTimeItem left,
392       @NonNull Operator operator,
393       @NonNull IDateTimeItem right,
394       @Nullable DynamicContext dynamicContext) {
395     IBooleanItem retval = null;
396     switch (operator) {
397     case EQ:
398       retval = OperationFunctions.opDateTimeEqual(left, right, dynamicContext);
399       break;
400     case GE: {
401       // pre-normalize for efficiency
402       IDateTimeItem leftNormalized = dynamicContext == null ? left : left.normalize(dynamicContext);
403       IDateTimeItem rightNormalized = dynamicContext == null ? right : right.normalize(dynamicContext);
404       retval = IBooleanItem.valueOf(
405           OperationFunctions.opDateTimeGreaterThan(leftNormalized, rightNormalized, null).toBoolean()
406               || OperationFunctions.opDateTimeEqual(leftNormalized, rightNormalized, null).toBoolean());
407       break;
408     }
409     case GT:
410       retval = OperationFunctions.opDateTimeGreaterThan(left, right, dynamicContext);
411       break;
412     case LE: {
413       // pre-normalize for efficiency
414       IDateTimeItem leftNormalized = dynamicContext == null ? left : left.normalize(dynamicContext);
415       IDateTimeItem rightNormalized = dynamicContext == null ? right : right.normalize(dynamicContext);
416       retval = IBooleanItem.valueOf(
417           OperationFunctions.opDateTimeLessThan(leftNormalized, rightNormalized, null).toBoolean()
418               || OperationFunctions.opDateTimeEqual(leftNormalized, rightNormalized, null).toBoolean());
419       break;
420     }
421     case LT:
422       retval = OperationFunctions.opDateTimeLessThan(left, right, dynamicContext);
423       break;
424     case NE:
425       retval = FnNot.fnNot(OperationFunctions.opDateTimeEqual(left, right, dynamicContext));
426       break;
427     }
428     assert retval != null : String.format("Unsupported operator '%s'", operator.name());
429 
430     return retval;
431   }
432 
433   /**
434    * Perform a duration-based comparison of the {@code right} item against the
435    * {@code left} item using the specified {@code operator}.
436    *
437    * @param left
438    *          the value to compare against
439    * @param operator
440    *          the comparison operator
441    * @param right
442    *          the value to compare with
443    * @return the comparison result
444    */
445   @NonNull
446   public static IBooleanItem durationCompare( // NOPMD - unavoidable
447       @NonNull IDurationItem left,
448       @NonNull Operator operator,
449       @NonNull IDurationItem right) {
450     IBooleanItem retval = null;
451     switch (operator) {
452     case EQ:
453       retval = OperationFunctions.opDurationEqual(left, right);
454       break;
455     case GE:
456       if (left instanceof IYearMonthDurationItem && right instanceof IYearMonthDurationItem) {
457         IBooleanItem gt = OperationFunctions.opYearMonthDurationGreaterThan(
458             (IYearMonthDurationItem) left,
459             (IYearMonthDurationItem) right);
460         IBooleanItem eq = OperationFunctions.opDurationEqual(left, right);
461         retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
462       } else if (left instanceof IDayTimeDurationItem && right instanceof IDayTimeDurationItem) {
463         IBooleanItem gt = OperationFunctions.opDayTimeDurationGreaterThan(
464             (IDayTimeDurationItem) left,
465             (IDayTimeDurationItem) right);
466         IBooleanItem eq = OperationFunctions.opDurationEqual(left, right);
467         retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
468       }
469       break;
470     case GT:
471       if (left instanceof IYearMonthDurationItem && right instanceof IYearMonthDurationItem) {
472         retval = OperationFunctions.opYearMonthDurationGreaterThan(
473             (IYearMonthDurationItem) left,
474             (IYearMonthDurationItem) right);
475       } else if (left instanceof IDayTimeDurationItem && right instanceof IDayTimeDurationItem) {
476         retval = OperationFunctions.opDayTimeDurationGreaterThan(
477             (IDayTimeDurationItem) left,
478             (IDayTimeDurationItem) right);
479       }
480       break;
481     case LE:
482       if (left instanceof IYearMonthDurationItem && right instanceof IYearMonthDurationItem) {
483         IBooleanItem lt = OperationFunctions.opYearMonthDurationLessThan(
484             (IYearMonthDurationItem) left,
485             (IYearMonthDurationItem) right);
486         IBooleanItem eq = OperationFunctions.opDurationEqual(left, right);
487         retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
488       } else if (left instanceof IDayTimeDurationItem && right instanceof IDayTimeDurationItem) {
489         IBooleanItem lt = OperationFunctions.opDayTimeDurationLessThan(
490             (IDayTimeDurationItem) left,
491             (IDayTimeDurationItem) right);
492         IBooleanItem eq = OperationFunctions.opDurationEqual(left, right);
493         retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
494       }
495       break;
496     case LT:
497       if (left instanceof IYearMonthDurationItem && right instanceof IYearMonthDurationItem) {
498         retval = OperationFunctions.opYearMonthDurationLessThan(
499             (IYearMonthDurationItem) left,
500             (IYearMonthDurationItem) right);
501       } else if (left instanceof IDayTimeDurationItem && right instanceof IDayTimeDurationItem) {
502         retval = OperationFunctions.opDayTimeDurationLessThan(
503             (IDayTimeDurationItem) left,
504             (IDayTimeDurationItem) right);
505       }
506       break;
507     case NE:
508       retval = FnNot.fnNot(OperationFunctions.opDurationEqual(left, right));
509       break;
510     }
511 
512     if (retval == null) {
513       throw new InvalidTypeMetapathException(
514           null,
515           String.format("The item types '%s' and '%s' are not comparable",
516               left.getClass().getName(),
517               right.getClass().getName()));
518     }
519     return retval;
520   }
521 
522   /**
523    * Perform a binary data-based comparison of the {@code right} item against the
524    * {@code left} item using the specified {@code operator}.
525    *
526    * @param left
527    *          the value to compare against
528    * @param operator
529    *          the comparison operator
530    * @param right
531    *          the value to compare with
532    * @return the comparison result
533    */
534   @NonNull
535   public static IBooleanItem binaryCompare(@NonNull IBase64BinaryItem left, @NonNull Operator operator,
536       @NonNull IBase64BinaryItem right) {
537     IBooleanItem retval = null;
538     switch (operator) {
539     case EQ:
540       retval = OperationFunctions.opBase64BinaryEqual(left, right);
541       break;
542     case GE: {
543       IBooleanItem gt = OperationFunctions.opBase64BinaryGreaterThan(left, right);
544       IBooleanItem eq = OperationFunctions.opBase64BinaryEqual(left, right);
545       retval = IBooleanItem.valueOf(gt.toBoolean() || eq.toBoolean());
546       break;
547     }
548     case GT:
549       retval = OperationFunctions.opBase64BinaryGreaterThan(left, right);
550       break;
551     case LE: {
552       IBooleanItem lt = OperationFunctions.opBase64BinaryLessThan(left, right);
553       IBooleanItem eq = OperationFunctions.opBase64BinaryEqual(left, right);
554       retval = IBooleanItem.valueOf(lt.toBoolean() || eq.toBoolean());
555       break;
556     }
557     case LT:
558       retval = OperationFunctions.opBase64BinaryLessThan(left, right);
559       break;
560     case NE:
561       retval = FnNot.fnNot(OperationFunctions.opBase64BinaryEqual(left, right));
562       break;
563     }
564     assert retval != null : String.format("Unsupported operator '%s'", operator.name());
565 
566     return retval;
567   }
568 }