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