001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2009-2010 Sun Microsystems, Inc. 015 * Portions Copyright 2014 Manuel Gaupp 016 * Portions Copyright 2011-2016 ForgeRock AS. 017 */ 018package org.forgerock.opendj.ldap.schema; 019 020import static java.util.Collections.*; 021 022import static org.forgerock.opendj.ldap.LdapException.*; 023import static org.forgerock.opendj.ldap.schema.ObjectClass.*; 024import static org.forgerock.opendj.ldap.schema.ObjectClassType.*; 025import static org.forgerock.opendj.ldap.schema.Schema.*; 026import static org.forgerock.opendj.ldap.schema.SchemaConstants.*; 027import static org.forgerock.opendj.ldap.schema.SchemaOptions.*; 028import static org.forgerock.opendj.ldap.schema.SchemaUtils.*; 029 030import static com.forgerock.opendj.ldap.CoreMessages.*; 031import static com.forgerock.opendj.util.StaticUtils.*; 032 033import java.util.ArrayList; 034import java.util.Collection; 035import java.util.Collections; 036import java.util.HashMap; 037import java.util.LinkedHashMap; 038import java.util.LinkedList; 039import java.util.List; 040import java.util.Map; 041import java.util.Set; 042import java.util.concurrent.atomic.AtomicInteger; 043import java.util.regex.Pattern; 044 045import org.forgerock.i18n.LocalizableMessage; 046import org.forgerock.i18n.LocalizedIllegalArgumentException; 047import org.forgerock.opendj.ldap.Attribute; 048import org.forgerock.opendj.ldap.ByteString; 049import org.forgerock.opendj.ldap.Connection; 050import org.forgerock.opendj.ldap.DN; 051import org.forgerock.opendj.ldap.DecodeException; 052import org.forgerock.opendj.ldap.Entry; 053import org.forgerock.opendj.ldap.EntryNotFoundException; 054import org.forgerock.opendj.ldap.Filter; 055import org.forgerock.opendj.ldap.LdapException; 056import org.forgerock.opendj.ldap.LdapPromise; 057import org.forgerock.opendj.ldap.ResultCode; 058import org.forgerock.opendj.ldap.SearchScope; 059import org.forgerock.opendj.ldap.requests.Requests; 060import org.forgerock.opendj.ldap.requests.SearchRequest; 061import org.forgerock.opendj.ldap.responses.SearchResultEntry; 062import org.forgerock.opendj.ldap.schema.DITContentRule.Builder; 063import org.forgerock.util.AsyncFunction; 064import org.forgerock.util.Function; 065import org.forgerock.util.Option; 066import org.forgerock.util.Options; 067import org.forgerock.util.Reject; 068import org.forgerock.util.promise.Promise; 069 070import com.forgerock.opendj.util.StaticUtils; 071import com.forgerock.opendj.util.SubstringReader; 072 073/** Schema builders should be used for incremental construction of new schemas. */ 074public final class SchemaBuilder { 075 076 /** Constant used for name to oid mapping when one name actually maps to multiple numerical OID. */ 077 public static final String AMBIGUOUS_OID = "<ambiguous-oid>"; 078 079 private static final String ATTR_SUBSCHEMA_SUBENTRY = "subschemaSubentry"; 080 081 private static final String[] SUBSCHEMA_ATTRS = { ATTR_LDAP_SYNTAXES, 082 ATTR_ATTRIBUTE_TYPES, ATTR_DIT_CONTENT_RULES, ATTR_DIT_STRUCTURE_RULES, 083 ATTR_MATCHING_RULE_USE, ATTR_MATCHING_RULES, ATTR_NAME_FORMS, ATTR_OBJECT_CLASSES }; 084 085 private static final Filter SUBSCHEMA_FILTER = Filter.valueOf("(objectClass=subschema)"); 086 087 private static final String[] SUBSCHEMA_SUBENTRY_ATTRS = { ATTR_SUBSCHEMA_SUBENTRY }; 088 089 /** 090 * Constructs a search request for retrieving the subschemaSubentry 091 * attribute from the named entry. 092 */ 093 private static SearchRequest getReadSchemaForEntrySearchRequest(final DN dn) { 094 return Requests.newSearchRequest(dn, SearchScope.BASE_OBJECT, Filter.objectClassPresent(), 095 SUBSCHEMA_SUBENTRY_ATTRS); 096 } 097 098 /** Constructs a search request for retrieving the named subschema sub-entry. */ 099 private static SearchRequest getReadSchemaSearchRequest(final DN dn) { 100 return Requests.newSearchRequest(dn, SearchScope.BASE_OBJECT, SUBSCHEMA_FILTER, 101 SUBSCHEMA_ATTRS); 102 } 103 104 private static DN getSubschemaSubentryDN(final DN name, final Entry entry) throws LdapException { 105 final Attribute subentryAttr = entry.getAttribute(ATTR_SUBSCHEMA_SUBENTRY); 106 107 if (subentryAttr == null || subentryAttr.isEmpty()) { 108 // Did not get the subschema sub-entry attribute. 109 throw newLdapException(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED, 110 ERR_NO_SUBSCHEMA_SUBENTRY_ATTR.get(name.toString()).toString()); 111 } 112 113 final String dnString = subentryAttr.iterator().next().toString(); 114 DN subschemaDN; 115 try { 116 subschemaDN = DN.valueOf(dnString); 117 } catch (final LocalizedIllegalArgumentException e) { 118 throw newLdapException(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED, 119 ERR_INVALID_SUBSCHEMA_SUBENTRY_ATTR.get(name.toString(), dnString, 120 e.getMessageObject()).toString()); 121 } 122 return subschemaDN; 123 } 124 125 /** Allows to perform modifications on element's builders before adding the result to this schema builder. */ 126 public interface SchemaBuilderHook { 127 /** 128 * Allow to modify the builder before its inclusion in schema. 129 * 130 * @param builder 131 * Schema's element builder. 132 */ 133 public void beforeAddSyntax(Syntax.Builder builder); 134 135 /** 136 * Allow to modify the builder before its inclusion in schema. 137 * 138 * @param builder 139 * Schema's element builder. 140 */ 141 public void beforeAddAttribute(AttributeType.Builder builder); 142 143 /** 144 * Allow to modify the builder before its inclusion in schema. 145 * 146 * @param builder 147 * Schema's element builder. 148 */ 149 public void beforeAddObjectClass(ObjectClass.Builder builder); 150 151 /** 152 * Allow to modify the builder before its inclusion in schema. 153 * 154 * @param builder 155 * Schema's element builder. 156 */ 157 public void beforeAddMatchingRuleUse(MatchingRuleUse.Builder builder); 158 159 /** 160 * Allow to modify the builder before its inclusion in schema. 161 * 162 * @param builder 163 * Schema's element builder. 164 */ 165 public void beforeAddMatchingRule(MatchingRule.Builder builder); 166 167 /** 168 * Allow to modify the builder before its inclusion in schema. 169 * 170 * @param builder 171 * Schema's element builder. 172 */ 173 public void beforeAddDitContentRule(DITContentRule.Builder builder); 174 175 /** 176 * Allow to modify the builder before its inclusion in schema. 177 * 178 * @param builder 179 * Schema's element builder. 180 */ 181 public void beforeAddDitStructureRule(DITStructureRule.Builder builder); 182 183 /** 184 * Allow to modify the builder before its inclusion in schema. 185 * 186 * @param builder 187 * Schema's element builder. 188 */ 189 public void beforeAddNameForm(NameForm.Builder builder); 190 } 191 192 private Map<Integer, DITStructureRule> id2StructureRules; 193 private Map<String, List<AttributeType>> name2AttributeTypes; 194 private Map<String, List<DITContentRule>> name2ContentRules; 195 private Map<String, List<MatchingRule>> name2MatchingRules; 196 private Map<String, List<MatchingRuleUse>> name2MatchingRuleUses; 197 private Map<String, List<NameForm>> name2NameForms; 198 private Map<String, List<ObjectClass>> name2ObjectClasses; 199 private Map<String, List<DITStructureRule>> name2StructureRules; 200 private Map<String, List<DITStructureRule>> nameForm2StructureRules; 201 private Map<String, AttributeType> numericOID2AttributeTypes; 202 private Map<String, DITContentRule> numericOID2ContentRules; 203 private Map<String, MatchingRule> numericOID2MatchingRules; 204 private Map<String, MatchingRuleUse> numericOID2MatchingRuleUses; 205 private Map<String, NameForm> numericOID2NameForms; 206 private Map<String, ObjectClass> numericOID2ObjectClasses; 207 private Map<String, Syntax> numericOID2Syntaxes; 208 private Map<String, List<NameForm>> objectClass2NameForms; 209 private String schemaName; 210 private List<LocalizableMessage> warnings; 211 private Options options; 212 213 /** A schema which should be copied into this builder on any mutation. */ 214 private Schema copyOnWriteSchema; 215 216 /** A unique ID which can be used to uniquely identify schemas constructed without a name. */ 217 private static final AtomicInteger NEXT_SCHEMA_ID = new AtomicInteger(); 218 219 /** Creates a new schema builder with no schema elements and default compatibility options. */ 220 public SchemaBuilder() { 221 preLazyInitBuilder(null, null); 222 } 223 224 /** 225 * Creates a new schema builder containing all of the schema elements 226 * contained in the provided subschema subentry. Any problems encountered 227 * while parsing the entry can be retrieved using the returned schema's 228 * {@link Schema#getWarnings()} method. 229 * 230 * @param entry 231 * The subschema subentry to be parsed. 232 * @throws NullPointerException 233 * If {@code entry} was {@code null}. 234 */ 235 public SchemaBuilder(final Entry entry) { 236 preLazyInitBuilder(entry.getName().toString(), null); 237 addSchema(entry, true, null); 238 } 239 240 /** 241 * Creates a new schema builder containing all of the schema elements from 242 * the provided schema and its compatibility options. 243 * 244 * @param schema 245 * The initial contents of the schema builder. 246 * @throws NullPointerException 247 * If {@code schema} was {@code null}. 248 */ 249 public SchemaBuilder(final Schema schema) { 250 preLazyInitBuilder(schema.getSchemaName(), schema); 251 } 252 253 /** 254 * Creates a new schema builder with no schema elements and default 255 * compatibility options. 256 * 257 * @param schemaName 258 * The user-friendly name of this schema which may be used for 259 * debugging purposes. 260 */ 261 public SchemaBuilder(final String schemaName) { 262 preLazyInitBuilder(schemaName, null); 263 } 264 265 private Boolean allowsMalformedNamesAndOptions() { 266 return options.get(ALLOW_MALFORMED_NAMES_AND_OPTIONS); 267 } 268 269 /** 270 * Adds the provided attribute type definition to this schema builder. 271 * 272 * @param definition 273 * The attribute type definition. 274 * @param overwrite 275 * {@code true} if any existing attribute type with the same OID 276 * should be overwritten. 277 * @return A reference to this schema builder. 278 * @throws ConflictingSchemaElementException 279 * If {@code overwrite} was {@code false} and a conflicting 280 * schema element was found. 281 * @throws LocalizedIllegalArgumentException 282 * If the provided attribute type definition could not be 283 * parsed. 284 * @throws NullPointerException 285 * If {@code definition} was {@code null}. 286 */ 287 public SchemaBuilder addAttributeType(final String definition, final boolean overwrite) { 288 return addAttributeType(definition, overwrite, null); 289 } 290 291 SchemaBuilder addAttributeType(final String definition, final boolean overwrite, SchemaBuilderHook hook) { 292 Reject.ifNull(definition); 293 294 lazyInitBuilder(); 295 296 try { 297 final SubstringReader reader = new SubstringReader(definition); 298 299 // We'll do this a character at a time. First, skip over any 300 // leading whitespace. 301 reader.skipWhitespaces(); 302 303 if (reader.remaining() <= 0) { 304 // This means that the definition was empty or contained only 305 // whitespace. That is illegal. 306 throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_ATTRTYPE_EMPTY_VALUE1.get(definition)); 307 } 308 309 // The next character must be an open parenthesis. If it is not, 310 // then that is an error. 311 final char c = reader.read(); 312 if (c != '(') { 313 final LocalizableMessage message = ERR_ATTR_SYNTAX_ATTRTYPE_EXPECTED_OPEN_PARENTHESIS.get( 314 definition, reader.pos() - 1, String.valueOf(c)); 315 throw new LocalizedIllegalArgumentException(message); 316 } 317 318 // Skip over any spaces immediately following the opening 319 // parenthesis. 320 reader.skipWhitespaces(); 321 322 // The next set of characters must be the OID. 323 final String oid = readOID(reader, allowsMalformedNamesAndOptions()); 324 AttributeType.Builder atBuilder = new AttributeType.Builder(oid, this); 325 atBuilder.definition(definition); 326 String superiorType = null; 327 String syntax = null; 328 // At this point, we should have a pretty specific syntax that 329 // describes what may come next, but some of the components are 330 // optional and it would be pretty easy to put something in the 331 // wrong order, so we will be very flexible about what we can 332 // accept. Just look at the next token, figure out what it is and 333 // how to treat what comes after it, then repeat until we get to 334 // the end of the definition. But before we start, set default 335 // values for everything else we might need to know. 336 while (true) { 337 final String tokenName = readTokenName(reader); 338 339 if (tokenName == null) { 340 // No more tokens. 341 break; 342 } else if ("name".equalsIgnoreCase(tokenName)) { 343 atBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions())); 344 } else if ("desc".equalsIgnoreCase(tokenName)) { 345 // This specifies the description for the attribute type. It 346 // is an arbitrary string of characters enclosed in single 347 // quotes. 348 atBuilder.description(readQuotedString(reader)); 349 } else if ("obsolete".equalsIgnoreCase(tokenName)) { 350 // This indicates whether the attribute type should be 351 // considered obsolete. 352 atBuilder.obsolete(true); 353 } else if ("sup".equalsIgnoreCase(tokenName)) { 354 // This specifies the name or OID of the superior attribute 355 // type from which this attribute type should inherit its 356 // properties. 357 superiorType = readOID(reader, allowsMalformedNamesAndOptions()); 358 } else if ("equality".equalsIgnoreCase(tokenName)) { 359 // This specifies the name or OID of the equality matching 360 // rule to use for this attribute type. 361 atBuilder.equalityMatchingRule(readOID(reader, allowsMalformedNamesAndOptions())); 362 } else if ("ordering".equalsIgnoreCase(tokenName)) { 363 // This specifies the name or OID of the ordering matching 364 // rule to use for this attribute type. 365 atBuilder.orderingMatchingRule(readOID(reader, allowsMalformedNamesAndOptions())); 366 } else if ("substr".equalsIgnoreCase(tokenName)) { 367 // This specifies the name or OID of the substring matching 368 // rule to use for this attribute type. 369 atBuilder.substringMatchingRule(readOID(reader, allowsMalformedNamesAndOptions())); 370 } else if ("syntax".equalsIgnoreCase(tokenName)) { 371 // This specifies the numeric OID of the syntax for this 372 // matching rule. It may optionally be immediately followed 373 // by an open curly brace, an integer definition, and a close 374 // curly brace to suggest the minimum number of characters 375 // that should be allowed in values of that type. This 376 // implementation will ignore any such length because it 377 // does not impose any practical limit on the length of attribute 378 // values. 379 syntax = readOIDLen(reader, allowsMalformedNamesAndOptions()); 380 } else if ("single-value".equalsIgnoreCase(tokenName)) { 381 // This indicates that attributes of this type are allowed 382 // to have at most one value. 383 atBuilder.singleValue(true); 384 } else if ("collective".equalsIgnoreCase(tokenName)) { 385 // This indicates that attributes of this type are collective 386 // (i.e., have their values generated dynamically in some way). 387 atBuilder.collective(true); 388 } else if ("no-user-modification".equalsIgnoreCase(tokenName)) { 389 // This indicates that the values of attributes of this type 390 // are not to be modified by end users. 391 atBuilder.noUserModification(true); 392 } else if ("usage".equalsIgnoreCase(tokenName)) { 393 // This specifies the usage string for this attribute type. 394 // It should be followed by one of the strings 395 // "userApplications", "directoryOperation", 396 // "distributedOperation", or "dSAOperation". 397 int length = 0; 398 399 reader.skipWhitespaces(); 400 reader.mark(); 401 402 while (" )".indexOf(reader.read()) == -1) { 403 length++; 404 } 405 reader.reset(); 406 final String usageStr = reader.read(length); 407 if ("userapplications".equalsIgnoreCase(usageStr)) { 408 atBuilder.usage(AttributeUsage.USER_APPLICATIONS); 409 } else if ("directoryoperation".equalsIgnoreCase(usageStr)) { 410 atBuilder.usage(AttributeUsage.DIRECTORY_OPERATION); 411 } else if ("distributedoperation".equalsIgnoreCase(usageStr)) { 412 atBuilder.usage(AttributeUsage.DISTRIBUTED_OPERATION); 413 } else if ("dsaoperation".equalsIgnoreCase(usageStr)) { 414 atBuilder.usage(AttributeUsage.DSA_OPERATION); 415 } else { 416 throw new LocalizedIllegalArgumentException( 417 WARN_ATTR_SYNTAX_ATTRTYPE_INVALID_ATTRIBUTE_USAGE1.get(definition, usageStr)); 418 } 419 } else if (tokenName.matches("^X-[A-Za-z_-]+$")) { 420 // This must be a non-standard property and it must be 421 // followed by either a single definition in single quotes 422 // or an open parenthesis followed by one or more values in 423 // single quotes separated by spaces followed by a close 424 // parenthesis. 425 atBuilder.extraProperties(tokenName, readExtensions(reader)); 426 } else { 427 throw new LocalizedIllegalArgumentException( 428 ERR_ATTR_SYNTAX_ATTRTYPE_ILLEGAL_TOKEN1.get(definition, tokenName)); 429 } 430 } 431 432 final List<String> approxRules = atBuilder.getExtraProperties().get(SCHEMA_PROPERTY_APPROX_RULE); 433 if (approxRules != null && !approxRules.isEmpty()) { 434 atBuilder.approximateMatchingRule(approxRules.get(0)); 435 } 436 437 if (superiorType == null && syntax == null && !options.get(ALLOW_ATTRIBUTE_TYPES_WITH_NO_SUP_OR_SYNTAX)) { 438 throw new LocalizedIllegalArgumentException( 439 WARN_ATTR_SYNTAX_ATTRTYPE_MISSING_SYNTAX_AND_SUPERIOR.get(definition)); 440 } 441 442 atBuilder.superiorType(superiorType) 443 .syntax(syntax); 444 445 if (hook != null) { 446 hook.beforeAddAttribute(atBuilder); 447 } 448 return atBuilder.addToSchema(overwrite); 449 } catch (final DecodeException e) { 450 final LocalizableMessage msg = ERR_ATTR_SYNTAX_ATTRTYPE_INVALID1.get(definition, e.getMessageObject()); 451 throw new LocalizedIllegalArgumentException(msg, e.getCause()); 452 } 453 } 454 455 /** 456 * Adds the provided DIT content rule definition to this schema builder. 457 * 458 * @param definition 459 * The DIT content rule definition. 460 * @param overwrite 461 * {@code true} if any existing DIT content rule with the same 462 * OID should be overwritten. 463 * @return A reference to this schema builder. 464 * @throws ConflictingSchemaElementException 465 * If {@code overwrite} was {@code false} and a conflicting 466 * schema element was found. 467 * @throws LocalizedIllegalArgumentException 468 * If the provided DIT content rule definition could not be 469 * parsed. 470 * @throws NullPointerException 471 * If {@code definition} was {@code null}. 472 */ 473 public SchemaBuilder addDITContentRule(final String definition, final boolean overwrite) { 474 return addDITContentRule(definition, overwrite, null); 475 } 476 477 SchemaBuilder addDITContentRule(final String definition, final boolean overwrite, SchemaBuilderHook hook) { 478 Reject.ifNull(definition); 479 480 lazyInitBuilder(); 481 482 try { 483 final SubstringReader reader = new SubstringReader(definition); 484 485 // We'll do this a character at a time. First, skip over any 486 // leading whitespace. 487 reader.skipWhitespaces(); 488 489 if (reader.remaining() <= 0) { 490 // This means that the value was empty or contained only 491 // whitespace. That is illegal. 492 throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DCR_EMPTY_VALUE1.get(definition)); 493 } 494 495 // The next character must be an open parenthesis. If it is not, 496 // then that is an error. 497 final char c = reader.read(); 498 if (c != '(') { 499 final LocalizableMessage message = ERR_ATTR_SYNTAX_DCR_EXPECTED_OPEN_PARENTHESIS.get( 500 definition, reader.pos() - 1, String.valueOf(c)); 501 throw new LocalizedIllegalArgumentException(message); 502 } 503 504 // Skip over any spaces immediately following the opening 505 // parenthesis. 506 reader.skipWhitespaces(); 507 508 // The next set of characters must be the OID. 509 final DITContentRule.Builder contentRuleBuilder = 510 buildDITContentRule(readOID(reader, allowsMalformedNamesAndOptions())); 511 contentRuleBuilder.definition(definition); 512 513 // At this point, we should have a pretty specific syntax that 514 // describes what may come next, but some of the components are 515 // optional and it would be pretty easy to put something in the 516 // wrong order, so we will be very flexible about what we can 517 // accept. Just look at the next token, figure out what it is and 518 // how to treat what comes after it, then repeat until we get to 519 // the end of the value. But before we start, set default values 520 // for everything else we might need to know. 521 while (true) { 522 final String tokenName = readTokenName(reader); 523 524 if (tokenName == null) { 525 // No more tokens. 526 break; 527 } else if ("name".equalsIgnoreCase(tokenName)) { 528 contentRuleBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions())); 529 } else if ("desc".equalsIgnoreCase(tokenName)) { 530 // This specifies the description for the attribute type. It 531 // is an arbitrary string of characters enclosed in single 532 // quotes. 533 contentRuleBuilder.description(readQuotedString(reader)); 534 } else if ("obsolete".equalsIgnoreCase(tokenName)) { 535 // This indicates whether the attribute type should be 536 // considered obsolete. 537 contentRuleBuilder.obsolete(true); 538 } else if ("aux".equalsIgnoreCase(tokenName)) { 539 contentRuleBuilder.auxiliaryObjectClasses(readOIDs(reader, allowsMalformedNamesAndOptions())); 540 } else if ("must".equalsIgnoreCase(tokenName)) { 541 contentRuleBuilder.requiredAttributes(readOIDs(reader, allowsMalformedNamesAndOptions())); 542 } else if ("may".equalsIgnoreCase(tokenName)) { 543 contentRuleBuilder.optionalAttributes(readOIDs(reader, allowsMalformedNamesAndOptions())); 544 } else if ("not".equalsIgnoreCase(tokenName)) { 545 contentRuleBuilder.prohibitedAttributes(readOIDs(reader, allowsMalformedNamesAndOptions())); 546 } else if (tokenName.matches("^X-[A-Za-z_-]+$")) { 547 // This must be a non-standard property and it must be 548 // followed by either a single definition in single quotes 549 // or an open parenthesis followed by one or more values in 550 // single quotes separated by spaces followed by a close 551 // parenthesis. 552 contentRuleBuilder.extraProperties(tokenName, readExtensions(reader)); 553 } else { 554 throw new LocalizedIllegalArgumentException( 555 ERR_ATTR_SYNTAX_DCR_ILLEGAL_TOKEN1.get(definition, tokenName)); 556 } 557 } 558 559 if (hook != null) { 560 hook.beforeAddDitContentRule(contentRuleBuilder); 561 } 562 return contentRuleBuilder.addToSchema(overwrite); 563 } catch (final DecodeException e) { 564 final LocalizableMessage msg = ERR_ATTR_SYNTAX_DCR_INVALID1.get(definition, e.getMessageObject()); 565 throw new LocalizedIllegalArgumentException(msg, e.getCause()); 566 } 567 } 568 569 /** 570 * Adds the provided DIT structure rule definition to this schema builder. 571 * 572 * @param definition 573 * The DIT structure rule definition. 574 * @param overwrite 575 * {@code true} if any existing DIT structure rule with the same 576 * OID should be overwritten. 577 * @return A reference to this schema builder. 578 * @throws ConflictingSchemaElementException 579 * If {@code overwrite} was {@code false} and a conflicting 580 * schema element was found. 581 * @throws LocalizedIllegalArgumentException 582 * If the provided DIT structure rule definition could not be 583 * parsed. 584 * @throws NullPointerException 585 * If {@code definition} was {@code null}. 586 */ 587 public SchemaBuilder addDITStructureRule(final String definition, final boolean overwrite) { 588 return addDITStructureRule(definition, overwrite, null); 589 } 590 591 SchemaBuilder addDITStructureRule(final String definition, final boolean overwrite, SchemaBuilderHook hook) { 592 Reject.ifNull(definition); 593 594 lazyInitBuilder(); 595 596 try { 597 final SubstringReader reader = new SubstringReader(definition); 598 599 // We'll do this a character at a time. First, skip over any 600 // leading whitespace. 601 reader.skipWhitespaces(); 602 603 if (reader.remaining() <= 0) { 604 // This means that the value was empty or contained only 605 // whitespace. That is illegal. 606 throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DSR_EMPTY_VALUE1.get(definition)); 607 } 608 609 // The next character must be an open parenthesis. If it is not, 610 // then that is an error. 611 final char c = reader.read(); 612 if (c != '(') { 613 final LocalizableMessage message = ERR_ATTR_SYNTAX_DSR_EXPECTED_OPEN_PARENTHESIS.get( 614 definition, reader.pos() - 1, String.valueOf(c)); 615 throw new LocalizedIllegalArgumentException(message); 616 } 617 618 // Skip over any spaces immediately following the opening 619 // parenthesis. 620 reader.skipWhitespaces(); 621 622 // The next set of characters must be the OID. 623 final DITStructureRule.Builder ruleBuilder = new DITStructureRule.Builder(readRuleID(reader), this); 624 String nameForm = null; 625 626 // At this point, we should have a pretty specific syntax that 627 // describes what may come next, but some of the components are 628 // optional and it would be pretty easy to put something in the 629 // wrong order, so we will be very flexible about what we can 630 // accept. Just look at the next token, figure out what it is and 631 // how to treat what comes after it, then repeat until we get to 632 // the end of the value. But before we start, set default values 633 // for everything else we might need to know. 634 while (true) { 635 final String tokenName = readTokenName(reader); 636 637 if (tokenName == null) { 638 // No more tokens. 639 break; 640 } else if ("name".equalsIgnoreCase(tokenName)) { 641 ruleBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions())); 642 } else if ("desc".equalsIgnoreCase(tokenName)) { 643 // This specifies the description for the attribute type. It 644 // is an arbitrary string of characters enclosed in single 645 // quotes. 646 ruleBuilder.description(readQuotedString(reader)); 647 } else if ("obsolete".equalsIgnoreCase(tokenName)) { 648 // This indicates whether the attribute type should be 649 // considered obsolete. 650 ruleBuilder.obsolete(true); 651 } else if ("form".equalsIgnoreCase(tokenName)) { 652 nameForm = readOID(reader, allowsMalformedNamesAndOptions()); 653 } else if ("sup".equalsIgnoreCase(tokenName)) { 654 ruleBuilder.superiorRules(readRuleIDs(reader)); 655 } else if (tokenName.matches("^X-[A-Za-z_-]+$")) { 656 // This must be a non-standard property and it must be 657 // followed by either a single definition in single quotes 658 // or an open parenthesis followed by one or more values in 659 // single quotes separated by spaces followed by a close 660 // parenthesis. 661 ruleBuilder.extraProperties(tokenName, readExtensions(reader)); 662 } else { 663 throw new LocalizedIllegalArgumentException( 664 ERR_ATTR_SYNTAX_DSR_ILLEGAL_TOKEN1.get(definition, tokenName)); 665 } 666 } 667 668 if (nameForm == null) { 669 throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DSR_NO_NAME_FORM.get(definition)); 670 } 671 ruleBuilder.nameForm(nameForm); 672 673 if (hook != null) { 674 hook.beforeAddDitStructureRule(ruleBuilder); 675 } 676 return ruleBuilder.addToSchema(overwrite); 677 } catch (final DecodeException e) { 678 final LocalizableMessage msg = ERR_ATTR_SYNTAX_DSR_INVALID1.get(definition, e.getMessageObject()); 679 throw new LocalizedIllegalArgumentException(msg, e.getCause()); 680 } 681 } 682 683 /** 684 * Adds the provided enumeration syntax definition to this schema builder. 685 * 686 * @param oid 687 * The OID of the enumeration syntax definition. 688 * @param description 689 * The description of the enumeration syntax definition. 690 * @param overwrite 691 * {@code true} if any existing syntax with the same OID should 692 * be overwritten. 693 * @param enumerations 694 * The range of values which attribute values must match in order 695 * to be valid. 696 * @return A reference to this schema builder. 697 * @throws ConflictingSchemaElementException 698 * If {@code overwrite} was {@code false} and a conflicting 699 * schema element was found. 700 */ 701 public SchemaBuilder addEnumerationSyntax(final String oid, final String description, 702 final boolean overwrite, final String... enumerations) { 703 Reject.ifNull((Object) enumerations); 704 return buildSyntax(oid) 705 .description(description) 706 .extraProperties("X-ENUM", enumerations) 707 .addToSchema(overwrite); 708 } 709 710 /** 711 * Adds the provided matching rule definition to this schema builder. 712 * 713 * @param definition 714 * The matching rule definition. 715 * @param overwrite 716 * {@code true} if any existing matching rule with the same OID 717 * should be overwritten. 718 * @return A reference to this schema builder. 719 * @throws ConflictingSchemaElementException 720 * If {@code overwrite} was {@code false} and a conflicting 721 * schema element was found. 722 * @throws LocalizedIllegalArgumentException 723 * If the provided matching rule definition could not be parsed. 724 * @throws NullPointerException 725 * If {@code definition} was {@code null}. 726 */ 727 public SchemaBuilder addMatchingRule(final String definition, final boolean overwrite) { 728 return addMatchingRule(definition, overwrite, null); 729 } 730 731 SchemaBuilder addMatchingRule(final String definition, final boolean overwrite, SchemaBuilderHook hook) { 732 Reject.ifNull(definition); 733 734 lazyInitBuilder(); 735 736 try { 737 final SubstringReader reader = new SubstringReader(definition); 738 739 // We'll do this a character at a time. First, skip over any 740 // leading whitespace. 741 reader.skipWhitespaces(); 742 743 if (reader.remaining() <= 0) { 744 // This means that the value was empty or contained only 745 // whitespace. That is illegal. 746 throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_MR_EMPTY_VALUE1.get(definition)); 747 } 748 749 // The next character must be an open parenthesis. If it is not, 750 // then that is an error. 751 final char c = reader.read(); 752 if (c != '(') { 753 final LocalizableMessage message = ERR_ATTR_SYNTAX_MR_EXPECTED_OPEN_PARENTHESIS.get( 754 definition, reader.pos() - 1, String.valueOf(c)); 755 throw new LocalizedIllegalArgumentException(message); 756 } 757 758 // Skip over any spaces immediately following the opening 759 // parenthesis. 760 reader.skipWhitespaces(); 761 762 // The next set of characters must be the OID. 763 final MatchingRule.Builder matchingRuleBuilder = new MatchingRule.Builder( 764 readOID(reader, allowsMalformedNamesAndOptions()), this); 765 matchingRuleBuilder.definition(definition); 766 767 String syntax = null; 768 // At this point, we should have a pretty specific syntax that 769 // describes what may come next, but some of the components are 770 // optional and it would be pretty easy to put something in the 771 // wrong order, so we will be very flexible about what we can 772 // accept. Just look at the next token, figure out what it is and 773 // how to treat what comes after it, then repeat until we get to 774 // the end of the value. But before we start, set default values 775 // for everything else we might need to know. 776 while (true) { 777 final String tokenName = readTokenName(reader); 778 779 if (tokenName == null) { 780 // No more tokens. 781 break; 782 } else if ("name".equalsIgnoreCase(tokenName)) { 783 matchingRuleBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions())); 784 } else if ("desc".equalsIgnoreCase(tokenName)) { 785 // This specifies the description for the matching rule. It 786 // is an arbitrary string of characters enclosed in single 787 // quotes. 788 matchingRuleBuilder.description(readQuotedString(reader)); 789 } else if ("obsolete".equalsIgnoreCase(tokenName)) { 790 // This indicates whether the matching rule should be 791 // considered obsolete. We do not need to do any more 792 // parsing for this token. 793 matchingRuleBuilder.obsolete(true); 794 } else if ("syntax".equalsIgnoreCase(tokenName)) { 795 syntax = readOID(reader, allowsMalformedNamesAndOptions()); 796 matchingRuleBuilder.syntaxOID(syntax); 797 } else if (tokenName.matches("^X-[A-Za-z_-]+$")) { 798 // This must be a non-standard property and it must be 799 // followed by either a single definition in single quotes 800 // or an open parenthesis followed by one or more values in 801 // single quotes separated by spaces followed by a close 802 // parenthesis. 803 final List<String> extensions = readExtensions(reader); 804 matchingRuleBuilder.extraProperties(tokenName, extensions.toArray(new String[extensions.size()])); 805 } else { 806 throw new LocalizedIllegalArgumentException( 807 ERR_ATTR_SYNTAX_MR_ILLEGAL_TOKEN1.get(definition, tokenName)); 808 } 809 } 810 811 // Make sure that a syntax was specified. 812 if (syntax == null) { 813 throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_MR_NO_SYNTAX.get(definition)); 814 } 815 if (hook != null) { 816 hook.beforeAddMatchingRule(matchingRuleBuilder); 817 } 818 matchingRuleBuilder.addToSchema(overwrite); 819 } catch (final DecodeException e) { 820 final LocalizableMessage msg = 821 ERR_ATTR_SYNTAX_MR_INVALID1.get(definition, e.getMessageObject()); 822 throw new LocalizedIllegalArgumentException(msg, e.getCause()); 823 } 824 return this; 825 } 826 827 /** 828 * Adds the provided matching rule use definition to this schema builder. 829 * 830 * @param definition 831 * The matching rule use definition. 832 * @param overwrite 833 * {@code true} if any existing matching rule use with the same 834 * OID should be overwritten. 835 * @return A reference to this schema builder. 836 * @throws ConflictingSchemaElementException 837 * If {@code overwrite} was {@code false} and a conflicting 838 * schema element was found. 839 * @throws LocalizedIllegalArgumentException 840 * If the provided matching rule use definition could not be 841 * parsed. 842 * @throws NullPointerException 843 * If {@code definition} was {@code null}. 844 */ 845 public SchemaBuilder addMatchingRuleUse(final String definition, final boolean overwrite) { 846 return addMatchingRuleUse(definition, overwrite, null); 847 } 848 849 SchemaBuilder addMatchingRuleUse(final String definition, final boolean overwrite, SchemaBuilderHook hook) { 850 Reject.ifNull(definition); 851 852 lazyInitBuilder(); 853 854 try { 855 final SubstringReader reader = new SubstringReader(definition); 856 857 // We'll do this a character at a time. First, skip over any 858 // leading whitespace. 859 reader.skipWhitespaces(); 860 861 if (reader.remaining() <= 0) { 862 // This means that the value was empty or contained only 863 // whitespace. That is illegal. 864 throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_MRUSE_EMPTY_VALUE1.get(definition)); 865 } 866 867 // The next character must be an open parenthesis. If it is not, 868 // then that is an error. 869 final char c = reader.read(); 870 if (c != '(') { 871 final LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_EXPECTED_OPEN_PARENTHESIS.get( 872 definition, reader.pos() - 1, String.valueOf(c)); 873 throw new LocalizedIllegalArgumentException(message); 874 } 875 876 // Skip over any spaces immediately following the opening 877 // parenthesis. 878 reader.skipWhitespaces(); 879 880 // The next set of characters must be the OID. 881 final MatchingRuleUse.Builder useBuilder = 882 buildMatchingRuleUse(readOID(reader, allowsMalformedNamesAndOptions())); 883 Set<String> attributes = null; 884 885 // At this point, we should have a pretty specific syntax that 886 // describes what may come next, but some of the components are 887 // optional and it would be pretty easy to put something in the 888 // wrong order, so we will be very flexible about what we can 889 // accept. Just look at the next token, figure out what it is and 890 // how to treat what comes after it, then repeat until we get to 891 // the end of the value. But before we start, set default values 892 // for everything else we might need to know. 893 while (true) { 894 final String tokenName = readTokenName(reader); 895 896 if (tokenName == null) { 897 // No more tokens. 898 break; 899 } else if ("name".equalsIgnoreCase(tokenName)) { 900 useBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions())); 901 } else if ("desc".equalsIgnoreCase(tokenName)) { 902 // This specifies the description for the attribute type. It 903 // is an arbitrary string of characters enclosed in single 904 // quotes. 905 useBuilder.description(readQuotedString(reader)); 906 } else if ("obsolete".equalsIgnoreCase(tokenName)) { 907 // This indicates whether the attribute type should be 908 // considered obsolete. 909 useBuilder.obsolete(true); 910 } else if ("applies".equalsIgnoreCase(tokenName)) { 911 attributes = readOIDs(reader, allowsMalformedNamesAndOptions()); 912 } else if (tokenName.matches("^X-[A-Za-z_-]+$")) { 913 // This must be a non-standard property and it must be 914 // followed by either a single definition in single quotes 915 // or an open parenthesis followed by one or more values in 916 // single quotes separated by spaces followed by a close 917 // parenthesis. 918 useBuilder.extraProperties(tokenName, readExtensions(reader)); 919 } else { 920 throw new LocalizedIllegalArgumentException( 921 ERR_ATTR_SYNTAX_MRUSE_ILLEGAL_TOKEN1.get(definition, tokenName)); 922 } 923 } 924 925 // Make sure that the set of attributes was defined. 926 if (attributes == null || attributes.isEmpty()) { 927 throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_MRUSE_NO_ATTR.get(definition)); 928 } 929 useBuilder.attributes(attributes); 930 931 if (hook != null) { 932 hook.beforeAddMatchingRuleUse(useBuilder); 933 } 934 return useBuilder.addToSchema(overwrite); 935 } catch (final DecodeException e) { 936 final LocalizableMessage msg = ERR_ATTR_SYNTAX_MRUSE_INVALID1.get(definition, e.getMessageObject()); 937 throw new LocalizedIllegalArgumentException(msg, e.getCause()); 938 } 939 } 940 941 /** 942 * Returns a builder which can be used for incrementally constructing a new 943 * attribute type before adding it to the schema. Example usage: 944 * 945 * <pre> 946 * SchemaBuilder builder = ...; 947 * builder.buildAttributeType("attributetype-oid").name("attribute type name").addToSchema(); 948 * </pre> 949 * 950 * @param oid 951 * The OID of the attribute type definition. 952 * @return A builder to continue building the AttributeType. 953 */ 954 public AttributeType.Builder buildAttributeType(final String oid) { 955 lazyInitBuilder(); 956 return new AttributeType.Builder(oid, this); 957 } 958 959 /** 960 * Returns a builder which can be used for incrementally constructing a new 961 * DIT structure rule before adding it to the schema. Example usage: 962 * 963 * <pre> 964 * SchemaBuilder builder = ...; 965 * final int myRuleID = ...; 966 * builder.buildDITStructureRule(myRuleID).name("DIT structure rule name").addToSchema(); 967 * </pre> 968 * 969 * @param ruleID 970 * The ID of the DIT structure rule. 971 * @return A builder to continue building the DITStructureRule. 972 */ 973 public DITStructureRule.Builder buildDITStructureRule(final int ruleID) { 974 lazyInitBuilder(); 975 return new DITStructureRule.Builder(ruleID, this); 976 } 977 978 /** 979 * Returns a builder which can be used for incrementally constructing a new matching rule before adding it to the 980 * schema. Example usage: 981 * 982 * <pre> 983 * SchemaBuilder builder = ...; 984 * builder.buildMatchingRule("matchingrule-oid").name("matching rule name").addToSchema(); 985 * </pre> 986 * 987 * @param oid 988 * The OID of the matching rule definition. 989 * @return A builder to continue building the MatchingRule. 990 */ 991 public MatchingRule.Builder buildMatchingRule(final String oid) { 992 lazyInitBuilder(); 993 return new MatchingRule.Builder(oid, this); 994 } 995 996 /** 997 * Returns a builder which can be used for incrementally constructing a new 998 * matching rule use before adding it to the schema. Example usage: 999 * 1000 * <pre> 1001 * SchemaBuilder builder = ...; 1002 * builder.buildMatchingRuleUse("matchingrule-oid") 1003 * .name("matching rule use name") 1004 * .addToSchema(); 1005 * </pre> 1006 * 1007 * @param oid 1008 * The OID of the matching rule definition. 1009 * @return A builder to continue building the MatchingRuleUse. 1010 */ 1011 public MatchingRuleUse.Builder buildMatchingRuleUse(final String oid) { 1012 lazyInitBuilder(); 1013 return new MatchingRuleUse.Builder(oid, this); 1014 } 1015 1016 /** 1017 * Adds the provided name form definition to this schema builder. 1018 * 1019 * @param definition 1020 * The name form definition. 1021 * @param overwrite 1022 * {@code true} if any existing name form with the same OID 1023 * should be overwritten. 1024 * @return A reference to this schema builder. 1025 * @throws ConflictingSchemaElementException 1026 * If {@code overwrite} was {@code false} and a conflicting 1027 * schema element was found. 1028 * @throws LocalizedIllegalArgumentException 1029 * If the provided name form definition could not be parsed. 1030 * @throws NullPointerException 1031 * If {@code definition} was {@code null}. 1032 */ 1033 public SchemaBuilder addNameForm(final String definition, final boolean overwrite) { 1034 return addNameForm(definition, overwrite, null); 1035 } 1036 1037 SchemaBuilder addNameForm(final String definition, final boolean overwrite, SchemaBuilderHook hook) { 1038 Reject.ifNull(definition); 1039 1040 lazyInitBuilder(); 1041 1042 try { 1043 final SubstringReader reader = new SubstringReader(definition); 1044 1045 // We'll do this a character at a time. First, skip over any 1046 // leading whitespace. 1047 reader.skipWhitespaces(); 1048 1049 if (reader.remaining() <= 0) { 1050 // This means that the value was empty or contained only 1051 // whitespace. That is illegal. 1052 throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_NAME_FORM_EMPTY_VALUE1.get(definition)); 1053 } 1054 1055 // The next character must be an open parenthesis. If it is not, 1056 // then that is an error. 1057 final char c = reader.read(); 1058 if (c != '(') { 1059 final LocalizableMessage message = ERR_ATTR_SYNTAX_NAME_FORM_EXPECTED_OPEN_PARENTHESIS.get( 1060 definition, reader.pos() - 1, c); 1061 throw new LocalizedIllegalArgumentException(message); 1062 } 1063 1064 // Skip over any spaces immediately following the opening 1065 // parenthesis. 1066 reader.skipWhitespaces(); 1067 1068 // The next set of characters must be the OID. 1069 final NameForm.Builder nameFormBuilder = new NameForm.Builder( 1070 readOID(reader, allowsMalformedNamesAndOptions()), this); 1071 nameFormBuilder.definition(definition); 1072 1073 // Required properties : 1074 String structuralOID = null; 1075 Collection<String> requiredAttributes = Collections.emptyList(); 1076 1077 // At this point, we should have a pretty specific syntax that 1078 // describes what may come next, but some of the components are 1079 // optional and it would be pretty easy to put something in the 1080 // wrong order, so we will be very flexible about what we can 1081 // accept. Just look at the next token, figure out what it is and 1082 // how to treat what comes after it, then repeat until we get to 1083 // the end of the value. But before we start, set default values 1084 // for everything else we might need to know. 1085 while (true) { 1086 final String tokenName = readTokenName(reader); 1087 1088 if (tokenName == null) { 1089 // No more tokens. 1090 break; 1091 } else if ("name".equalsIgnoreCase(tokenName)) { 1092 nameFormBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions())); 1093 } else if ("desc".equalsIgnoreCase(tokenName)) { 1094 // This specifies the description for the attribute type. It 1095 // is an arbitrary string of characters enclosed in single 1096 // quotes. 1097 nameFormBuilder.description(readQuotedString(reader)); 1098 } else if ("obsolete".equalsIgnoreCase(tokenName)) { 1099 // This indicates whether the attribute type should be 1100 // considered obsolete. We do not need to do any more 1101 // parsing for this token. 1102 nameFormBuilder.obsolete(true); 1103 } else if ("oc".equalsIgnoreCase(tokenName)) { 1104 structuralOID = readOID(reader, allowsMalformedNamesAndOptions()); 1105 nameFormBuilder.structuralObjectClassOID(structuralOID); 1106 } else if ("must".equalsIgnoreCase(tokenName)) { 1107 requiredAttributes = readOIDs(reader, allowsMalformedNamesAndOptions()); 1108 nameFormBuilder.requiredAttributes(requiredAttributes); 1109 } else if ("may".equalsIgnoreCase(tokenName)) { 1110 nameFormBuilder.optionalAttributes(readOIDs(reader, allowsMalformedNamesAndOptions())); 1111 } else if (tokenName.matches("^X-[A-Za-z_-]+$")) { 1112 // This must be a non-standard property and it must be 1113 // followed by either a single definition in single quotes 1114 // or an open parenthesis followed by one or more values in 1115 // single quotes separated by spaces followed by a close 1116 // parenthesis. 1117 final List<String> extensions = readExtensions(reader); 1118 nameFormBuilder.extraProperties(tokenName, extensions.toArray(new String[extensions.size()])); 1119 } else { 1120 throw new LocalizedIllegalArgumentException( 1121 ERR_ATTR_SYNTAX_NAME_FORM_ILLEGAL_TOKEN1.get(definition, tokenName)); 1122 } 1123 } 1124 1125 // Make sure that a structural class was specified. If not, then 1126 // it cannot be valid and the name form cannot be build. 1127 if (structuralOID == null) { 1128 throw new LocalizedIllegalArgumentException( 1129 ERR_ATTR_SYNTAX_NAME_FORM_NO_STRUCTURAL_CLASS1.get(definition)); 1130 } 1131 1132 if (requiredAttributes.isEmpty()) { 1133 throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_NAME_FORM_NO_REQUIRED_ATTR.get(definition)); 1134 } 1135 1136 if (hook != null) { 1137 hook.beforeAddNameForm(nameFormBuilder); 1138 } 1139 nameFormBuilder.addToSchema(overwrite); 1140 } catch (final DecodeException e) { 1141 final LocalizableMessage msg = 1142 ERR_ATTR_SYNTAX_NAME_FORM_INVALID1.get(definition, e.getMessageObject()); 1143 throw new LocalizedIllegalArgumentException(msg, e.getCause()); 1144 } 1145 return this; 1146 } 1147 1148 /** 1149 * Returns a builder which can be used for incrementally constructing a new 1150 * DIT content rule before adding it to the schema. Example usage: 1151 * 1152 * <pre> 1153 * SchemaBuilder builder = ...; 1154 * builder.buildDITContentRule("structuralobjectclass-oid").name("DIT content rule name").addToSchema(); 1155 * </pre> 1156 * 1157 * @param structuralClassOID 1158 * The OID of the structural objectclass for the DIT content rule to build. 1159 * @return A builder to continue building the DITContentRule. 1160 */ 1161 public Builder buildDITContentRule(String structuralClassOID) { 1162 lazyInitBuilder(); 1163 return new DITContentRule.Builder(structuralClassOID, this); 1164 } 1165 1166 /** 1167 * Returns a builder which can be used for incrementally constructing a new 1168 * name form before adding it to the schema. Example usage: 1169 * 1170 * <pre> 1171 * SchemaBuilder builder = ...; 1172 * builder.buildNameForm("1.2.3.4").name("myNameform").addToSchema(); 1173 * </pre> 1174 * 1175 * @param oid 1176 * The OID of the name form definition. 1177 * @return A builder to continue building the NameForm. 1178 */ 1179 public NameForm.Builder buildNameForm(final String oid) { 1180 lazyInitBuilder(); 1181 return new NameForm.Builder(oid, this); 1182 } 1183 1184 /** 1185 * Returns a builder which can be used for incrementally constructing a new 1186 * object class before adding it to the schema. Example usage: 1187 * 1188 * <pre> 1189 * SchemaBuilder builder = ...; 1190 * builder.buildObjectClass("objectclass-oid").name("object class name").addToSchema(); 1191 * </pre> 1192 * 1193 * @param oid 1194 * The OID of the object class definition. 1195 * @return A builder to continue building the ObjectClass. 1196 */ 1197 public ObjectClass.Builder buildObjectClass(final String oid) { 1198 lazyInitBuilder(); 1199 return new ObjectClass.Builder(oid, this); 1200 } 1201 1202 /** 1203 * Returns a builder which can be used for incrementally constructing a new 1204 * syntax before adding it to the schema. Example usage: 1205 * 1206 * <pre> 1207 * SchemaBuilder builder = ...; 1208 * builder.buildSyntax("1.2.3.4").addToSchema(); 1209 * </pre> 1210 * 1211 * @param oid 1212 * The OID of the syntax definition. 1213 * @return A builder to continue building the syntax. 1214 */ 1215 public Syntax.Builder buildSyntax(final String oid) { 1216 lazyInitBuilder(); 1217 return new Syntax.Builder(oid, this); 1218 } 1219 1220 /** 1221 * Returns an attribute type builder whose fields are initialized to the 1222 * values of the provided attribute type. This method should be used when 1223 * duplicating attribute types from external schemas or when modifying 1224 * existing attribute types. 1225 * 1226 * @param attributeType 1227 * The attribute type source. 1228 * @return A builder to continue building the AttributeType. 1229 */ 1230 public AttributeType.Builder buildAttributeType(final AttributeType attributeType) { 1231 lazyInitBuilder(); 1232 return new AttributeType.Builder(attributeType, this); 1233 } 1234 1235 /** 1236 * Returns a DIT content rule builder whose fields are initialized to the 1237 * values of the provided DIT content rule. This method should be used when 1238 * duplicating DIT content rules from external schemas or when modifying 1239 * existing DIT content rules. 1240 * 1241 * @param contentRule 1242 * The DIT content rule source. 1243 * @return A builder to continue building the DITContentRule. 1244 */ 1245 public DITContentRule.Builder buildDITContentRule(DITContentRule contentRule) { 1246 lazyInitBuilder(); 1247 return new DITContentRule.Builder(contentRule, this); 1248 } 1249 1250 /** 1251 * Returns an DIT structure rule builder whose fields are initialized to the 1252 * values of the provided rule. This method should be used when duplicating 1253 * structure rules from external schemas or when modifying existing 1254 * structure rules. 1255 * 1256 * @param structureRule 1257 * The DIT structure rule source. 1258 * @return A builder to continue building the DITStructureRule. 1259 */ 1260 public DITStructureRule.Builder buildDITStructureRule(final DITStructureRule structureRule) { 1261 lazyInitBuilder(); 1262 return new DITStructureRule.Builder(structureRule, this); 1263 } 1264 1265 /** 1266 * Returns a matching rule builder whose fields are initialized to the 1267 * values of the provided matching rule. This method should be used when 1268 * duplicating matching rules from external schemas or when modifying 1269 * existing matching rules. 1270 * 1271 * @param matchingRule 1272 * The matching rule source. 1273 * @return A builder to continue building the MatchingRule. 1274 */ 1275 public MatchingRule.Builder buildMatchingRule(final MatchingRule matchingRule) { 1276 lazyInitBuilder(); 1277 return new MatchingRule.Builder(matchingRule, this); 1278 } 1279 1280 /** 1281 * Returns a matching rule use builder whose fields are initialized to the 1282 * values of the provided matching rule use object. This method should be used when 1283 * duplicating matching rule uses from external schemas or when modifying 1284 * existing matching rule uses. 1285 * 1286 * @param matchingRuleUse 1287 * The matching rule use source. 1288 * @return A builder to continue building the MatchingRuleUse. 1289 */ 1290 public MatchingRuleUse.Builder buildMatchingRuleUse(final MatchingRuleUse matchingRuleUse) { 1291 lazyInitBuilder(); 1292 return new MatchingRuleUse.Builder(matchingRuleUse, this); 1293 } 1294 1295 /** 1296 * Returns a name form builder whose fields are initialized to the 1297 * values of the provided name form. This method should be used when 1298 * duplicating name forms from external schemas or when modifying 1299 * existing names forms. 1300 * 1301 * @param nameForm 1302 * The name form source. 1303 * @return A builder to continue building the NameForm. 1304 */ 1305 public NameForm.Builder buildNameForm(final NameForm nameForm) { 1306 lazyInitBuilder(); 1307 return new NameForm.Builder(nameForm, this); 1308 } 1309 1310 /** 1311 * Returns an object class builder whose fields are initialized to the 1312 * values of the provided object class. This method should be used when 1313 * duplicating object classes from external schemas or when modifying 1314 * existing object classes. 1315 * 1316 * @param objectClass 1317 * The object class source. 1318 * @return A builder to continue building the ObjectClass. 1319 */ 1320 public ObjectClass.Builder buildObjectClass(final ObjectClass objectClass) { 1321 lazyInitBuilder(); 1322 return new ObjectClass.Builder(objectClass, this); 1323 } 1324 1325 /** 1326 * Returns a syntax builder whose fields are initialized to the 1327 * values of the provided syntax. This method should be used when 1328 * duplicating syntaxes from external schemas or when modifying 1329 * existing syntaxes. 1330 * 1331 * @param syntax 1332 * The syntax source. 1333 * @return A builder to continue building the Syntax. 1334 */ 1335 public Syntax.Builder buildSyntax(final Syntax syntax) { 1336 lazyInitBuilder(); 1337 return new Syntax.Builder(syntax, this); 1338 } 1339 1340 /** 1341 * Adds the provided object class definition to this schema builder. 1342 * 1343 * @param definition 1344 * The object class definition. 1345 * @param overwrite 1346 * {@code true} if any existing object class with the same OID 1347 * should be overwritten. 1348 * @return A reference to this schema builder. 1349 * @throws ConflictingSchemaElementException 1350 * If {@code overwrite} was {@code false} and a conflicting 1351 * schema element was found. 1352 * @throws LocalizedIllegalArgumentException 1353 * If the provided object class definition could not be parsed. 1354 * @throws NullPointerException 1355 * If {@code definition} was {@code null}. 1356 */ 1357 public SchemaBuilder addObjectClass(final String definition, final boolean overwrite) { 1358 return addObjectClass(definition, overwrite, null); 1359 } 1360 1361 SchemaBuilder addObjectClass(final String definition, final boolean overwrite, SchemaBuilderHook hook) { 1362 Reject.ifNull(definition); 1363 1364 lazyInitBuilder(); 1365 1366 try { 1367 final SubstringReader reader = new SubstringReader(definition); 1368 1369 // We'll do this a character at a time. First, skip over any 1370 // leading whitespace. 1371 reader.skipWhitespaces(); 1372 1373 if (reader.remaining() <= 0) { 1374 // This means that the value was empty or contained only 1375 // whitespace. That is illegal. 1376 throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_OBJECTCLASS_EMPTY_VALUE1.get(definition)); 1377 } 1378 1379 // The next character must be an open parenthesis. If it is not, 1380 // then that is an error. 1381 final char c = reader.read(); 1382 if (c != '(') { 1383 final LocalizableMessage message = ERR_ATTR_SYNTAX_OBJECTCLASS_EXPECTED_OPEN_PARENTHESIS1.get( 1384 definition, reader.pos() - 1, String.valueOf(c)); 1385 throw new LocalizedIllegalArgumentException(message); 1386 } 1387 1388 // Skip over any spaces immediately following the opening 1389 // parenthesis. 1390 reader.skipWhitespaces(); 1391 1392 // The next set of characters is the OID. 1393 final String oid = readOID(reader, allowsMalformedNamesAndOptions()); 1394 Set<String> superiorClasses = emptySet(); 1395 ObjectClassType ocType = null; 1396 ObjectClass.Builder ocBuilder = new ObjectClass.Builder(oid, this).definition(definition); 1397 1398 // At this point, we should have a pretty specific syntax that 1399 // describes what may come next, but some of the components are 1400 // optional and it would be pretty easy to put something in the 1401 // wrong order, so we will be very flexible about what we can 1402 // accept. Just look at the next token, figure out what it is and 1403 // how to treat what comes after it, then repeat until we get to 1404 // the end of the value. But before we start, set default values 1405 // for everything else we might need to know. 1406 while (true) { 1407 final String tokenName = readTokenName(reader); 1408 1409 if (tokenName == null) { 1410 // No more tokens. 1411 break; 1412 } else if ("name".equalsIgnoreCase(tokenName)) { 1413 ocBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions())); 1414 } else if ("desc".equalsIgnoreCase(tokenName)) { 1415 // This specifies the description for the attribute type. It 1416 // is an arbitrary string of characters enclosed in single 1417 // quotes. 1418 ocBuilder.description(readQuotedString(reader)); 1419 } else if ("obsolete".equalsIgnoreCase(tokenName)) { 1420 // This indicates whether the attribute type should be 1421 // considered obsolete. 1422 ocBuilder.obsolete(true); 1423 } else if ("sup".equalsIgnoreCase(tokenName)) { 1424 superiorClasses = readOIDs(reader, allowsMalformedNamesAndOptions()); 1425 } else if ("abstract".equalsIgnoreCase(tokenName)) { 1426 // This indicates that entries must not include this 1427 // objectclass unless they also include a non-abstract 1428 // objectclass that inherits from this class. 1429 ocType = ABSTRACT; 1430 } else if ("structural".equalsIgnoreCase(tokenName)) { 1431 ocType = STRUCTURAL; 1432 } else if ("auxiliary".equalsIgnoreCase(tokenName)) { 1433 ocType = AUXILIARY; 1434 } else if ("must".equalsIgnoreCase(tokenName)) { 1435 ocBuilder.requiredAttributes(readOIDs(reader, allowsMalformedNamesAndOptions())); 1436 } else if ("may".equalsIgnoreCase(tokenName)) { 1437 ocBuilder.optionalAttributes(readOIDs(reader, allowsMalformedNamesAndOptions())); 1438 } else if (tokenName.matches("^X-[A-Za-z_-]+$")) { 1439 // This must be a non-standard property and it must be 1440 // followed by either a single definition in single quotes 1441 // or an open parenthesis followed by one or more values in 1442 // single quotes separated by spaces followed by a close 1443 // parenthesis. 1444 final List<String> extensions = readExtensions(reader); 1445 ocBuilder.extraProperties(tokenName, extensions.toArray(new String[extensions.size()])); 1446 } else { 1447 throw new LocalizedIllegalArgumentException( 1448 ERR_ATTR_SYNTAX_OBJECTCLASS_ILLEGAL_TOKEN1.get(definition, tokenName)); 1449 } 1450 } 1451 if (hook != null) { 1452 hook.beforeAddObjectClass(ocBuilder); 1453 } 1454 1455 if (EXTENSIBLE_OBJECT_OBJECTCLASS_OID.equals(oid)) { 1456 addObjectClass(newExtensibleObjectObjectClass( 1457 ocBuilder.getDescription(), ocBuilder.getExtraProperties(), this), overwrite); 1458 return this; 1459 } 1460 1461 ocType = ocType != null ? ocType : STRUCTURAL; 1462 ocBuilder.superiorObjectClasses(superiorClasses) 1463 .type(ocType); 1464 return ocBuilder.addToSchema(overwrite); 1465 } catch (final DecodeException e) { 1466 throw new LocalizedIllegalArgumentException( 1467 ERR_ATTR_SYNTAX_OBJECTCLASS_INVALID1.get(definition, e.getMessageObject()), e.getCause()); 1468 } 1469 } 1470 1471 /** 1472 * Adds the provided pattern syntax definition to this schema builder. 1473 * 1474 * @param oid 1475 * The OID of the pattern syntax definition. 1476 * @param description 1477 * The description of the pattern syntax definition. 1478 * @param pattern 1479 * The regular expression pattern which attribute values must 1480 * match in order to be valid. 1481 * @param overwrite 1482 * {@code true} if any existing syntax with the same OID should 1483 * be overwritten. 1484 * @return A reference to this schema builder. 1485 * @throws ConflictingSchemaElementException 1486 * If {@code overwrite} was {@code false} and a conflicting 1487 * schema element was found. 1488 */ 1489 public SchemaBuilder addPatternSyntax(final String oid, final String description, 1490 final Pattern pattern, final boolean overwrite) { 1491 Reject.ifNull(pattern); 1492 return buildSyntax(oid) 1493 .description(description) 1494 .extraProperties("X-PATTERN", pattern.toString()) 1495 .addToSchema(overwrite); 1496 } 1497 1498 /** 1499 * Reads the schema elements contained in the named subschema sub-entry and 1500 * adds them to this schema builder. 1501 * <p> 1502 * If the requested schema is not returned by the Directory Server then the 1503 * request will fail with an {@link EntryNotFoundException}. 1504 * 1505 * @param connection 1506 * A connection to the Directory Server whose schema is to be 1507 * read. 1508 * @param name 1509 * The distinguished name of the subschema sub-entry. 1510 * @param overwrite 1511 * {@code true} if existing schema elements with the same 1512 * conflicting OIDs should be overwritten. 1513 * @return A reference to this schema builder. 1514 * @throws LdapException 1515 * If the result code indicates that the request failed for some 1516 * reason. 1517 * @throws UnsupportedOperationException 1518 * If the connection does not support search operations. 1519 * @throws IllegalStateException 1520 * If the connection has already been closed, i.e. if 1521 * {@code isClosed() == true}. 1522 * @throws NullPointerException 1523 * If the {@code connection} or {@code name} was {@code null}. 1524 */ 1525 public SchemaBuilder addSchema(final Connection connection, final DN name, 1526 final boolean overwrite) throws LdapException { 1527 // The call to addSchema will perform copyOnWrite. 1528 final SearchRequest request = getReadSchemaSearchRequest(name); 1529 final Entry entry = connection.searchSingleEntry(request); 1530 return addSchema(entry, overwrite, null); 1531 } 1532 1533 /** 1534 * Adds all of the schema elements contained in the provided subschema 1535 * subentry to this schema builder. Any problems encountered while parsing 1536 * the entry can be retrieved using the returned schema's 1537 * {@link Schema#getWarnings()} method. 1538 * 1539 * @param entry 1540 * The subschema subentry to be parsed. 1541 * @param overwrite 1542 * {@code true} if existing schema elements with the same 1543 * conflicting OIDs should be overwritten. 1544 * @return A reference to this schema builder. 1545 * @throws NullPointerException 1546 * If {@code entry} was {@code null}. 1547 */ 1548 public SchemaBuilder addSchema(final Entry entry, final boolean overwrite) { 1549 return addSchema(entry, overwrite, null); 1550 } 1551 1552 /** 1553 * Adds all of the schema elements contained in the provided subschema 1554 * subentry to this schema builder. Any problems encountered while parsing 1555 * the entry can be retrieved using the returned schema's 1556 * {@link Schema#getWarnings()} method. 1557 * 1558 * @param entry 1559 * The subschema subentry to be parsed. 1560 * @param overwrite 1561 * {@code true} if existing schema elements with the same 1562 * conflicting OIDs should be overwritten. 1563 * @param hook 1564 * Allows to perform modifications on element's builders before adding the result to this schema builder. 1565 * @return A reference to this schema builder. 1566 * @throws NullPointerException 1567 * If {@code entry} was {@code null}. 1568 */ 1569 public SchemaBuilder addSchema(final Entry entry, final boolean overwrite, SchemaBuilderHook hook) { 1570 Reject.ifNull(entry); 1571 1572 lazyInitBuilder(); 1573 1574 Attribute attr = entry.getAttribute(Schema.ATTR_LDAP_SYNTAXES); 1575 if (attr != null) { 1576 for (final ByteString def : attr) { 1577 try { 1578 addSyntax(def.toString(), overwrite, hook); 1579 } catch (final LocalizedIllegalArgumentException e) { 1580 warnings.add(e.getMessageObject()); 1581 } 1582 } 1583 } 1584 1585 attr = entry.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES); 1586 if (attr != null) { 1587 for (final ByteString def : attr) { 1588 try { 1589 addAttributeType(def.toString(), overwrite, hook); 1590 } catch (final LocalizedIllegalArgumentException e) { 1591 warnings.add(e.getMessageObject()); 1592 } 1593 } 1594 } 1595 1596 attr = entry.getAttribute(Schema.ATTR_OBJECT_CLASSES); 1597 if (attr != null) { 1598 for (final ByteString def : attr) { 1599 try { 1600 addObjectClass(def.toString(), overwrite, hook); 1601 } catch (final LocalizedIllegalArgumentException e) { 1602 warnings.add(e.getMessageObject()); 1603 } 1604 } 1605 } 1606 1607 attr = entry.getAttribute(Schema.ATTR_MATCHING_RULE_USE); 1608 if (attr != null) { 1609 for (final ByteString def : attr) { 1610 try { 1611 addMatchingRuleUse(def.toString(), overwrite, hook); 1612 } catch (final LocalizedIllegalArgumentException e) { 1613 warnings.add(e.getMessageObject()); 1614 } 1615 } 1616 } 1617 1618 attr = entry.getAttribute(Schema.ATTR_MATCHING_RULES); 1619 if (attr != null) { 1620 for (final ByteString def : attr) { 1621 try { 1622 addMatchingRule(def.toString(), overwrite, hook); 1623 } catch (final LocalizedIllegalArgumentException e) { 1624 warnings.add(e.getMessageObject()); 1625 } 1626 } 1627 } 1628 1629 attr = entry.getAttribute(Schema.ATTR_DIT_CONTENT_RULES); 1630 if (attr != null) { 1631 for (final ByteString def : attr) { 1632 try { 1633 addDITContentRule(def.toString(), overwrite, hook); 1634 } catch (final LocalizedIllegalArgumentException e) { 1635 warnings.add(e.getMessageObject()); 1636 } 1637 } 1638 } 1639 1640 attr = entry.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES); 1641 if (attr != null) { 1642 for (final ByteString def : attr) { 1643 try { 1644 addDITStructureRule(def.toString(), overwrite, hook); 1645 } catch (final LocalizedIllegalArgumentException e) { 1646 warnings.add(e.getMessageObject()); 1647 } 1648 } 1649 } 1650 1651 attr = entry.getAttribute(Schema.ATTR_NAME_FORMS); 1652 if (attr != null) { 1653 for (final ByteString def : attr) { 1654 try { 1655 addNameForm(def.toString(), overwrite, hook); 1656 } catch (final LocalizedIllegalArgumentException e) { 1657 warnings.add(e.getMessageObject()); 1658 } 1659 } 1660 } 1661 1662 return this; 1663 } 1664 1665 /** 1666 * Adds all of the schema elements in the provided schema to this schema 1667 * builder. 1668 * 1669 * @param schema 1670 * The schema to be copied into this schema builder. 1671 * @param overwrite 1672 * {@code true} if existing schema elements with the same 1673 * conflicting OIDs should be overwritten. 1674 * @return A reference to this schema builder. 1675 * @throws ConflictingSchemaElementException 1676 * If {@code overwrite} was {@code false} and conflicting schema 1677 * elements were found. 1678 * @throws NullPointerException 1679 * If {@code schema} was {@code null}. 1680 */ 1681 public SchemaBuilder addSchema(final Schema schema, final boolean overwrite) { 1682 Reject.ifNull(schema); 1683 1684 lazyInitBuilder(); 1685 1686 addSchema0(schema, overwrite); 1687 return this; 1688 } 1689 1690 /** 1691 * Asynchronously reads the schema elements contained in the named subschema 1692 * sub-entry and adds them to this schema builder. 1693 * <p> 1694 * If the requested schema is not returned by the Directory Server then the 1695 * request will fail with an {@link EntryNotFoundException}. 1696 * 1697 * @param connection 1698 * A connection to the Directory Server whose schema is to be 1699 * read. 1700 * @param name 1701 * The distinguished name of the subschema sub-entry. 1702 * the operation result when it is received, may be {@code null}. 1703 * @param overwrite 1704 * {@code true} if existing schema elements with the same 1705 * conflicting OIDs should be overwritten. 1706 * @return A promise representing the updated schema builder. 1707 * @throws UnsupportedOperationException 1708 * If the connection does not support search operations. 1709 * @throws IllegalStateException 1710 * If the connection has already been closed, i.e. if 1711 * {@code connection.isClosed() == true}. 1712 * @throws NullPointerException 1713 * If the {@code connection} or {@code name} was {@code null}. 1714 */ 1715 public LdapPromise<SchemaBuilder> addSchemaAsync(final Connection connection, final DN name, 1716 final boolean overwrite) { 1717 // The call to addSchema will perform copyOnWrite. 1718 return connection.searchSingleEntryAsync(getReadSchemaSearchRequest(name)).then( 1719 new Function<SearchResultEntry, SchemaBuilder, LdapException>() { 1720 @Override 1721 public SchemaBuilder apply(SearchResultEntry result) throws LdapException { 1722 addSchema(result, overwrite, null); 1723 return SchemaBuilder.this; 1724 } 1725 }); 1726 } 1727 1728 /** 1729 * Reads the schema elements contained in the subschema sub-entry which 1730 * applies to the named entry and adds them to this schema builder. 1731 * <p> 1732 * If the requested entry or its associated schema are not returned by the 1733 * Directory Server then the request will fail with an 1734 * {@link EntryNotFoundException}. 1735 * <p> 1736 * This implementation first reads the {@code subschemaSubentry} attribute 1737 * of the entry in order to identify the schema and then invokes 1738 * {@link #addSchemaForEntry(Connection, DN, boolean)} to read the schema. 1739 * 1740 * @param connection 1741 * A connection to the Directory Server whose schema is to be 1742 * read. 1743 * @param name 1744 * The distinguished name of the entry whose schema is to be 1745 * located. 1746 * @param overwrite 1747 * {@code true} if existing schema elements with the same 1748 * conflicting OIDs should be overwritten. 1749 * @return A reference to this schema builder. 1750 * @throws LdapException 1751 * If the result code indicates that the request failed for some 1752 * reason. 1753 * @throws UnsupportedOperationException 1754 * If the connection does not support search operations. 1755 * @throws IllegalStateException 1756 * If the connection has already been closed, i.e. if 1757 * {@code connection.isClosed() == true}. 1758 * @throws NullPointerException 1759 * If the {@code connection} or {@code name} was {@code null}. 1760 */ 1761 public SchemaBuilder addSchemaForEntry(final Connection connection, final DN name, 1762 final boolean overwrite) throws LdapException { 1763 // The call to addSchema will perform copyOnWrite. 1764 final SearchRequest request = getReadSchemaForEntrySearchRequest(name); 1765 final Entry entry = connection.searchSingleEntry(request); 1766 final DN subschemaDN = getSubschemaSubentryDN(name, entry); 1767 return addSchema(connection, subschemaDN, overwrite); 1768 } 1769 1770 /** 1771 * Asynchronously reads the schema elements contained in the subschema 1772 * sub-entry which applies to the named entry and adds them to this schema 1773 * builder. 1774 * <p> 1775 * If the requested entry or its associated schema are not returned by the 1776 * Directory Server then the request will fail with an 1777 * {@link EntryNotFoundException}. 1778 * <p> 1779 * This implementation first reads the {@code subschemaSubentry} attribute 1780 * of the entry in order to identify the schema and then invokes 1781 * {@link #addSchemaAsync(Connection, DN, boolean)} to read the schema. 1782 * 1783 * @param connection 1784 * A connection to the Directory Server whose schema is to be 1785 * read. 1786 * @param name 1787 * The distinguished name of the entry whose schema is to be 1788 * located. 1789 * @param overwrite 1790 * {@code true} if existing schema elements with the same 1791 * conflicting OIDs should be overwritten. 1792 * @return A promise representing the updated schema builder. 1793 * @throws UnsupportedOperationException 1794 * If the connection does not support search operations. 1795 * @throws IllegalStateException 1796 * If the connection has already been closed, i.e. if 1797 * {@code connection.isClosed() == true}. 1798 * @throws NullPointerException 1799 * If the {@code connection} or {@code name} was {@code null}. 1800 */ 1801 public LdapPromise<SchemaBuilder> addSchemaForEntryAsync(final Connection connection, final DN name, 1802 final boolean overwrite) { 1803 return connection.searchSingleEntryAsync(getReadSchemaForEntrySearchRequest(name)).thenAsync( 1804 new AsyncFunction<SearchResultEntry, SchemaBuilder, LdapException>() { 1805 @Override 1806 public Promise<SchemaBuilder, LdapException> apply(SearchResultEntry result) throws LdapException { 1807 final DN subschemaDN = getSubschemaSubentryDN(name, result); 1808 return addSchemaAsync(connection, subschemaDN, overwrite); 1809 } 1810 }); 1811 } 1812 1813 /** 1814 * Adds the provided substitution syntax definition to this schema builder. 1815 * 1816 * @param oid 1817 * The OID of the substitution syntax definition. 1818 * @param description 1819 * The description of the substitution syntax definition. 1820 * @param substituteSyntax 1821 * The OID of the syntax whose implementation should be 1822 * substituted. 1823 * @param overwrite 1824 * {@code true} if any existing syntax with the same OID should 1825 * be overwritten. 1826 * @return A reference to this schema builder. 1827 * @throws ConflictingSchemaElementException 1828 * If {@code overwrite} was {@code false} and a conflicting 1829 * schema element was found. 1830 */ 1831 public SchemaBuilder addSubstitutionSyntax(final String oid, final String description, 1832 final String substituteSyntax, final boolean overwrite) { 1833 Reject.ifNull(substituteSyntax); 1834 return buildSyntax(oid) 1835 .description(description) 1836 .extraProperties("X-SUBST", substituteSyntax) 1837 .addToSchema(overwrite); 1838 } 1839 1840 /** 1841 * Adds the provided syntax definition to this schema builder. 1842 * 1843 * @param definition 1844 * The syntax definition. 1845 * @param overwrite 1846 * {@code true} if any existing syntax with the same OID should 1847 * be overwritten. 1848 * @return A reference to this schema builder. 1849 * @throws ConflictingSchemaElementException 1850 * If {@code overwrite} was {@code false} and a conflicting 1851 * schema element was found. 1852 * @throws LocalizedIllegalArgumentException 1853 * If the provided syntax definition could not be parsed. 1854 * @throws NullPointerException 1855 * If {@code definition} was {@code null}. 1856 */ 1857 public SchemaBuilder addSyntax(final String definition, final boolean overwrite) { 1858 return addSyntax(definition, overwrite, null); 1859 } 1860 1861 SchemaBuilder addSyntax(final String definition, final boolean overwrite, SchemaBuilderHook hook) { 1862 Reject.ifNull(definition); 1863 1864 lazyInitBuilder(); 1865 1866 try { 1867 final SubstringReader reader = new SubstringReader(definition); 1868 1869 // We'll do this a character at a time. First, skip over any 1870 // leading whitespace. 1871 reader.skipWhitespaces(); 1872 1873 if (reader.remaining() <= 0) { 1874 // This means that the value was empty or contained only 1875 // whitespace. That is illegal. 1876 throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_ATTRSYNTAX_EMPTY_VALUE1.get(definition)); 1877 } 1878 1879 // The next character must be an open parenthesis. If it is not, 1880 // then that is an error. 1881 final char c = reader.read(); 1882 if (c != '(') { 1883 final LocalizableMessage message = ERR_ATTR_SYNTAX_ATTRSYNTAX_EXPECTED_OPEN_PARENTHESIS.get( 1884 definition, reader.pos() - 1, String.valueOf(c)); 1885 throw new LocalizedIllegalArgumentException(message); 1886 } 1887 1888 // Skip over any spaces immediately following the opening 1889 // parenthesis. 1890 reader.skipWhitespaces(); 1891 1892 // The next set of characters must be the OID. 1893 final String oid = readOID(reader, allowsMalformedNamesAndOptions()); 1894 final Syntax.Builder syntaxBuilder = new Syntax.Builder(oid, this).definition(definition); 1895 1896 // At this point, we should have a pretty specific syntax that 1897 // describes what may come next, but some of the components are 1898 // optional and it would be pretty easy to put something in the 1899 // wrong order, so we will be very flexible about what we can 1900 // accept. Just look at the next token, figure out what it is and 1901 // how to treat what comes after it, then repeat until we get to 1902 // the end of the value. But before we start, set default values 1903 // for everything else we might need to know. 1904 while (true) { 1905 final String tokenName = readTokenName(reader); 1906 1907 if (tokenName == null) { 1908 // No more tokens. 1909 break; 1910 } else if ("desc".equalsIgnoreCase(tokenName)) { 1911 // This specifies the description for the syntax. It is an 1912 // arbitrary string of characters enclosed in single quotes. 1913 syntaxBuilder.description(readQuotedString(reader)); 1914 } else if (tokenName.matches("^X-[A-Za-z_-]+$")) { 1915 // This must be a non-standard property and it must be 1916 // followed by either a single definition in single quotes 1917 // or an open parenthesis followed by one or more values in 1918 // single quotes separated by spaces followed by a close 1919 // parenthesis. 1920 final List<String> extensions = readExtensions(reader); 1921 syntaxBuilder.extraProperties(tokenName, extensions.toArray(new String[extensions.size()])); 1922 } else { 1923 throw new LocalizedIllegalArgumentException( 1924 ERR_ATTR_SYNTAX_ATTRSYNTAX_ILLEGAL_TOKEN1.get(definition, tokenName)); 1925 } 1926 } 1927 1928 if (hook != null) { 1929 hook.beforeAddSyntax(syntaxBuilder); 1930 } 1931 syntaxBuilder.addToSchema(overwrite); 1932 } catch (final DecodeException e) { 1933 final LocalizableMessage msg = 1934 ERR_ATTR_SYNTAX_ATTRSYNTAX_INVALID1.get(definition, e.getMessageObject()); 1935 throw new LocalizedIllegalArgumentException(msg, e.getCause()); 1936 } 1937 return this; 1938 } 1939 1940 Options getOptions() { 1941 lazyInitBuilder(); 1942 1943 return options; 1944 } 1945 1946 /** 1947 * Removes the named attribute type from this schema builder. 1948 * 1949 * @param nameOrOid 1950 * The name or OID of the attribute type to be removed. 1951 * @return {@code true} if the attribute type was found. 1952 */ 1953 public boolean removeAttributeType(final String nameOrOid) { 1954 lazyInitBuilder(); 1955 1956 final AttributeType element = numericOID2AttributeTypes.get(nameOrOid); 1957 if (element != null) { 1958 removeAttributeType(element); 1959 return true; 1960 } 1961 final List<AttributeType> elements = name2AttributeTypes.get(toLowerCase(nameOrOid)); 1962 if (elements != null) { 1963 for (final AttributeType e : elements) { 1964 removeAttributeType(e); 1965 } 1966 return true; 1967 } 1968 return false; 1969 } 1970 1971 /** 1972 * Removes the named DIT content rule from this schema builder. 1973 * 1974 * @param nameOrOid 1975 * The name or OID of the DIT content rule to be removed. 1976 * @return {@code true} if the DIT content rule was found. 1977 */ 1978 public boolean removeDITContentRule(final String nameOrOid) { 1979 lazyInitBuilder(); 1980 1981 final DITContentRule element = numericOID2ContentRules.get(nameOrOid); 1982 if (element != null) { 1983 removeDITContentRule(element); 1984 return true; 1985 } 1986 final List<DITContentRule> elements = name2ContentRules.get(toLowerCase(nameOrOid)); 1987 if (elements != null) { 1988 for (final DITContentRule e : elements) { 1989 removeDITContentRule(e); 1990 } 1991 return true; 1992 } 1993 return false; 1994 } 1995 1996 /** 1997 * Removes the specified DIT structure rule from this schema builder. 1998 * 1999 * @param ruleID 2000 * The ID of the DIT structure rule to be removed. 2001 * @return {@code true} if the DIT structure rule was found. 2002 */ 2003 public boolean removeDITStructureRule(final int ruleID) { 2004 lazyInitBuilder(); 2005 2006 final DITStructureRule element = id2StructureRules.get(ruleID); 2007 if (element != null) { 2008 removeDITStructureRule(element); 2009 return true; 2010 } 2011 return false; 2012 } 2013 2014 /** 2015 * Removes the named matching rule from this schema builder. 2016 * 2017 * @param nameOrOid 2018 * The name or OID of the matching rule to be removed. 2019 * @return {@code true} if the matching rule was found. 2020 */ 2021 public boolean removeMatchingRule(final String nameOrOid) { 2022 lazyInitBuilder(); 2023 2024 final MatchingRule element = numericOID2MatchingRules.get(nameOrOid); 2025 if (element != null) { 2026 removeMatchingRule(element); 2027 return true; 2028 } 2029 final List<MatchingRule> elements = name2MatchingRules.get(toLowerCase(nameOrOid)); 2030 if (elements != null) { 2031 for (final MatchingRule e : elements) { 2032 removeMatchingRule(e); 2033 } 2034 return true; 2035 } 2036 return false; 2037 } 2038 2039 /** 2040 * Removes the named matching rule use from this schema builder. 2041 * 2042 * @param nameOrOid 2043 * The name or OID of the matching rule use to be removed. 2044 * @return {@code true} if the matching rule use was found. 2045 */ 2046 public boolean removeMatchingRuleUse(final String nameOrOid) { 2047 lazyInitBuilder(); 2048 2049 final MatchingRuleUse element = numericOID2MatchingRuleUses.get(nameOrOid); 2050 if (element != null) { 2051 removeMatchingRuleUse(element); 2052 return true; 2053 } 2054 final List<MatchingRuleUse> elements = name2MatchingRuleUses.get(toLowerCase(nameOrOid)); 2055 if (elements != null) { 2056 for (final MatchingRuleUse e : elements) { 2057 removeMatchingRuleUse(e); 2058 } 2059 return true; 2060 } 2061 return false; 2062 } 2063 2064 /** 2065 * Removes the named name form from this schema builder. 2066 * 2067 * @param nameOrOid 2068 * The name or OID of the name form to be removed. 2069 * @return {@code true} if the name form was found. 2070 */ 2071 public boolean removeNameForm(final String nameOrOid) { 2072 lazyInitBuilder(); 2073 2074 final NameForm element = numericOID2NameForms.get(nameOrOid); 2075 if (element != null) { 2076 removeNameForm(element); 2077 return true; 2078 } 2079 final List<NameForm> elements = name2NameForms.get(toLowerCase(nameOrOid)); 2080 if (elements != null) { 2081 for (final NameForm e : elements) { 2082 removeNameForm(e); 2083 } 2084 return true; 2085 } 2086 return false; 2087 } 2088 2089 /** 2090 * Removes the named object class from this schema builder. 2091 * 2092 * @param nameOrOid 2093 * The name or OID of the object class to be removed. 2094 * @return {@code true} if the object class was found. 2095 */ 2096 public boolean removeObjectClass(final String nameOrOid) { 2097 lazyInitBuilder(); 2098 2099 final ObjectClass element = numericOID2ObjectClasses.get(nameOrOid); 2100 if (element != null) { 2101 removeObjectClass(element); 2102 return true; 2103 } 2104 final List<ObjectClass> elements = name2ObjectClasses.get(toLowerCase(nameOrOid)); 2105 if (elements != null) { 2106 for (final ObjectClass e : elements) { 2107 removeObjectClass(e); 2108 } 2109 return true; 2110 } 2111 return false; 2112 } 2113 2114 /** 2115 * Removes the named syntax from this schema builder. 2116 * 2117 * @param numericOID 2118 * The name of the syntax to be removed. 2119 * @return {@code true} if the syntax was found. 2120 */ 2121 public boolean removeSyntax(final String numericOID) { 2122 lazyInitBuilder(); 2123 2124 final Syntax element = numericOID2Syntaxes.get(numericOID); 2125 if (element != null) { 2126 removeSyntax(element); 2127 return true; 2128 } 2129 return false; 2130 } 2131 2132 /** 2133 * Sets a schema option overriding any previous values for the option. 2134 * 2135 * @param <T> 2136 * The option type. 2137 * @param option 2138 * Option with which the specified value is to be associated. 2139 * @param value 2140 * Value to be associated with the specified option. 2141 * @return A reference to this schema builder. 2142 * @throws UnsupportedOperationException 2143 * If the schema builder options are read only. 2144 */ 2145 public <T> SchemaBuilder setOption(final Option<T> option, T value) { 2146 getOptions().set(option, value); 2147 return this; 2148 } 2149 2150 /** 2151 * Returns a strict {@code Schema} containing all of the schema elements 2152 * contained in this schema builder as well as the same set of schema 2153 * compatibility options. 2154 * <p> 2155 * This method does not alter the contents of this schema builder. 2156 * 2157 * @return A {@code Schema} containing all of the schema elements contained 2158 * in this schema builder as well as the same set of schema 2159 * compatibility options 2160 */ 2161 public Schema toSchema() { 2162 // If this schema builder was initialized from another schema and no 2163 // modifications have been made since then we can simply return the 2164 // original schema. 2165 if (copyOnWriteSchema != null) { 2166 return copyOnWriteSchema; 2167 } 2168 2169 // We still need to ensure that this builder has been initialized 2170 // (otherwise some fields may still be null). 2171 lazyInitBuilder(); 2172 2173 final String localSchemaName; 2174 if (schemaName != null) { 2175 localSchemaName = schemaName + "-" + NEXT_SCHEMA_ID.getAndIncrement(); 2176 } else { 2177 localSchemaName = "Schema#" + NEXT_SCHEMA_ID.getAndIncrement(); 2178 } 2179 2180 Syntax defaultSyntax = numericOID2Syntaxes.get(options.get(DEFAULT_SYNTAX_OID)); 2181 if (defaultSyntax == null) { 2182 defaultSyntax = Schema.getCoreSchema().getDefaultSyntax(); 2183 } 2184 2185 MatchingRule defaultMatchingRule = numericOID2MatchingRules.get(options.get(DEFAULT_MATCHING_RULE_OID)); 2186 if (defaultMatchingRule == null) { 2187 defaultMatchingRule = Schema.getCoreSchema().getDefaultMatchingRule(); 2188 } 2189 2190 final Schema schema = 2191 new Schema.StrictImpl(localSchemaName, options, 2192 defaultSyntax, defaultMatchingRule, numericOID2Syntaxes, 2193 numericOID2MatchingRules, numericOID2MatchingRuleUses, 2194 numericOID2AttributeTypes, numericOID2ObjectClasses, numericOID2NameForms, 2195 numericOID2ContentRules, id2StructureRules, name2MatchingRules, 2196 name2MatchingRuleUses, name2AttributeTypes, name2ObjectClasses, 2197 name2NameForms, name2ContentRules, name2StructureRules, 2198 objectClass2NameForms, nameForm2StructureRules, warnings).asStrictSchema(); 2199 validate(schema); 2200 2201 // Re-init this builder so that it can continue to be used afterwards. 2202 preLazyInitBuilder(schemaName, schema); 2203 2204 return schema; 2205 } 2206 2207 SchemaBuilder addAttributeType(final AttributeType attribute, final boolean overwrite) { 2208 AttributeType conflictingAttribute; 2209 if (numericOID2AttributeTypes.containsKey(attribute.getOID())) { 2210 conflictingAttribute = numericOID2AttributeTypes.get(attribute.getOID()); 2211 if (!overwrite) { 2212 final LocalizableMessage message = 2213 ERR_SCHEMA_CONFLICTING_ATTRIBUTE_OID.get(attribute.getNameOrOID(), 2214 attribute.getOID(), conflictingAttribute.getNameOrOID()); 2215 throw new ConflictingSchemaElementException(message); 2216 } 2217 removeAttributeType(conflictingAttribute); 2218 } 2219 2220 numericOID2AttributeTypes.put(attribute.getOID(), attribute); 2221 for (final String name : attribute.getNames()) { 2222 final String lowerName = StaticUtils.toLowerCase(name); 2223 List<AttributeType> attrs = name2AttributeTypes.get(lowerName); 2224 if (attrs == null) { 2225 name2AttributeTypes.put(lowerName, Collections.singletonList(attribute)); 2226 } else if (attrs.size() == 1) { 2227 attrs = new ArrayList<>(attrs); 2228 attrs.add(attribute); 2229 name2AttributeTypes.put(lowerName, attrs); 2230 } else { 2231 attrs.add(attribute); 2232 } 2233 } 2234 2235 return this; 2236 } 2237 2238 SchemaBuilder addDITContentRule(final DITContentRule rule, final boolean overwrite) { 2239 DITContentRule conflictingRule; 2240 if (numericOID2ContentRules.containsKey(rule.getStructuralClassOID())) { 2241 conflictingRule = numericOID2ContentRules.get(rule.getStructuralClassOID()); 2242 if (!overwrite) { 2243 final LocalizableMessage message = 2244 ERR_SCHEMA_CONFLICTING_DIT_CONTENT_RULE1.get(rule.getNameOrOID(), rule 2245 .getStructuralClassOID(), conflictingRule.getNameOrOID()); 2246 throw new ConflictingSchemaElementException(message); 2247 } 2248 removeDITContentRule(conflictingRule); 2249 } 2250 2251 numericOID2ContentRules.put(rule.getStructuralClassOID(), rule); 2252 for (final String name : rule.getNames()) { 2253 final String lowerName = StaticUtils.toLowerCase(name); 2254 List<DITContentRule> rules = name2ContentRules.get(lowerName); 2255 if (rules == null) { 2256 name2ContentRules.put(lowerName, Collections.singletonList(rule)); 2257 } else if (rules.size() == 1) { 2258 rules = new ArrayList<>(rules); 2259 rules.add(rule); 2260 name2ContentRules.put(lowerName, rules); 2261 } else { 2262 rules.add(rule); 2263 } 2264 } 2265 2266 return this; 2267 } 2268 2269 SchemaBuilder addDITStructureRule(final DITStructureRule rule, final boolean overwrite) { 2270 DITStructureRule conflictingRule; 2271 if (id2StructureRules.containsKey(rule.getRuleID())) { 2272 conflictingRule = id2StructureRules.get(rule.getRuleID()); 2273 if (!overwrite) { 2274 final LocalizableMessage message = 2275 ERR_SCHEMA_CONFLICTING_DIT_STRUCTURE_RULE_ID.get(rule.getNameOrRuleID(), 2276 rule.getRuleID(), conflictingRule.getNameOrRuleID()); 2277 throw new ConflictingSchemaElementException(message); 2278 } 2279 removeDITStructureRule(conflictingRule); 2280 } 2281 2282 id2StructureRules.put(rule.getRuleID(), rule); 2283 for (final String name : rule.getNames()) { 2284 final String lowerName = StaticUtils.toLowerCase(name); 2285 List<DITStructureRule> rules = name2StructureRules.get(lowerName); 2286 if (rules == null) { 2287 name2StructureRules.put(lowerName, Collections.singletonList(rule)); 2288 } else if (rules.size() == 1) { 2289 rules = new ArrayList<>(rules); 2290 rules.add(rule); 2291 name2StructureRules.put(lowerName, rules); 2292 } else { 2293 rules.add(rule); 2294 } 2295 } 2296 2297 return this; 2298 } 2299 2300 SchemaBuilder addMatchingRuleUse(final MatchingRuleUse use, final boolean overwrite) { 2301 MatchingRuleUse conflictingUse; 2302 if (numericOID2MatchingRuleUses.containsKey(use.getMatchingRuleOID())) { 2303 conflictingUse = numericOID2MatchingRuleUses.get(use.getMatchingRuleOID()); 2304 if (!overwrite) { 2305 final LocalizableMessage message = 2306 ERR_SCHEMA_CONFLICTING_MATCHING_RULE_USE.get(use.getNameOrOID(), use 2307 .getMatchingRuleOID(), conflictingUse.getNameOrOID()); 2308 throw new ConflictingSchemaElementException(message); 2309 } 2310 removeMatchingRuleUse(conflictingUse); 2311 } 2312 2313 numericOID2MatchingRuleUses.put(use.getMatchingRuleOID(), use); 2314 for (final String name : use.getNames()) { 2315 final String lowerName = StaticUtils.toLowerCase(name); 2316 List<MatchingRuleUse> uses = name2MatchingRuleUses.get(lowerName); 2317 if (uses == null) { 2318 name2MatchingRuleUses.put(lowerName, Collections.singletonList(use)); 2319 } else if (uses.size() == 1) { 2320 uses = new ArrayList<>(uses); 2321 uses.add(use); 2322 name2MatchingRuleUses.put(lowerName, uses); 2323 } else { 2324 uses.add(use); 2325 } 2326 } 2327 2328 return this; 2329 } 2330 2331 SchemaBuilder addMatchingRule(final MatchingRule rule, final boolean overwrite) { 2332 Reject.ifTrue(rule.isValidated(), 2333 "Matching rule has already been validated, it can't be added"); 2334 MatchingRule conflictingRule; 2335 if (numericOID2MatchingRules.containsKey(rule.getOID())) { 2336 conflictingRule = numericOID2MatchingRules.get(rule.getOID()); 2337 if (!overwrite) { 2338 final LocalizableMessage message = 2339 ERR_SCHEMA_CONFLICTING_MR_OID.get(rule.getNameOrOID(), rule.getOID(), 2340 conflictingRule.getNameOrOID()); 2341 throw new ConflictingSchemaElementException(message); 2342 } 2343 removeMatchingRule(conflictingRule); 2344 } 2345 2346 numericOID2MatchingRules.put(rule.getOID(), rule); 2347 for (final String name : rule.getNames()) { 2348 final String lowerName = StaticUtils.toLowerCase(name); 2349 List<MatchingRule> rules = name2MatchingRules.get(lowerName); 2350 if (rules == null) { 2351 name2MatchingRules.put(lowerName, Collections.singletonList(rule)); 2352 } else if (rules.size() == 1) { 2353 rules = new ArrayList<>(rules); 2354 rules.add(rule); 2355 name2MatchingRules.put(lowerName, rules); 2356 } else { 2357 rules.add(rule); 2358 } 2359 } 2360 return this; 2361 } 2362 2363 SchemaBuilder addNameForm(final NameForm form, final boolean overwrite) { 2364 NameForm conflictingForm; 2365 if (numericOID2NameForms.containsKey(form.getOID())) { 2366 conflictingForm = numericOID2NameForms.get(form.getOID()); 2367 if (!overwrite) { 2368 final LocalizableMessage message = 2369 ERR_SCHEMA_CONFLICTING_NAME_FORM_OID.get(form.getNameOrOID(), 2370 form.getOID(), conflictingForm.getNameOrOID()); 2371 throw new ConflictingSchemaElementException(message); 2372 } 2373 removeNameForm(conflictingForm); 2374 } 2375 2376 numericOID2NameForms.put(form.getOID(), form); 2377 for (final String name : form.getNames()) { 2378 final String lowerName = StaticUtils.toLowerCase(name); 2379 List<NameForm> forms = name2NameForms.get(lowerName); 2380 if (forms == null) { 2381 name2NameForms.put(lowerName, Collections.singletonList(form)); 2382 } else if (forms.size() == 1) { 2383 forms = new ArrayList<>(forms); 2384 forms.add(form); 2385 name2NameForms.put(lowerName, forms); 2386 } else { 2387 forms.add(form); 2388 } 2389 } 2390 return this; 2391 } 2392 2393 SchemaBuilder addObjectClass(final ObjectClass oc, final boolean overwrite) { 2394 ObjectClass conflictingOC; 2395 if (numericOID2ObjectClasses.containsKey(oc.getOID())) { 2396 conflictingOC = numericOID2ObjectClasses.get(oc.getOID()); 2397 if (!overwrite) { 2398 final LocalizableMessage message = 2399 ERR_SCHEMA_CONFLICTING_OBJECTCLASS_OID1.get(oc.getNameOrOID(), oc.getOID(), 2400 conflictingOC.getNameOrOID()); 2401 throw new ConflictingSchemaElementException(message); 2402 } 2403 removeObjectClass(conflictingOC); 2404 } 2405 2406 numericOID2ObjectClasses.put(oc.getOID(), oc); 2407 for (final String name : oc.getNames()) { 2408 final String lowerName = StaticUtils.toLowerCase(name); 2409 List<ObjectClass> classes = name2ObjectClasses.get(lowerName); 2410 if (classes == null) { 2411 name2ObjectClasses.put(lowerName, Collections.singletonList(oc)); 2412 } else if (classes.size() == 1) { 2413 classes = new ArrayList<>(classes); 2414 classes.add(oc); 2415 name2ObjectClasses.put(lowerName, classes); 2416 } else { 2417 classes.add(oc); 2418 } 2419 } 2420 2421 return this; 2422 } 2423 2424 private void addSchema0(final Schema schema, final boolean overwrite) { 2425 // All of the schema elements must be duplicated because validation will 2426 // cause them to update all their internal references which, although 2427 // unlikely, may be different in the new schema. 2428 2429 for (final Syntax syntax : schema.getSyntaxes()) { 2430 buildSyntax(syntax).addToSchema(overwrite); 2431 } 2432 2433 for (final MatchingRule matchingRule : schema.getMatchingRules()) { 2434 buildMatchingRule(matchingRule).addToSchema(overwrite); 2435 } 2436 2437 for (final MatchingRuleUse matchingRuleUse : schema.getMatchingRuleUses()) { 2438 buildMatchingRuleUse(matchingRuleUse).addToSchema(overwrite); 2439 } 2440 2441 for (final AttributeType attributeType : schema.getAttributeTypes()) { 2442 buildAttributeType(attributeType).addToSchema(overwrite); 2443 } 2444 2445 for (final ObjectClass objectClass : schema.getObjectClasses()) { 2446 buildObjectClass(objectClass).addToSchema(overwrite); 2447 } 2448 2449 for (final NameForm nameForm : schema.getNameForms()) { 2450 buildNameForm(nameForm).addToSchema(overwrite); 2451 } 2452 2453 for (final DITContentRule contentRule : schema.getDITContentRules()) { 2454 buildDITContentRule(contentRule).addToSchema(overwrite); 2455 } 2456 2457 for (final DITStructureRule structureRule : schema.getDITStuctureRules()) { 2458 buildDITStructureRule(structureRule).addToSchema(overwrite); 2459 } 2460 } 2461 2462 SchemaBuilder addSyntax(final Syntax syntax, final boolean overwrite) { 2463 Reject.ifTrue(syntax.isValidated(), "Syntax has already been validated, it can't be added"); 2464 Syntax conflictingSyntax; 2465 if (numericOID2Syntaxes.containsKey(syntax.getOID())) { 2466 conflictingSyntax = numericOID2Syntaxes.get(syntax.getOID()); 2467 if (!overwrite) { 2468 final LocalizableMessage message = ERR_SCHEMA_CONFLICTING_SYNTAX_OID.get(syntax.toString(), 2469 syntax.getOID(), conflictingSyntax.getOID()); 2470 throw new ConflictingSchemaElementException(message); 2471 } 2472 removeSyntax(conflictingSyntax); 2473 } 2474 2475 numericOID2Syntaxes.put(syntax.getOID(), syntax); 2476 return this; 2477 } 2478 2479 private void lazyInitBuilder() { 2480 // Lazy initialization. 2481 if (numericOID2Syntaxes == null) { 2482 options = Options.defaultOptions(); 2483 2484 numericOID2Syntaxes = new LinkedHashMap<>(); 2485 numericOID2MatchingRules = new LinkedHashMap<>(); 2486 numericOID2MatchingRuleUses = new LinkedHashMap<>(); 2487 numericOID2AttributeTypes = new LinkedHashMap<>(); 2488 numericOID2ObjectClasses = new LinkedHashMap<>(); 2489 numericOID2NameForms = new LinkedHashMap<>(); 2490 numericOID2ContentRules = new LinkedHashMap<>(); 2491 id2StructureRules = new LinkedHashMap<>(); 2492 2493 name2MatchingRules = new LinkedHashMap<>(); 2494 name2MatchingRuleUses = new LinkedHashMap<>(); 2495 name2AttributeTypes = new LinkedHashMap<>(); 2496 name2ObjectClasses = new LinkedHashMap<>(); 2497 name2NameForms = new LinkedHashMap<>(); 2498 name2ContentRules = new LinkedHashMap<>(); 2499 name2StructureRules = new LinkedHashMap<>(); 2500 2501 objectClass2NameForms = new HashMap<>(); 2502 nameForm2StructureRules = new HashMap<>(); 2503 warnings = new LinkedList<>(); 2504 2505 if (copyOnWriteSchema != null) { 2506 // Copy the schema. 2507 addSchema0(copyOnWriteSchema, true); 2508 options = Options.copyOf(copyOnWriteSchema.getOptions()); 2509 copyOnWriteSchema = null; 2510 } 2511 } 2512 } 2513 2514 private void preLazyInitBuilder(final String schemaName, final Schema copyOnWriteSchema) { 2515 this.schemaName = schemaName; 2516 this.copyOnWriteSchema = copyOnWriteSchema; 2517 2518 this.options = null; 2519 2520 this.numericOID2Syntaxes = null; 2521 this.numericOID2MatchingRules = null; 2522 this.numericOID2MatchingRuleUses = null; 2523 this.numericOID2AttributeTypes = null; 2524 this.numericOID2ObjectClasses = null; 2525 this.numericOID2NameForms = null; 2526 this.numericOID2ContentRules = null; 2527 this.id2StructureRules = null; 2528 2529 this.name2MatchingRules = null; 2530 this.name2MatchingRuleUses = null; 2531 this.name2AttributeTypes = null; 2532 this.name2ObjectClasses = null; 2533 this.name2NameForms = null; 2534 this.name2ContentRules = null; 2535 this.name2StructureRules = null; 2536 2537 this.objectClass2NameForms = null; 2538 this.nameForm2StructureRules = null; 2539 this.warnings = null; 2540 } 2541 2542 private void removeAttributeType(final AttributeType attributeType) { 2543 numericOID2AttributeTypes.remove(attributeType.getOID()); 2544 for (final String name : attributeType.getNames()) { 2545 final String lowerName = StaticUtils.toLowerCase(name); 2546 final List<AttributeType> attributes = name2AttributeTypes.get(lowerName); 2547 if (attributes != null && attributes.contains(attributeType)) { 2548 if (attributes.size() <= 1) { 2549 name2AttributeTypes.remove(lowerName); 2550 } else { 2551 attributes.remove(attributeType); 2552 } 2553 } 2554 } 2555 } 2556 2557 private void removeDITContentRule(final DITContentRule rule) { 2558 numericOID2ContentRules.remove(rule.getStructuralClassOID()); 2559 for (final String name : rule.getNames()) { 2560 final String lowerName = StaticUtils.toLowerCase(name); 2561 final List<DITContentRule> rules = name2ContentRules.get(lowerName); 2562 if (rules != null && rules.contains(rule)) { 2563 if (rules.size() <= 1) { 2564 name2ContentRules.remove(lowerName); 2565 } else { 2566 rules.remove(rule); 2567 } 2568 } 2569 } 2570 } 2571 2572 private void removeDITStructureRule(final DITStructureRule rule) { 2573 id2StructureRules.remove(rule.getRuleID()); 2574 for (final String name : rule.getNames()) { 2575 final String lowerName = StaticUtils.toLowerCase(name); 2576 final List<DITStructureRule> rules = name2StructureRules.get(lowerName); 2577 if (rules != null && rules.contains(rule)) { 2578 if (rules.size() <= 1) { 2579 name2StructureRules.remove(lowerName); 2580 } else { 2581 rules.remove(rule); 2582 } 2583 } 2584 } 2585 } 2586 2587 private void removeMatchingRule(final MatchingRule rule) { 2588 numericOID2MatchingRules.remove(rule.getOID()); 2589 for (final String name : rule.getNames()) { 2590 final String lowerName = StaticUtils.toLowerCase(name); 2591 final List<MatchingRule> rules = name2MatchingRules.get(lowerName); 2592 if (rules != null && rules.contains(rule)) { 2593 if (rules.size() <= 1) { 2594 name2MatchingRules.remove(lowerName); 2595 } else { 2596 rules.remove(rule); 2597 } 2598 } 2599 } 2600 } 2601 2602 private void removeMatchingRuleUse(final MatchingRuleUse use) { 2603 numericOID2MatchingRuleUses.remove(use.getMatchingRuleOID()); 2604 for (final String name : use.getNames()) { 2605 final String lowerName = StaticUtils.toLowerCase(name); 2606 final List<MatchingRuleUse> uses = name2MatchingRuleUses.get(lowerName); 2607 if (uses != null && uses.contains(use)) { 2608 if (uses.size() <= 1) { 2609 name2MatchingRuleUses.remove(lowerName); 2610 } else { 2611 uses.remove(use); 2612 } 2613 } 2614 } 2615 } 2616 2617 private void removeNameForm(final NameForm form) { 2618 numericOID2NameForms.remove(form.getOID()); 2619 name2NameForms.remove(form.getOID()); 2620 for (final String name : form.getNames()) { 2621 final String lowerName = StaticUtils.toLowerCase(name); 2622 final List<NameForm> forms = name2NameForms.get(lowerName); 2623 if (forms != null && forms.contains(form)) { 2624 if (forms.size() <= 1) { 2625 name2NameForms.remove(lowerName); 2626 } else { 2627 forms.remove(form); 2628 } 2629 } 2630 } 2631 } 2632 2633 private void removeObjectClass(final ObjectClass oc) { 2634 numericOID2ObjectClasses.remove(oc.getOID()); 2635 name2ObjectClasses.remove(oc.getOID()); 2636 for (final String name : oc.getNames()) { 2637 final String lowerName = StaticUtils.toLowerCase(name); 2638 final List<ObjectClass> classes = name2ObjectClasses.get(lowerName); 2639 if (classes != null && classes.contains(oc)) { 2640 if (classes.size() <= 1) { 2641 name2ObjectClasses.remove(lowerName); 2642 } else { 2643 classes.remove(oc); 2644 } 2645 } 2646 } 2647 } 2648 2649 private void removeSyntax(final Syntax syntax) { 2650 for (Map.Entry<String, List<String>> property : syntax.getExtraProperties().entrySet()) { 2651 if ("x-enum".equalsIgnoreCase(property.getKey())) { 2652 removeMatchingRule(OMR_OID_GENERIC_ENUM + "." + syntax.getOID()); 2653 break; 2654 } 2655 } 2656 numericOID2Syntaxes.remove(syntax.getOID()); 2657 } 2658 2659 private void validate(final Schema schema) { 2660 // Verify all references in all elements 2661 for (final Syntax syntax : numericOID2Syntaxes.values().toArray( 2662 new Syntax[numericOID2Syntaxes.values().size()])) { 2663 try { 2664 syntax.validate(schema, warnings); 2665 } catch (final SchemaException e) { 2666 removeSyntax(syntax); 2667 warnings.add(ERR_SYNTAX_VALIDATION_FAIL 2668 .get(syntax.toString(), e.getMessageObject())); 2669 } 2670 } 2671 2672 for (final MatchingRule rule : numericOID2MatchingRules.values().toArray( 2673 new MatchingRule[numericOID2MatchingRules.values().size()])) { 2674 try { 2675 rule.validate(schema, warnings); 2676 } catch (final SchemaException e) { 2677 removeMatchingRule(rule); 2678 warnings.add(ERR_MR_VALIDATION_FAIL.get(rule.toString(), e.getMessageObject())); 2679 } 2680 } 2681 2682 // Attribute types need special processing because they have 2683 // hierarchical dependencies. 2684 final List<AttributeType> invalidAttributeTypes = new LinkedList<>(); 2685 for (final AttributeType attributeType : numericOID2AttributeTypes.values()) { 2686 attributeType.validate(schema, invalidAttributeTypes, warnings); 2687 } 2688 2689 for (final AttributeType attributeType : invalidAttributeTypes) { 2690 removeAttributeType(attributeType); 2691 } 2692 2693 // Object classes need special processing because they have hierarchical 2694 // dependencies. 2695 final List<ObjectClass> invalidObjectClasses = new LinkedList<>(); 2696 for (final ObjectClass objectClass : numericOID2ObjectClasses.values()) { 2697 objectClass.validate(schema, invalidObjectClasses, warnings); 2698 } 2699 2700 for (final ObjectClass objectClass : invalidObjectClasses) { 2701 removeObjectClass(objectClass); 2702 } 2703 2704 for (final MatchingRuleUse use : numericOID2MatchingRuleUses.values().toArray( 2705 new MatchingRuleUse[numericOID2MatchingRuleUses.values().size()])) { 2706 try { 2707 use.validate(schema); 2708 } catch (final SchemaException e) { 2709 removeMatchingRuleUse(use); 2710 warnings.add(ERR_MRU_VALIDATION_FAIL.get(use.toString(), e.getMessageObject())); 2711 } 2712 } 2713 2714 for (final NameForm form : numericOID2NameForms.values().toArray( 2715 new NameForm[numericOID2NameForms.values().size()])) { 2716 try { 2717 form.validate(schema); 2718 2719 // build the objectClass2NameForms map 2720 final String ocOID = form.getStructuralClass().getOID(); 2721 List<NameForm> forms = objectClass2NameForms.get(ocOID); 2722 if (forms == null) { 2723 objectClass2NameForms.put(ocOID, Collections.singletonList(form)); 2724 } else if (forms.size() == 1) { 2725 forms = new ArrayList<>(forms); 2726 forms.add(form); 2727 objectClass2NameForms.put(ocOID, forms); 2728 } else { 2729 forms.add(form); 2730 } 2731 } catch (final SchemaException e) { 2732 removeNameForm(form); 2733 warnings.add(ERR_NAMEFORM_VALIDATION_FAIL 2734 .get(form.toString(), e.getMessageObject())); 2735 } 2736 } 2737 2738 for (final DITContentRule rule : numericOID2ContentRules.values().toArray( 2739 new DITContentRule[numericOID2ContentRules.values().size()])) { 2740 try { 2741 rule.validate(schema, warnings); 2742 } catch (final SchemaException e) { 2743 removeDITContentRule(rule); 2744 warnings.add(ERR_DCR_VALIDATION_FAIL.get(rule.toString(), e.getMessageObject())); 2745 } 2746 } 2747 2748 // DIT structure rules need special processing because they have 2749 // hierarchical dependencies. 2750 final List<DITStructureRule> invalidStructureRules = new LinkedList<>(); 2751 for (final DITStructureRule rule : id2StructureRules.values()) { 2752 rule.validate(schema, invalidStructureRules, warnings); 2753 } 2754 2755 for (final DITStructureRule rule : invalidStructureRules) { 2756 removeDITStructureRule(rule); 2757 } 2758 2759 for (final DITStructureRule rule : id2StructureRules.values()) { 2760 // build the nameForm2StructureRules map 2761 final String ocOID = rule.getNameForm().getOID(); 2762 List<DITStructureRule> rules = nameForm2StructureRules.get(ocOID); 2763 if (rules == null) { 2764 nameForm2StructureRules.put(ocOID, Collections.singletonList(rule)); 2765 } else if (rules.size() == 1) { 2766 rules = new ArrayList<>(rules); 2767 rules.add(rule); 2768 nameForm2StructureRules.put(ocOID, rules); 2769 } else { 2770 rules.add(rule); 2771 } 2772 } 2773 } 2774}