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 2008 Sun Microsystems, Inc.
015 * Portions Copyright 2013-2016 ForgeRock AS.
016 */
017package org.opends.server.core;
018
019import static org.opends.messages.CoreMessages.*;
020import static org.opends.server.config.ConfigConstants.*;
021import static org.opends.server.util.StaticUtils.*;
022
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.FileOutputStream;
026import java.util.Collection;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.Map.Entry;
030
031import org.forgerock.i18n.LocalizableMessage;
032import org.forgerock.i18n.slf4j.LocalizedLogger;
033import org.forgerock.opendj.io.ASN1;
034import org.forgerock.opendj.io.ASN1Reader;
035import org.forgerock.opendj.io.ASN1Writer;
036import org.forgerock.opendj.ldap.ByteString;
037import org.opends.server.api.CompressedSchema;
038import org.opends.server.types.DirectoryException;
039
040/**
041 * This class provides a default implementation of a compressed schema manager
042 * that will store the schema definitions in a binary file
043 * (config/schematokens.dat).
044 */
045public final class DefaultCompressedSchema extends CompressedSchema
046{
047  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
048
049  /** Synchronizes calls to save. */
050  private final Object saveLock = new Object();
051
052  /**
053   * Creates a new instance of this compressed schema manager.
054   *
055   * @param serverContext
056   *            The server context.
057   */
058  public DefaultCompressedSchema(ServerContext serverContext)
059  {
060    super(serverContext);
061    load();
062  }
063
064  @Override
065  protected void storeAttribute(final byte[] encodedAttribute,
066      final String attributeName, final Iterable<String> attributeOptions)
067      throws DirectoryException
068  {
069    save();
070  }
071
072  @Override
073  protected void storeObjectClasses(final byte[] encodedObjectClasses,
074      final Collection<String> objectClassNames) throws DirectoryException
075  {
076    save();
077  }
078
079  /** Loads the compressed schema information from disk. */
080  private void load()
081  {
082    // Determine the location of the compressed schema data file. It should
083    // be in the config directory with a name of "schematokens.dat". If that
084    // file doesn't exist, then don't do anything.
085    final String path = DirectoryServer.getInstanceRoot() + File.separator
086        + CONFIG_DIR_NAME + File.separator + COMPRESSED_SCHEMA_FILE_NAME;
087    if (!new File(path).exists())
088    {
089      return;
090    }
091
092    try (FileInputStream inputStream = new FileInputStream(path))
093    {
094      final ASN1Reader reader = ASN1.getReader(inputStream);
095
096      // The first element in the file should be a sequence of object class
097      // sets. Each object class set will itself be a sequence of octet
098      // strings, where the first one is the token and the remaining elements
099      // are the names of the associated object classes.
100      reader.readStartSequence();
101      while (reader.hasNextElement())
102      {
103        reader.readStartSequence();
104        final byte[] encodedObjectClasses = reader.readOctetString()
105            .toByteArray();
106        final List<String> objectClassNames = new LinkedList<>();
107        while (reader.hasNextElement())
108        {
109          objectClassNames.add(reader.readOctetStringAsString());
110        }
111        reader.readEndSequence();
112        loadObjectClasses(encodedObjectClasses, objectClassNames);
113      }
114      reader.readEndSequence();
115
116      // The second element in the file should be an integer element that holds
117      // the value to use to initialize the object class counter.
118      reader.readInteger(); // No longer used.
119
120      // The third element in the file should be a sequence of attribute
121      // description components. Each attribute description component will
122      // itself be a sequence of octet strings, where the first one is the
123      // token, the second is the attribute name, and all remaining elements are
124      // the attribute options.
125      reader.readStartSequence();
126      while (reader.hasNextElement())
127      {
128        reader.readStartSequence();
129        final byte[] encodedAttribute = reader.readOctetString().toByteArray();
130        final String attributeName = reader.readOctetStringAsString();
131        final List<String> attributeOptions = new LinkedList<>();
132        while (reader.hasNextElement())
133        {
134          attributeOptions.add(reader.readOctetStringAsString());
135        }
136        reader.readEndSequence();
137        loadAttribute(encodedAttribute, attributeName, attributeOptions);
138      }
139      reader.readEndSequence();
140
141      // The fourth element in the file should be an integer element that holds
142      // the value to use to initialize the attribute description counter.
143      reader.readInteger(); // No longer used.
144    }
145    catch (final Exception e)
146    {
147      logger.traceException(e);
148
149      // FIXME -- Should we do something else here?
150      throw new RuntimeException(e);
151    }
152  }
153
154  /**
155   * Writes the compressed schema information to disk.
156   *
157   * @throws DirectoryException
158   *           If a problem occurs while writing the updated information.
159   */
160  private void save() throws DirectoryException
161  {
162    synchronized (saveLock)
163    {
164      // Determine the location of the "live" compressed schema data file, and
165      // then append ".tmp" to get the name of the temporary file that we will use.
166      final String path = DirectoryServer.getInstanceRoot() + File.separator
167          + CONFIG_DIR_NAME + File.separator + COMPRESSED_SCHEMA_FILE_NAME;
168      final String tempPath = path + ".tmp";
169      try (FileOutputStream outputStream = new FileOutputStream(tempPath))
170      {
171        final ASN1Writer writer = ASN1.getWriter(outputStream);
172
173        // The first element in the file should be a sequence of object class
174        // sets. Each object class set will itself be a sequence of octet
175        // strings, where the first one is the token and the remaining elements
176        // are the names of the associated object classes.
177        writer.writeStartSequence();
178        int ocCounter = 1;
179        for (final Entry<byte[], Collection<String>> mapEntry :
180            getAllObjectClasses())
181        {
182          writer.writeStartSequence();
183          writer.writeOctetString(ByteString.wrap(mapEntry.getKey()));
184          final Collection<String> objectClassNames = mapEntry.getValue();
185          for (final String ocName : objectClassNames)
186          {
187            writer.writeOctetString(ocName);
188          }
189          writer.writeEndSequence();
190          ocCounter++;
191        }
192        writer.writeEndSequence();
193
194        // The second element in the file should be an integer element that
195        // holds the value to use to initialize the object class counter.
196        writer.writeInteger(ocCounter); // No longer used.
197
198        // The third element in the file should be a sequence of attribute
199        // description components. Each attribute description component will
200        // itself be a sequence of octet strings, where the first one is the
201        // token, the second is the attribute name, and all remaining elements
202        // are the attribute options.
203        writer.writeStartSequence();
204        int adCounter = 1;
205        for (final Entry<byte[], Entry<String, Iterable<String>>> mapEntry : getAllAttributes())
206        {
207          writer.writeStartSequence();
208          writer.writeOctetString(ByteString.wrap(mapEntry.getKey()));
209          writer.writeOctetString(mapEntry.getValue().getKey());
210          for (final String option : mapEntry.getValue().getValue())
211          {
212            writer.writeOctetString(option);
213          }
214          writer.writeEndSequence();
215          adCounter++;
216        }
217        writer.writeEndSequence();
218
219        // The fourth element in the file should be an integer element that
220        // holds the value to use to initialize the attribute description
221        // counter.
222        writer.writeInteger(adCounter); // No longer used.
223
224        // Close the writer and swing the temp file into place.
225        outputStream.close();
226        final File liveFile = new File(path);
227        final File tempFile = new File(tempPath);
228
229        if (liveFile.exists())
230        {
231          final File saveFile = new File(liveFile.getAbsolutePath() + ".save");
232          if (saveFile.exists())
233          {
234            saveFile.delete();
235          }
236          liveFile.renameTo(saveFile);
237        }
238        tempFile.renameTo(liveFile);
239      }
240      catch (final Exception e)
241      {
242        logger.traceException(e);
243
244        final LocalizableMessage message = ERR_COMPRESSEDSCHEMA_CANNOT_WRITE_UPDATED_DATA
245            .get(stackTraceToSingleLineString(e));
246        throw new DirectoryException(
247            DirectoryServer.getServerErrorResultCode(), message, e);
248      }
249    }
250  }
251}