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