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