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 2006-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2017 ForgeRock AS.
016 */
017package org.opends.server.core;
018
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.FileNotFoundException;
022import java.io.FilenameFilter;
023import java.io.IOException;
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029
030import org.forgerock.i18n.LocalizableMessage;
031import org.forgerock.i18n.slf4j.LocalizedLogger;
032import org.forgerock.opendj.config.server.ConfigException;
033import org.forgerock.opendj.ldap.Attribute;
034import org.forgerock.opendj.ldap.AttributeDescription;
035import org.forgerock.opendj.ldap.Entry;
036import org.forgerock.opendj.ldap.ModificationType;
037import org.forgerock.opendj.ldap.ResultCode;
038import org.forgerock.opendj.ldap.schema.AttributeType;
039import org.forgerock.opendj.ldap.schema.CoreSchema;
040import org.forgerock.opendj.ldap.schema.DITContentRule;
041import org.forgerock.opendj.ldap.schema.DITStructureRule;
042import org.forgerock.opendj.ldap.schema.MatchingRule;
043import org.forgerock.opendj.ldap.schema.MatchingRuleUse;
044import org.forgerock.opendj.ldap.schema.NameForm;
045import org.forgerock.opendj.ldap.schema.ObjectClass;
046import org.forgerock.opendj.ldap.schema.SchemaBuilder;
047import org.forgerock.opendj.ldap.schema.SchemaBuilder.SchemaBuilderHook;
048import org.forgerock.opendj.ldap.schema.Syntax;
049import org.forgerock.opendj.ldap.schema.AttributeType.Builder;
050import org.forgerock.opendj.ldif.LDIFEntryReader;
051import org.opends.server.types.DirectoryException;
052import org.opends.server.types.InitializationException;
053import org.opends.server.types.Modification;
054import org.opends.server.types.Schema;
055import org.opends.server.types.Schema.SchemaUpdater;
056
057import static org.forgerock.opendj.adapter.server3x.Converters.toAttribute;
058import static org.forgerock.opendj.ldap.schema.SchemaValidationPolicy.*;
059import static org.opends.messages.ConfigMessages.*;
060import static org.opends.server.util.StaticUtils.*;
061import static org.opends.server.util.ServerConstants.SCHEMA_PROPERTY_FILENAME;
062
063/**
064 * This class defines a utility that will be used to manage the interaction with
065 * the Directory Server schema.  It will be used to initially load all of the
066 * matching rules and attribute syntaxes that have been defined in the
067 * configuration, and will then read the actual schema definitions.  At present,
068 * only attribute types and objectclasses are supported in the schema config
069 * files.  Other components like DIT content rules, DIT structure rules, name
070 * forms, and matching rule use definitions will be ignored.
071 */
072public class SchemaConfigManager
073{
074  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
075
076  private static final String CORE_SCHEMA_FILE = "00-core.ldif";
077  private static final String RFC_3112_SCHEMA_FILE = "03-rfc3112.ldif";
078
079  /** The schema that has been parsed from the server configuration. */
080  private Schema schema;
081
082  private final ServerContext serverContext;
083
084  /**
085   * Creates a new instance of this schema config manager.
086   *
087   * @param serverContext
088   *            The server context.
089   */
090  public SchemaConfigManager(ServerContext serverContext)
091  {
092    this.serverContext = serverContext;
093    try
094    {
095      // the manager will build the schema from scratch, but we need to start from core schema for SDK schema
096      schema = new Schema(org.forgerock.opendj.ldap.schema.Schema.getCoreSchema());
097    }
098    catch (DirectoryException unexpected)
099    {
100      // the core schema should not have any warning
101      throw new RuntimeException(unexpected);
102    }
103  }
104
105  /**
106   * Retrieves the path to the directory containing the server schema files.
107   *
108   * @return  The path to the directory containing the server schema files.
109   */
110  public static String getSchemaDirectoryPath()
111  {
112    File schemaDir = DirectoryServer.getEnvironmentConfig().getSchemaDirectory();
113    return schemaDir != null ? schemaDir.getAbsolutePath() : null;
114  }
115
116  /**
117   * Retrieves a reference to the schema information that has been read from the server
118   * configuration.
119   * <p>
120   * Note that this information will not be complete until the {@link #initializeMatchingRules()},
121   * {@link #initializeAttributeSyntaxes()} methods have been called.
122   *
123   * @return A reference to the schema information that has been read from the server configuration.
124   */
125  public Schema getSchema()
126  {
127    return schema;
128  }
129
130  /**
131   * Initializes all the matching rules defined in the Directory Server
132   * configuration.  This should only be called at Directory Server startup.
133   *
134   * @throws  ConfigException  If a configuration problem causes the matching
135   *                           rule initialization process to fail.
136   *
137   * @throws  InitializationException  If a problem occurs while initializing
138   *                                   the matching rules that is not related to
139   *                                   the server configuration.
140   */
141  public void initializeMatchingRules()
142         throws ConfigException, InitializationException
143  {
144    MatchingRuleConfigManager matchingRuleConfigManager = new MatchingRuleConfigManager(serverContext);
145    matchingRuleConfigManager.initializeMatchingRules();
146  }
147
148  /**
149   * Initializes all the attribute syntaxes defined in the Directory Server
150   * configuration.  This should only be called at Directory Server startup.
151   *
152   * @throws  ConfigException  If a configuration problem causes the syntax
153   *                           initialization process to fail.
154   *
155   * @throws  InitializationException  If a problem occurs while initializing
156   *                                   the syntaxes that is not related to the
157   *                                   server configuration.
158   */
159  public void initializeAttributeSyntaxes()
160         throws ConfigException, InitializationException
161  {
162    AttributeSyntaxConfigManager syntaxConfigManager = new AttributeSyntaxConfigManager(serverContext);
163    syntaxConfigManager.initializeAttributeSyntaxes();
164  }
165
166  /** Filter implementation that accepts only ldif files. */
167  public static class SchemaFileFilter implements FilenameFilter
168  {
169    @Override
170    public boolean accept(File directory, String filename)
171    {
172      return filename.endsWith(".ldif");
173    }
174  }
175
176  /**
177   * Initializes all the attribute type, object class, name form, DIT content
178   * rule, DIT structure rule, and matching rule use definitions by reading the
179   * server schema files.  These files will be located in a single directory and
180   * will be processed in lexicographic order.  However, to make the order
181   * easier to understand, they may be prefixed with a two digit number (with a
182   * leading zero if necessary) so that they will be read in numeric order.
183   * This should only be called at Directory Server startup.
184   *
185   * @throws  ConfigException  If a configuration problem causes the schema
186   *                           element initialization to fail.
187   *
188   * @throws  InitializationException  If a problem occurs while initializing
189   *                                   the schema elements that is not related
190   *                                   to the server configuration.
191   */
192  public void initializeSchemaFromFiles()
193         throws ConfigException, InitializationException
194  {
195    // Construct the path to the directory that should contain the schema files
196    // and make sure that it exists and is a directory.  Get a list of the files
197    // in that directory sorted in alphabetic order.
198    String schemaInstanceDirPath  = getSchemaDirectoryPath();
199    File schemaInstanceDir = schemaInstanceDirPath != null ? new File(schemaInstanceDirPath) : null;
200    long oldestModificationTime   = -1L;
201    long youngestModificationTime = -1L;
202    List<String> fileNames;
203
204    try
205    {
206      if (schemaInstanceDir == null || !schemaInstanceDir.exists())
207      {
208        throw new InitializationException(ERR_CONFIG_SCHEMA_NO_SCHEMA_DIR.get(schemaInstanceDirPath));
209      }
210      if (!schemaInstanceDir.isDirectory())
211      {
212        throw new InitializationException(ERR_CONFIG_SCHEMA_DIR_NOT_DIRECTORY.get(schemaInstanceDirPath));
213      }
214
215      File[] schemaInstanceDirFiles = schemaInstanceDir.listFiles(new SchemaFileFilter());
216      fileNames = new ArrayList<>(schemaInstanceDirFiles.length);
217
218      for (File f : schemaInstanceDirFiles)
219      {
220        if (f.isFile())
221        {
222          fileNames.add(f.getName());
223        }
224
225        long modificationTime = f.lastModified();
226        if (oldestModificationTime <= 0L ||
227            modificationTime < oldestModificationTime)
228        {
229          oldestModificationTime = modificationTime;
230        }
231
232        if (youngestModificationTime <= 0 ||
233            modificationTime > youngestModificationTime)
234        {
235          youngestModificationTime = modificationTime;
236        }
237      }
238
239      Collections.sort(fileNames);
240    }
241    catch (InitializationException ie)
242    {
243      logger.traceException(ie);
244
245      throw ie;
246    }
247    catch (Exception e)
248    {
249      logger.traceException(e);
250
251      LocalizableMessage message = ERR_CONFIG_SCHEMA_CANNOT_LIST_FILES.get(
252          schemaInstanceDirPath, getExceptionMessage(e));
253      throw new InitializationException(message, e);
254    }
255
256    // If the oldest and youngest modification timestamps didn't get set for
257    // some reason, then set them to the current time.
258    if (oldestModificationTime <= 0)
259    {
260      oldestModificationTime = System.currentTimeMillis();
261    }
262
263    if (youngestModificationTime <= 0)
264    {
265      youngestModificationTime = oldestModificationTime;
266    }
267
268    schema.setOldestModificationTime(oldestModificationTime);
269    schema.setYoungestModificationTime(youngestModificationTime);
270
271    // Iterate through the schema files and read them as an LDIF file containing
272    // a single entry.  Then get the attributeTypes and objectClasses attributes
273    // from that entry and parse them to initialize the server schema.
274    Map<String, Attribute> extraAttrs = new HashMap<>();
275    for (String schemaFile : fileNames)
276    {
277      loadSchemaFile(schema, schemaFile, extraAttrs, false);
278    }
279    if (!extraAttrs.isEmpty())
280    {
281      for (String attrName : extraAttrs.keySet())
282      {
283        schema.addExtraAttribute(attrName, toAttribute(extraAttrs.get(attrName)));
284      }
285    }
286  }
287
288  /**
289   * Loads the contents of the specified schema file into the provided schema.
290   *
291   * @param  schema      The schema in which the contents of the schema file are
292   *                     to be loaded.
293   * @param  schemaFile  The name of the schema file to be loaded into the
294   *                     provided schema.
295   * @param extraAttrs   The map of extra attributes that will be completed by this method.
296   *                     Maybe {@code null} if loading extra attributes is not required.
297   * @throws  ConfigException  If a configuration problem causes the schema
298   *                           element initialization to fail.
299   * @throws  InitializationException  If a problem occurs while initializing
300   *                                   the schema elements that is not related
301   *                                   to the server configuration.
302   */
303  public static void loadSchemaFile(Schema schema,
304                                    Map<String, Attribute> extraAttrs,
305                                    String schemaFile)
306         throws ConfigException, InitializationException
307  {
308    loadSchemaFile(schema, schemaFile, extraAttrs, true);
309  }
310
311  /**
312   * Loads the contents of the specified schema file into the provided schema and returns the list
313   * of modifications.
314   *
315   * @param schema
316   *          The schema in which the contents of the schema file are to be loaded.
317   * @param schemaFile
318   *          The name of the schema file to be loaded into the provided schema.
319   * @param extraAttrs
320   *          The map of extra attributes that will be completed by this method.
321   *          Maybe {@code null} if loading extra attributes is not required.
322   * @return A list of the modifications that could be performed in order to obtain the contents of
323   *         the file.
324   * @throws ConfigException
325   *           If a configuration problem causes the schema element initialization to fail.
326   * @throws InitializationException
327   *           If a problem occurs while initializing the schema elements that is not related to the
328   *           server configuration.
329   */
330  public static List<Modification> loadSchemaFileReturnModifications(Schema schema,
331                                                                     String schemaFile,
332                                                                     Map<String, Attribute> extraAttrs)
333      throws ConfigException, InitializationException
334  {
335    final Entry entry = loadSchemaFile(schema, schemaFile, extraAttrs, true);
336    if (entry != null)
337    {
338      return createAddModifications(entry,
339          CoreSchema.getLDAPSyntaxesAttributeType(),
340          CoreSchema.getAttributeTypesAttributeType(),
341          CoreSchema.getObjectClassesAttributeType(),
342          CoreSchema.getNameFormsAttributeType(),
343          CoreSchema.getDITContentRulesAttributeType(),
344          CoreSchema.getDITStructureRulesAttributeType(),
345          CoreSchema.getMatchingRuleUseAttributeType());
346    }
347    return Collections.emptyList();
348  }
349
350  private static List<Modification> createAddModifications(Entry entry, AttributeType... attrTypes)
351  {
352    List<Modification> mods = new ArrayList<>(entry.getAttributeCount());
353    for (AttributeType attrType : attrTypes)
354    {
355      for (Attribute a : entry.getAllAttributes(AttributeDescription.create(attrType)))
356      {
357        mods.add(new Modification(ModificationType.ADD, toAttribute(a)));
358      }
359    }
360    return mods;
361  }
362
363  /**
364   * Loads the contents of the specified schema file into the provided schema.
365   *
366   * @param  schema       The schema in which the contents of the schema file
367   *                      are to be loaded.
368   * @param  schemaFile   The name of the schema file to be loaded into the
369   *                      provided schema.
370   * @param extraAttrs    The map of extra attributes that will be completed by this method.
371   *                      Maybe {@code null} if loading extra attributes is not required.
372   * @param  failOnError  If {@code true}, indicates that this method should
373   *                      throw an exception if certain kinds of errors occur.
374   *                      If {@code false}, indicates that this method should
375   *                      log an error message and return without an exception.
376   *                      This should only be {@code false} when called from
377   *                      {@code initializeSchemaFromFiles}.
378   * @return the schema entry that has been read from the schema file
379   * @throws  ConfigException  If a configuration problem causes the schema
380   *                           element initialization to fail.
381   * @throws  InitializationException  If a problem occurs while initializing
382   *                                   the schema elements that is not related
383   *                                   to the server configuration.
384   */
385  private static Entry loadSchemaFile(Schema schema, String schemaFile,
386                                      Map<String, Attribute> extraAttrs,
387                                      boolean failOnError)
388      throws ConfigException, InitializationException
389  {
390    final Entry entry = readSchemaEntryFromFile(schemaFile, failOnError);
391    if (entry != null)
392    {
393      updateSchemaWithEntry(schema, schemaFile, failOnError, entry);
394
395      if (extraAttrs != null)
396      {
397        for (Attribute attribute : entry.getAllAttributes())
398        {
399          final String oid = attribute.getAttributeDescription().getAttributeType().getOID();
400          if (!isSchemaAttribute(oid))
401          {
402            extraAttrs.put(
403                    attribute.getAttributeDescription().getAttributeType().getNameOrOID(), attribute);
404          }
405        }
406      }
407    }
408    return entry;
409  }
410
411  /**
412   * Checks if a given attribute oid corresponds to an attribute that is used by the definition of the schema.
413   *
414   * @param attrOid
415   *            The oid of the attribute to be checked.
416   * @return {@code true} if the attribute is part of the schema definition, false otherwise
417   */
418  public static boolean isSchemaAttribute(String attrOid)
419  {
420    return attrOid.equals(CoreSchema.getAttributeTypesAttributeType().getOID())
421            || attrOid.equals("attributetypes-oid")
422            || attrOid.equals(CoreSchema.getDITContentRulesAttributeType().getOID())
423            || attrOid.equals("ditcontentrules-oid")
424            || attrOid.equals(CoreSchema.getDITStructureRulesAttributeType().getOID())
425            || attrOid.equals("ditstructurerules-oid")
426            || attrOid.equals(CoreSchema.getLDAPSyntaxesAttributeType().getOID())
427            || attrOid.equals("ldapsyntaxes-oid")
428            || attrOid.equals(CoreSchema.getMatchingRulesAttributeType().getOID())
429            || attrOid.equals("matchingrules-oid")
430            || attrOid.equals(CoreSchema.getMatchingRuleUseAttributeType().getOID())
431            || attrOid.equals("matchingruleuse-oid")
432            || attrOid.equals(CoreSchema.getNameFormsAttributeType().getOID())
433            || attrOid.equals("nameforms-oid")
434            || attrOid.equals(CoreSchema.getNameFormDescriptionSyntax().getOID())
435            || attrOid.equals("nameformdescription-oid")
436            || attrOid.equals(CoreSchema.getObjectClassesAttributeType().getOID())
437            || attrOid.equals("objectclasses-oid")
438            || attrOid.equals(CoreSchema.getObjectClassAttributeType().getOID())
439            || attrOid.equals("objectclass-oid")
440            || attrOid.equals(CoreSchema.getCNAttributeType().getOID())
441            || attrOid.equals("cn-oid");
442  }
443
444  private static void updateSchemaWithEntry(Schema schema, String schemaFile, boolean failOnError,
445      final Entry schemaEntry) throws ConfigException
446  {
447    try
448    {
449      // immediately overwrite these definitions which are already defined in the SDK core schema
450      final boolean overwriteCoreSchemaDefinitions =
451          CORE_SCHEMA_FILE.equals(schemaFile) || RFC_3112_SCHEMA_FILE.equals(schemaFile);
452      updateSchema(schema, schemaFile, schemaEntry, overwriteCoreSchemaDefinitions);
453    }
454    catch (DirectoryException e)
455    {
456      if (e.getResultCode().equals(ResultCode.CONSTRAINT_VIOLATION))
457      {
458        // Register it with the schema. We will allow duplicates, with the
459        // later definition overriding any earlier definition, but we want
460        // to trap them and log a warning.
461        logger.warn(WARN_CONFIG_CONFLICTING_DEFINITIONS_IN_SCHEMA_FILE, schemaFile, e.getMessageObject());
462        try
463        {
464          updateSchema(schema, schemaFile, schemaEntry, true);
465        }
466        catch (DirectoryException e2)
467        {
468          // This should never happen
469          logger.traceException(e2);
470        }
471      }
472      else
473      {
474        reportError(failOnError, e,
475            WARN_CONFIG_SCHEMA_CANNOT_PARSE_DEFINITIONS_IN_SCHEMA_FILE.get(schemaFile, e.getMessageObject()));
476      }
477    }
478  }
479
480  private static Entry readSchemaEntryFromFile(String schemaFile, boolean failOnError)
481      throws ConfigException, InitializationException
482  {
483    // Create an LDIF reader to use when reading the files.
484    String schemaDirPath = getSchemaDirectoryPath();
485    File f = new File(schemaDirPath, schemaFile);
486    try (final FileInputStream in = new FileInputStream(f);
487        final LDIFEntryReader reader = new LDIFEntryReader(in))
488    {
489      reader.setSchemaValidationPolicy(ignoreAll());
490
491      if (!reader.hasNext())
492      {
493        // The file was empty -- skip it.
494        return null;
495      }
496      final Entry entry = reader.readEntry();
497      if (reader.hasNext())
498      {
499        // If there are any more entries in the file, then print a warning message.
500        logger.warn(WARN_CONFIG_SCHEMA_MULTIPLE_ENTRIES_IN_FILE, schemaFile, schemaDirPath);
501      }
502      return entry;
503    }
504    catch (FileNotFoundException e)
505    {
506      logger.traceException(e);
507
508      LocalizableMessage message =
509          WARN_CONFIG_SCHEMA_CANNOT_OPEN_FILE.get(schemaFile, schemaDirPath, getExceptionMessage(e));
510
511      if (failOnError)
512      {
513        throw new ConfigException(message);
514      }
515      logger.error(message);
516      return null;
517    }
518    catch (IOException e)
519    {
520      logger.traceException(e);
521
522      LocalizableMessage message =
523          WARN_CONFIG_SCHEMA_CANNOT_READ_LDIF_ENTRY.get(schemaFile, schemaDirPath, getExceptionMessage(e));
524
525      if (failOnError)
526      {
527        throw new InitializationException(message, e);
528      }
529      logger.error(message);
530      return null;
531    }
532  }
533
534    private static void updateSchema(Schema schema, final String schemaFile, final Entry schemaEntry,
535            final boolean overwrite) throws DirectoryException
536  {
537    schema.updateSchema(new SchemaUpdater()
538    {
539      @Override
540      public org.forgerock.opendj.ldap.schema.Schema update(SchemaBuilder builder)
541      {
542        return builder.addSchema(schemaEntry, overwrite, new SchemaBuilderHook() {
543            @Override
544            public void beforeAddSyntax(Syntax.Builder builder) {
545                builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME)
546                       .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile);
547            }
548            @Override
549            public void beforeAddObjectClass(ObjectClass.Builder builder) {
550                builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME)
551                       .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile);
552            }
553            @Override
554            public void beforeAddNameForm(NameForm.Builder builder) {
555                builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME)
556                       .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile);
557            }
558            @Override
559            public void beforeAddMatchingRuleUse(MatchingRuleUse.Builder builder) {
560                builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME)
561                       .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile);
562            }
563            @Override
564            public void beforeAddMatchingRule(MatchingRule.Builder builder) {
565                builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME)
566                       .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile);
567            }
568            @Override
569            public void beforeAddDitStructureRule(DITStructureRule.Builder builder) {
570                builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME)
571                       .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile);
572            }
573            @Override
574            public void beforeAddDitContentRule(DITContentRule.Builder builder) {
575                builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME)
576                       .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile);
577            }
578            @Override
579            public void beforeAddAttribute(Builder builder) {
580                builder.removeExtraProperty(SCHEMA_PROPERTY_FILENAME)
581                       .extraProperties(SCHEMA_PROPERTY_FILENAME, schemaFile);
582            }
583        }).toSchema();
584      }
585    });
586  }
587
588  private static void reportError(boolean failOnError, Exception e,
589      LocalizableMessage message) throws ConfigException
590  {
591    if (failOnError)
592    {
593      throw new ConfigException(message, e);
594    }
595    logger.error(message);
596  }
597}