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 use 044 * schema element. 045 */ 046@NotMutable() 047@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 048public final class MatchingRuleUseDefinition 049 extends SchemaElement 050{ 051 /** 052 * The serial version UID for this serializable class. 053 */ 054 private static final long serialVersionUID = 2366143311976256897L; 055 056 057 058 // Indicates whether this matching rule use is declared obsolete. 059 private final boolean isObsolete; 060 061 // The set of extensions for this matching rule use. 062 private final Map<String,String[]> extensions; 063 064 // The description for this matching rule use. 065 private final String description; 066 067 // The string representation of this matching rule use. 068 private final String matchingRuleUseString; 069 070 // The OID for this matching rule use. 071 private final String oid; 072 073 // The set of attribute types to to which this matching rule use applies. 074 private final String[] applicableTypes; 075 076 // The set of names for this matching rule use. 077 private final String[] names; 078 079 080 081 /** 082 * Creates a new matching rule use from the provided string representation. 083 * 084 * @param s The string representation of the matching rule use to create, 085 * using the syntax described in RFC 4512 section 4.1.4. It must 086 * not be {@code null}. 087 * 088 * @throws LDAPException If the provided string cannot be decoded as a 089 * matching rule use definition. 090 */ 091 public MatchingRuleUseDefinition(final String s) 092 throws LDAPException 093 { 094 ensureNotNull(s); 095 096 matchingRuleUseString = s.trim(); 097 098 // The first character must be an opening parenthesis. 099 final int length = matchingRuleUseString.length(); 100 if (length == 0) 101 { 102 throw new LDAPException(ResultCode.DECODING_ERROR, 103 ERR_MRU_DECODE_EMPTY.get()); 104 } 105 else if (matchingRuleUseString.charAt(0) != '(') 106 { 107 throw new LDAPException(ResultCode.DECODING_ERROR, 108 ERR_MRU_DECODE_NO_OPENING_PAREN.get( 109 matchingRuleUseString)); 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(matchingRuleUseString, 1, length); 116 117 StringBuilder buffer = new StringBuilder(); 118 pos = readOID(matchingRuleUseString, pos, length, buffer); 119 oid = buffer.toString(); 120 121 122 // Technically, matching rule use elements are supposed to appear in a 123 // specific order, but we'll be lenient and allow remaining elements to come 124 // in any order. 125 final ArrayList<String> nameList = new ArrayList<String>(1); 126 final ArrayList<String> typeList = new ArrayList<String>(1); 127 String descr = null; 128 Boolean obsolete = 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(matchingRuleUseString, 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) && (matchingRuleUseString.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 = matchingRuleUseString.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_MRU_DECODE_CLOSE_NOT_AT_END.get( 163 matchingRuleUseString)); 164 } 165 break; 166 } 167 else if (lowerToken.equals("name")) 168 { 169 if (nameList.isEmpty()) 170 { 171 pos = skipSpaces(matchingRuleUseString, pos, length); 172 pos = readQDStrings(matchingRuleUseString, pos, length, nameList); 173 } 174 else 175 { 176 throw new LDAPException(ResultCode.DECODING_ERROR, 177 ERR_MRU_DECODE_MULTIPLE_ELEMENTS.get( 178 matchingRuleUseString, "NAME")); 179 } 180 } 181 else if (lowerToken.equals("desc")) 182 { 183 if (descr == null) 184 { 185 pos = skipSpaces(matchingRuleUseString, pos, length); 186 187 buffer = new StringBuilder(); 188 pos = readQDString(matchingRuleUseString, pos, length, buffer); 189 descr = buffer.toString(); 190 } 191 else 192 { 193 throw new LDAPException(ResultCode.DECODING_ERROR, 194 ERR_MRU_DECODE_MULTIPLE_ELEMENTS.get( 195 matchingRuleUseString, "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_MRU_DECODE_MULTIPLE_ELEMENTS.get( 208 matchingRuleUseString, "OBSOLETE")); 209 } 210 } 211 else if (lowerToken.equals("applies")) 212 { 213 if (typeList.isEmpty()) 214 { 215 pos = skipSpaces(matchingRuleUseString, pos, length); 216 pos = readOIDs(matchingRuleUseString, pos, length, typeList); 217 } 218 else 219 { 220 throw new LDAPException(ResultCode.DECODING_ERROR, 221 ERR_MRU_DECODE_MULTIPLE_ELEMENTS.get( 222 matchingRuleUseString, "APPLIES")); 223 } 224 } 225 else if (lowerToken.startsWith("x-")) 226 { 227 pos = skipSpaces(matchingRuleUseString, pos, length); 228 229 final ArrayList<String> valueList = new ArrayList<String>(); 230 pos = readQDStrings(matchingRuleUseString, pos, length, valueList); 231 232 final String[] values = new String[valueList.size()]; 233 valueList.toArray(values); 234 235 if (exts.containsKey(token)) 236 { 237 throw new LDAPException(ResultCode.DECODING_ERROR, 238 ERR_MRU_DECODE_DUP_EXT.get( 239 matchingRuleUseString, token)); 240 } 241 242 exts.put(token, values); 243 } 244 else 245 { 246 throw new LDAPException(ResultCode.DECODING_ERROR, 247 ERR_MRU_DECODE_UNEXPECTED_TOKEN.get( 248 matchingRuleUseString, token)); 249 } 250 } 251 252 description = descr; 253 254 names = new String[nameList.size()]; 255 nameList.toArray(names); 256 257 if (typeList.isEmpty()) 258 { 259 throw new LDAPException(ResultCode.DECODING_ERROR, 260 ERR_MRU_DECODE_NO_APPLIES.get( 261 matchingRuleUseString)); 262 } 263 264 applicableTypes = new String[typeList.size()]; 265 typeList.toArray(applicableTypes); 266 267 isObsolete = (obsolete != null); 268 269 extensions = Collections.unmodifiableMap(exts); 270 } 271 272 273 274 /** 275 * Creates a new matching rule use with the provided information. 276 * 277 * @param oid The OID for this matching rule use. It must not 278 * be {@code null}. 279 * @param names The set of names for this matching rule use. It 280 * may be {@code null} or empty if the matching rule 281 * should only be referenced by OID. 282 * @param description The description for this matching rule use. It 283 * may be {@code null} if there is no description. 284 * @param isObsolete Indicates whether this matching rule use is 285 * declared obsolete. 286 * @param applicableTypes The set of attribute types to which this matching 287 * rule use applies. It must not be empty or 288 * {@code null}. 289 * @param extensions The set of extensions for this matching rule use. 290 * It may be {@code null} or empty if there should 291 * not be any extensions. 292 */ 293 public MatchingRuleUseDefinition(final String oid, final String[] names, 294 final String description, 295 final boolean isObsolete, 296 final String[] applicableTypes, 297 final Map<String,String[]> extensions) 298 { 299 ensureNotNull(oid, applicableTypes); 300 ensureFalse(applicableTypes.length == 0); 301 302 this.oid = oid; 303 this.description = description; 304 this.isObsolete = isObsolete; 305 this.applicableTypes = applicableTypes; 306 307 if (names == null) 308 { 309 this.names = NO_STRINGS; 310 } 311 else 312 { 313 this.names = names; 314 } 315 316 if (extensions == null) 317 { 318 this.extensions = Collections.emptyMap(); 319 } 320 else 321 { 322 this.extensions = Collections.unmodifiableMap(extensions); 323 } 324 325 final StringBuilder buffer = new StringBuilder(); 326 createDefinitionString(buffer); 327 matchingRuleUseString = buffer.toString(); 328 } 329 330 331 332 /** 333 * Constructs a string representation of this matching rule use definition in 334 * the provided buffer. 335 * 336 * @param buffer The buffer in which to construct a string representation of 337 * this matching rule use definition. 338 */ 339 private void createDefinitionString(final StringBuilder buffer) 340 { 341 buffer.append("( "); 342 buffer.append(oid); 343 344 if (names.length == 1) 345 { 346 buffer.append(" NAME '"); 347 buffer.append(names[0]); 348 buffer.append('\''); 349 } 350 else if (names.length > 1) 351 { 352 buffer.append(" NAME ("); 353 for (final String name : names) 354 { 355 buffer.append(" '"); 356 buffer.append(name); 357 buffer.append('\''); 358 } 359 buffer.append(" )"); 360 } 361 362 if (description != null) 363 { 364 buffer.append(" DESC '"); 365 encodeValue(description, buffer); 366 buffer.append('\''); 367 } 368 369 if (isObsolete) 370 { 371 buffer.append(" OBSOLETE"); 372 } 373 374 if (applicableTypes.length == 1) 375 { 376 buffer.append(" APPLIES "); 377 buffer.append(applicableTypes[0]); 378 } 379 else if (applicableTypes.length > 1) 380 { 381 buffer.append(" APPLIES ("); 382 for (int i=0; i < applicableTypes.length; i++) 383 { 384 if (i > 0) 385 { 386 buffer.append(" $"); 387 } 388 389 buffer.append(' '); 390 buffer.append(applicableTypes[i]); 391 } 392 buffer.append(" )"); 393 } 394 395 for (final Map.Entry<String,String[]> e : extensions.entrySet()) 396 { 397 final String name = e.getKey(); 398 final String[] values = e.getValue(); 399 if (values.length == 1) 400 { 401 buffer.append(' '); 402 buffer.append(name); 403 buffer.append(" '"); 404 encodeValue(values[0], buffer); 405 buffer.append('\''); 406 } 407 else 408 { 409 buffer.append(' '); 410 buffer.append(name); 411 buffer.append(" ("); 412 for (final String value : values) 413 { 414 buffer.append(" '"); 415 encodeValue(value, buffer); 416 buffer.append('\''); 417 } 418 buffer.append(" )"); 419 } 420 } 421 422 buffer.append(" )"); 423 } 424 425 426 427 /** 428 * Retrieves the OID for this matching rule use. 429 * 430 * @return The OID for this matching rule use. 431 */ 432 public String getOID() 433 { 434 return oid; 435 } 436 437 438 439 /** 440 * Retrieves the set of names for this matching rule use. 441 * 442 * @return The set of names for this matching rule use, or an empty array if 443 * it does not have any names. 444 */ 445 public String[] getNames() 446 { 447 return names; 448 } 449 450 451 452 /** 453 * Retrieves the primary name that can be used to reference this matching 454 * rule use. If one or more names are defined, then the first name will be 455 * used. Otherwise, the OID will be returned. 456 * 457 * @return The primary name that can be used to reference this matching rule 458 * use. 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 use. 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 use, 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 use, if available. 501 * 502 * @return The description for this matching rule use, or {@code null} if 503 * there is no description defined. 504 */ 505 public String getDescription() 506 { 507 return description; 508 } 509 510 511 512 /** 513 * Indicates whether this matching rule use is declared obsolete. 514 * 515 * @return {@code true} if this matching rule use 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 names or OIDs of the attribute types to which this matching 527 * rule use applies. 528 * 529 * @return The names or OIDs of the attribute types to which this matching 530 * rule use applies. 531 */ 532 public String[] getApplicableAttributeTypes() 533 { 534 return applicableTypes; 535 } 536 537 538 539 /** 540 * Retrieves the set of extensions for this matching rule use. They will be 541 * mapped from the extension name (which should start with "X-") to the set 542 * of values for that extension. 543 * 544 * @return The set of extensions for this matching rule use. 545 */ 546 public Map<String,String[]> getExtensions() 547 { 548 return extensions; 549 } 550 551 552 553 /** 554 * {@inheritDoc} 555 */ 556 @Override() 557 public int hashCode() 558 { 559 return oid.hashCode(); 560 } 561 562 563 564 /** 565 * {@inheritDoc} 566 */ 567 @Override() 568 public boolean equals(final Object o) 569 { 570 if (o == null) 571 { 572 return false; 573 } 574 575 if (o == this) 576 { 577 return true; 578 } 579 580 if (! (o instanceof MatchingRuleUseDefinition)) 581 { 582 return false; 583 } 584 585 final MatchingRuleUseDefinition d = (MatchingRuleUseDefinition) o; 586 return (oid.equals(d.oid) && 587 stringsEqualIgnoreCaseOrderIndependent(names, d.names) && 588 stringsEqualIgnoreCaseOrderIndependent(applicableTypes, 589 d.applicableTypes) && 590 bothNullOrEqualIgnoreCase(description, d.description) && 591 (isObsolete == d.isObsolete) && 592 extensionsEqual(extensions, d.extensions)); 593 } 594 595 596 597 /** 598 * Retrieves a string representation of this matching rule definition, in the 599 * format described in RFC 4512 section 4.1.4. 600 * 601 * @return A string representation of this matching rule use definition. 602 */ 603 @Override() 604 public String toString() 605 { 606 return matchingRuleUseString; 607 } 608}