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}