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