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 2006-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2012-2015 ForgeRock AS.
026 */
027package org.opends.server.backends.jeb;
028
029import static com.sleepycat.je.OperationStatus.*;
030
031import static org.forgerock.util.Utils.*;
032import static org.opends.messages.BackendMessages.*;
033import static org.opends.server.core.DirectoryServer.*;
034
035import java.io.IOException;
036import java.io.OutputStream;
037import java.util.zip.DataFormatException;
038import java.util.zip.DeflaterOutputStream;
039import java.util.zip.InflaterOutputStream;
040
041import org.forgerock.i18n.slf4j.LocalizedLogger;
042import org.forgerock.opendj.io.ASN1;
043import org.forgerock.opendj.io.ASN1Reader;
044import org.forgerock.opendj.io.ASN1Writer;
045import org.forgerock.opendj.ldap.ByteString;
046import org.forgerock.opendj.ldap.ByteStringBuilder;
047import org.forgerock.opendj.ldap.DecodeException;
048import org.opends.server.api.CompressedSchema;
049import org.opends.server.core.DirectoryServer;
050import org.opends.server.types.DirectoryException;
051import org.opends.server.types.Entry;
052import org.opends.server.types.LDAPException;
053
054import com.sleepycat.je.*;
055
056/**
057 * Represents the database containing the LDAP entries. The database key is
058 * the entry ID and the value is the entry contents.
059 */
060public class ID2Entry extends DatabaseContainer
061{
062  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
063
064  /** Parameters for compression and encryption. */
065  private DataConfig dataConfig;
066
067  /** Cached encoding buffers. */
068  private static final ThreadLocal<EntryCodec> ENTRY_CODEC_CACHE = new ThreadLocal<EntryCodec>()
069  {
070    @Override
071    protected EntryCodec initialValue()
072    {
073      return new EntryCodec();
074    }
075  };
076
077  private static EntryCodec acquireEntryCodec()
078  {
079    EntryCodec codec = ENTRY_CODEC_CACHE.get();
080    if (codec.maxBufferSize != getMaxInternalBufferSize())
081    {
082      // Setting has changed, so recreate the codec.
083      codec = new EntryCodec();
084      ENTRY_CODEC_CACHE.set(codec);
085    }
086    return codec;
087  }
088
089  /**
090   * A cached set of ByteStringBuilder buffers and ASN1Writer used to encode
091   * entries.
092   */
093  private static final class EntryCodec
094  {
095    private static final int BUFFER_INIT_SIZE = 512;
096
097    private final ByteStringBuilder encodedBuffer = new ByteStringBuilder();
098    private final ByteStringBuilder entryBuffer = new ByteStringBuilder();
099    private final ByteStringBuilder compressedEntryBuffer = new ByteStringBuilder();
100    private final ASN1Writer writer;
101    private final int maxBufferSize;
102
103    private EntryCodec()
104    {
105      this.maxBufferSize = getMaxInternalBufferSize();
106      this.writer = ASN1.getWriter(encodedBuffer, maxBufferSize);
107    }
108
109    private void release()
110    {
111      closeSilently(writer);
112      encodedBuffer.clearAndTruncate(maxBufferSize, BUFFER_INIT_SIZE);
113      entryBuffer.clearAndTruncate(maxBufferSize, BUFFER_INIT_SIZE);
114      compressedEntryBuffer.clearAndTruncate(maxBufferSize, BUFFER_INIT_SIZE);
115    }
116
117    private Entry decode(ByteString bytes, CompressedSchema compressedSchema)
118        throws DirectoryException, DecodeException, LDAPException,
119        DataFormatException, IOException
120    {
121      // Get the format version.
122      byte formatVersion = bytes.byteAt(0);
123      if(formatVersion != JebFormat.FORMAT_VERSION)
124      {
125        throw DecodeException.error(ERR_INCOMPATIBLE_ENTRY_VERSION.get(formatVersion));
126      }
127
128      // Read the ASN1 sequence.
129      ASN1Reader reader = ASN1.getReader(bytes.subSequence(1, bytes.length()));
130      reader.readStartSequence();
131
132      // See if it was compressed.
133      int uncompressedSize = (int)reader.readInteger();
134      if(uncompressedSize > 0)
135      {
136        // It was compressed.
137        reader.readOctetString(compressedEntryBuffer);
138
139        OutputStream decompressor = null;
140        try
141        {
142          // TODO: Should handle the case where uncompress fails
143          decompressor = new InflaterOutputStream(entryBuffer.asOutputStream());
144          compressedEntryBuffer.copyTo(decompressor);
145        }
146        finally {
147          closeSilently(decompressor);
148        }
149
150        // Since we are used the cached buffers (ByteStringBuilders),
151        // the decoded attribute values will not refer back to the
152        // original buffer.
153        return Entry.decode(entryBuffer.asReader(), compressedSchema);
154      }
155      else
156      {
157        // Since we don't have to do any decompression, we can just decode
158        // the entry directly.
159        ByteString encodedEntry = reader.readOctetString();
160        return Entry.decode(encodedEntry.asReader(), compressedSchema);
161      }
162    }
163
164    private ByteString encodeCopy(Entry entry, DataConfig dataConfig)
165        throws DirectoryException
166    {
167      encodeVolatile(entry, dataConfig);
168      return encodedBuffer.toByteString();
169    }
170
171    private DatabaseEntry encodeInternal(Entry entry, DataConfig dataConfig)
172        throws DirectoryException
173    {
174      encodeVolatile(entry, dataConfig);
175      return new DatabaseEntry(encodedBuffer.getBackingArray(), 0, encodedBuffer.length());
176    }
177
178    private void encodeVolatile(Entry entry, DataConfig dataConfig) throws DirectoryException
179    {
180      // Encode the entry for later use.
181      entry.encode(entryBuffer, dataConfig.getEntryEncodeConfig());
182
183      // First write the DB format version byte.
184      encodedBuffer.append(JebFormat.FORMAT_VERSION);
185
186      try
187      {
188        // Then start the ASN1 sequence.
189        writer.writeStartSequence(JebFormat.TAG_DATABASE_ENTRY);
190
191        if (dataConfig.isCompressed())
192        {
193          OutputStream compressor = null;
194          try {
195            compressor = new DeflaterOutputStream(compressedEntryBuffer.asOutputStream());
196            entryBuffer.copyTo(compressor);
197          }
198          finally {
199            closeSilently(compressor);
200          }
201
202          // Compression needed and successful.
203          writer.writeInteger(entryBuffer.length());
204          writer.writeOctetString(compressedEntryBuffer);
205        }
206        else
207        {
208          writer.writeInteger(0);
209          writer.writeOctetString(entryBuffer);
210        }
211
212        writer.writeEndSequence();
213      }
214      catch(IOException ioe)
215      {
216        // TODO: This should never happen with byte buffer.
217        logger.traceException(ioe);
218      }
219    }
220  }
221
222  /**
223   * Create a new ID2Entry object.
224   *
225   * @param name The name of the entry database.
226   * @param dataConfig The desired compression and encryption options for data
227   * stored in the entry database.
228   * @param env The JE Environment.
229   * @param entryContainer The entryContainer of the entry database.
230   * @throws DatabaseException If an error occurs in the JE database.
231   *
232   */
233  ID2Entry(String name, DataConfig dataConfig, Environment env, EntryContainer entryContainer)
234      throws DatabaseException
235  {
236    super(name, env, entryContainer);
237    this.dataConfig = dataConfig;
238    this.dbConfig = JEBUtils.toDatabaseConfigNoDuplicates(env);
239  }
240
241  /**
242   * Decodes an entry from its database representation.
243   * <p>
244   * An entry on disk is ASN1 encoded in this format:
245   *
246   * <pre>
247   * DatabaseEntry ::= [APPLICATION 0] IMPLICIT SEQUENCE {
248   *  uncompressedSize      INTEGER,      -- A zero value means not compressed.
249   *  dataBytes             OCTET STRING  -- Optionally compressed encoding of
250   *                                         the data bytes.
251   * }
252   *
253   * ID2EntryValue ::= DatabaseEntry
254   *  -- Where dataBytes contains an encoding of DirectoryServerEntry.
255   *
256   * DirectoryServerEntry ::= [APPLICATION 1] IMPLICIT SEQUENCE {
257   *  dn                      LDAPDN,
258   *  objectClasses           SET OF LDAPString,
259   *  userAttributes          AttributeList,
260   *  operationalAttributes   AttributeList
261   * }
262   * </pre>
263   *
264   * @param bytes A byte array containing the encoded database value.
265   * @param compressedSchema The compressed schema manager to use when decoding.
266   * @return The decoded entry.
267   * @throws DecodeException If the data is not in the expected ASN.1 encoding
268   * format.
269   * @throws LDAPException If the data is not in the expected ASN.1 encoding
270   * format.
271   * @throws DataFormatException If an error occurs while trying to decompress
272   * compressed data.
273   * @throws DirectoryException If a Directory Server error occurs.
274   * @throws IOException if an error occurs while reading the ASN1 sequence.
275   */
276  public static Entry entryFromDatabase(ByteString bytes,
277      CompressedSchema compressedSchema) throws DirectoryException,
278      DecodeException, LDAPException, DataFormatException, IOException
279  {
280    EntryCodec codec = acquireEntryCodec();
281    try
282    {
283      return codec.decode(bytes, compressedSchema);
284    }
285    finally
286    {
287      codec.release();
288    }
289  }
290
291  /**
292   * Encodes an entry to the raw database format, with optional compression.
293   *
294   * @param entry The entry to encode.
295   * @param dataConfig Compression and cryptographic options.
296   * @return A ByteSTring containing the encoded database value.
297   *
298   * @throws  DirectoryException  If a problem occurs while attempting to encode
299   *                              the entry.
300   */
301  static ByteString entryToDatabase(Entry entry, DataConfig dataConfig) throws DirectoryException
302  {
303    EntryCodec codec = acquireEntryCodec();
304    try
305    {
306      return codec.encodeCopy(entry, dataConfig);
307    }
308    finally
309    {
310      codec.release();
311    }
312  }
313
314
315
316  /**
317   * Insert a record into the entry database.
318   *
319   * @param txn The database transaction or null if none.
320   * @param id The entry ID which forms the key.
321   * @param entry The LDAP entry.
322   * @return true if the entry was inserted, false if a record with that
323   *         ID already existed.
324   * @throws DatabaseException If an error occurs in the JE database.
325   * @throws  DirectoryException  If a problem occurs while attempting to encode
326   *                              the entry.
327   */
328  boolean insert(Transaction txn, EntryID id, Entry entry)
329       throws DatabaseException, DirectoryException
330  {
331    DatabaseEntry key = id.getDatabaseEntry();
332    EntryCodec codec = acquireEntryCodec();
333    try
334    {
335      DatabaseEntry data = codec.encodeInternal(entry, dataConfig);
336      return insert(txn, key, data) == SUCCESS;
337    }
338    finally
339    {
340      codec.release();
341    }
342  }
343
344  /**
345   * Write a record in the entry database.
346   *
347   * @param txn The database transaction or null if none.
348   * @param id The entry ID which forms the key.
349   * @param entry The LDAP entry.
350   * @return true if the entry was written, false if it was not.
351   * @throws DatabaseException If an error occurs in the JE database.
352   * @throws  DirectoryException  If a problem occurs while attempting to encode
353   *                              the entry.
354   */
355  public boolean put(Transaction txn, EntryID id, Entry entry)
356       throws DatabaseException, DirectoryException
357  {
358    DatabaseEntry key = id.getDatabaseEntry();
359    EntryCodec codec = acquireEntryCodec();
360    try
361    {
362      DatabaseEntry data = codec.encodeInternal(entry, dataConfig);
363      return put(txn, key, data) == SUCCESS;
364    }
365    finally
366    {
367      codec.release();
368    }
369  }
370
371  /**
372   * Remove a record from the entry database.
373   *
374   * @param txn The database transaction or null if none.
375   * @param id The entry ID which forms the key.
376   * @return true if the entry was removed, false if it was not.
377   * @throws DatabaseException If an error occurs in the JE database.
378   */
379  boolean remove(Transaction txn, EntryID id) throws DatabaseException
380  {
381    DatabaseEntry key = id.getDatabaseEntry();
382    return delete(txn, key) == SUCCESS;
383  }
384
385  /**
386   * Fetch a record from the entry database.
387   *
388   * @param txn The database transaction or null if none.
389   * @param id The desired entry ID which forms the key.
390   * @param lockMode The JE locking mode to be used for the read.
391   * @return The requested entry, or null if there is no such record.
392   * @throws DirectoryException If a problem occurs while getting the entry.
393   * @throws DatabaseException If an error occurs in the JE database.
394   */
395  public Entry get(Transaction txn, EntryID id, LockMode lockMode)
396       throws DirectoryException, DatabaseException
397  {
398    DatabaseEntry key = id.getDatabaseEntry();
399    DatabaseEntry data = new DatabaseEntry();
400
401    if (read(txn, key, data, lockMode) != SUCCESS)
402    {
403      return null;
404    }
405
406    try
407    {
408      Entry entry = entryFromDatabase(ByteString.wrap(data.getData()),
409          entryContainer.getRootContainer().getCompressedSchema());
410      entry.processVirtualAttributes();
411      return entry;
412    }
413    catch (Exception e)
414    {
415      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), ERR_ENTRY_DATABASE_CORRUPT.get(id));
416    }
417  }
418
419  /**
420   * Set the desired compression and encryption options for data
421   * stored in the entry database.
422   *
423   * @param dataConfig The desired compression and encryption options for data
424   * stored in the entry database.
425   */
426  public void setDataConfig(DataConfig dataConfig)
427  {
428    this.dataConfig = dataConfig;
429  }
430}