001/* 002 * Copyright 2009-2014 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2009-2014 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}