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 2014-2016 ForgeRock AS. 015 */ 016package org.opends.server.core; 017 018import static org.forgerock.util.Utils.*; 019import static org.opends.messages.ConfigMessages.*; 020import static org.opends.server.replication.plugin.HistoricalCsnOrderingMatchingRuleImpl.*; 021import static org.opends.server.schema.AciSyntax.*; 022import static org.opends.server.schema.SubtreeSpecificationSyntax.*; 023import static org.opends.server.util.StaticUtils.*; 024 025import java.io.File; 026import java.io.FileReader; 027import java.io.FilenameFilter; 028import java.io.IOException; 029import java.util.ArrayList; 030import java.util.Collections; 031import java.util.List; 032 033import org.forgerock.i18n.LocalizableMessage; 034import org.forgerock.i18n.slf4j.LocalizedLogger; 035import org.forgerock.opendj.config.ClassPropertyDefinition; 036import org.forgerock.opendj.config.server.ConfigException; 037import org.forgerock.opendj.ldap.Entry; 038import org.forgerock.opendj.ldap.schema.Schema; 039import org.forgerock.opendj.ldap.schema.SchemaBuilder; 040import org.forgerock.opendj.ldif.EntryReader; 041import org.forgerock.opendj.ldif.LDIFEntryReader; 042import org.forgerock.opendj.server.config.meta.SchemaProviderCfgDefn; 043import org.forgerock.opendj.server.config.server.RootCfg; 044import org.forgerock.opendj.server.config.server.SchemaProviderCfg; 045import org.forgerock.util.Utils; 046import org.opends.server.schema.SchemaProvider; 047import org.opends.server.types.DirectoryException; 048import org.opends.server.types.InitializationException; 049import org.opends.server.types.Schema.SchemaUpdater; 050import org.opends.server.util.ActivateOnceSDKSchemaIsUsed; 051 052/** 053 * Responsible for loading the server schema. 054 * <p> 055 * The schema is loaded in three steps : 056 * <ul> 057 * <li>Start from the core schema.</li> 058 * <li>Load schema elements from the schema providers defined in configuration.</li> 059 * <li>Load all schema files located in the schema directory.</li> 060 * </ul> 061 */ 062@ActivateOnceSDKSchemaIsUsed 063public final class SchemaHandler 064{ 065 private static final String CORE_SCHEMA_PROVIDER_NAME = "Core Schema"; 066 067 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 068 069 private ServerContext serverContext; 070 071 private long oldestModificationTime = -1L; 072 073 private long youngestModificationTime = -1L; 074 075 /** 076 * Creates a new instance. 077 */ 078 public SchemaHandler() 079 { 080 // no implementation. 081 } 082 083 /** 084 * Initialize this schema handler. 085 * 086 * @param serverContext 087 * The server context. 088 * @throws ConfigException 089 * If a configuration problem arises in the process of performing 090 * the initialization. 091 * @throws InitializationException 092 * If a problem that is not configuration-related occurs during 093 * initialization. 094 */ 095 public void initialize(final ServerContext serverContext) throws InitializationException, ConfigException 096 { 097 this.serverContext = serverContext; 098 099 final org.opends.server.types.Schema schema = serverContext.getSchema(); 100 101 schema.exclusiveLock(); 102 try 103 { 104 // Start from the core schema (TODO: or start with empty schema and add core schema in core schema provider ?) 105 final SchemaBuilder schemaBuilder = new SchemaBuilder(Schema.getCoreSchema()); 106 107 // Take providers into account. 108 loadSchemaFromProviders(serverContext.getRootConfig(), schemaBuilder); 109 110 // Take schema files into account (TODO : or load files using provider mechanism ?) 111 completeSchemaFromFiles(schemaBuilder); 112 113 try 114 { 115 schema.updateSchema(new SchemaUpdater() 116 { 117 @Override 118 public Schema update(SchemaBuilder ignored) 119 { 120 // see RemoteSchemaLoader.readSchema() 121 addAciSyntax(schemaBuilder); 122 addSubtreeSpecificationSyntax(schemaBuilder); 123 addHistoricalCsnOrderingMatchingRule(schemaBuilder); 124 125 // Uses the builder incrementally updated instead of the default provided by the method. 126 // This is why it is necessary to explicitly lock/unlock the schema updater. 127 return schemaBuilder.toSchema(); 128 } 129 }); 130 } 131 catch (DirectoryException e) 132 { 133 throw new ConfigException(e.getMessageObject(), e); 134 } 135 } 136 finally 137 { 138 schema.exclusiveUnlock(); 139 } 140 } 141 142 /** 143 * Load the schema from provided root configuration. 144 * 145 * @param rootConfiguration 146 * The root to retrieve schema provider configurations. 147 * @param schemaBuilder 148 * The schema builder that providers should update. 149 * @param schemaUpdater 150 * The updater that providers should use when applying a configuration change. 151 */ 152 private void loadSchemaFromProviders(final RootCfg rootConfiguration, final SchemaBuilder schemaBuilder) 153 throws ConfigException, InitializationException { 154 for (final String name : rootConfiguration.listSchemaProviders()) 155 { 156 final SchemaProviderCfg config = rootConfiguration.getSchemaProvider(name); 157 if (config.isEnabled()) 158 { 159 loadSchemaProvider(config.getJavaClass(), config, schemaBuilder, true); 160 } 161 else if (name.equals(CORE_SCHEMA_PROVIDER_NAME)) 162 { 163 // TODO : use correct message ERR_CORE_SCHEMA_NOT_ENABLED 164 throw new ConfigException(LocalizableMessage.raw("Core Schema can't be disabled")); 165 } 166 } 167 } 168 169 /** 170 * Load the schema provider from the provided class name. 171 * <p> 172 * If {@code} initialize} is {@code true}, then the provider is initialized, 173 * and the provided schema builder is updated with schema elements from the provider. 174 */ 175 private <T extends SchemaProviderCfg> SchemaProvider<T> loadSchemaProvider(final String className, 176 final T config, final SchemaBuilder schemaBuilder, final boolean initialize) 177 throws InitializationException 178 { 179 try 180 { 181 final ClassPropertyDefinition propertyDef = SchemaProviderCfgDefn.getInstance().getJavaClassPropertyDefinition(); 182 final Class<? extends SchemaProvider> providerClass = propertyDef.loadClass(className, SchemaProvider.class); 183 final SchemaProvider<T> provider = providerClass.newInstance(); 184 185 if (initialize) 186 { 187 provider.initialize(serverContext, config, schemaBuilder); 188 } 189 else 190 { 191 final List<LocalizableMessage> unacceptableReasons = new ArrayList<>(); 192 if (!provider.isConfigurationAcceptable(config, unacceptableReasons)) 193 { 194 final String reasons = Utils.joinAsString(". ", unacceptableReasons); 195 // TODO : fix message, eg CONFIG SCHEMA PROVIDER CONFIG NOT ACCEPTABLE 196 throw new InitializationException(ERR_CONFIG_ALERTHANDLER_CONFIG_NOT_ACCEPTABLE.get(config.dn(), reasons)); 197 } 198 } 199 return provider; 200 } 201 catch (Exception e) 202 { 203 // TODO : fix message 204 throw new InitializationException(ERR_CONFIG_SCHEMA_SYNTAX_CANNOT_INITIALIZE.get( 205 className, config.dn(), stackTraceToSingleLineString(e)), e); 206 } 207 } 208 209 /** 210 * Retrieves the path to the directory containing the server schema files. 211 * 212 * @return The path to the directory containing the server schema files. 213 */ 214 private File getSchemaDirectoryPath() throws InitializationException 215 { 216 final File dir = serverContext.getEnvironment().getSchemaDirectory(); 217 if (dir == null) 218 { 219 throw new InitializationException(ERR_CONFIG_SCHEMA_NO_SCHEMA_DIR.get(null)); 220 } 221 if (!dir.exists()) 222 { 223 throw new InitializationException(ERR_CONFIG_SCHEMA_NO_SCHEMA_DIR.get(dir.getPath())); 224 } 225 if (!dir.isDirectory()) 226 { 227 throw new InitializationException(ERR_CONFIG_SCHEMA_DIR_NOT_DIRECTORY.get(dir.getPath())); 228 } 229 return dir; 230 } 231 232 /** Returns the LDIF reader on provided LDIF file. The caller must ensure the reader is closed. */ 233 private EntryReader getLDIFReader(final File ldifFile, final Schema schema) 234 throws InitializationException 235 { 236 try 237 { 238 final LDIFEntryReader reader = new LDIFEntryReader(new FileReader(ldifFile)); 239 reader.setSchema(schema); 240 return reader; 241 } 242 catch (Exception e) 243 { 244 // TODO : fix message 245 throw new InitializationException(ERR_CONFIG_FILE_CANNOT_OPEN_FOR_READ.get(ldifFile.getAbsolutePath(), e), e); 246 } 247 } 248 249 /** 250 * Complete the schema with schema files. 251 * 252 * @param schemaBuilder 253 * The schema builder to update with the content of the schema files. 254 * @throws ConfigException 255 * If a configuration problem causes the schema element 256 * initialization to fail. 257 * @throws InitializationException 258 * If a problem occurs while initializing the schema elements that 259 * is not related to the server configuration. 260 */ 261 private void completeSchemaFromFiles(final SchemaBuilder schemaBuilder) 262 throws ConfigException, InitializationException 263 { 264 final File schemaDirectory = getSchemaDirectoryPath(); 265 for (String schemaFile : getSchemaFileNames(schemaDirectory)) 266 { 267 loadSchemaFile(schemaFile, schemaBuilder, Schema.getDefaultSchema()); 268 } 269 } 270 271 /** Returns the list of names of schema files contained in the provided directory. */ 272 private List<String> getSchemaFileNames(final File schemaDirectory) throws InitializationException { 273 try 274 { 275 final File[] schemaFiles = schemaDirectory.listFiles(new SchemaFileFilter()); 276 final List<String> schemaFileNames = new ArrayList<>(schemaFiles.length); 277 278 for (final File f : schemaFiles) 279 { 280 if (f.isFile()) 281 { 282 schemaFileNames.add(f.getName()); 283 } 284 285 final long modificationTime = f.lastModified(); 286 if (oldestModificationTime <= 0L 287 || modificationTime < oldestModificationTime) 288 { 289 oldestModificationTime = modificationTime; 290 } 291 292 if (youngestModificationTime <= 0 293 || modificationTime > youngestModificationTime) 294 { 295 youngestModificationTime = modificationTime; 296 } 297 } 298 // If the oldest and youngest modification timestamps didn't get set 299 // then set them to the current time. 300 if (oldestModificationTime <= 0) 301 { 302 oldestModificationTime = System.currentTimeMillis(); 303 } 304 305 if (youngestModificationTime <= 0) 306 { 307 youngestModificationTime = oldestModificationTime; 308 } 309 Collections.sort(schemaFileNames); 310 return schemaFileNames; 311 } 312 catch (Exception e) 313 { 314 throw new InitializationException(ERR_CONFIG_SCHEMA_CANNOT_LIST_FILES 315 .get(schemaDirectory, getExceptionMessage(e)), e); 316 } 317 } 318 319 /** Returns the schema entry from the provided reader. */ 320 private Entry readSchemaEntry(final EntryReader reader, final File schemaFile) throws InitializationException { 321 try 322 { 323 Entry entry = null; 324 if (reader.hasNext()) 325 { 326 entry = reader.readEntry(); 327 if (reader.hasNext()) 328 { 329 // TODO : fix message 330 logger.warn(WARN_CONFIG_SCHEMA_MULTIPLE_ENTRIES_IN_FILE, schemaFile, ""); 331 } 332 return entry; 333 } 334 else 335 { 336 // TODO : fix message - should be SCHEMA NO LDIF ENTRY 337 throw new InitializationException(WARN_CONFIG_SCHEMA_CANNOT_READ_LDIF_ENTRY.get( 338 schemaFile, "", "")); 339 } 340 } 341 catch (IOException e) 342 { 343 // TODO : fix message 344 throw new InitializationException(WARN_CONFIG_SCHEMA_CANNOT_READ_LDIF_ENTRY.get( 345 schemaFile, "", getExceptionMessage(e)), e); 346 } 347 finally 348 { 349 closeSilently(reader); 350 } 351 } 352 353 /** 354 * Add the schema from the provided schema file to the provided schema 355 * builder. 356 * 357 * @param schemaFileName 358 * The name of the schema file to be loaded 359 * @param schemaBuilder 360 * The schema builder in which the contents of the schema file are to 361 * be loaded. 362 * @param readSchema 363 * The schema used to read the file. 364 * @throws InitializationException 365 * If a problem occurs while initializing the schema elements. 366 */ 367 private void loadSchemaFile(final String schemaFileName, final SchemaBuilder schemaBuilder, final Schema readSchema) 368 throws InitializationException 369 { 370 EntryReader reader = null; 371 try 372 { 373 File schemaFile = new File(getSchemaDirectoryPath(), schemaFileName); 374 reader = getLDIFReader(schemaFile, readSchema); 375 final Entry entry = readSchemaEntry(reader, schemaFile); 376 // TODO : there is no more file information attached to schema elements - we should add support for this 377 // in order to be able to redirect schema elements in the correct file when doing backups 378 schemaBuilder.addSchema(entry, true); 379 } 380 finally { 381 Utils.closeSilently(reader); 382 } 383 } 384 385 /** A file filter implementation that accepts only LDIF files. */ 386 private static class SchemaFileFilter implements FilenameFilter 387 { 388 private static final String LDIF_SUFFIX = ".ldif"; 389 390 @Override 391 public boolean accept(File directory, String filename) 392 { 393 return filename.endsWith(LDIF_SUFFIX); 394 } 395 } 396}