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 less than a given value.
059 * <BR><BR>
060 * The fields that are required to be included in a "less 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 "less 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 "less-than or equal to" filter) or
081 * {@code false} (to indicate that it should be a strict "less-than"
082 * filter). If this is not specified, then the default behavior will be to
083 * perform a strict "less-than" evaluation.
084 * </LI>
085 * <LI>
086 * {@code matchAllElements} -- Indicates whether all elements of an array
087 * must be less 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 "less than" filter that will match any
106 * JSON object with a top-level field named "loginFailureCount" with a value
107 * that is less than or equal to 3:
108 * <PRE>
109 * { "filterType" : "lessThan",
110 * "field" : "loginFailureCount",
111 * "value" : 3,
112 * "allowEquals" : true }
113 * </PRE>
114 * The above filter can be created with the code:
115 * <PRE>
116 * LessThanJSONObjectFilter filter =
117 * new LessThanJSONObjectFilter("loginFailureCount", 3);
118 * filter.setAllowEquals(true);
119 * </PRE>
120 */
121 @Mutable()
122 @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
123 public final class LessThanJSONObjectFilter
124 extends JSONObjectFilter
125 {
126 /**
127 * The value that should be used for the filterType element of the JSON object
128 * that represents a "less than" filter.
129 */
130 public static final String FILTER_TYPE = "lessThan";
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 = -6023453566718838004L;
197
198
199
200 // Indicates whether to match equivalent values in addition to those that are
201 // strictly less 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 "less than" filters. It cannot be used as a regular
222 * "less than" filter.
223 */
224 LessThanJSONObjectFilter()
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 less 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 LessThanJSONObjectFilter(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 LessThanJSONObjectFilter(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 LessThanJSONObjectFilter(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 LessThanJSONObjectFilter(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 LessThanJSONObjectFilter(final String field, final JSONValue value)
326 {
327 this(Collections.singletonList(field), value);
328 }
329
330
331
332 /**
333 * Creates a new instance of this filter type with the provided information.
334 *
335 * @param field The field path specifier for this filter. It must not be
336 * {@code null} or empty. See the class-level documentation
337 * for the {@link JSONObjectFilter} class for information about
338 * field path specifiers.
339 * @param value The target value for this filter. It must not be
340 * {@code null}, and it must be either a {@link JSONNumber} or
341 * a {@link JSONString}.
342 */
343 public LessThanJSONObjectFilter(final List<String> field,
344 final JSONValue value)
345 {
346 Validator.ensureNotNull(field);
347 Validator.ensureFalse(field.isEmpty());
348
349 Validator.ensureNotNull(value);
350 Validator.ensureTrue((value instanceof JSONNumber) ||
351 (value instanceof JSONString));
352
353 this.field = Collections.unmodifiableList(new ArrayList<String>(field));
354 this.value = value;
355
356 allowEquals = false;
357 matchAllElements = false;
358 caseSensitive = false;
359 }
360
361
362
363 /**
364 * Retrieves the field path specifier for this filter.
365 *
366 * @return The field path specifier for this filter.
367 */
368 public List<String> getField()
369 {
370 return field;
371 }
372
373
374
375 /**
376 * Sets the field path specifier for this filter.
377 *
378 * @param field The field path specifier for this filter. It must not be
379 * {@code null} or empty. See the class-level documentation
380 * for the {@link JSONObjectFilter} class for information about
381 * field path specifiers.
382 */
383 public void setField(final String... field)
384 {
385 setField(StaticUtils.toList(field));
386 }
387
388
389
390 /**
391 * Sets the field path specifier for this filter.
392 *
393 * @param field The field path specifier for this filter. It must not be
394 * {@code null} or empty. See the class-level documentation
395 * for the {@link JSONObjectFilter} class for information about
396 * field path specifiers.
397 */
398 public void setField(final List<String> field)
399 {
400 Validator.ensureNotNull(field);
401 Validator.ensureFalse(field.isEmpty());
402
403 this.field = Collections.unmodifiableList(new ArrayList<String>(field));
404 }
405
406
407
408 /**
409 * Retrieves the target value for this filter.
410 *
411 * @return The target value for this filter.
412 */
413 public JSONValue getValue()
414 {
415 return value;
416 }
417
418
419
420 /**
421 * Specifies the target value for this filter.
422 *
423 * @param value The target value for this filter.
424 */
425 public void setValue(final long value)
426 {
427 setValue(new JSONNumber(value));
428 }
429
430
431
432 /**
433 * Specifies the target value for this filter.
434 *
435 * @param value The target value for this filter.
436 */
437 public void setValue(final double value)
438 {
439 setValue(new JSONNumber(value));
440 }
441
442
443
444 /**
445 * Specifies the target value for this filter.
446 *
447 * @param value The target value for this filter. It must not be
448 * {@code null}.
449 */
450 public void setValue(final String value)
451 {
452 Validator.ensureNotNull(value);
453
454 setValue(new JSONString(value));
455 }
456
457
458
459 /**
460 * Specifies the target value for this filter.
461 *
462 * @param value The target value for this filter. It must not be
463 * {@code null}, and it must be either a {@link JSONNumber} or
464 * a {@link JSONString}.
465 */
466 public void setValue(final JSONValue value)
467 {
468 Validator.ensureNotNull(value);
469 Validator.ensureTrue((value instanceof JSONNumber) ||
470 (value instanceof JSONString));
471
472 this.value = value;
473 }
474
475
476
477 /**
478 * Indicates whether this filter will match values that are considered equal
479 * to the provided value in addition to those that are strictly less than
480 * that value.
481 *
482 * @return {@code true} if this filter should behave like a "less than or
483 * equal to" filter, or {@code false} if it should behave strictly
484 * like a "less than" filter.
485 */
486 public boolean allowEquals()
487 {
488 return allowEquals;
489 }
490
491
492
493 /**
494 * Specifies whether this filter should match values that are considered equal
495 * to the provided value in addition to those that are strictly less than
496 * that value.
497 *
498 * @param allowEquals Indicates whether this filter should match values that
499 * are considered equal to the provided value in addition
500 * to those that are strictly less than this value.
501 */
502 public void setAllowEquals(final boolean allowEquals)
503 {
504 this.allowEquals = allowEquals;
505 }
506
507
508
509 /**
510 * Indicates whether, if the specified field has a value that is an array, to
511 * require all elements of that array to match the criteria for this filter
512 * rather than merely requiring at least one value to match.
513 *
514 * @return {@code true} if the criteria contained in this filter will be
515 * required to match all elements of an array, or {@code false} if
516 * merely one or more values will be required to match.
517 */
518 public boolean matchAllElements()
519 {
520 return matchAllElements;
521 }
522
523
524
525 /**
526 * Specifies whether, if the value of the target field is an array, all
527 * elements of that array will be required to match the criteria of this
528 * filter. This will be ignored if the value of the target field is not an
529 * array.
530 *
531 * @param matchAllElements {@code true} to indicate that all elements of an
532 * array will be required to match the criteria of
533 * this filter, or {@code false} to indicate that
534 * merely one or more values will be required to
535 * match.
536 */
537 public void setMatchAllElements(final boolean matchAllElements)
538 {
539 this.matchAllElements = matchAllElements;
540 }
541
542
543
544 /**
545 * Indicates whether string matching should be performed in a case-sensitive
546 * manner.
547 *
548 * @return {@code true} if string matching should be case sensitive, or
549 * {@code false} if not.
550 */
551 public boolean caseSensitive()
552 {
553 return caseSensitive;
554 }
555
556
557
558 /**
559 * Specifies whether string matching should be performed in a case-sensitive
560 * manner.
561 *
562 * @param caseSensitive Indicates whether string matching should be
563 * case sensitive.
564 */
565 public void setCaseSensitive(final boolean caseSensitive)
566 {
567 this.caseSensitive = caseSensitive;
568 }
569
570
571
572 /**
573 * {@inheritDoc}
574 */
575 @Override()
576 public String getFilterType()
577 {
578 return FILTER_TYPE;
579 }
580
581
582
583 /**
584 * {@inheritDoc}
585 */
586 @Override()
587 protected Set<String> getRequiredFieldNames()
588 {
589 return REQUIRED_FIELD_NAMES;
590 }
591
592
593
594 /**
595 * {@inheritDoc}
596 */
597 @Override()
598 protected Set<String> getOptionalFieldNames()
599 {
600 return OPTIONAL_FIELD_NAMES;
601 }
602
603
604
605 /**
606 * {@inheritDoc}
607 */
608 @Override()
609 public boolean matchesJSONObject(final JSONObject o)
610 {
611 final List<JSONValue> candidates = getValues(o, field);
612 if (candidates.isEmpty())
613 {
614 return false;
615 }
616
617 for (final JSONValue v : candidates)
618 {
619 if (v instanceof JSONArray)
620 {
621 boolean matchOne = false;
622 boolean matchAll = true;
623 for (final JSONValue arrayValue : ((JSONArray) v).getValues())
624 {
625 if (matches(arrayValue))
626 {
627 if (! matchAllElements)
628 {
629 return true;
630 }
631 matchOne = true;
632 }
633 else
634 {
635 matchAll = false;
636 if (matchAllElements)
637 {
638 break;
639 }
640 }
641 }
642
643 if (matchAllElements && matchOne && matchAll)
644 {
645 return true;
646 }
647 }
648 else if (matches(v))
649 {
650 return true;
651 }
652 }
653
654 return false;
655 }
656
657
658
659 /**
660 * Indicates whether the provided value matches the criteria of this filter.
661 *
662 * @param v The value for which to make the determination.
663 *
664 * @return {@code true} if the provided value matches the criteria of this
665 * filter, or {@code false} if not.
666 */
667 private boolean matches(final JSONValue v)
668 {
669 if ((v instanceof JSONNumber) && (value instanceof JSONNumber))
670 {
671 final BigDecimal targetValue = ((JSONNumber) value).getValue();
672 final BigDecimal objectValue = ((JSONNumber) v).getValue();
673 if (allowEquals)
674 {
675 return (objectValue.compareTo(targetValue) <= 0);
676 }
677 else
678 {
679 return (objectValue.compareTo(targetValue) < 0);
680 }
681 }
682 else if ((v instanceof JSONString) && (value instanceof JSONString))
683 {
684 final String targetValue = ((JSONString) value).stringValue();
685 final String objectValue = ((JSONString) v).stringValue();
686 if (allowEquals)
687 {
688 if (caseSensitive)
689 {
690 return (objectValue.compareTo(targetValue) <= 0);
691 }
692 else
693 {
694 return (objectValue.compareToIgnoreCase(targetValue) <= 0);
695 }
696 }
697 else
698 {
699 if (caseSensitive)
700 {
701 return (objectValue.compareTo(targetValue) < 0);
702 }
703 else
704 {
705 return (objectValue.compareToIgnoreCase(targetValue) < 0);
706 }
707 }
708 }
709 else
710 {
711 return false;
712 }
713 }
714
715
716
717 /**
718 * {@inheritDoc}
719 */
720 @Override()
721 public JSONObject toJSONObject()
722 {
723 final LinkedHashMap<String,JSONValue> fields =
724 new LinkedHashMap<String,JSONValue>(6);
725
726 fields.put(FIELD_FILTER_TYPE, new JSONString(FILTER_TYPE));
727
728 if (field.size() == 1)
729 {
730 fields.put(FIELD_FIELD_PATH, new JSONString(field.get(0)));
731 }
732 else
733 {
734 final ArrayList<JSONValue> fieldNameValues =
735 new ArrayList<JSONValue>(field.size());
736 for (final String s : field)
737 {
738 fieldNameValues.add(new JSONString(s));
739 }
740 fields.put(FIELD_FIELD_PATH, new JSONArray(fieldNameValues));
741 }
742
743 fields.put(FIELD_VALUE, value);
744
745 if (allowEquals)
746 {
747 fields.put(FIELD_ALLOW_EQUALS, JSONBoolean.TRUE);
748 }
749
750 if (matchAllElements)
751 {
752 fields.put(FIELD_MATCH_ALL_ELEMENTS, JSONBoolean.TRUE);
753 }
754
755 if (caseSensitive)
756 {
757 fields.put(FIELD_CASE_SENSITIVE, JSONBoolean.TRUE);
758 }
759
760 return new JSONObject(fields);
761 }
762
763
764
765 /**
766 * {@inheritDoc}
767 */
768 @Override()
769 protected LessThanJSONObjectFilter decodeFilter(final JSONObject filterObject)
770 throws JSONException
771 {
772 final List<String> fieldPath =
773 getStrings(filterObject, FIELD_FIELD_PATH, false, null);
774
775 final boolean isAllowEquals = getBoolean(filterObject,
776 FIELD_ALLOW_EQUALS, false);
777
778 final boolean isMatchAllElements = getBoolean(filterObject,
779 FIELD_MATCH_ALL_ELEMENTS, false);
780
781 final boolean isCaseSensitive = getBoolean(filterObject,
782 FIELD_CASE_SENSITIVE, false);
783
784 return new LessThanJSONObjectFilter(fieldPath,
785 filterObject.getField(FIELD_VALUE), isAllowEquals, isMatchAllElements,
786 isCaseSensitive);
787 }
788 }