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