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}