/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2014-2016 ForgeRock AS. */ package org.opends.server.core; import static org.opends.server.util.ServerConstants.SCHEMA_PROPERTY_FILENAME; import static org.opends.messages.ConfigMessages.WARN_CONFIG_CONFLICTING_DEFINITIONS_IN_SCHEMA_FILE; import static org.opends.messages.ConfigMessages.WARN_CONFIG_SCHEMA_CANNOT_PARSE_DEFINITIONS_IN_SCHEMA_FILE; import static org.opends.messages.SchemaMessages.ERR_SCHEMA_HAS_WARNINGS; import static org.forgerock.util.Utils.*; import static org.opends.messages.ConfigMessages.*; import static org.opends.server.util.StaticUtils.*; import java.io.File; import java.io.FileReader; import java.io.FilenameFilter; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.opends.server.replication.plugin.HistoricalCsnOrderingMatchingRuleImpl; import org.opends.server.schema.AciSyntax; import org.opends.server.schema.SubtreeSpecificationSyntax; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.config.ClassPropertyDefinition; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.opendj.ldap.Entry; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.schema.DITContentRule; import org.forgerock.opendj.ldap.schema.DITStructureRule; import org.forgerock.opendj.ldap.schema.MatchingRule; import org.forgerock.opendj.ldap.schema.MatchingRuleUse; import org.forgerock.opendj.ldap.schema.NameForm; import org.forgerock.opendj.ldap.schema.ObjectClass; import org.forgerock.opendj.ldap.schema.Schema; import org.forgerock.opendj.ldap.schema.SchemaBuilder; import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy; import org.forgerock.opendj.ldap.schema.Syntax; import org.forgerock.opendj.ldap.schema.AttributeType.Builder; import org.forgerock.opendj.ldap.schema.SchemaBuilder.SchemaBuilderHook; import org.forgerock.opendj.ldif.EntryReader; import org.forgerock.opendj.ldif.LDIFEntryReader; import org.forgerock.opendj.server.config.meta.SchemaProviderCfgDefn; import org.forgerock.opendj.server.config.server.RootCfg; import org.forgerock.opendj.server.config.server.SchemaProviderCfg; import org.forgerock.util.Option; import org.forgerock.util.Utils; import org.opends.server.schema.SchemaProvider; import org.opends.server.types.DirectoryException; import org.opends.server.types.InitializationException; import org.opends.server.util.ActivateOnceSDKSchemaIsUsed; /** * Responsible for loading the server schema. *

* The schema is loaded in three steps : *

*/ @ActivateOnceSDKSchemaIsUsed public final class SchemaHandler { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); private static final String CORE_SCHEMA_PROVIDER_NAME = "Core Schema"; private static final String CORE_SCHEMA_FILE = "00-core.ldif"; private static final String RFC_3112_SCHEMA_FILE = "03-rfc3112.ldif"; private ServerContext serverContext; /** * The schema. *

* @GuardedBy("exclusiveLock") */ private volatile Schema schemaNG; /** Guards updates to the schema. */ private final Lock exclusiveLock = new ReentrantLock(); private long oldestModificationTime = -1L; private long youngestModificationTime = -1L; /** * Creates a new instance. */ public SchemaHandler() { // no implementation. } /** * Initialize this schema handler. * * @param serverContext * The server context. * @throws ConfigException * If a configuration problem arises in the process of performing * the initialization. * @throws InitializationException * If a problem that is not configuration-related occurs during * initialization. */ public void initialize(final ServerContext serverContext) throws InitializationException, ConfigException { this.serverContext = serverContext; exclusiveLock.lock(); try { // Start from the core schema (TODO: or start with empty schema and add core schema in core schema provider ?) final SchemaBuilder schemaBuilder = new SchemaBuilder(Schema.getCoreSchema()); // Take providers into account. loadSchemaFromProviders(serverContext.getRootConfig(), schemaBuilder); // Take schema files into account completeSchemaFromFiles(schemaBuilder); try { // see RemoteSchemaLoader.readSchema() ==> why ?? // Add server specific syntaxes and matching rules AciSyntax.addAciSyntax(schemaBuilder); SubtreeSpecificationSyntax.addSubtreeSpecificationSyntax(schemaBuilder); HistoricalCsnOrderingMatchingRuleImpl.addHistoricalCsnOrderingMatchingRule(schemaBuilder); switchSchema(schemaBuilder.toSchema()); } catch (DirectoryException e) { throw new ConfigException(e.getMessageObject(), e); } } finally { exclusiveLock.unlock(); } } /** * Update the schema using the provided schema updater. *

* An implicit lock is performed, so it is in general not necessary * to call the {code lock()} and {code unlock() methods. * However, these method should be used if/when the SchemaBuilder passed * as an argument to the updater is not used to return the schema * (see for example usage in {@code CoreSchemaProvider} class). This * case should remain exceptional. * * @param updater * the updater that returns a new schema * @throws DirectoryException if there is any problem updating the schema */ public void updateSchema(SchemaUpdater updater) throws DirectoryException { exclusiveLock.lock(); try { switchSchema(updater.update(new SchemaBuilder(schemaNG))); } finally { exclusiveLock.unlock(); } } /** * Updates the schema option if the new value differs from the old value. * * @param the schema option's type * @param option the schema option to update * @param newValue the new value for the schema option * @throws DirectoryException if there is any problem updating the schema */ public void updateSchemaOption(final Option option, final T newValue) throws DirectoryException { final T oldValue = schemaNG.getOption(option); if (!oldValue.equals(newValue)) { updateSchema(new SchemaUpdater() { @Override public Schema update(SchemaBuilder builder) { return builder.setOption(option, newValue).toSchema(); } }); } } /** Takes an exclusive lock on the schema. */ public void exclusiveLock() { exclusiveLock.lock(); } /** Releases an exclusive lock on the schema. */ public void exclusiveUnlock() { exclusiveLock.unlock(); } /** * Load the schema from provided root configuration. * * @param rootConfiguration * The root to retrieve schema provider configurations. * @param schemaBuilder * The schema builder that providers should update. * @param schemaUpdater * The updater that providers should use when applying a configuration change. */ private void loadSchemaFromProviders(final RootCfg rootConfiguration, final SchemaBuilder schemaBuilder) throws ConfigException, InitializationException { for (final String name : rootConfiguration.listSchemaProviders()) { final SchemaProviderCfg config = rootConfiguration.getSchemaProvider(name); if (config.isEnabled()) { loadSchemaProvider(config.getJavaClass(), config, schemaBuilder, true); } else if (name.equals(CORE_SCHEMA_PROVIDER_NAME)) { // TODO : use correct message ERR_CORE_SCHEMA_NOT_ENABLED throw new ConfigException(LocalizableMessage.raw("Core Schema can't be disabled")); } } } /** * Load the schema provider from the provided class name. *

* If {@code} initialize} is {@code true}, then the provider is initialized, * and the provided schema builder is updated with schema elements from the provider. */ private SchemaProvider loadSchemaProvider(final String className, final T config, final SchemaBuilder schemaBuilder, final boolean initialize) throws InitializationException { try { final ClassPropertyDefinition propertyDef = SchemaProviderCfgDefn.getInstance().getJavaClassPropertyDefinition(); final Class providerClass = propertyDef.loadClass(className, SchemaProvider.class); final SchemaProvider provider = providerClass.newInstance(); if (initialize) { provider.initialize(serverContext, config, schemaBuilder); } else { final List unacceptableReasons = new ArrayList<>(); if (!provider.isConfigurationAcceptable(config, unacceptableReasons)) { final String reasons = Utils.joinAsString(". ", unacceptableReasons); // TODO : fix message, eg CONFIG SCHEMA PROVIDER CONFIG NOT ACCEPTABLE throw new InitializationException(ERR_CONFIG_ALERTHANDLER_CONFIG_NOT_ACCEPTABLE.get(config.dn(), reasons)); } } return provider; } catch (Exception e) { // TODO : fix message throw new InitializationException(ERR_CONFIG_SCHEMA_SYNTAX_CANNOT_INITIALIZE.get( className, config.dn(), stackTraceToSingleLineString(e)), e); } } /** * Retrieves the path to the directory containing the server schema files. * * @return The path to the directory containing the server schema files. */ private File getSchemaDirectoryPath() throws InitializationException { final File dir = serverContext.getEnvironment().getSchemaDirectory(); if (dir == null) { throw new InitializationException(ERR_CONFIG_SCHEMA_NO_SCHEMA_DIR.get(null)); } if (!dir.exists()) { throw new InitializationException(ERR_CONFIG_SCHEMA_NO_SCHEMA_DIR.get(dir.getPath())); } if (!dir.isDirectory()) { throw new InitializationException(ERR_CONFIG_SCHEMA_DIR_NOT_DIRECTORY.get(dir.getPath())); } return dir; } /** Returns the LDIF reader on provided LDIF file. The caller must ensure the reader is closed. */ private EntryReader getLDIFReader(final File ldifFile, final Schema schema) throws InitializationException { try { final LDIFEntryReader reader = new LDIFEntryReader(new FileReader(ldifFile)); reader.setSchema(schema); reader.setSchemaValidationPolicy(SchemaValidationPolicy.ignoreAll()); return reader; } catch (Exception e) { // TODO : fix message throw new InitializationException(ERR_CONFIG_FILE_CANNOT_OPEN_FOR_READ.get(ldifFile.getAbsolutePath(), e), e); } } /** * Complete the schema with schema files. * * @param schemaBuilder * The schema builder to update with the content of the schema files. * @throws ConfigException * If a configuration problem causes the schema element * initialization to fail. * @throws InitializationException * If a problem occurs while initializing the schema elements that * is not related to the server configuration. */ private void completeSchemaFromFiles(final SchemaBuilder schemaBuilder) throws ConfigException, InitializationException { final File schemaDirectory = getSchemaDirectoryPath(); for (String schemaFile : getSchemaFileNames(schemaDirectory)) { loadSchemaFile(new File(schemaDirectory, schemaFile), schemaBuilder, Schema.getDefaultSchema()); } } /** Returns the list of names of schema files contained in the provided directory. */ private List getSchemaFileNames(final File schemaDirectory) throws InitializationException { try { final File[] schemaFiles = schemaDirectory.listFiles(new SchemaFileFilter()); final List schemaFileNames = new ArrayList<>(schemaFiles.length); for (final File f : schemaFiles) { if (f.isFile()) { schemaFileNames.add(f.getName()); } final long modificationTime = f.lastModified(); if (oldestModificationTime <= 0L || modificationTime < oldestModificationTime) { oldestModificationTime = modificationTime; } if (youngestModificationTime <= 0 || modificationTime > youngestModificationTime) { youngestModificationTime = modificationTime; } } // If the oldest and youngest modification timestamps didn't get set // then set them to the current time. if (oldestModificationTime <= 0) { oldestModificationTime = System.currentTimeMillis(); } if (youngestModificationTime <= 0) { youngestModificationTime = oldestModificationTime; } Collections.sort(schemaFileNames); return schemaFileNames; } catch (Exception e) { logger.traceException(e); throw new InitializationException(ERR_CONFIG_SCHEMA_CANNOT_LIST_FILES .get(schemaDirectory, getExceptionMessage(e)), e); } } /** Returns the schema entry from the provided reader. */ private Entry readSchemaEntry(final EntryReader reader, final File schemaFile) throws InitializationException { try { Entry entry = null; if (reader.hasNext()) { entry = reader.readEntry(); if (reader.hasNext()) { // TODO : fix message logger.warn(WARN_CONFIG_SCHEMA_MULTIPLE_ENTRIES_IN_FILE, schemaFile, ""); } return entry; } else { // TODO : fix message - should be SCHEMA NO LDIF ENTRY throw new InitializationException(WARN_CONFIG_SCHEMA_CANNOT_READ_LDIF_ENTRY.get( schemaFile, "", "")); } } catch (IOException e) { // TODO : fix message throw new InitializationException(WARN_CONFIG_SCHEMA_CANNOT_READ_LDIF_ENTRY.get( schemaFile, "", getExceptionMessage(e)), e); } finally { closeSilently(reader); } } /** * Add the schema from the provided schema file to the provided schema * builder. * * @param schemaFile * the schema file to be loaded * @param schemaBuilder * The schema builder in which the contents of the schema file are to * be loaded. * @param readSchema * The schema used to read the file. * @throws InitializationException * If a problem occurs while initializing the schema elements. */ private void loadSchemaFile(final File schemaFile, final SchemaBuilder schemaBuilder, final Schema readSchema) throws InitializationException, ConfigException { EntryReader reader = null; try { reader = getLDIFReader(schemaFile, readSchema); final Entry entry = readSchemaEntry(reader, schemaFile); boolean failOnError = true; updateSchemaBuilderWithEntry(schemaBuilder, entry, schemaFile.getName(), failOnError); } finally { Utils.closeSilently(reader); } } private void updateSchemaBuilderWithEntry(SchemaBuilder schemaBuilder, Entry schemaEntry, String schemaFile, boolean failOnError) throws ConfigException { // immediately overwrite these definitions which are already defined in the SDK core schema final boolean overwriteCoreSchemaDefinitions = CORE_SCHEMA_FILE.equals(schemaFile) || RFC_3112_SCHEMA_FILE.equals(schemaFile); updateSchemaBuilderWithEntry0(schemaBuilder, schemaEntry, schemaFile, overwriteCoreSchemaDefinitions); // check that the update is correct Collection warnings = schemaBuilder.toSchema().getWarnings(); if (!warnings.isEmpty()) { if (!overwriteCoreSchemaDefinitions) { // TODO: use correct message = warnings for schema file logger.warn(WARN_CONFIG_CONFLICTING_DEFINITIONS_IN_SCHEMA_FILE, schemaFile, warnings); // try to update again with overwriting updateSchemaBuilderWithEntry0(schemaBuilder, schemaEntry, schemaFile, true); warnings = schemaBuilder.toSchema().getWarnings(); if (!warnings.isEmpty()) { // TODO: use correct message: warnings for schema file with overwrite=true reportSchemaWarnings(WARN_CONFIG_CONFLICTING_DEFINITIONS_IN_SCHEMA_FILE.get(schemaFile, warnings), failOnError); } } else { // TODO: use correct message: warnings for schema file with overwrite=true reportSchemaWarnings(WARN_CONFIG_CONFLICTING_DEFINITIONS_IN_SCHEMA_FILE.get(schemaFile, warnings), failOnError); } } } private void updateSchemaBuilderWithEntry0(final SchemaBuilder schemaBuilder, final Entry schemaEntry, final String schemaFile, final boolean overwrite) { schemaBuilder.addSchema(schemaEntry, overwrite, new SchemaBuilderHook() { @Override public void beforeAddSyntax(Syntax.Builder builder) { builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME).extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); } @Override public void beforeAddObjectClass(ObjectClass.Builder builder) { builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME).extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); } @Override public void beforeAddNameForm(NameForm.Builder builder) { builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME).extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); } @Override public void beforeAddMatchingRuleUse(MatchingRuleUse.Builder builder) { builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME).extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); } @Override public void beforeAddMatchingRule(MatchingRule.Builder builder) { builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME).extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); } @Override public void beforeAddDitStructureRule(DITStructureRule.Builder builder) { builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME).extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); } @Override public void beforeAddDitContentRule(DITContentRule.Builder builder) { builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME).extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); } @Override public void beforeAddAttribute(Builder builder) { builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME).extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile); } }); } private void switchSchema(Schema newSchema) throws DirectoryException { rejectSchemaWithWarnings(newSchema); schemaNG = newSchema.asNonStrictSchema(); Schema.setDefaultSchema(schemaNG); } private void rejectSchemaWithWarnings(Schema newSchema) throws DirectoryException { Collection warnings = newSchema.getWarnings(); if (!warnings.isEmpty()) { throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, ERR_SCHEMA_HAS_WARNINGS.get(warnings.size(), Utils.joinAsString("; ", warnings))); } } private void reportSchemaWarnings(LocalizableMessage message, boolean failOnError) throws ConfigException { if (failOnError) { throw new ConfigException(message); } logger.error(message); } /** A file filter implementation that accepts only LDIF files. */ private static class SchemaFileFilter implements FilenameFilter { private static final String LDIF_SUFFIX = ".ldif"; @Override public boolean accept(File directory, String filename) { return filename.endsWith(LDIF_SUFFIX); } } /** Interface to update a schema provided a schema builder. */ public interface SchemaUpdater { /** * Returns an updated schema. * * @param builder * The builder on the current schema * @return the new schema */ Schema update(SchemaBuilder builder); } }