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