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