001/* 002 * Copyright 2007-2019 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2019 Ping Identity Corporation 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.sdk.schema; 022 023 024 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.Map; 029import java.util.LinkedHashMap; 030 031import com.unboundid.ldap.sdk.LDAPException; 032import com.unboundid.ldap.sdk.ResultCode; 033import com.unboundid.util.NotMutable; 034import com.unboundid.util.StaticUtils; 035import com.unboundid.util.ThreadSafety; 036import com.unboundid.util.ThreadSafetyLevel; 037import com.unboundid.util.Validator; 038 039import static com.unboundid.ldap.sdk.schema.SchemaMessages.*; 040 041 042 043/** 044 * This class provides a data structure that describes an LDAP matching rule use 045 * schema element. 046 */ 047@NotMutable() 048@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 049public final class MatchingRuleUseDefinition 050 extends SchemaElement 051{ 052 /** 053 * The serial version UID for this serializable class. 054 */ 055 private static final long serialVersionUID = 2366143311976256897L; 056 057 058 059 // Indicates whether this matching rule use is declared obsolete. 060 private final boolean isObsolete; 061 062 // The set of extensions for this matching rule use. 063 private final Map<String,String[]> extensions; 064 065 // The description for this matching rule use. 066 private final String description; 067 068 // The string representation of this matching rule use. 069 private final String matchingRuleUseString; 070 071 // The OID for this matching rule use. 072 private final String oid; 073 074 // The set of attribute types to to which this matching rule use applies. 075 private final String[] applicableTypes; 076 077 // The set of names for this matching rule use. 078 private final String[] names; 079 080 081 082 /** 083 * Creates a new matching rule use from the provided string representation. 084 * 085 * @param s The string representation of the matching rule use to create, 086 * using the syntax described in RFC 4512 section 4.1.4. It must 087 * not be {@code null}. 088 * 089 * @throws LDAPException If the provided string cannot be decoded as a 090 * matching rule use definition. 091 */ 092 public MatchingRuleUseDefinition(final String s) 093 throws LDAPException 094 { 095 Validator.ensureNotNull(s); 096 097 matchingRuleUseString = s.trim(); 098 099 // The first character must be an opening parenthesis. 100 final int length = matchingRuleUseString.length(); 101 if (length == 0) 102 { 103 throw new LDAPException(ResultCode.DECODING_ERROR, 104 ERR_MRU_DECODE_EMPTY.get()); 105 } 106 else if (matchingRuleUseString.charAt(0) != '(') 107 { 108 throw new LDAPException(ResultCode.DECODING_ERROR, 109 ERR_MRU_DECODE_NO_OPENING_PAREN.get( 110 matchingRuleUseString)); 111 } 112 113 114 // Skip over any spaces until we reach the start of the OID, then read the 115 // OID until we find the next space. 116 int pos = skipSpaces(matchingRuleUseString, 1, length); 117 118 StringBuilder buffer = new StringBuilder(); 119 pos = readOID(matchingRuleUseString, pos, length, buffer); 120 oid = buffer.toString(); 121 122 123 // Technically, matching rule use elements are supposed to appear in a 124 // specific order, but we'll be lenient and allow remaining elements to come 125 // in any order. 126 final ArrayList<String> nameList = new ArrayList<>(1); 127 final ArrayList<String> typeList = new ArrayList<>(1); 128 String descr = null; 129 Boolean obsolete = null; 130 final Map<String,String[]> exts = 131 new LinkedHashMap<>(StaticUtils.computeMapCapacity(5)); 132 133 while (true) 134 { 135 // Skip over any spaces until we find the next element. 136 pos = skipSpaces(matchingRuleUseString, pos, length); 137 138 // Read until we find the next space or the end of the string. Use that 139 // token to figure out what to do next. 140 final int tokenStartPos = pos; 141 while ((pos < length) && (matchingRuleUseString.charAt(pos) != ' ')) 142 { 143 pos++; 144 } 145 146 // It's possible that the token could be smashed right up against the 147 // closing parenthesis. If that's the case, then extract just the token 148 // and handle the closing parenthesis the next time through. 149 String token = matchingRuleUseString.substring(tokenStartPos, pos); 150 if ((token.length() > 1) && (token.endsWith(")"))) 151 { 152 token = token.substring(0, token.length() - 1); 153 pos--; 154 } 155 156 final String lowerToken = StaticUtils.toLowerCase(token); 157 if (lowerToken.equals(")")) 158 { 159 // This indicates that we're at the end of the value. There should not 160 // be any more closing characters. 161 if (pos < length) 162 { 163 throw new LDAPException(ResultCode.DECODING_ERROR, 164 ERR_MRU_DECODE_CLOSE_NOT_AT_END.get( 165 matchingRuleUseString)); 166 } 167 break; 168 } 169 else if (lowerToken.equals("name")) 170 { 171 if (nameList.isEmpty()) 172 { 173 pos = skipSpaces(matchingRuleUseString, pos, length); 174 pos = readQDStrings(matchingRuleUseString, pos, length, nameList); 175 } 176 else 177 { 178 throw new LDAPException(ResultCode.DECODING_ERROR, 179 ERR_MRU_DECODE_MULTIPLE_ELEMENTS.get( 180 matchingRuleUseString, "NAME")); 181 } 182 } 183 else if (lowerToken.equals("desc")) 184 { 185 if (descr == null) 186 { 187 pos = skipSpaces(matchingRuleUseString, pos, length); 188 189 buffer = new StringBuilder(); 190 pos = readQDString(matchingRuleUseString, pos, length, buffer); 191 descr = buffer.toString(); 192 } 193 else 194 { 195 throw new LDAPException(ResultCode.DECODING_ERROR, 196 ERR_MRU_DECODE_MULTIPLE_ELEMENTS.get( 197 matchingRuleUseString, "DESC")); 198 } 199 } 200 else if (lowerToken.equals("obsolete")) 201 { 202 if (obsolete == null) 203 { 204 obsolete = true; 205 } 206 else 207 { 208 throw new LDAPException(ResultCode.DECODING_ERROR, 209 ERR_MRU_DECODE_MULTIPLE_ELEMENTS.get( 210 matchingRuleUseString, "OBSOLETE")); 211 } 212 } 213 else if (lowerToken.equals("applies")) 214 { 215 if (typeList.isEmpty()) 216 { 217 pos = skipSpaces(matchingRuleUseString, pos, length); 218 pos = readOIDs(matchingRuleUseString, pos, length, typeList); 219 } 220 else 221 { 222 throw new LDAPException(ResultCode.DECODING_ERROR, 223 ERR_MRU_DECODE_MULTIPLE_ELEMENTS.get( 224 matchingRuleUseString, "APPLIES")); 225 } 226 } 227 else if (lowerToken.startsWith("x-")) 228 { 229 pos = skipSpaces(matchingRuleUseString, pos, length); 230 231 final ArrayList<String> valueList = new ArrayList<>(5); 232 pos = readQDStrings(matchingRuleUseString, pos, length, valueList); 233 234 final String[] values = new String[valueList.size()]; 235 valueList.toArray(values); 236 237 if (exts.containsKey(token)) 238 { 239 throw new LDAPException(ResultCode.DECODING_ERROR, 240 ERR_MRU_DECODE_DUP_EXT.get( 241 matchingRuleUseString, token)); 242 } 243 244 exts.put(token, values); 245 } 246 else 247 { 248 throw new LDAPException(ResultCode.DECODING_ERROR, 249 ERR_MRU_DECODE_UNEXPECTED_TOKEN.get( 250 matchingRuleUseString, token)); 251 } 252 } 253 254 description = descr; 255 256 names = new String[nameList.size()]; 257 nameList.toArray(names); 258 259 if (typeList.isEmpty()) 260 { 261 throw new LDAPException(ResultCode.DECODING_ERROR, 262 ERR_MRU_DECODE_NO_APPLIES.get( 263 matchingRuleUseString)); 264 } 265 266 applicableTypes = new String[typeList.size()]; 267 typeList.toArray(applicableTypes); 268 269 isObsolete = (obsolete != null); 270 271 extensions = Collections.unmodifiableMap(exts); 272 } 273 274 275 276 /** 277 * Creates a new matching rule use with the provided information. 278 * 279 * @param oid The OID for this matching rule use. It must not 280 * be {@code null}. 281 * @param name The name for this matching rule use. It may be 282 * {@code null} or empty if the matching rule use 283 * should only be referenced by OID. 284 * @param description The description for this matching rule use. It 285 * may be {@code null} if there is no description. 286 * @param applicableTypes The set of attribute types to which this matching 287 * rule use applies. It must not be empty or 288 * {@code null}. 289 * @param extensions The set of extensions for this matching rule use. 290 * It may be {@code null} or empty if there should 291 * not be any extensions. 292 */ 293 public MatchingRuleUseDefinition(final String oid, final String name, 294 final String description, 295 final String[] applicableTypes, 296 final Map<String,String[]> extensions) 297 { 298 this(oid, ((name == null) ? null : new String[] { name }), description, 299 false, applicableTypes, extensions); 300 } 301 302 303 304 /** 305 * Creates a new matching rule use with the provided information. 306 * 307 * @param oid The OID for this matching rule use. It must not 308 * be {@code null}. 309 * @param name The name for this matching rule use. It may be 310 * {@code null} or empty if the matching rule use 311 * should only be referenced by OID. 312 * @param description The description for this matching rule use. It 313 * may be {@code null} if there is no description. 314 * @param applicableTypes The set of attribute types to which this matching 315 * rule use applies. It must not be empty or 316 * {@code null}. 317 * @param extensions The set of extensions for this matching rule use. 318 * It may be {@code null} or empty if there should 319 * not be any extensions. 320 */ 321 public MatchingRuleUseDefinition(final String oid, final String name, 322 final String description, 323 final Collection<String> applicableTypes, 324 final Map<String,String[]> extensions) 325 { 326 this(oid, ((name == null) ? null : new String[] { name }), description, 327 false, toArray(applicableTypes), extensions); 328 } 329 330 331 332 /** 333 * Creates a new matching rule use with the provided information. 334 * 335 * @param oid The OID for this matching rule use. It must not 336 * be {@code null}. 337 * @param names The set of names for this matching rule use. It 338 * may be {@code null} or empty if the matching rule 339 * use should only be referenced by OID. 340 * @param description The description for this matching rule use. It 341 * may be {@code null} if there is no description. 342 * @param isObsolete Indicates whether this matching rule use is 343 * declared obsolete. 344 * @param applicableTypes The set of attribute types to which this matching 345 * rule use applies. It must not be empty or 346 * {@code null}. 347 * @param extensions The set of extensions for this matching rule use. 348 * It may be {@code null} or empty if there should 349 * not be any extensions. 350 */ 351 public MatchingRuleUseDefinition(final String oid, final String[] names, 352 final String description, 353 final boolean isObsolete, 354 final String[] applicableTypes, 355 final Map<String,String[]> extensions) 356 { 357 Validator.ensureNotNull(oid, applicableTypes); 358 Validator.ensureFalse(applicableTypes.length == 0); 359 360 this.oid = oid; 361 this.description = description; 362 this.isObsolete = isObsolete; 363 this.applicableTypes = applicableTypes; 364 365 if (names == null) 366 { 367 this.names = StaticUtils.NO_STRINGS; 368 } 369 else 370 { 371 this.names = names; 372 } 373 374 if (extensions == null) 375 { 376 this.extensions = Collections.emptyMap(); 377 } 378 else 379 { 380 this.extensions = Collections.unmodifiableMap(extensions); 381 } 382 383 final StringBuilder buffer = new StringBuilder(); 384 createDefinitionString(buffer); 385 matchingRuleUseString = buffer.toString(); 386 } 387 388 389 390 /** 391 * Constructs a string representation of this matching rule use definition in 392 * the provided buffer. 393 * 394 * @param buffer The buffer in which to construct a string representation of 395 * this matching rule use definition. 396 */ 397 private void createDefinitionString(final StringBuilder buffer) 398 { 399 buffer.append("( "); 400 buffer.append(oid); 401 402 if (names.length == 1) 403 { 404 buffer.append(" NAME '"); 405 buffer.append(names[0]); 406 buffer.append('\''); 407 } 408 else if (names.length > 1) 409 { 410 buffer.append(" NAME ("); 411 for (final String name : names) 412 { 413 buffer.append(" '"); 414 buffer.append(name); 415 buffer.append('\''); 416 } 417 buffer.append(" )"); 418 } 419 420 if (description != null) 421 { 422 buffer.append(" DESC '"); 423 encodeValue(description, buffer); 424 buffer.append('\''); 425 } 426 427 if (isObsolete) 428 { 429 buffer.append(" OBSOLETE"); 430 } 431 432 if (applicableTypes.length == 1) 433 { 434 buffer.append(" APPLIES "); 435 buffer.append(applicableTypes[0]); 436 } 437 else if (applicableTypes.length > 1) 438 { 439 buffer.append(" APPLIES ("); 440 for (int i=0; i < applicableTypes.length; i++) 441 { 442 if (i > 0) 443 { 444 buffer.append(" $"); 445 } 446 447 buffer.append(' '); 448 buffer.append(applicableTypes[i]); 449 } 450 buffer.append(" )"); 451 } 452 453 for (final Map.Entry<String,String[]> e : extensions.entrySet()) 454 { 455 final String name = e.getKey(); 456 final String[] values = e.getValue(); 457 if (values.length == 1) 458 { 459 buffer.append(' '); 460 buffer.append(name); 461 buffer.append(" '"); 462 encodeValue(values[0], buffer); 463 buffer.append('\''); 464 } 465 else 466 { 467 buffer.append(' '); 468 buffer.append(name); 469 buffer.append(" ("); 470 for (final String value : values) 471 { 472 buffer.append(" '"); 473 encodeValue(value, buffer); 474 buffer.append('\''); 475 } 476 buffer.append(" )"); 477 } 478 } 479 480 buffer.append(" )"); 481 } 482 483 484 485 /** 486 * Retrieves the OID for this matching rule use. 487 * 488 * @return The OID for this matching rule use. 489 */ 490 public String getOID() 491 { 492 return oid; 493 } 494 495 496 497 /** 498 * Retrieves the set of names for this matching rule use. 499 * 500 * @return The set of names for this matching rule use, or an empty array if 501 * it does not have any names. 502 */ 503 public String[] getNames() 504 { 505 return names; 506 } 507 508 509 510 /** 511 * Retrieves the primary name that can be used to reference this matching 512 * rule use. If one or more names are defined, then the first name will be 513 * used. Otherwise, the OID will be returned. 514 * 515 * @return The primary name that can be used to reference this matching rule 516 * use. 517 */ 518 public String getNameOrOID() 519 { 520 if (names.length == 0) 521 { 522 return oid; 523 } 524 else 525 { 526 return names[0]; 527 } 528 } 529 530 531 532 /** 533 * Indicates whether the provided string matches the OID or any of the names 534 * for this matching rule use. 535 * 536 * @param s The string for which to make the determination. It must not be 537 * {@code null}. 538 * 539 * @return {@code true} if the provided string matches the OID or any of the 540 * names for this matching rule use, or {@code false} if not. 541 */ 542 public boolean hasNameOrOID(final String s) 543 { 544 for (final String name : names) 545 { 546 if (s.equalsIgnoreCase(name)) 547 { 548 return true; 549 } 550 } 551 552 return s.equalsIgnoreCase(oid); 553 } 554 555 556 557 /** 558 * Retrieves the description for this matching rule use, if available. 559 * 560 * @return The description for this matching rule use, or {@code null} if 561 * there is no description defined. 562 */ 563 public String getDescription() 564 { 565 return description; 566 } 567 568 569 570 /** 571 * Indicates whether this matching rule use is declared obsolete. 572 * 573 * @return {@code true} if this matching rule use is declared obsolete, or 574 * {@code false} if it is not. 575 */ 576 public boolean isObsolete() 577 { 578 return isObsolete; 579 } 580 581 582 583 /** 584 * Retrieves the names or OIDs of the attribute types to which this matching 585 * rule use applies. 586 * 587 * @return The names or OIDs of the attribute types to which this matching 588 * rule use applies. 589 */ 590 public String[] getApplicableAttributeTypes() 591 { 592 return applicableTypes; 593 } 594 595 596 597 /** 598 * Retrieves the set of extensions for this matching rule use. They will be 599 * mapped from the extension name (which should start with "X-") to the set 600 * of values for that extension. 601 * 602 * @return The set of extensions for this matching rule use. 603 */ 604 public Map<String,String[]> getExtensions() 605 { 606 return extensions; 607 } 608 609 610 611 /** 612 * {@inheritDoc} 613 */ 614 @Override() 615 public int hashCode() 616 { 617 return oid.hashCode(); 618 } 619 620 621 622 /** 623 * {@inheritDoc} 624 */ 625 @Override() 626 public boolean equals(final Object o) 627 { 628 if (o == null) 629 { 630 return false; 631 } 632 633 if (o == this) 634 { 635 return true; 636 } 637 638 if (! (o instanceof MatchingRuleUseDefinition)) 639 { 640 return false; 641 } 642 643 final MatchingRuleUseDefinition d = (MatchingRuleUseDefinition) o; 644 return (oid.equals(d.oid) && 645 StaticUtils.stringsEqualIgnoreCaseOrderIndependent(names, d.names) && 646 StaticUtils.stringsEqualIgnoreCaseOrderIndependent(applicableTypes, 647 d.applicableTypes) && 648 StaticUtils.bothNullOrEqualIgnoreCase(description, d.description) && 649 (isObsolete == d.isObsolete) && 650 extensionsEqual(extensions, d.extensions)); 651 } 652 653 654 655 /** 656 * Retrieves a string representation of this matching rule definition, in the 657 * format described in RFC 4512 section 4.1.4. 658 * 659 * @return A string representation of this matching rule use definition. 660 */ 661 @Override() 662 public String toString() 663 { 664 return matchingRuleUseString; 665 } 666}