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