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.util.ArrayList;
026    import java.util.Arrays;
027    import java.util.Collections;
028    import java.util.HashSet;
029    import java.util.LinkedHashMap;
030    import java.util.List;
031    import java.util.Set;
032    import java.util.regex.Matcher;
033    import java.util.regex.Pattern;
034    
035    import com.unboundid.util.Debug;
036    import com.unboundid.util.Mutable;
037    import com.unboundid.util.StaticUtils;
038    import com.unboundid.util.ThreadSafety;
039    import com.unboundid.util.ThreadSafetyLevel;
040    import com.unboundid.util.Validator;
041    import com.unboundid.util.json.JSONArray;
042    import com.unboundid.util.json.JSONBoolean;
043    import com.unboundid.util.json.JSONException;
044    import com.unboundid.util.json.JSONObject;
045    import com.unboundid.util.json.JSONString;
046    import com.unboundid.util.json.JSONValue;
047    
048    import static com.unboundid.ldap.sdk.unboundidds.jsonfilter.JFMessages.*;
049    
050    
051    
052    /**
053     * <BLOCKQUOTE>
054     *   <B>NOTE:</B>  This class is part of the Commercial Edition of the UnboundID
055     *   LDAP SDK for Java.  It is not available for use in applications that
056     *   include only the Standard Edition of the LDAP SDK, and is not supported for
057     *   use in conjunction with non-UnboundID products.
058     * </BLOCKQUOTE>
059     * This class provides an implementation of a JSON object filter that can be
060     * used to identify JSON objects that have a particular value for a specified
061     * field.
062     * <BR><BR>
063     * The fields that are required to be included in a "regular expression" filter
064     * are:
065     * <UL>
066     *   <LI>
067     *     {@code field} -- A field path specifier for the JSON field for which to
068     *     make the determination.  This may be either a single string or an array
069     *     of strings as described in the "Targeting Fields in JSON Objects" section
070     *     of the class-level documentation for {@link JSONObjectFilter}.
071     *   </LI>
072     *   <LI>
073     *     {@code regularExpression} -- The regular expression to use to identify
074     *     matching values.  It must be compatible for use with the Java
075     *     {@code java.util.regex.Pattern} class.
076     *   </LI>
077     * </UL>
078     * The fields that may optionally be included in a "regular expression" filter
079     * are:
080     * <UL>
081     *   <LI>
082     *     {@code matchAllElements} -- Indicates whether all elements of an array
083     *     must match the provided regular expression.  If present, this field must
084     *     have a Boolean value of {@code true} (to indicate that all elements of
085     *     the array must match the regular expression) or {@code false} (to
086     *     indicate that at least one element of the array must match the regular
087     *     expression).  If this is not specified, then the default behavior will be
088     *     to require only at least one matching element.  This field will be
089     *     ignored for JSON objects in which the specified field has a value that is
090     *     not an array.
091     *   </LI>
092     * </UL>
093     * <H2>Example</H2>
094     * The following is an example of a "regular expression" filter that will match
095     * any JSON object with a top-level field named "userID" with a value that
096     * starts with an ASCII letter and contains only ASCII letters and numeric
097     * digits:
098     * <PRE>
099     *   { "filterType" : "regularExpression",
100     *     "field" : "userID",
101     *     "regularExpression" : "^[a-zA-Z][a-zA-Z0-9]*$" }
102     * </PRE>
103     * The above filter can be created with the code:
104     * <PRE>
105     *   RegularExpressionJSONObjectFilter filter =
106              new RegularExpressionJSONObjectFilter("userID",
107                   "^[a-zA-Z][a-zA-Z0-9]*$");
108     * </PRE>
109     */
110    @Mutable()
111    @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
112    public final class RegularExpressionJSONObjectFilter
113           extends JSONObjectFilter
114    {
115      /**
116       * The value that should be used for the filterType element of the JSON object
117       * that represents a "regular expression" filter.
118       */
119      public static final String FILTER_TYPE = "regularExpression";
120    
121    
122    
123      /**
124       * The name of the JSON field that is used to specify the field in the target
125       * JSON object for which to make the determination.
126       */
127      public static final String FIELD_FIELD_PATH = "field";
128    
129    
130    
131      /**
132       * The name of the JSON field that is used to specify the regular expression
133       * that values should match.
134       */
135      public static final String FIELD_REGULAR_EXPRESSION = "regularExpression";
136    
137    
138    
139      /**
140       * The name of the JSON field that is used to indicate whether all values of
141       * an array should be required to match the provided regular expression.
142       */
143      public static final String FIELD_MATCH_ALL_ELEMENTS = "matchAllElements";
144    
145    
146    
147      /**
148       * The pre-allocated set of required field names.
149       */
150      private static final Set<String> REQUIRED_FIELD_NAMES =
151           Collections.unmodifiableSet(new HashSet<String>(
152                Arrays.asList(FIELD_FIELD_PATH, FIELD_REGULAR_EXPRESSION)));
153    
154    
155    
156      /**
157       * The pre-allocated set of optional field names.
158       */
159      private static final Set<String> OPTIONAL_FIELD_NAMES =
160           Collections.unmodifiableSet(new HashSet<String>(
161                Collections.singletonList(FIELD_MATCH_ALL_ELEMENTS)));
162    
163    
164    
165      /**
166       * The serial version UID for this serializable class.
167       */
168      private static final long serialVersionUID = 7678844742777504519L;
169    
170    
171    
172      // Indicates whether to require all elements of an array to match the
173      // regular expression
174      private volatile boolean matchAllElements;
175    
176      // The field path specifier for the target field.
177      private volatile List<String> field;
178    
179      // The regular expression to match.
180      private volatile Pattern regularExpression;
181    
182    
183    
184      /**
185       * Creates an instance of this filter type that can only be used for decoding
186       * JSON objects as "regular expression" filters.  It cannot be used as a
187       * regular "regular expression" filter.
188       */
189      RegularExpressionJSONObjectFilter()
190      {
191        field = null;
192        regularExpression = null;
193        matchAllElements = false;
194      }
195    
196    
197    
198      /**
199       * Creates a new instance of this filter type with the provided information.
200       *
201       * @param  field              The field path specifier for the target field.
202       * @param  regularExpression  The regular expression pattern to match.
203       * @param  matchAllElements   Indicates whether all elements of an array are
204       *                            required to match the regular expression rather
205       *                            than merely at least one element.
206       */
207      private RegularExpressionJSONObjectFilter(final List<String> field,
208                                                final Pattern regularExpression,
209                                                final boolean matchAllElements)
210      {
211        this.field = field;
212        this.regularExpression = regularExpression;
213        this.matchAllElements = matchAllElements;
214      }
215    
216    
217    
218      /**
219       * Creates a new instance of this filter type with the provided information.
220       *
221       * @param  field              The name of the top-level field to target with
222       *                            this filter.  It must not be {@code null} .  See
223       *                            the class-level documentation for the
224       *                            {@link JSONObjectFilter} class for information
225       *                            about field path specifiers.
226       * @param  regularExpression  The regular expression to match.  It must not
227       *                            be {@code null}, and it must be compatible for
228       *                            use with the {@code java.util.regex.Pattern}
229       *                            class.
230       *
231       * @throws  JSONException  If the provided string cannot be parsed as a valid
232       *                         regular expression.
233       */
234      public RegularExpressionJSONObjectFilter(final String field,
235                                               final String regularExpression)
236             throws JSONException
237      {
238        this(Collections.singletonList(field), regularExpression);
239      }
240    
241    
242    
243      /**
244       * Creates a new instance of this filter type with the provided information.
245       *
246       * @param  field              The name of the top-level field to target with
247       *                            this filter.  It must not be {@code null} .  See
248       *                            the class-level documentation for the
249       *                            {@link JSONObjectFilter} class for information
250       *                            about field path specifiers.
251       * @param  regularExpression  The regular expression pattern to match.  It
252       *                            must not be {@code null}.
253       */
254      public RegularExpressionJSONObjectFilter(final String field,
255                                               final Pattern regularExpression)
256      {
257        this(Collections.singletonList(field), regularExpression);
258      }
259    
260    
261    
262      /**
263       * Creates a new instance of this filter type with the provided information.
264       *
265       * @param  field              The field path specifier for this filter.  It
266       *                            must not be {@code null} or empty.  See the
267       *                            class-level documentation for the
268       *                            {@link JSONObjectFilter} class for information
269       *                            about field path specifiers.
270       * @param  regularExpression  The regular expression to match.  It must not
271       *                            be {@code null}, and it must be compatible for
272       *                            use with the {@code java.util.regex.Pattern}
273       *                            class.
274       *
275       * @throws  JSONException  If the provided string cannot be parsed as a valid
276       *                         regular expression.
277       */
278      public RegularExpressionJSONObjectFilter(final List<String> field,
279                                               final String regularExpression)
280             throws JSONException
281      {
282        Validator.ensureNotNull(field);
283        Validator.ensureFalse(field.isEmpty());
284    
285        Validator.ensureNotNull(regularExpression);
286    
287        this.field = Collections.unmodifiableList(new ArrayList<String>(field));
288    
289        try
290        {
291          this.regularExpression = Pattern.compile(regularExpression);
292        }
293        catch (final Exception e)
294        {
295          Debug.debugException(e);
296          throw new JSONException(
297               ERR_REGEX_FILTER_INVALID_REGEX.get(regularExpression,
298                    StaticUtils.getExceptionMessage(e)),
299               e);
300        }
301    
302        matchAllElements = false;
303      }
304    
305    
306    
307      /**
308       * Creates a new instance of this filter type with the provided information.
309       *
310       * @param  field              The field path specifier for this filter.  It
311       *                            must not be {@code null} or empty.  See the
312       *                            class-level documentation for the
313       *                            {@link JSONObjectFilter} class for information
314       *                            about field path specifiers.
315       * @param  regularExpression  The regular expression pattern to match.  It
316       *                            must not be {@code null}.
317       */
318      public RegularExpressionJSONObjectFilter(final List<String> field,
319                                               final Pattern regularExpression)
320      {
321        Validator.ensureNotNull(field);
322        Validator.ensureFalse(field.isEmpty());
323    
324        Validator.ensureNotNull(regularExpression);
325    
326        this.field = Collections.unmodifiableList(new ArrayList<String>(field));
327        this.regularExpression = regularExpression;
328    
329        matchAllElements = false;
330      }
331    
332    
333    
334      /**
335       * Retrieves the field path specifier for this filter.
336       *
337       * @return The field path specifier for this filter.
338       */
339      public List<String> getField()
340      {
341        return field;
342      }
343    
344    
345    
346      /**
347       * Sets the field path specifier for this filter.
348       *
349       * @param  field  The field path specifier for this filter.  It must not be
350       *                {@code null} or empty.  See the class-level documentation
351       *                for the {@link JSONObjectFilter} class for information about
352       *                field path specifiers.
353       */
354      public void setField(final String... field)
355      {
356        setField(StaticUtils.toList(field));
357      }
358    
359    
360    
361      /**
362       * Sets the field path specifier for this filter.
363       *
364       * @param  field  The field path specifier for this filter.  It must not be
365       *                {@code null} or empty.  See the class-level documentation
366       *                for the {@link JSONObjectFilter} class for information about
367       *                field path specifiers.
368       */
369      public void setField(final List<String> field)
370      {
371        Validator.ensureNotNull(field);
372        Validator.ensureFalse(field.isEmpty());
373    
374        this.field= Collections.unmodifiableList(new ArrayList<String>(field));
375      }
376    
377    
378    
379      /**
380       * Retrieves the regular expression pattern for this filter.
381       *
382       * @return  The regular expression pattern for this filter.
383       */
384      public Pattern getRegularExpression()
385      {
386        return regularExpression;
387      }
388    
389    
390    
391      /**
392       * Specifies the regular expression for this filter.
393       *
394       * @param  regularExpression  The regular expression to match.  It must not
395       *                            be {@code null}, and it must be compatible for
396       *                            use with the {@code java.util.regex.Pattern}
397       *                            class.
398       *
399       * @throws  JSONException  If the provided string cannot be parsed as a valid
400       *                         regular expression.
401       */
402      public void setRegularExpression(final String regularExpression)
403             throws JSONException
404      {
405        Validator.ensureNotNull(regularExpression);
406    
407        try
408        {
409          this.regularExpression = Pattern.compile(regularExpression);
410        }
411        catch (final Exception e)
412        {
413          Debug.debugException(e);
414          throw new JSONException(
415               ERR_REGEX_FILTER_INVALID_REGEX.get(regularExpression,
416                    StaticUtils.getExceptionMessage(e)),
417               e);
418        }
419      }
420    
421    
422    
423      /**
424       * Specifies the regular expression for this filter.
425       *
426       * @param  regularExpression  The regular expression pattern to match.  It
427       *                            must not be {@code null}.
428       */
429      public void setRegularExpression(final Pattern regularExpression)
430      {
431        Validator.ensureNotNull(regularExpression);
432    
433        this.regularExpression = regularExpression;
434      }
435    
436    
437    
438      /**
439       * Indicates whether, if the target field is an array of values, the regular
440       * expression will be required to match all elements in the array rather than
441       * at least one element.
442       *
443       * @return  {@code true} if the regular expression will be required to match
444       *          all elements of an array, or {@code false} if it will only be
445       *          required to match at least one element.
446       */
447      public boolean matchAllElements()
448      {
449        return matchAllElements;
450      }
451    
452    
453    
454      /**
455       * Specifies whether the regular expression will be required to match all
456       * elements of an array rather than at least one element.
457       *
458       * @param  matchAllElements  Indicates whether the regular expression will be
459       *                           required to match all elements of an array rather
460       *                           than at least one element.
461       */
462      public void setMatchAllElements(final boolean matchAllElements)
463      {
464        this.matchAllElements = matchAllElements;
465      }
466    
467    
468    
469      /**
470       * {@inheritDoc}
471       */
472      @Override()
473      public String getFilterType()
474      {
475        return FILTER_TYPE;
476      }
477    
478    
479    
480      /**
481       * {@inheritDoc}
482       */
483      @Override()
484      protected Set<String> getRequiredFieldNames()
485      {
486        return REQUIRED_FIELD_NAMES;
487      }
488    
489    
490    
491      /**
492       * {@inheritDoc}
493       */
494      @Override()
495      protected Set<String> getOptionalFieldNames()
496      {
497        return OPTIONAL_FIELD_NAMES;
498      }
499    
500    
501    
502      /**
503       * {@inheritDoc}
504       */
505      @Override()
506      public boolean matchesJSONObject(final JSONObject o)
507      {
508        final List<JSONValue> candidates = getValues(o, field);
509        if (candidates.isEmpty())
510        {
511          return false;
512        }
513    
514        for (final JSONValue v : candidates)
515        {
516          if (v instanceof JSONString)
517          {
518            final Matcher matcher =
519                 regularExpression.matcher(((JSONString) v).stringValue());
520            if (matcher.matches())
521            {
522              return true;
523            }
524          }
525          else if (v instanceof JSONArray)
526          {
527            boolean matchOne = false;
528            boolean matchAll = true;
529            for (final JSONValue arrayValue : ((JSONArray) v).getValues())
530            {
531              if (! (arrayValue instanceof JSONString))
532              {
533                matchAll = false;
534                if (matchAllElements)
535                {
536                  break;
537                }
538              }
539    
540              final Matcher matcher = regularExpression.matcher(
541                   ((JSONString) arrayValue).stringValue());
542              if (matcher.matches())
543              {
544                if (! matchAllElements)
545                {
546                  return true;
547                }
548                matchOne = true;
549              }
550              else
551              {
552                matchAll = false;
553                if (matchAllElements)
554                {
555                  break;
556                }
557              }
558            }
559    
560            if (matchOne && matchAll)
561            {
562              return true;
563            }
564          }
565        }
566    
567        return false;
568      }
569    
570    
571    
572      /**
573       * {@inheritDoc}
574       */
575      @Override()
576      public JSONObject toJSONObject()
577      {
578        final LinkedHashMap<String,JSONValue> fields =
579             new LinkedHashMap<String,JSONValue>(4);
580    
581        fields.put(FIELD_FILTER_TYPE, new JSONString(FILTER_TYPE));
582    
583        if (field.size() == 1)
584        {
585          fields.put(FIELD_FIELD_PATH, new JSONString(field.get(0)));
586        }
587        else
588        {
589          final ArrayList<JSONValue> fieldNameValues =
590               new ArrayList<JSONValue>(field.size());
591          for (final String s : field)
592          {
593            fieldNameValues.add(new JSONString(s));
594          }
595          fields.put(FIELD_FIELD_PATH, new JSONArray(fieldNameValues));
596        }
597    
598        fields.put(FIELD_REGULAR_EXPRESSION,
599             new JSONString(regularExpression.toString()));
600    
601        if (matchAllElements)
602        {
603          fields.put(FIELD_MATCH_ALL_ELEMENTS, JSONBoolean.TRUE);
604        }
605    
606        return new JSONObject(fields);
607      }
608    
609    
610    
611      /**
612       * {@inheritDoc}
613       */
614      @Override()
615      protected RegularExpressionJSONObjectFilter decodeFilter(
616                     final JSONObject filterObject)
617                throws JSONException
618      {
619        final List<String> fieldPath =
620             getStrings(filterObject, FIELD_FIELD_PATH, false, null);
621    
622        final String regex = getString(filterObject, FIELD_REGULAR_EXPRESSION,
623             null, true);
624    
625        final Pattern pattern;
626        try
627        {
628          pattern = Pattern.compile(regex);
629        }
630        catch (final Exception e)
631        {
632          Debug.debugException(e);
633          throw new JSONException(
634               ERR_REGEX_FILTER_DECODE_INVALID_REGEX.get(
635                    String.valueOf(filterObject), FIELD_REGULAR_EXPRESSION,
636                    StaticUtils.getExceptionMessage(e)),
637               e);
638        }
639    
640        final boolean matchAll =
641             getBoolean(filterObject, FIELD_MATCH_ALL_ELEMENTS, false);
642    
643        return new RegularExpressionJSONObjectFilter(fieldPath, pattern, matchAll);
644      }
645    }