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