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