001/*
002 * Copyright 2009-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2009-2017 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 */
021package com.unboundid.ldap.matchingrules;
022
023
024
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.Iterator;
028import java.util.List;
029
030import com.unboundid.asn1.ASN1OctetString;
031import com.unboundid.ldap.sdk.LDAPException;
032import com.unboundid.ldap.sdk.ResultCode;
033import com.unboundid.util.ThreadSafety;
034import com.unboundid.util.ThreadSafetyLevel;
035
036import static com.unboundid.ldap.matchingrules.MatchingRuleMessages.*;
037import static com.unboundid.util.Debug.*;
038import 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)
049public 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}