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}