001 /*
002 * Copyright 2015 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2015 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021 package com.unboundid.ldap.sdk.unboundidds.jsonfilter;
022
023
024
025 import java.math.BigDecimal;
026 import java.util.ArrayList;
027 import java.util.Arrays;
028 import java.util.Collections;
029 import java.util.HashSet;
030 import java.util.LinkedHashMap;
031 import java.util.List;
032 import java.util.Set;
033
034 import com.unboundid.util.Mutable;
035 import com.unboundid.util.StaticUtils;
036 import com.unboundid.util.ThreadSafety;
037 import com.unboundid.util.ThreadSafetyLevel;
038 import com.unboundid.util.Validator;
039 import com.unboundid.util.json.JSONArray;
040 import com.unboundid.util.json.JSONBoolean;
041 import com.unboundid.util.json.JSONException;
042 import com.unboundid.util.json.JSONNumber;
043 import com.unboundid.util.json.JSONObject;
044 import com.unboundid.util.json.JSONString;
045 import com.unboundid.util.json.JSONValue;
046
047
048
049 /**
050 * <BLOCKQUOTE>
051 * <B>NOTE:</B> This class is part of the Commercial Edition of the UnboundID
052 * LDAP SDK for Java. It is not available for use in applications that
053 * include only the Standard Edition of the LDAP SDK, and is not supported for
054 * use in conjunction with non-UnboundID products.
055 * </BLOCKQUOTE>
056 * This class provides an implementation of a JSON object filter that can be
057 * used to identify JSON objects that have at least one value for a specified
058 * field that is greater than a given value.
059 * <BR><BR>
060 * The fields that are required to be included in a "greater than" filter are:
061 * <UL>
062 * <LI>
063 * {@code field} -- A field path specifier for the JSON field for which to
064 * make the determination. This may be either a single string or an array
065 * of strings as described in the "Targeting Fields in JSON Objects" section
066 * of the class-level documentation for {@link JSONObjectFilter}.
067 * </LI>
068 * <LI>
069 * {@code value} -- The value to use in the matching. It must be either a
070 * string (which will be compared against other strings using lexicographic
071 * comparison) or a number.
072 * </LI>
073 * </UL>
074 * The fields that may optionally be included in a "greater than" filter are:
075 * <UL>
076 * <LI>
077 * {@code allowEquals} -- Indicates whether to match JSON objects that have
078 * a value for the specified field that matches the provided value. If
079 * present, this field must have a Boolean value of either {@code true} (to
080 * indicate that it should be a "greater-than or equal to" filter) or
081 * {@code false} (to indicate that it should be a strict "greater-than"
082 * filter). If this is not specified, then the default behavior will be to
083 * perform a strict "greater-than" evaluation.
084 * </LI>
085 * <LI>
086 * {@code matchAllElements} -- Indicates whether all elements of an array
087 * must be greater than (or possibly equal to) the specified value. If
088 * present, this field must have a Boolean value of {@code true} (to
089 * indicate that all elements of the array must match the criteria for this
090 * filter) or {@code false} (to indicate that at least one element of the
091 * array must match the criteria for this filter). If this is not
092 * specified, then the default behavior will be to require only at least
093 * one matching element. This field will be ignored for JSON objects in
094 * which the specified field has a value that is not an array.
095 * </LI>
096 * <LI>
097 * {@code caseSensitive} -- Indicates whether string values should be
098 * treated in a case-sensitive manner. If present, this field must have a
099 * Boolean value of either {@code true} or {@code false}. If it is not
100 * provided, then a default value of {@code false} will be assumed so that
101 * strings are treated in a case-insensitive manner.
102 * </LI>
103 * </UL>
104 * <H2>Example</H2>
105 * The following is an example of a "greater than" filter that will match any
106 * JSON object with a top-level field named "salary" with a value that is
107 * greater than or equal to 50000:
108 * <PRE>
109 * { "filterType" : "greaterThan",
110 * "field" : "salary",
111 * "value" : 50000,
112 * "allowEquals" : true }
113 * </PRE>
114 * The above filter can be created with the code:
115 * <PRE>
116 * GreaterThanJSONObjectFilter filter =
117 * new GreaterThanJSONObjectFilter("salary", 50000);
118 * filter.setAllowEquals(true);
119 * </PRE>
120 */
121 @Mutable()
122 @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
123 public final class GreaterThanJSONObjectFilter
124 extends JSONObjectFilter
125 {
126 /**
127 * The value that should be used for the filterType element of the JSON object
128 * that represents a "greater than" filter.
129 */
130 public static final String FILTER_TYPE = "greaterThan";
131
132
133
134 /**
135 * The name of the JSON field that is used to specify the field in the target
136 * JSON object for which to make the determination.
137 */
138 public static final String FIELD_FIELD_PATH = "field";
139
140
141
142 /**
143 * The name of the JSON field that is used to specify the value to use for
144 * the matching.
145 */
146 public static final String FIELD_VALUE = "value";
147
148
149
150 /**
151 * The name of the JSON field that is used to indicate whether to match JSON
152 * objects with a value that is considered equal to the provided value.
153 */
154 public static final String FIELD_ALLOW_EQUALS = "allowEquals";
155
156
157
158 /**
159 * The name of the JSON field that is used to indicate whether to match all
160 * elements of an array rather than just one or more.
161 */
162 public static final String FIELD_MATCH_ALL_ELEMENTS = "matchAllElements";
163
164
165
166 /**
167 * The name of the JSON field that is used to indicate whether string matching
168 * should be case-sensitive.
169 */
170 public static final String FIELD_CASE_SENSITIVE = "caseSensitive";
171
172
173
174 /**
175 * The pre-allocated set of required field names.
176 */
177 private static final Set<String> REQUIRED_FIELD_NAMES =
178 Collections.unmodifiableSet(new HashSet<String>(
179 Arrays.asList(FIELD_FIELD_PATH, FIELD_VALUE)));
180
181
182
183 /**
184 * The pre-allocated set of optional field names.
185 */
186 private static final Set<String> OPTIONAL_FIELD_NAMES =
187 Collections.unmodifiableSet(new HashSet<String>(
188 Arrays.asList(FIELD_ALLOW_EQUALS, FIELD_MATCH_ALL_ELEMENTS,
189 FIELD_CASE_SENSITIVE)));
190
191
192
193 /**
194 * The serial version UID for this serializable class.
195 */
196 private static final long serialVersionUID = -8397741931424599570L;
197
198
199
200 // Indicates whether to match equivalent values in addition to those that are
201 // strictly greater than the target value.
202 private volatile boolean allowEquals;
203
204 // Indicates whether string matching should be case-sensitive.
205 private volatile boolean caseSensitive;
206
207 // Indicates whether to match all elements of an array rather than just one or
208 // more.
209 private volatile boolean matchAllElements;
210
211 // The expected value for the target field.
212 private volatile JSONValue value;
213
214 // The field path specifier for the target field.
215 private volatile List<String> field;
216
217
218
219 /**
220 * Creates an instance of this filter type that can only be used for decoding
221 * JSON objects as "greater than" filters. It cannot be used as a regular
222 * "greater than" filter.
223 */
224 GreaterThanJSONObjectFilter()
225 {
226 field = null;
227 value = null;
228 allowEquals = false;
229 matchAllElements = false;
230 caseSensitive = false;
231 }
232
233
234
235 /**
236 * Creates a new instance of this filter type with the provided information.
237 *
238 * @param field The field path specifier for the target field.
239 * @param value The expected value for the target field.
240 * @param allowEquals Indicates whether to match values that are equal
241 * to the provided value in addition to those that
242 * are strictly greater than that value.
243 * @param matchAllElements Indicates whether, if the value of the target
244 * field is an array, all elements of that array
245 * will be required to match the criteria of this
246 * filter.
247 * @param caseSensitive Indicates whether string matching should be
248 * case sensitive.
249 */
250 private GreaterThanJSONObjectFilter(final List<String> field,
251 final JSONValue value,
252 final boolean allowEquals,
253 final boolean matchAllElements,
254 final boolean caseSensitive)
255 {
256 this.field = field;
257 this.value = value;
258 this.allowEquals = allowEquals;
259 this.matchAllElements = matchAllElements;
260 this.caseSensitive = caseSensitive;
261 }
262
263
264
265 /**
266 * Creates a new instance of this filter type with the provided information.
267 *
268 * @param field The name of the top-level field to target with this filter.
269 * It must not be {@code null} . See the class-level
270 * documentation for the {@link JSONObjectFilter} class for
271 * information about field path specifiers.
272 * @param value The target value for this filter.
273 */
274 public GreaterThanJSONObjectFilter(final String field, final long value)
275 {
276 this(Collections.singletonList(field), new JSONNumber(value));
277 }
278
279
280
281 /**
282 * Creates a new instance of this filter type with the provided information.
283 *
284 * @param field The name of the top-level field to target with this filter.
285 * It must not be {@code null} . See the class-level
286 * documentation for the {@link JSONObjectFilter} class for
287 * information about field path specifiers.
288 * @param value The target value for this filter.
289 */
290 public GreaterThanJSONObjectFilter(final String field, final double value)
291 {
292 this(Collections.singletonList(field), new JSONNumber(value));
293 }
294
295
296
297 /**
298 * Creates a new instance of this filter type with the provided information.
299 *
300 * @param field The name of the top-level field to target with this filter.
301 * It must not be {@code null} . See the class-level
302 * documentation for the {@link JSONObjectFilter} class for
303 * information about field path specifiers.
304 * @param value The target value for this filter. It must not be
305 * {@code null}.
306 */
307 public GreaterThanJSONObjectFilter(final String field, final String value)
308 {
309 this(Collections.singletonList(field), new JSONString(value));
310 }
311
312
313
314 /**
315 * Creates a new instance of this filter type with the provided information.
316 *
317 * @param field The name of the top-level field to target with this filter.
318 * It must not be {@code null} . See the class-level
319 * documentation for the {@link JSONObjectFilter} class for
320 * information about field path specifiers.
321 * @param value The target value for this filter. It must not be
322 * {@code null}, and it must be either a {@link JSONNumber} or
323 * a {@link JSONString}.
324 */
325 public GreaterThanJSONObjectFilter(final String field,
326 final JSONValue value)
327 {
328 this(Collections.singletonList(field), value);
329 }
330
331
332
333 /**
334 * Creates a new instance of this filter type with the provided information.
335 *
336 * @param field The field path specifier for this filter. It must not be
337 * {@code null} or empty. See the class-level documentation
338 * for the {@link JSONObjectFilter} class for information about
339 * field path specifiers.
340 * @param value The target value for this filter. It must not be
341 * {@code null}, and it must be either a {@link JSONNumber} or
342 * a {@link JSONString}.
343 */
344 public GreaterThanJSONObjectFilter(final List<String> field,
345 final JSONValue value)
346 {
347 Validator.ensureNotNull(field);
348 Validator.ensureFalse(field.isEmpty());
349
350 Validator.ensureNotNull(value);
351 Validator.ensureTrue((value instanceof JSONNumber) ||
352 (value instanceof JSONString));
353
354 this.field = Collections.unmodifiableList(new ArrayList<String>(field));
355 this.value = value;
356
357 allowEquals = false;
358 matchAllElements = false;
359 caseSensitive = false;
360 }
361
362
363
364 /**
365 * Retrieves the field path specifier for this filter.
366 *
367 * @return The field path specifier for this filter.
368 */
369 public List<String> getField()
370 {
371 return field;
372 }
373
374
375
376 /**
377 * Sets the field path specifier for this filter.
378 *
379 * @param field The field path specifier for this filter. It must not be
380 * {@code null} or empty. See the class-level documentation
381 * for the {@link JSONObjectFilter} class for information about
382 * field path specifiers.
383 */
384 public void setField(final String... field)
385 {
386 setField(StaticUtils.toList(field));
387 }
388
389
390
391 /**
392 * Sets the field path specifier for this filter.
393 *
394 * @param field The field path specifier for this filter. It must not be
395 * {@code null} or empty. See the class-level documentation
396 * for the {@link JSONObjectFilter} class for information about
397 * field path specifiers.
398 */
399 public void setField(final List<String> field)
400 {
401 Validator.ensureNotNull(field);
402 Validator.ensureFalse(field.isEmpty());
403
404 this.field = Collections.unmodifiableList(new ArrayList<String>(field));
405 }
406
407
408
409 /**
410 * Retrieves the target value for this filter.
411 *
412 * @return The target value for this filter.
413 */
414 public JSONValue getValue()
415 {
416 return value;
417 }
418
419
420
421 /**
422 * Specifies the target value for this filter.
423 *
424 * @param value The target value for this filter.
425 */
426 public void setValue(final long value)
427 {
428 setValue(new JSONNumber(value));
429 }
430
431
432
433 /**
434 * Specifies the target value for this filter.
435 *
436 * @param value The target value for this filter.
437 */
438 public void setValue(final double value)
439 {
440 setValue(new JSONNumber(value));
441 }
442
443
444
445 /**
446 * Specifies the target value for this filter.
447 *
448 * @param value The target value for this filter. It must not be
449 * {@code null}.
450 */
451 public void setValue(final String value)
452 {
453 Validator.ensureNotNull(value);
454
455 setValue(new JSONString(value));
456 }
457
458
459
460 /**
461 * Specifies the target value for this filter.
462 *
463 * @param value The target value for this filter. It must not be
464 * {@code null}, and it must be either a {@link JSONNumber} or
465 * a {@link JSONString}.
466 */
467 public void setValue(final JSONValue value)
468 {
469 Validator.ensureNotNull(value);
470 Validator.ensureTrue((value instanceof JSONNumber) ||
471 (value instanceof JSONString));
472
473 this.value = value;
474 }
475
476
477
478 /**
479 * Indicates whether this filter will match values that are considered equal
480 * to the provided value in addition to those that are strictly greater than
481 * that value.
482 *
483 * @return {@code true} if this filter should behave like a "greater than or
484 * equal to" filter, or {@code false} if it should behave strictly
485 * like a "greater than" filter.
486 */
487 public boolean allowEquals()
488 {
489 return allowEquals;
490 }
491
492
493
494 /**
495 * Specifies whether this filter should match values that are considered equal
496 * to the provided value in addition to those that are strictly greater than
497 * that value.
498 *
499 * @param allowEquals Indicates whether this filter should match values that
500 * are considered equal to the provided value in addition
501 * to those that are strictly greater than this value.
502 */
503 public void setAllowEquals(final boolean allowEquals)
504 {
505 this.allowEquals = allowEquals;
506 }
507
508
509
510 /**
511 * Indicates whether, if the specified field has a value that is an array, to
512 * require all elements of that array to match the criteria for this filter
513 * rather than merely requiring at least one value to match.
514 *
515 * @return {@code true} if the criteria contained in this filter will be
516 * required to match all elements of an array, or {@code false} if
517 * merely one or more values will be required to match.
518 */
519 public boolean matchAllElements()
520 {
521 return matchAllElements;
522 }
523
524
525
526 /**
527 * Specifies whether, if the value of the target field is an array, all
528 * elements of that array will be required to match the criteria of this
529 * filter. This will be ignored if the value of the target field is not an
530 * array.
531 *
532 * @param matchAllElements {@code true} to indicate that all elements of an
533 * array will be required to match the criteria of
534 * this filter, or {@code false} to indicate that
535 * merely one or more values will be required to
536 * match.
537 */
538 public void setMatchAllElements(final boolean matchAllElements)
539 {
540 this.matchAllElements = matchAllElements;
541 }
542
543
544
545 /**
546 * Indicates whether string matching should be performed in a case-sensitive
547 * manner.
548 *
549 * @return {@code true} if string matching should be case sensitive, or
550 * {@code false} if not.
551 */
552 public boolean caseSensitive()
553 {
554 return caseSensitive;
555 }
556
557
558
559 /**
560 * Specifies whether string matching should be performed in a case-sensitive
561 * manner.
562 *
563 * @param caseSensitive Indicates whether string matching should be
564 * case sensitive.
565 */
566 public void setCaseSensitive(final boolean caseSensitive)
567 {
568 this.caseSensitive = caseSensitive;
569 }
570
571
572
573 /**
574 * {@inheritDoc}
575 */
576 @Override()
577 public String getFilterType()
578 {
579 return FILTER_TYPE;
580 }
581
582
583
584 /**
585 * {@inheritDoc}
586 */
587 @Override()
588 protected Set<String> getRequiredFieldNames()
589 {
590 return REQUIRED_FIELD_NAMES;
591 }
592
593
594
595 /**
596 * {@inheritDoc}
597 */
598 @Override()
599 protected Set<String> getOptionalFieldNames()
600 {
601 return OPTIONAL_FIELD_NAMES;
602 }
603
604
605
606 /**
607 * {@inheritDoc}
608 */
609 @Override()
610 public boolean matchesJSONObject(final JSONObject o)
611 {
612 final List<JSONValue> candidates = getValues(o, field);
613 if (candidates.isEmpty())
614 {
615 return false;
616 }
617
618 for (final JSONValue v : candidates)
619 {
620 if (v instanceof JSONArray)
621 {
622 boolean matchOne = false;
623 boolean matchAll = true;
624 for (final JSONValue arrayValue : ((JSONArray) v).getValues())
625 {
626 if (matches(arrayValue))
627 {
628 if (! matchAllElements)
629 {
630 return true;
631 }
632 matchOne = true;
633 }
634 else
635 {
636 matchAll = false;
637 if (matchAllElements)
638 {
639 break;
640 }
641 }
642 }
643
644 if (matchAllElements && matchOne && matchAll)
645 {
646 return true;
647 }
648 }
649 else if (matches(v))
650 {
651 return true;
652 }
653 }
654
655 return false;
656 }
657
658
659
660 /**
661 * Indicates whether the provided value matches the criteria of this filter.
662 *
663 * @param v The value for which to make the determination.
664 *
665 * @return {@code true} if the provided value matches the criteria of this
666 * filter, or {@code false} if not.
667 */
668 private boolean matches(final JSONValue v)
669 {
670 if ((v instanceof JSONNumber) && (value instanceof JSONNumber))
671 {
672 final BigDecimal targetValue = ((JSONNumber) value).getValue();
673 final BigDecimal objectValue = ((JSONNumber) v).getValue();
674 if (allowEquals)
675 {
676 return (objectValue.compareTo(targetValue) >= 0);
677 }
678 else
679 {
680 return (objectValue.compareTo(targetValue) > 0);
681 }
682 }
683 else if ((v instanceof JSONString) && (value instanceof JSONString))
684 {
685 final String targetValue = ((JSONString) value).stringValue();
686 final String objectValue = ((JSONString) v).stringValue();
687 if (allowEquals)
688 {
689 if (caseSensitive)
690 {
691 return (objectValue.compareTo(targetValue) >= 0);
692 }
693 else
694 {
695 return (objectValue.compareToIgnoreCase(targetValue) >= 0);
696 }
697 }
698 else
699 {
700 if (caseSensitive)
701 {
702 return (objectValue.compareTo(targetValue) > 0);
703 }
704 else
705 {
706 return (objectValue.compareToIgnoreCase(targetValue) > 0);
707 }
708 }
709 }
710 else
711 {
712 return false;
713 }
714 }
715
716
717
718 /**
719 * {@inheritDoc}
720 */
721 @Override()
722 public JSONObject toJSONObject()
723 {
724 final LinkedHashMap<String,JSONValue> fields =
725 new LinkedHashMap<String,JSONValue>(6);
726
727 fields.put(FIELD_FILTER_TYPE, new JSONString(FILTER_TYPE));
728
729 if (field.size() == 1)
730 {
731 fields.put(FIELD_FIELD_PATH, new JSONString(field.get(0)));
732 }
733 else
734 {
735 final ArrayList<JSONValue> fieldNameValues =
736 new ArrayList<JSONValue>(field.size());
737 for (final String s : field)
738 {
739 fieldNameValues.add(new JSONString(s));
740 }
741 fields.put(FIELD_FIELD_PATH, new JSONArray(fieldNameValues));
742 }
743
744 fields.put(FIELD_VALUE, value);
745
746 if (allowEquals)
747 {
748 fields.put(FIELD_ALLOW_EQUALS, JSONBoolean.TRUE);
749 }
750
751 if (matchAllElements)
752 {
753 fields.put(FIELD_MATCH_ALL_ELEMENTS, JSONBoolean.TRUE);
754 }
755
756 if (caseSensitive)
757 {
758 fields.put(FIELD_CASE_SENSITIVE, JSONBoolean.TRUE);
759 }
760
761 return new JSONObject(fields);
762 }
763
764
765
766 /**
767 * {@inheritDoc}
768 */
769 @Override()
770 protected GreaterThanJSONObjectFilter decodeFilter(
771 final JSONObject filterObject)
772 throws JSONException
773 {
774 final List<String> fieldPath =
775 getStrings(filterObject, FIELD_FIELD_PATH, false, null);
776
777 final boolean isAllowEquals = getBoolean(filterObject,
778 FIELD_ALLOW_EQUALS, false);
779
780 final boolean isMatchAllElements = getBoolean(filterObject,
781 FIELD_MATCH_ALL_ELEMENTS, false);
782
783 final boolean isCaseSensitive = getBoolean(filterObject,
784 FIELD_CASE_SENSITIVE, false);
785
786 return new GreaterThanJSONObjectFilter(fieldPath,
787 filterObject.getField(FIELD_VALUE), isAllowEquals, isMatchAllElements,
788 isCaseSensitive);
789 }
790 }