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}