001/* 002 * Copyright 2007-2014 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2014 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 names The set of names for this matching rule. It may be 279 * {@code null} or empty if the matching rule should only 280 * be 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 isObsolete Indicates whether this matching rule is declared 284 * obsolete. 285 * @param syntaxOID The syntax OID for this matching rule. It must not be 286 * {@code null}. 287 * @param extensions The set of extensions for this matching rule. 288 * It may be {@code null} or empty if there should not be 289 * any extensions. 290 */ 291 public MatchingRuleDefinition(final String oid, final String[] names, 292 final String description, 293 final boolean isObsolete, 294 final String syntaxOID, 295 final Map<String,String[]> extensions) 296 { 297 ensureNotNull(oid, syntaxOID); 298 299 this.oid = oid; 300 this.description = description; 301 this.isObsolete = isObsolete; 302 this.syntaxOID = syntaxOID; 303 304 if (names == null) 305 { 306 this.names = NO_STRINGS; 307 } 308 else 309 { 310 this.names = names; 311 } 312 313 if (extensions == null) 314 { 315 this.extensions = Collections.emptyMap(); 316 } 317 else 318 { 319 this.extensions = Collections.unmodifiableMap(extensions); 320 } 321 322 final StringBuilder buffer = new StringBuilder(); 323 createDefinitionString(buffer); 324 matchingRuleString = buffer.toString(); 325 } 326 327 328 329 /** 330 * Constructs a string representation of this matching rule definition in the 331 * provided buffer. 332 * 333 * @param buffer The buffer in which to construct a string representation of 334 * this matching rule definition. 335 */ 336 private void createDefinitionString(final StringBuilder buffer) 337 { 338 buffer.append("( "); 339 buffer.append(oid); 340 341 if (names.length == 1) 342 { 343 buffer.append(" NAME '"); 344 buffer.append(names[0]); 345 buffer.append('\''); 346 } 347 else if (names.length > 1) 348 { 349 buffer.append(" NAME ("); 350 for (final String name : names) 351 { 352 buffer.append(" '"); 353 buffer.append(name); 354 buffer.append('\''); 355 } 356 buffer.append(" )"); 357 } 358 359 if (description != null) 360 { 361 buffer.append(" DESC '"); 362 encodeValue(description, buffer); 363 buffer.append('\''); 364 } 365 366 if (isObsolete) 367 { 368 buffer.append(" OBSOLETE"); 369 } 370 371 buffer.append(" SYNTAX "); 372 buffer.append(syntaxOID); 373 374 for (final Map.Entry<String,String[]> e : extensions.entrySet()) 375 { 376 final String name = e.getKey(); 377 final String[] values = e.getValue(); 378 if (values.length == 1) 379 { 380 buffer.append(' '); 381 buffer.append(name); 382 buffer.append(" '"); 383 encodeValue(values[0], buffer); 384 buffer.append('\''); 385 } 386 else 387 { 388 buffer.append(' '); 389 buffer.append(name); 390 buffer.append(" ("); 391 for (final String value : values) 392 { 393 buffer.append(" '"); 394 encodeValue(value, buffer); 395 buffer.append('\''); 396 } 397 buffer.append(" )"); 398 } 399 } 400 401 buffer.append(" )"); 402 } 403 404 405 406 /** 407 * Retrieves the OID for this matching rule. 408 * 409 * @return The OID for this matching rule. 410 */ 411 public String getOID() 412 { 413 return oid; 414 } 415 416 417 418 /** 419 * Retrieves the set of names for this matching rule. 420 * 421 * @return The set of names for this matching rule, or an empty array if it 422 * does not have any names. 423 */ 424 public String[] getNames() 425 { 426 return names; 427 } 428 429 430 431 /** 432 * Retrieves the primary name that can be used to reference this matching 433 * rule. If one or more names are defined, then the first name will be used. 434 * Otherwise, the OID will be returned. 435 * 436 * @return The primary name that can be used to reference this matching rule. 437 */ 438 public String getNameOrOID() 439 { 440 if (names.length == 0) 441 { 442 return oid; 443 } 444 else 445 { 446 return names[0]; 447 } 448 } 449 450 451 452 /** 453 * Indicates whether the provided string matches the OID or any of the names 454 * for this matching rule. 455 * 456 * @param s The string for which to make the determination. It must not be 457 * {@code null}. 458 * 459 * @return {@code true} if the provided string matches the OID or any of the 460 * names for this matching rule, or {@code false} if not. 461 */ 462 public boolean hasNameOrOID(final String s) 463 { 464 for (final String name : names) 465 { 466 if (s.equalsIgnoreCase(name)) 467 { 468 return true; 469 } 470 } 471 472 return s.equalsIgnoreCase(oid); 473 } 474 475 476 477 /** 478 * Retrieves the description for this matching rule, if available. 479 * 480 * @return The description for this matching rule, or {@code null} if there 481 * is no description defined. 482 */ 483 public String getDescription() 484 { 485 return description; 486 } 487 488 489 490 /** 491 * Indicates whether this matching rule is declared obsolete. 492 * 493 * @return {@code true} if this matching rule is declared obsolete, or 494 * {@code false} if it is not. 495 */ 496 public boolean isObsolete() 497 { 498 return isObsolete; 499 } 500 501 502 503 /** 504 * Retrieves the OID of the syntax for this matching rule. 505 * 506 * @return The OID of the syntax for this matching rule. 507 */ 508 public String getSyntaxOID() 509 { 510 return syntaxOID; 511 } 512 513 514 515 /** 516 * Retrieves the set of extensions for this matching rule. They will be 517 * mapped from the extension name (which should start with "X-") to the set 518 * of values for that extension. 519 * 520 * @return The set of extensions for this matching rule. 521 */ 522 public Map<String,String[]> getExtensions() 523 { 524 return extensions; 525 } 526 527 528 529 /** 530 * {@inheritDoc} 531 */ 532 @Override() 533 public int hashCode() 534 { 535 return oid.hashCode(); 536 } 537 538 539 540 /** 541 * {@inheritDoc} 542 */ 543 @Override() 544 public boolean equals(final Object o) 545 { 546 if (o == null) 547 { 548 return false; 549 } 550 551 if (o == this) 552 { 553 return true; 554 } 555 556 if (! (o instanceof MatchingRuleDefinition)) 557 { 558 return false; 559 } 560 561 final MatchingRuleDefinition d = (MatchingRuleDefinition) o; 562 return (oid.equals(d.oid) && 563 syntaxOID.equals(d.syntaxOID) && 564 stringsEqualIgnoreCaseOrderIndependent(names, d.names) && 565 bothNullOrEqualIgnoreCase(description, d.description) && 566 (isObsolete == d.isObsolete) && 567 extensionsEqual(extensions, d.extensions)); 568 } 569 570 571 572 /** 573 * Retrieves a string representation of this matching rule definition, in the 574 * format described in RFC 4512 section 4.1.3. 575 * 576 * @return A string representation of this matching rule definition. 577 */ 578 @Override() 579 public String toString() 580 { 581 return matchingRuleString; 582 } 583}