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