/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
* add the following below this CDDL HEADER, with the fields enclosed
* by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2006-2010 Sun Microsystems, Inc.
* Portions copyright 2012-2013 ForgeRock AS.
*/
package org.opends.server.backends.jeb;
import org.opends.messages.Message;
import static org.opends.server.core.DirectoryServer.getMaxInternalBufferSize;
import static org.opends.server.loggers.debug.DebugLogger.*;
import org.opends.server.loggers.debug.DebugTracer;
import static org.opends.messages.JebMessages.*;
import com.sleepycat.je.*;
import org.opends.server.types.*;
import org.opends.server.core.DirectoryServer;
import org.opends.server.protocols.asn1.ASN1Writer;
import org.opends.server.protocols.asn1.ASN1;
import org.opends.server.protocols.asn1.ASN1Reader;
import org.opends.server.protocols.asn1.ASN1Exception;
import org.opends.server.api.CompressedSchema;
import java.io.IOException;
import java.util.zip.DataFormatException;
/**
* Represents the database containing the LDAP entries. The database key is
* the entry ID and the value is the entry contents.
*
*/
public class ID2Entry extends DatabaseContainer
{
/**
* The tracer object for the debug logger.
*/
private static final DebugTracer TRACER = getTracer();
/**
* Parameters for compression and encryption.
*/
private DataConfig dataConfig;
/**
* Cached encoding buffers.
*/
private static final ThreadLocal<EntryCodec> ENTRY_CODEC_CACHE =
new ThreadLocal<EntryCodec>()
{
protected EntryCodec initialValue()
{
return new EntryCodec();
}
};
private static EntryCodec acquireEntryCodec()
{
EntryCodec codec = ENTRY_CODEC_CACHE.get();
if (codec.maxBufferSize != getMaxInternalBufferSize())
{
// Setting has changed, so recreate the codec.
codec = new EntryCodec();
ENTRY_CODEC_CACHE.set(codec);
}
return codec;
}
/**
* A cached set of ByteStringBuilder buffers and ASN1Writer used to encode
* entries.
*/
private static class EntryCodec
{
private static final int BUFFER_INIT_SIZE = 512;
private final ByteStringBuilder encodedBuffer = new ByteStringBuilder();
private final ByteStringBuilder entryBuffer = new ByteStringBuilder();
private final ByteStringBuilder compressedEntryBuffer =
new ByteStringBuilder();
private final ASN1Writer writer;
private final int maxBufferSize;
private EntryCodec()
{
this.maxBufferSize = getMaxInternalBufferSize();
this.writer = ASN1.getWriter(encodedBuffer, maxBufferSize);
}
private void release()
{
try
{
writer.close(); // Clears encodedBuffer as well.
}
catch (Exception ignored)
{
// Unreachable.
}
if (entryBuffer.capacity() < maxBufferSize)
{
entryBuffer.clear();
}
else
{
entryBuffer.clear(BUFFER_INIT_SIZE);
}
if (compressedEntryBuffer.capacity() < maxBufferSize)
{
compressedEntryBuffer.clear();
}
else
{
compressedEntryBuffer.clear(BUFFER_INIT_SIZE);
}
}
private Entry decode(ByteString bytes, CompressedSchema compressedSchema)
throws DirectoryException, ASN1Exception, LDAPException,
DataFormatException, IOException
{
// Get the format version.
byte formatVersion = bytes.byteAt(0);
if(formatVersion != JebFormat.FORMAT_VERSION)
{
Message message =
ERR_JEB_INCOMPATIBLE_ENTRY_VERSION.get(formatVersion);
throw new ASN1Exception(message);
}
// Read the ASN1 sequence.
ASN1Reader reader = ASN1.getReader(bytes.subSequence(1, bytes.length()));
reader.readStartSequence();
// See if it was compressed.
int uncompressedSize = (int)reader.readInteger();
if(uncompressedSize > 0)
{
// It was compressed.
reader.readOctetString(compressedEntryBuffer);
CryptoManager cryptoManager = DirectoryServer.getCryptoManager();
// TODO: Should handle the case where uncompress returns < 0
compressedEntryBuffer.uncompress(entryBuffer, cryptoManager,
uncompressedSize);
// Since we are used the cached buffers (ByteStringBuilders),
// the decoded attribute values will not refer back to the
// original buffer.
return Entry.decode(entryBuffer.asReader(), compressedSchema);
}
else
{
// Since we don't have to do any decompression, we can just decode
// the entry directly.
ByteString encodedEntry = reader.readOctetString();
return Entry.decode(encodedEntry.asReader(), compressedSchema);
}
}
private ByteString encodeCopy(Entry entry, DataConfig dataConfig)
throws DirectoryException
{
encodeVolatile(entry, dataConfig);
return encodedBuffer.toByteString();
}
private DatabaseEntry encodeInternal(Entry entry, DataConfig dataConfig)
throws DirectoryException
{
encodeVolatile(entry, dataConfig);
return new DatabaseEntry(encodedBuffer.getBackingArray(), 0,
encodedBuffer.length());
}
private void encodeVolatile(Entry entry, DataConfig dataConfig)
throws DirectoryException
{
// Encode the entry for later use.
entry.encode(entryBuffer, dataConfig.getEntryEncodeConfig());
// First write the DB format version byte.
encodedBuffer.append(JebFormat.FORMAT_VERSION);
try
{
// Then start the ASN1 sequence.
writer.writeStartSequence(JebFormat.TAG_DATABASE_ENTRY);
// Do optional compression.
CryptoManager cryptoManager = DirectoryServer.getCryptoManager();
if (dataConfig.isCompressed() && cryptoManager != null &&
entryBuffer.compress(compressedEntryBuffer, cryptoManager))
{
// Compression needed and successful.
writer.writeInteger(entryBuffer.length());
writer.writeOctetString(compressedEntryBuffer);
}
else
{
writer.writeInteger(0);
writer.writeOctetString(entryBuffer);
}
writer.writeEndSequence();
}
catch(IOException ioe)
{
// TODO: This should never happen with byte buffer.
if(debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, ioe);
}
}
}
}
/**
* Create a new ID2Entry object.
*
* @param name The name of the entry database.
* @param dataConfig The desired compression and encryption options for data
* stored in the entry database.
* @param env The JE Environment.
* @param entryContainer The entryContainer of the entry database.
* @throws DatabaseException If an error occurs in the JE database.
*
*/
ID2Entry(String name, DataConfig dataConfig,
Environment env, EntryContainer entryContainer)
throws DatabaseException
{
super(name, env, entryContainer);
this.dataConfig = dataConfig;
DatabaseConfig dbNodupsConfig = new DatabaseConfig();
if(env.getConfig().getReadOnly())
{
dbNodupsConfig.setReadOnly(true);
dbNodupsConfig.setAllowCreate(false);
dbNodupsConfig.setTransactional(false);
}
else if(!env.getConfig().getTransactional())
{
dbNodupsConfig.setAllowCreate(true);
dbNodupsConfig.setTransactional(false);
dbNodupsConfig.setDeferredWrite(true);
}
else
{
dbNodupsConfig.setAllowCreate(true);
dbNodupsConfig.setTransactional(true);
}
this.dbConfig = dbNodupsConfig;
}
/**
* Decodes an entry from its database representation.
* <p>
* An entry on disk is ASN1 encoded in this format:
*
* <pre>
* DatabaseEntry ::= [APPLICATION 0] IMPLICIT SEQUENCE {
* uncompressedSize INTEGER, -- A zero value means not compressed.
* dataBytes OCTET STRING -- Optionally compressed encoding of
* the data bytes.
* }
*
* ID2EntryValue ::= DatabaseEntry
* -- Where dataBytes contains an encoding of DirectoryServerEntry.
*
* DirectoryServerEntry ::= [APPLICATION 1] IMPLICIT SEQUENCE {
* dn LDAPDN,
* objectClasses SET OF LDAPString,
* userAttributes AttributeList,
* operationalAttributes AttributeList
* }
* </pre>
*
* @param bytes A byte array containing the encoded database value.
* @param compressedSchema The compressed schema manager to use when decoding.
* @return The decoded entry.
* @throws ASN1Exception If the data is not in the expected ASN.1 encoding
* format.
* @throws LDAPException If the data is not in the expected ASN.1 encoding
* format.
* @throws DataFormatException If an error occurs while trying to decompress
* compressed data.
* @throws DirectoryException If a Directory Server error occurs.
* @throws IOException if an error occurs while reading the ASN1 sequence.
*/
public static Entry entryFromDatabase(ByteString bytes,
CompressedSchema compressedSchema) throws DirectoryException,
ASN1Exception, LDAPException, DataFormatException, IOException
{
EntryCodec codec = acquireEntryCodec();
try
{
return codec.decode(bytes, compressedSchema);
}
finally
{
codec.release();
}
}
/**
* Encodes an entry to the raw database format, with optional compression.
*
* @param entry The entry to encode.
* @param dataConfig Compression and cryptographic options.
* @return A ByteSTring containing the encoded database value.
*
* @throws DirectoryException If a problem occurs while attempting to encode
* the entry.
*/
public static ByteString entryToDatabase(Entry entry, DataConfig dataConfig)
throws DirectoryException
{
EntryCodec codec = acquireEntryCodec();
try
{
return codec.encodeCopy(entry, dataConfig);
}
finally
{
codec.release();
}
}
/**
* Insert a record into the entry database.
*
* @param txn The database transaction or null if none.
* @param id The entry ID which forms the key.
* @param entry The LDAP entry.
* @return true if the entry was inserted, false if a record with that
* ID already existed.
* @throws DatabaseException If an error occurs in the JE database.
* @throws DirectoryException If a problem occurs while attempting to encode
* the entry.
*/
public boolean insert(Transaction txn, EntryID id, Entry entry)
throws DatabaseException, DirectoryException
{
DatabaseEntry key = id.getDatabaseEntry();
EntryCodec codec = acquireEntryCodec();
try
{
DatabaseEntry data = codec.encodeInternal(entry, dataConfig);
OperationStatus status = insert(txn, key, data);
return (status == OperationStatus.SUCCESS);
}
finally
{
codec.release();
}
}
/**
* Write a record in the entry database.
*
* @param txn The database transaction or null if none.
* @param id The entry ID which forms the key.
* @param entry The LDAP entry.
* @return true if the entry was written, false if it was not.
* @throws DatabaseException If an error occurs in the JE database.
* @throws DirectoryException If a problem occurs while attempting to encode
* the entry.
*/
public boolean put(Transaction txn, EntryID id, Entry entry)
throws DatabaseException, DirectoryException
{
DatabaseEntry key = id.getDatabaseEntry();
EntryCodec codec = acquireEntryCodec();
try
{
DatabaseEntry data = codec.encodeInternal(entry, dataConfig);
OperationStatus status = put(txn, key, data);
return (status == OperationStatus.SUCCESS);
}
finally
{
codec.release();
}
}
/**
* Write a pre-formatted record into the entry database.
*
* @param txn The database transaction or null if none.
* @param key The key containing a pre-formatted entry ID.
* @param data The data value containing a pre-formatted LDAP entry.
* @return true if the entry was written, false if it was not.
* @throws DatabaseException If an error occurs in the JE database.
*/
public OperationStatus put(Transaction txn, DatabaseEntry key,
DatabaseEntry data)
throws DatabaseException
{
return super.put(txn, key, data);
}
/**
* Remove a record from the entry database.
*
* @param txn The database transaction or null if none.
* @param id The entry ID which forms the key.
* @return true if the entry was removed, false if it was not.
* @throws DatabaseException If an error occurs in the JE database.
*/
public boolean remove(Transaction txn, EntryID id)
throws DatabaseException
{
DatabaseEntry key = id.getDatabaseEntry();
OperationStatus status = delete(txn, key);
if (status != OperationStatus.SUCCESS)
{
return false;
}
return true;
}
/**
* Fetch a record from the entry database.
*
* @param txn The database transaction or null if none.
* @param id The desired entry ID which forms the key.
* @param lockMode The JE locking mode to be used for the read.
* @return The requested entry, or null if there is no such record.
* @throws DirectoryException If a problem occurs while getting the entry.
* @throws DatabaseException If an error occurs in the JE database.
*/
public Entry get(Transaction txn, EntryID id, LockMode lockMode)
throws DirectoryException, DatabaseException
{
DatabaseEntry key = id.getDatabaseEntry();
DatabaseEntry data = new DatabaseEntry();
OperationStatus status;
status = read(txn, key, data, lockMode);
if (status != OperationStatus.SUCCESS)
{
return null;
}
try
{
Entry entry = entryFromDatabase(ByteString.wrap(data.getData()),
entryContainer.getRootContainer().getCompressedSchema());
entry.processVirtualAttributes();
return entry;
}
catch (Exception e)
{
Message message = ERR_JEB_ENTRY_DATABASE_CORRUPT.get(id.toString());
throw new DirectoryException(
DirectoryServer.getServerErrorResultCode(), message);
}
}
/**
* Set the desired compression and encryption options for data
* stored in the entry database.
*
* @param dataConfig The desired compression and encryption options for data
* stored in the entry database.
*/
public void setDataConfig(DataConfig dataConfig)
{
this.dataConfig = dataConfig;
}
}