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 2006-2010 Sun Microsystems, Inc. 015 * Portions Copyright 2011-2017 ForgeRock AS. 016 */ 017package org.opends.server.core; 018 019import java.io.File; 020import java.io.FileInputStream; 021import java.io.FileNotFoundException; 022import java.io.FilenameFilter; 023import java.io.IOException; 024import java.util.ArrayList; 025import java.util.Collections; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029 030import org.forgerock.i18n.LocalizableMessage; 031import org.forgerock.i18n.slf4j.LocalizedLogger; 032import org.forgerock.opendj.config.server.ConfigException; 033import org.forgerock.opendj.ldap.Attribute; 034import org.forgerock.opendj.ldap.AttributeDescription; 035import org.forgerock.opendj.ldap.Entry; 036import org.forgerock.opendj.ldap.ModificationType; 037import org.forgerock.opendj.ldap.ResultCode; 038import org.forgerock.opendj.ldap.schema.AttributeType; 039import org.forgerock.opendj.ldap.schema.CoreSchema; 040import org.forgerock.opendj.ldap.schema.DITContentRule; 041import org.forgerock.opendj.ldap.schema.DITStructureRule; 042import org.forgerock.opendj.ldap.schema.MatchingRule; 043import org.forgerock.opendj.ldap.schema.MatchingRuleUse; 044import org.forgerock.opendj.ldap.schema.NameForm; 045import org.forgerock.opendj.ldap.schema.ObjectClass; 046import org.forgerock.opendj.ldap.schema.SchemaBuilder; 047import org.forgerock.opendj.ldap.schema.SchemaBuilder.SchemaBuilderHook; 048import org.forgerock.opendj.ldap.schema.Syntax; 049import org.forgerock.opendj.ldap.schema.AttributeType.Builder; 050import org.forgerock.opendj.ldif.LDIFEntryReader; 051import org.opends.server.types.DirectoryException; 052import org.opends.server.types.InitializationException; 053import org.opends.server.types.Modification; 054import org.opends.server.types.Schema; 055import org.opends.server.types.Schema.SchemaUpdater; 056 057import static org.forgerock.opendj.adapter.server3x.Converters.toAttribute; 058import static org.forgerock.opendj.ldap.schema.SchemaValidationPolicy.*; 059import static org.opends.messages.ConfigMessages.*; 060import static org.opends.server.util.StaticUtils.*; 061import static org.opends.server.util.ServerConstants.SCHEMA_PROPERTY_FILENAME; 062 063/** 064 * This class defines a utility that will be used to manage the interaction with 065 * the Directory Server schema. It will be used to initially load all of the 066 * matching rules and attribute syntaxes that have been defined in the 067 * configuration, and will then read the actual schema definitions. At present, 068 * only attribute types and objectclasses are supported in the schema config 069 * files. Other components like DIT content rules, DIT structure rules, name 070 * forms, and matching rule use definitions will be ignored. 071 */ 072public class SchemaConfigManager 073{ 074 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 075 076 private static final String CORE_SCHEMA_FILE = "00-core.ldif"; 077 private static final String RFC_3112_SCHEMA_FILE = "03-rfc3112.ldif"; 078 079 /** The schema that has been parsed from the server configuration. */ 080 private Schema schema; 081 082 private final ServerContext serverContext; 083 084 /** 085 * Creates a new instance of this schema config manager. 086 * 087 * @param serverContext 088 * The server context. 089 */ 090 public SchemaConfigManager(ServerContext serverContext) 091 { 092 this.serverContext = serverContext; 093 try 094 { 095 // the manager will build the schema from scratch, but we need to start from core schema for SDK schema 096 schema = new Schema(org.forgerock.opendj.ldap.schema.Schema.getCoreSchema()); 097 } 098 catch (DirectoryException unexpected) 099 { 100 // the core schema should not have any warning 101 throw new RuntimeException(unexpected); 102 } 103 } 104 105 /** 106 * Retrieves the path to the directory containing the server schema files. 107 * 108 * @return The path to the directory containing the server schema files. 109 */ 110 public static String getSchemaDirectoryPath() 111 { 112 File schemaDir = DirectoryServer.getEnvironmentConfig().getSchemaDirectory(); 113 return schemaDir != null ? schemaDir.getAbsolutePath() : null; 114 } 115 116 /** 117 * Retrieves a reference to the schema information that has been read from the server 118 * configuration. 119 * <p> 120 * Note that this information will not be complete until the {@link #initializeMatchingRules()}, 121 * {@link #initializeAttributeSyntaxes()} methods have been called. 122 * 123 * @return A reference to the schema information that has been read from the server configuration. 124 */ 125 public Schema getSchema() 126 { 127 return schema; 128 } 129 130 /** 131 * Initializes all the matching rules defined in the Directory Server 132 * configuration. This should only be called at Directory Server startup. 133 * 134 * @throws ConfigException If a configuration problem causes the matching 135 * rule initialization process to fail. 136 * 137 * @throws InitializationException If a problem occurs while initializing 138 * the matching rules that is not related to 139 * the server configuration. 140 */ 141 public void initializeMatchingRules() 142 throws ConfigException, InitializationException 143 { 144 MatchingRuleConfigManager matchingRuleConfigManager = new MatchingRuleConfigManager(serverContext); 145 matchingRuleConfigManager.initializeMatchingRules(); 146 } 147 148 /** 149 * Initializes all the attribute syntaxes defined in the Directory Server 150 * configuration. This should only be called at Directory Server startup. 151 * 152 * @throws ConfigException If a configuration problem causes the syntax 153 * initialization process to fail. 154 * 155 * @throws InitializationException If a problem occurs while initializing 156 * the syntaxes that is not related to the 157 * server configuration. 158 */ 159 public void initializeAttributeSyntaxes() 160 throws ConfigException, InitializationException 161 { 162 AttributeSyntaxConfigManager syntaxConfigManager = new AttributeSyntaxConfigManager(serverContext); 163 syntaxConfigManager.initializeAttributeSyntaxes(); 164 } 165 166 /** Filter implementation that accepts only ldif files. */ 167 public static class SchemaFileFilter implements FilenameFilter 168 { 169 @Override 170 public boolean accept(File directory, String filename) 171 { 172 return filename.endsWith(".ldif"); 173 } 174 } 175 176 /** 177 * Initializes all the attribute type, object class, name form, DIT content 178 * rule, DIT structure rule, and matching rule use definitions by reading the 179 * server schema files. These files will be located in a single directory and 180 * will be processed in lexicographic order. However, to make the order 181 * easier to understand, they may be prefixed with a two digit number (with a 182 * leading zero if necessary) so that they will be read in numeric order. 183 * This should only be called at Directory Server startup. 184 * 185 * @throws ConfigException If a configuration problem causes the schema 186 * element initialization to fail. 187 * 188 * @throws InitializationException If a problem occurs while initializing 189 * the schema elements that is not related 190 * to the server configuration. 191 */ 192 public void initializeSchemaFromFiles() 193 throws ConfigException, InitializationException 194 { 195 // Construct the path to the directory that should contain the schema files 196 // and make sure that it exists and is a directory. Get a list of the files 197 // in that directory sorted in alphabetic order. 198 String schemaInstanceDirPath = getSchemaDirectoryPath(); 199 File schemaInstanceDir = schemaInstanceDirPath != null ? new File(schemaInstanceDirPath) : null; 200 long oldestModificationTime = -1L; 201 long youngestModificationTime = -1L; 202 List<String> fileNames; 203 204 try 205 { 206 if (schemaInstanceDir == null || !schemaInstanceDir.exists()) 207 { 208 throw new InitializationException(ERR_CONFIG_SCHEMA_NO_SCHEMA_DIR.get(schemaInstanceDirPath)); 209 } 210 if (!schemaInstanceDir.isDirectory()) 211 { 212 throw new InitializationException(ERR_CONFIG_SCHEMA_DIR_NOT_DIRECTORY.get(schemaInstanceDirPath)); 213 } 214 215 File[] schemaInstanceDirFiles = schemaInstanceDir.listFiles(new SchemaFileFilter()); 216 fileNames = new ArrayList<>(schemaInstanceDirFiles.length); 217 218 for (File f : schemaInstanceDirFiles) 219 { 220 if (f.isFile()) 221 { 222 fileNames.add(f.getName()); 223 } 224 225 long modificationTime = f.lastModified(); 226 if (oldestModificationTime <= 0L || 227 modificationTime < oldestModificationTime) 228 { 229 oldestModificationTime = modificationTime; 230 } 231 232 if (youngestModificationTime <= 0 || 233 modificationTime > youngestModificationTime) 234 { 235 youngestModificationTime = modificationTime; 236 } 237 } 238 239 Collections.sort(fileNames); 240 } 241 catch (InitializationException ie) 242 { 243 logger.traceException(ie); 244 245 throw ie; 246 } 247 catch (Exception e) 248 { 249 logger.traceException(e); 250 251 LocalizableMessage message = ERR_CONFIG_SCHEMA_CANNOT_LIST_FILES.get( 252 schemaInstanceDirPath, getExceptionMessage(e)); 253 throw new InitializationException(message, e); 254 } 255 256 // If the oldest and youngest modification timestamps didn't get set for 257 // some reason, then set them to the current time. 258 if (oldestModificationTime <= 0) 259 { 260 oldestModificationTime = System.currentTimeMillis(); 261 } 262 263 if (youngestModificationTime <= 0) 264 { 265 youngestModificationTime = oldestModificationTime; 266 } 267 268 schema.setOldestModificationTime(oldestModificationTime); 269 schema.setYoungestModificationTime(youngestModificationTime); 270 271 // Iterate through the schema files and read them as an LDIF file containing 272 // a single entry. Then get the attributeTypes and objectClasses attributes 273 // from that entry and parse them to initialize the server schema. 274 Map<String, Attribute> extraAttrs = new HashMap<>(); 275 for (String schemaFile : fileNames) 276 { 277 loadSchemaFile(schema, schemaFile, extraAttrs, false); 278 } 279 if (!extraAttrs.isEmpty()) 280 { 281 for (String attrName : extraAttrs.keySet()) 282 { 283 schema.addExtraAttribute(attrName, toAttribute(extraAttrs.get(attrName))); 284 } 285 } 286 } 287 288 /** 289 * Loads the contents of the specified schema file into the provided schema. 290 * 291 * @param schema The schema in which the contents of the schema file are 292 * to be loaded. 293 * @param schemaFile The name of the schema file to be loaded into the 294 * provided schema. 295 * @param extraAttrs The map of extra attributes that will be completed by this method. 296 * Maybe {@code null} if loading extra attributes is not required. 297 * @throws ConfigException If a configuration problem causes the schema 298 * element initialization to fail. 299 * @throws InitializationException If a problem occurs while initializing 300 * the schema elements that is not related 301 * to the server configuration. 302 */ 303 public static void loadSchemaFile(Schema schema, 304 Map<String, Attribute> extraAttrs, 305 String schemaFile) 306 throws ConfigException, InitializationException 307 { 308 loadSchemaFile(schema, schemaFile, extraAttrs, true); 309 } 310 311 /** 312 * Loads the contents of the specified schema file into the provided schema and returns the list 313 * of modifications. 314 * 315 * @param schema 316 * The schema in which the contents of the schema file are to be loaded. 317 * @param schemaFile 318 * The name of the schema file to be loaded into the provided schema. 319 * @param extraAttrs 320 * The map of extra attributes that will be completed by this method. 321 * Maybe {@code null} if loading extra attributes is not required. 322 * @return A list of the modifications that could be performed in order to obtain the contents of 323 * the file. 324 * @throws ConfigException 325 * If a configuration problem causes the schema element initialization to fail. 326 * @throws InitializationException 327 * If a problem occurs while initializing the schema elements that is not related to the 328 * server configuration. 329 */ 330 public static List<Modification> loadSchemaFileReturnModifications(Schema schema, 331 String schemaFile, 332 Map<String, Attribute> extraAttrs) 333 throws ConfigException, InitializationException 334 { 335 final Entry entry = loadSchemaFile(schema, schemaFile, extraAttrs, true); 336 if (entry != null) 337 { 338 return createAddModifications(entry, 339 CoreSchema.getLDAPSyntaxesAttributeType(), 340 CoreSchema.getAttributeTypesAttributeType(), 341 CoreSchema.getObjectClassesAttributeType(), 342 CoreSchema.getNameFormsAttributeType(), 343 CoreSchema.getDITContentRulesAttributeType(), 344 CoreSchema.getDITStructureRulesAttributeType(), 345 CoreSchema.getMatchingRuleUseAttributeType()); 346 } 347 return Collections.emptyList(); 348 } 349 350 private static List<Modification> createAddModifications(Entry entry, AttributeType... attrTypes) 351 { 352 List<Modification> mods = new ArrayList<>(entry.getAttributeCount()); 353 for (AttributeType attrType : attrTypes) 354 { 355 for (Attribute a : entry.getAllAttributes(AttributeDescription.create(attrType))) 356 { 357 mods.add(new Modification(ModificationType.ADD, toAttribute(a))); 358 } 359 } 360 return mods; 361 } 362 363 /** 364 * Loads the contents of the specified schema file into the provided schema. 365 * 366 * @param schema The schema in which the contents of the schema file 367 * are to be loaded. 368 * @param schemaFile The name of the schema file to be loaded into the 369 * provided schema. 370 * @param extraAttrs The map of extra attributes that will be completed by this method. 371 * Maybe {@code null} if loading extra attributes is not required. 372 * @param failOnError If {@code true}, indicates that this method should 373 * throw an exception if certain kinds of errors occur. 374 * If {@code false}, indicates that this method should 375 * log an error message and return without an exception. 376 * This should only be {@code false} when called from 377 * {@code initializeSchemaFromFiles}. 378 * @return the schema entry that has been read from the schema file 379 * @throws ConfigException If a configuration problem causes the schema 380 * element initialization to fail. 381 * @throws InitializationException If a problem occurs while initializing 382 * the schema elements that is not related 383 * to the server configuration. 384 */ 385 private static Entry loadSchemaFile(Schema schema, String schemaFile, 386 Map<String, Attribute> extraAttrs, 387 boolean failOnError) 388 throws ConfigException, InitializationException 389 { 390 final Entry entry = readSchemaEntryFromFile(schemaFile, failOnError); 391 if (entry != null) 392 { 393 updateSchemaWithEntry(schema, schemaFile, failOnError, entry); 394 395 if (extraAttrs != null) 396 { 397 for (Attribute attribute : entry.getAllAttributes()) 398 { 399 final String oid = attribute.getAttributeDescription().getAttributeType().getOID(); 400 if (!isSchemaAttribute(oid)) 401 { 402 extraAttrs.put( 403 attribute.getAttributeDescription().getAttributeType().getNameOrOID(), attribute); 404 } 405 } 406 } 407 } 408 return entry; 409 } 410 411 /** 412 * Checks if a given attribute oid corresponds to an attribute that is used by the definition of the schema. 413 * 414 * @param attrOid 415 * The oid of the attribute to be checked. 416 * @return {@code true} if the attribute is part of the schema definition, false otherwise 417 */ 418 public static boolean isSchemaAttribute(String attrOid) 419 { 420 return attrOid.equals(CoreSchema.getAttributeTypesAttributeType().getOID()) 421 || attrOid.equals("attributetypes-oid") 422 || attrOid.equals(CoreSchema.getDITContentRulesAttributeType().getOID()) 423 || attrOid.equals("ditcontentrules-oid") 424 || attrOid.equals(CoreSchema.getDITStructureRulesAttributeType().getOID()) 425 || attrOid.equals("ditstructurerules-oid") 426 || attrOid.equals(CoreSchema.getLDAPSyntaxesAttributeType().getOID()) 427 || attrOid.equals("ldapsyntaxes-oid") 428 || attrOid.equals(CoreSchema.getMatchingRulesAttributeType().getOID()) 429 || attrOid.equals("matchingrules-oid") 430 || attrOid.equals(CoreSchema.getMatchingRuleUseAttributeType().getOID()) 431 || attrOid.equals("matchingruleuse-oid") 432 || attrOid.equals(CoreSchema.getNameFormsAttributeType().getOID()) 433 || attrOid.equals("nameforms-oid") 434 || attrOid.equals(CoreSchema.getNameFormDescriptionSyntax().getOID()) 435 || attrOid.equals("nameformdescription-oid") 436 || attrOid.equals(CoreSchema.getObjectClassesAttributeType().getOID()) 437 || attrOid.equals("objectclasses-oid") 438 || attrOid.equals(CoreSchema.getObjectClassAttributeType().getOID()) 439 || attrOid.equals("objectclass-oid") 440 || attrOid.equals(CoreSchema.getCNAttributeType().getOID()) 441 || attrOid.equals("cn-oid"); 442 } 443 444 private static void updateSchemaWithEntry(Schema schema, String schemaFile, boolean failOnError, 445 final Entry schemaEntry) throws ConfigException 446 { 447 try 448 { 449 // immediately overwrite these definitions which are already defined in the SDK core schema 450 final boolean overwriteCoreSchemaDefinitions = 451 CORE_SCHEMA_FILE.equals(schemaFile) || RFC_3112_SCHEMA_FILE.equals(schemaFile); 452 updateSchema(schema, schemaFile, schemaEntry, overwriteCoreSchemaDefinitions); 453 } 454 catch (DirectoryException e) 455 { 456 if (e.getResultCode().equals(ResultCode.CONSTRAINT_VIOLATION)) 457 { 458 // Register it with the schema. We will allow duplicates, with the 459 // later definition overriding any earlier definition, but we want 460 // to trap them and log a warning. 461 logger.warn(WARN_CONFIG_CONFLICTING_DEFINITIONS_IN_SCHEMA_FILE, schemaFile, e.getMessageObject()); 462 try 463 { 464 updateSchema(schema, schemaFile, schemaEntry, true); 465 } 466 catch (DirectoryException e2) 467 { 468 // This should never happen 469 logger.traceException(e2); 470 } 471 } 472 else 473 { 474 reportError(failOnError, e, 475 WARN_CONFIG_SCHEMA_CANNOT_PARSE_DEFINITIONS_IN_SCHEMA_FILE.get(schemaFile, e.getMessageObject())); 476 } 477 } 478 } 479 480 private static Entry readSchemaEntryFromFile(String schemaFile, boolean failOnError) 481 throws ConfigException, InitializationException 482 { 483 // Create an LDIF reader to use when reading the files. 484 String schemaDirPath = getSchemaDirectoryPath(); 485 File f = new File(schemaDirPath, schemaFile); 486 try (final FileInputStream in = new FileInputStream(f); 487 final LDIFEntryReader reader = new LDIFEntryReader(in)) 488 { 489 reader.setSchemaValidationPolicy(ignoreAll()); 490 491 if (!reader.hasNext()) 492 { 493 // The file was empty -- skip it. 494 return null; 495 } 496 final Entry entry = reader.readEntry(); 497 if (reader.hasNext()) 498 { 499 // If there are any more entries in the file, then print a warning message. 500 logger.warn(WARN_CONFIG_SCHEMA_MULTIPLE_ENTRIES_IN_FILE, schemaFile, schemaDirPath); 501 } 502 return entry; 503 } 504 catch (FileNotFoundException e) 505 { 506 logger.traceException(e); 507 508 LocalizableMessage message = 509 WARN_CONFIG_SCHEMA_CANNOT_OPEN_FILE.get(schemaFile, schemaDirPath, getExceptionMessage(e)); 510 511 if (failOnError) 512 { 513 throw new ConfigException(message); 514 } 515 logger.error(message); 516 return null; 517 } 518 catch (IOException e) 519 { 520 logger.traceException(e); 521 522 LocalizableMessage message = 523 WARN_CONFIG_SCHEMA_CANNOT_READ_LDIF_ENTRY.get(schemaFile, schemaDirPath, getExceptionMessage(e)); 524 525 if (failOnError) 526 { 527 throw new InitializationException(message, e); 528 } 529 logger.error(message); 530 return null; 531 } 532 } 533 534 private static void updateSchema(Schema schema, final String schemaFile, final Entry schemaEntry, 535 final boolean overwrite) throws DirectoryException 536 { 537 schema.updateSchema(new SchemaUpdater() 538 { 539 @Override 540 public org.forgerock.opendj.ldap.schema.Schema update(SchemaBuilder builder) 541 { 542 return builder.addSchema(schemaEntry, overwrite, new SchemaBuilderHook() { 543 @Override 544 public void beforeAddSyntax(Syntax.Builder builder) { 545 builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME) 546 .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); 547 } 548 @Override 549 public void beforeAddObjectClass(ObjectClass.Builder builder) { 550 builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME) 551 .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); 552 } 553 @Override 554 public void beforeAddNameForm(NameForm.Builder builder) { 555 builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME) 556 .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); 557 } 558 @Override 559 public void beforeAddMatchingRuleUse(MatchingRuleUse.Builder builder) { 560 builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME) 561 .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); 562 } 563 @Override 564 public void beforeAddMatchingRule(MatchingRule.Builder builder) { 565 builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME) 566 .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); 567 } 568 @Override 569 public void beforeAddDitStructureRule(DITStructureRule.Builder builder) { 570 builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME) 571 .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); 572 } 573 @Override 574 public void beforeAddDitContentRule(DITContentRule.Builder builder) { 575 builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME) 576 .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); 577 } 578 @Override 579 public void beforeAddAttribute(Builder builder) { 580 builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME) 581 .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); 582 } 583 }).toSchema(); 584 } 585 }); 586 } 587 588 private static void reportError(boolean failOnError, Exception e, 589 LocalizableMessage message) throws ConfigException 590 { 591 if (failOnError) 592 { 593 throw new ConfigException(message, e); 594 } 595 logger.error(message); 596 } 597}