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}