001    /*
002     * Copyright 2009-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2009-2016 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.matchingrules;
022    
023    
024    
025    import java.util.ArrayList;
026    import java.util.Collections;
027    import java.util.Iterator;
028    import java.util.List;
029    
030    import com.unboundid.asn1.ASN1OctetString;
031    import com.unboundid.ldap.sdk.LDAPException;
032    import com.unboundid.ldap.sdk.ResultCode;
033    
034    import static com.unboundid.ldap.matchingrules.MatchingRuleMessages.*;
035    import static com.unboundid.util.Debug.*;
036    import static com.unboundid.util.StaticUtils.*;
037    
038    
039    
040    /**
041     * This class provides an implementation of a matching rule that may be used to
042     * process values containing lists of items, in which each item is separated by
043     * a dollar sign ($) character.  Substring matching is also supported, but
044     * ordering matching is not.
045     */
046    public final class CaseIgnoreListMatchingRule
047           extends MatchingRule
048    {
049      /**
050       * The singleton instance that will be returned from the {@code getInstance}
051       * method.
052       */
053      private static final CaseIgnoreListMatchingRule INSTANCE =
054           new CaseIgnoreListMatchingRule();
055    
056    
057    
058      /**
059       * The name for the caseIgnoreListMatch equality matching rule.
060       */
061      public static final String EQUALITY_RULE_NAME = "caseIgnoreListMatch";
062    
063    
064    
065      /**
066       * The name for the caseIgnoreListMatch equality matching rule, formatted in
067       * all lowercase characters.
068       */
069      static final String LOWER_EQUALITY_RULE_NAME =
070           toLowerCase(EQUALITY_RULE_NAME);
071    
072    
073    
074      /**
075       * The OID for the caseIgnoreListMatch equality matching rule.
076       */
077      public static final String EQUALITY_RULE_OID = "2.5.13.11";
078    
079    
080    
081      /**
082       * The name for the caseIgnoreListSubstringsMatch substring matching rule.
083       */
084      public static final String SUBSTRING_RULE_NAME =
085           "caseIgnoreListSubstringsMatch";
086    
087    
088    
089      /**
090       * The name for the caseIgnoreListSubstringsMatch substring matching rule,
091       * formatted in all lowercase characters.
092       */
093      static final String LOWER_SUBSTRING_RULE_NAME =
094           toLowerCase(SUBSTRING_RULE_NAME);
095    
096    
097    
098      /**
099       * The OID for the caseIgnoreListSubstringsMatch substring matching rule.
100       */
101      public static final String SUBSTRING_RULE_OID = "2.5.13.12";
102    
103    
104    
105      /**
106       * The serial version UID for this serializable class.
107       */
108      private static final long serialVersionUID = 7795143670808983466L;
109    
110    
111    
112      /**
113       * Creates a new instance of this case-ignore list matching rule.
114       */
115      public CaseIgnoreListMatchingRule()
116      {
117        // No implementation is required.
118      }
119    
120    
121    
122      /**
123       * Retrieves a singleton instance of this matching rule.
124       *
125       * @return  A singleton instance of this matching rule.
126       */
127      public static CaseIgnoreListMatchingRule getInstance()
128      {
129        return INSTANCE;
130      }
131    
132    
133    
134      /**
135       * {@inheritDoc}
136       */
137      @Override()
138      public String getEqualityMatchingRuleName()
139      {
140        return EQUALITY_RULE_NAME;
141      }
142    
143    
144    
145      /**
146       * {@inheritDoc}
147       */
148      @Override()
149      public String getEqualityMatchingRuleOID()
150      {
151        return EQUALITY_RULE_OID;
152      }
153    
154    
155    
156      /**
157       * {@inheritDoc}
158       */
159      @Override()
160      public String getOrderingMatchingRuleName()
161      {
162        return null;
163      }
164    
165    
166    
167      /**
168       * {@inheritDoc}
169       */
170      @Override()
171      public String getOrderingMatchingRuleOID()
172      {
173        return null;
174      }
175    
176    
177    
178      /**
179       * {@inheritDoc}
180       */
181      @Override()
182      public String getSubstringMatchingRuleName()
183      {
184        return SUBSTRING_RULE_NAME;
185      }
186    
187    
188    
189      /**
190       * {@inheritDoc}
191       */
192      @Override()
193      public String getSubstringMatchingRuleOID()
194      {
195        return SUBSTRING_RULE_OID;
196      }
197    
198    
199    
200      /**
201       * {@inheritDoc}
202       */
203      @Override()
204      public boolean valuesMatch(final ASN1OctetString value1,
205                                 final ASN1OctetString value2)
206             throws LDAPException
207      {
208        return normalize(value1).equals(normalize(value2));
209      }
210    
211    
212    
213      /**
214       * {@inheritDoc}
215       */
216      @Override()
217      public boolean matchesSubstring(final ASN1OctetString value,
218                                      final ASN1OctetString subInitial,
219                                      final ASN1OctetString[] subAny,
220                                      final ASN1OctetString subFinal)
221             throws LDAPException
222      {
223        String normStr = normalize(value).stringValue();
224    
225        if (subInitial != null)
226        {
227          final String normSubInitial = normalizeSubstring(subInitial,
228               SUBSTRING_TYPE_SUBINITIAL).stringValue();
229          if (normSubInitial.indexOf('$') >= 0)
230          {
231            throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
232                 ERR_CASE_IGNORE_LIST_SUBSTRING_COMPONENT_CONTAINS_DOLLAR.get(
233                      normSubInitial));
234          }
235    
236          if (! normStr.startsWith(normSubInitial))
237          {
238            return false;
239          }
240    
241          normStr = normStr.substring(normSubInitial.length());
242        }
243    
244        if (subFinal != null)
245        {
246          final String normSubFinal = normalizeSubstring(subFinal,
247               SUBSTRING_TYPE_SUBFINAL).stringValue();
248          if (normSubFinal.indexOf('$') >= 0)
249          {
250            throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
251                 ERR_CASE_IGNORE_LIST_SUBSTRING_COMPONENT_CONTAINS_DOLLAR.get(
252                      normSubFinal));
253          }
254    
255          if (! normStr.endsWith(normSubFinal))
256          {
257    
258            return false;
259          }
260    
261          normStr = normStr.substring(0, normStr.length() - normSubFinal.length());
262        }
263    
264        if (subAny != null)
265        {
266          for (final ASN1OctetString s : subAny)
267          {
268            final String normSubAny =
269                 normalizeSubstring(s, SUBSTRING_TYPE_SUBANY).stringValue();
270            if (normSubAny.indexOf('$') >= 0)
271            {
272              throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
273                   ERR_CASE_IGNORE_LIST_SUBSTRING_COMPONENT_CONTAINS_DOLLAR.get(
274                        normSubAny));
275            }
276    
277            final int pos = normStr.indexOf(normSubAny);
278            if (pos < 0)
279            {
280              return false;
281            }
282    
283            normStr = normStr.substring(pos + normSubAny.length());
284          }
285        }
286    
287        return true;
288      }
289    
290    
291    
292      /**
293       * {@inheritDoc}
294       */
295      @Override()
296      public int compareValues(final ASN1OctetString value1,
297                               final ASN1OctetString value2)
298             throws LDAPException
299      {
300        throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING,
301             ERR_CASE_IGNORE_LIST_ORDERING_MATCHING_NOT_SUPPORTED.get());
302      }
303    
304    
305    
306      /**
307       * {@inheritDoc}
308       */
309      @Override()
310      public ASN1OctetString normalize(final ASN1OctetString value)
311             throws LDAPException
312      {
313        final List<String>     items    = getLowercaseItems(value);
314        final Iterator<String> iterator = items.iterator();
315    
316        final StringBuilder buffer = new StringBuilder();
317        while (iterator.hasNext())
318        {
319          normalizeItem(buffer, iterator.next());
320          if (iterator.hasNext())
321          {
322            buffer.append('$');
323          }
324        }
325    
326        return new ASN1OctetString(buffer.toString());
327      }
328    
329    
330    
331      /**
332       * {@inheritDoc}
333       */
334      @Override()
335      public ASN1OctetString normalizeSubstring(final ASN1OctetString value,
336                                                final byte substringType)
337             throws LDAPException
338      {
339        return CaseIgnoreStringMatchingRule.getInstance().normalizeSubstring(value,
340             substringType);
341      }
342    
343    
344    
345      /**
346       * Retrieves a list of the items contained in the provided value.  The items
347       * will use the case of the provided value.
348       *
349       * @param  value  The value for which to obtain the list of items.  It must
350       *                not be {@code null}.
351       *
352       * @return  An unmodifiable list of the items contained in the provided value.
353       *
354       * @throws  LDAPException  If the provided value does not represent a valid
355       *                         list in accordance with this matching rule.
356       */
357      public static List<String> getItems(final ASN1OctetString value)
358             throws LDAPException
359      {
360        return getItems(value.stringValue());
361      }
362    
363    
364    
365      /**
366       * Retrieves a list of the items contained in the provided value.  The items
367       * will use the case of the provided value.
368       *
369       * @param  value  The value for which to obtain the list of items.  It must
370       *                not be {@code null}.
371       *
372       * @return  An unmodifiable list of the items contained in the provided value.
373       *
374       * @throws  LDAPException  If the provided value does not represent a valid
375       *                         list in accordance with this matching rule.
376       */
377      public static List<String> getItems(final String value)
378             throws LDAPException
379      {
380        final ArrayList<String> items = new ArrayList<String>(10);
381    
382        final int length = value.length();
383        final StringBuilder buffer = new StringBuilder();
384        for (int i=0; i < length; i++)
385        {
386          final char c = value.charAt(i);
387          if (c == '\\')
388          {
389            try
390            {
391              buffer.append(decodeHexChar(value, i+1));
392              i += 2;
393            }
394            catch (Exception e)
395            {
396              debugException(e);
397              throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
398                   ERR_CASE_IGNORE_LIST_MALFORMED_HEX_CHAR.get(value), e);
399            }
400          }
401          else if (c == '$')
402          {
403            final String s = buffer.toString().trim();
404            if (s.length() == 0)
405            {
406              throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
407                   ERR_CASE_IGNORE_LIST_EMPTY_ITEM.get(value));
408            }
409    
410            items.add(s);
411            buffer.delete(0, buffer.length());
412          }
413          else
414          {
415            buffer.append(c);
416          }
417        }
418    
419        final String s = buffer.toString().trim();
420        if (s.length() == 0)
421        {
422          if (items.isEmpty())
423          {
424            throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
425                 ERR_CASE_IGNORE_LIST_EMPTY_LIST.get(value));
426          }
427          else
428          {
429            throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
430                                    ERR_CASE_IGNORE_LIST_EMPTY_ITEM.get(value));
431          }
432        }
433        items.add(s);
434    
435        return Collections.unmodifiableList(items);
436      }
437    
438    
439    
440      /**
441       * Retrieves a list of the lowercase representations of the items contained in
442       * the provided value.
443       *
444       * @param  value  The value for which to obtain the list of items.  It must
445       *                not be {@code null}.
446       *
447       * @return  An unmodifiable list of the items contained in the provided value.
448       *
449       * @throws  LDAPException  If the provided value does not represent a valid
450       *                         list in accordance with this matching rule.
451       */
452      public static List<String> getLowercaseItems(final ASN1OctetString value)
453             throws LDAPException
454      {
455        return getLowercaseItems(value.stringValue());
456      }
457    
458    
459    
460      /**
461       * Retrieves a list of the lowercase representations of the items contained in
462       * the provided value.
463       *
464       * @param  value  The value for which to obtain the list of items.  It must
465       *                not be {@code null}.
466       *
467       * @return  An unmodifiable list of the items contained in the provided value.
468       *
469       * @throws  LDAPException  If the provided value does not represent a valid
470       *                         list in accordance with this matching rule.
471       */
472      public static List<String> getLowercaseItems(final String value)
473             throws LDAPException
474      {
475        return getItems(toLowerCase(value));
476      }
477    
478    
479    
480      /**
481       * Normalizes the provided list item.
482       *
483       * @param  buffer  The buffer to which to append the normalized representation
484       *                 of the given item.
485       * @param  item    The item to be normalized.  It must already be trimmed and
486       *                 all characters converted to lowercase.
487       */
488      static void normalizeItem(final StringBuilder buffer, final String item)
489      {
490        final int length = item.length();
491    
492        boolean lastWasSpace = false;
493        for (int i=0; i < length; i++)
494        {
495          final char c = item.charAt(i);
496          if (c == '\\')
497          {
498            buffer.append("\\5c");
499            lastWasSpace = false;
500          }
501          else if (c == '$')
502          {
503            buffer.append("\\24");
504            lastWasSpace = false;
505          }
506          else if (c == ' ')
507          {
508            if (! lastWasSpace)
509            {
510              buffer.append(' ');
511              lastWasSpace = true;
512            }
513          }
514          else
515          {
516            buffer.append(c);
517            lastWasSpace = false;
518          }
519        }
520      }
521    
522    
523    
524      /**
525       * Reads two characters from the specified position in the provided string and
526       * returns the character that they represent.
527       *
528       * @param  s  The string from which to take the hex characters.
529       * @param  p  The position at which the hex characters begin.
530       *
531       * @return  The character that was read and decoded.
532       *
533       * @throws  LDAPException  If either of the characters are not hexadecimal
534       *                         digits.
535       */
536      static char decodeHexChar(final String s, final int p)
537             throws LDAPException
538      {
539        char c = 0;
540    
541        for (int i=0, j=p; (i < 2); i++,j++)
542        {
543          c <<= 4;
544    
545          switch (s.charAt(j))
546          {
547            case '0':
548              break;
549            case '1':
550              c |= 0x01;
551              break;
552            case '2':
553              c |= 0x02;
554              break;
555            case '3':
556              c |= 0x03;
557              break;
558            case '4':
559              c |= 0x04;
560              break;
561            case '5':
562              c |= 0x05;
563              break;
564            case '6':
565              c |= 0x06;
566              break;
567            case '7':
568              c |= 0x07;
569              break;
570            case '8':
571              c |= 0x08;
572              break;
573            case '9':
574              c |= 0x09;
575              break;
576            case 'a':
577            case 'A':
578              c |= 0x0A;
579              break;
580            case 'b':
581            case 'B':
582              c |= 0x0B;
583              break;
584            case 'c':
585            case 'C':
586              c |= 0x0C;
587              break;
588            case 'd':
589            case 'D':
590              c |= 0x0D;
591              break;
592            case 'e':
593            case 'E':
594              c |= 0x0E;
595              break;
596            case 'f':
597            case 'F':
598              c |= 0x0F;
599              break;
600            default:
601              throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
602                   ERR_CASE_IGNORE_LIST_NOT_HEX_DIGIT.get(s.charAt(j)));
603          }
604        }
605    
606        return c;
607      }
608    }