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