/* * 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 * * * Portions Copyright 2006 Sun Microsystems, Inc. */ package org.opends.server.backends.jeb; import com.sleepycat.je.Cursor; import com.sleepycat.je.CursorConfig; import com.sleepycat.je.Database; import com.sleepycat.je.DatabaseConfig; import com.sleepycat.je.DatabaseEntry; import com.sleepycat.je.DatabaseException; import com.sleepycat.je.LockMode; import com.sleepycat.je.OperationStatus; import com.sleepycat.je.Transaction; import org.opends.server.core.DirectoryException; import org.opends.server.core.DirectoryServer; import org.opends.server.core.SearchOperation; import org.opends.server.protocols.asn1.ASN1OctetString; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeType; import org.opends.server.types.AttributeValue; import org.opends.server.types.DN; import org.opends.server.types.Entry; import org.opends.server.types.LDAPURL; import org.opends.server.types.Modification; import org.opends.server.types.ResultCode; import org.opends.server.types.SearchResultReference; import org.opends.server.types.SearchScope; import org.opends.server.util.StaticUtils; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import static org.opends.server.util.ServerConstants.ATTR_REFERRAL_URL; import static org.opends.server.loggers.Debug.debugException; import static org.opends.server.messages.JebMessages. MSGID_JEB_REFERRAL_RESULT_MESSAGE; import static org.opends.server.messages.MessageHandler.getMessage; /** * This class represents the referral database which contains URIs from referral * entries. The key is the DN of the referral entry and the value is that of a * labeled URI in the ref attribute for that entry. Duplicate keys are permitted * since a referral entry can contain multiple values of the ref attribute. Key * order is the same as in the DN database so that all referrals in a subtree * can be retrieved by cursoring through a range of the records. */ public class DN2URI { /** * The fully-qualified name of this class for debugging purposes. */ private static final String CLASS_NAME = "org.opends.server.backends.jeb.EntryContainer"; /** * The standard attribute type that is used to specify the set of referral * URLs in a referral entry. */ public AttributeType referralType = DirectoryServer.getAttributeType(ATTR_REFERRAL_URL); /** * The database container. */ private Container container; /** * The JE database configuration. */ private DatabaseConfig dbConfig; /** * The name of the database within the container. */ private String name; /** * A custom btree key comparator for the JE database. */ Comparator comparator = new EntryContainer.KeyReverseComparator(); /** * A cached per-thread JE database handle. */ private ThreadLocal threadLocalDatabase = new ThreadLocal(); /** * Create a new object representing a referral database in a given container. * * @param container The container of the referral database. * @param dbConfig The JE database configuration which will be used to * open the database. * @param name The name of the referral database. */ public DN2URI(Container container, DatabaseConfig dbConfig, String name) { this.container = container; this.dbConfig = dbConfig; this.name = name; } /** * Open the referral database. * * @throws DatabaseException If an error occurs in the JE database. */ public void open() throws DatabaseException { getDatabase(); } /** * Get a handle to the database. It returns a per-thread handle to avoid * any thread contention on the database handle. The container is * responsible for closing all handles. * * @return A database handle. * * @throws DatabaseException If an error occurs in the JE database. */ private Database getDatabase() throws DatabaseException { Database database = threadLocalDatabase.get(); if (database == null) { database = container.openDatabase(dbConfig, name); threadLocalDatabase.set(database); } return database; } /** * Insert a URI value in the referral database. * * @param txn A database transaction used for the update, or null if none is * required. * @param dn The DN of the referral entry. * @param labeledURI The labeled URI value of the ref attribute. * @return true if the record was inserted, false if it was not. * @throws DatabaseException If an error occurs in the JE database. */ public boolean insert(Transaction txn, DN dn, String labeledURI) throws DatabaseException { byte[] normDN = StaticUtils.getBytes(dn.toNormalizedString()); byte[] URIBytes = StaticUtils.getBytes(labeledURI); DatabaseEntry key = new DatabaseEntry(normDN); DatabaseEntry data = new DatabaseEntry(URIBytes); OperationStatus status; // The JE insert method does not permit duplicate keys so we must use the // put method. status = Container.put(getDatabase(), txn, key, data); if (status != OperationStatus.SUCCESS) { return false; } return true; } /** * Delete URI values for a given referral entry from the referral database. * * @param txn A database transaction used for the update, or null if none is * required. * @param dn The DN of the referral entry for which URI values are to be * deleted. * @return true if the values were deleted, false if not. * @throws DatabaseException If an error occurs in the JE database. */ public boolean delete(Transaction txn, DN dn) throws DatabaseException { byte[] normDN = StaticUtils.getBytes(dn.toNormalizedString()); DatabaseEntry key = new DatabaseEntry(normDN); OperationStatus status; status = Container.delete(getDatabase(), txn, key); if (status != OperationStatus.SUCCESS) { return false; } return true; } /** * Delete a single URI value from the referral database. * @param txn A database transaction used for the update, or null if none is * required. * @param dn The DN of the referral entry. * @param labeledURI The URI value to be deleted. * @return true if the value was deleted, false if not. * @throws DatabaseException If an error occurs in the JE database. */ public boolean delete(Transaction txn, DN dn, String labeledURI) throws DatabaseException { CursorConfig cursorConfig = null; byte[] normDN = StaticUtils.getBytes(dn.toNormalizedString()); byte[] URIBytes = StaticUtils.getBytes(labeledURI); DatabaseEntry key = new DatabaseEntry(normDN); DatabaseEntry data = new DatabaseEntry(URIBytes); OperationStatus status; Cursor cursor = getDatabase().openCursor(txn, cursorConfig); try { status = cursor.getSearchBoth(key, data, null); if (status == OperationStatus.SUCCESS) { status = cursor.delete(); } } finally { cursor.close(); } if (status != OperationStatus.SUCCESS) { return false; } return true; } /** * Update the referral database for an entry that has been modified. Does * not do anything unless the entry before the modification or the entry after * the modification is a referral entry. * * @param txn A database transaction used for the update, or null if none is * required. * @param before The entry before the modifications have been applied. * @param after The entry after the modifications have been applied. * @param mods The sequence of modifications made to the entry. * @throws DatabaseException If an error occurs in the JE database. */ public void modifyEntry(Transaction txn, Entry before, Entry after, List mods) throws DatabaseException { DN entryDN = before.getDN(); for (Modification mod : mods) { Attribute modAttr = mod.getAttribute(); AttributeType modAttrType = modAttr.getAttributeType(); if (modAttrType.equals(referralType)) { Attribute a = mod.getAttribute(); switch (mod.getModificationType()) { case ADD: if (a != null) { for (AttributeValue v : a.getValues()) { insert(txn, entryDN, v.getStringValue()); } } break; case DELETE: if (a == null || !a.hasValue()) { delete(txn, entryDN); } else { for (AttributeValue v : a.getValues()) { delete(txn, entryDN, v.getStringValue()); } } break; case INCREMENT: // Nonsensical. break; case REPLACE: delete(txn, entryDN); if (a != null) { for (AttributeValue v : a.getValues()) { insert(txn, entryDN, v.getStringValue()); } } break; } } } } /** * Update the referral database for an entry that has been replaced. Does * not do anything unless the entry before it was replaced or the entry after * it was replaced is a referral entry. * * @param txn A database transaction used for the update, or null if none is * required. * @param before The entry before it was replaced. * @param after The entry after it was replaced. * @throws DatabaseException If an error occurs in the JE database. */ public void replaceEntry(Transaction txn, Entry before, Entry after) throws DatabaseException { deleteEntry(txn, before); addEntry(txn, after); } /** * Update the referral database for a new entry. Does nothing if the entry * is not a referral entry. * @param txn A database transaction used for the update, or null if none is * required. * @param entry The entry to be added. * @throws DatabaseException If an error occurs in the JE database. */ public void addEntry(Transaction txn, Entry entry) throws DatabaseException { Set labeledURIs = entry.getReferralURLs(); if (labeledURIs != null) { DN dn = entry.getDN(); for (String labeledURI : labeledURIs) { insert(txn, dn, labeledURI); } } } /** * Update the referral database for a deleted entry. Does nothing if the entry * was not a referral entry. * @param txn A database transaction used for the update, or null if none is * required. * @param entry The entry to be deleted. * @throws DatabaseException If an error occurs in the JE database. */ public void deleteEntry(Transaction txn, Entry entry) throws DatabaseException { Set labeledURIs = entry.getReferralURLs(); if (labeledURIs != null) { delete(txn, entry.getDN()); } } /** * Checks whether the target of an operation is a referral entry and throws * a Directory referral exception if it is. * @param entry The target entry of the operation, or the base entry of a * search operation. * @param searchScope The scope of the search operation, or null if the * operation is not a search operation. * @throws DirectoryException If a referral is found at or above the target * DN. The referral URLs will be set appropriately for the references found * in the referral entry. */ public void checkTargetForReferral(Entry entry, SearchScope searchScope) throws DirectoryException { Set referralURLs = entry.getReferralURLs(); if (referralURLs != null) { throwReferralException(entry.getDN(), entry.getDN(), referralURLs, searchScope); } } /** * Throws a Directory referral exception for the case where a referral entry * exists at or above the target DN of an operation. * @param targetDN The target DN of the operation, or the base object of a * search operation. * @param referralDN The DN of the referral entry. * @param labeledURIs The set of labeled URIs in the referral entry. * @param searchScope The scope of the search operation, or null if the * operation is not a search operation. * @throws DirectoryException If a referral is found at or above the target * DN. The referral URLs will be set appropriately for the references found * in the referral entry. */ public void throwReferralException(DN targetDN, DN referralDN, Set labeledURIs, SearchScope searchScope) throws DirectoryException { ArrayList URIList = new ArrayList(labeledURIs.size()); for (String labeledURI : labeledURIs) { // Remove the label part of the labeled URI if there is a label. String uri = labeledURI; int i = labeledURI.indexOf(' '); if (i != -1) { uri = labeledURI.substring(0, i); } try { LDAPURL ldapurl = LDAPURL.decode(uri, false); if (ldapurl.getScheme().equalsIgnoreCase("ldap")) { DN urlBaseDN = targetDN; if (!referralDN.equals(ldapurl.getBaseDN())) { urlBaseDN = EntryContainer.modDN(targetDN, referralDN.getRDNComponents().length, ldapurl.getBaseDN()); } ldapurl.setBaseDN(urlBaseDN); if (searchScope == null) { // RFC 3296, 5.2. Target Object Considerations: // In cases where the URI to be returned is a LDAP URL, the server // SHOULD trim any present scope, filter, or attribute list from the // URI before returning it. Critical extensions MUST NOT be trimmed // or modified. StringBuilder builder = new StringBuilder(uri.length()); ldapurl.toString(builder, true); uri = builder.toString(); } else { // RFC 3296, 5.3. Base Object Considerations: // In cases where the URI to be returned is a LDAP URL, the server // MUST provide an explicit scope specifier from the LDAP URL prior // to returning it. ldapurl.getAttributes().clear(); ldapurl.setScope(searchScope); ldapurl.setFilter(null); uri = ldapurl.toString(); } } } catch (DirectoryException e) { assert debugException(CLASS_NAME, "throwReferralException", e); // Return the non-LDAP URI as is. } URIList.add(uri); } // Throw a directory referral exception containing the URIs. int msgID = MSGID_JEB_REFERRAL_RESULT_MESSAGE; String msg = getMessage(msgID, referralDN); throw new DirectoryException(ResultCode.REFERRAL, msg, msgID, referralDN, URIList, null); } /** * Process referral entries that are above the target DN of an operation. * @param targetDN The target DN of the operation, or the base object of a * search operation. * @param searchScope The scope of the search operation, or null if the * operation is not a search operation. * @throws DirectoryException If a referral is found at or above the target * DN. The referral URLs will be set appropriately for the references found * in the referral entry. */ public void targetEntryReferrals(DN targetDN, SearchScope searchScope) throws DirectoryException { Transaction txn = null; CursorConfig cursorConfig = null; try { Cursor cursor = getDatabase().openCursor(txn, cursorConfig); try { DatabaseEntry key = new DatabaseEntry(); DatabaseEntry data = new DatabaseEntry(); // Go up through the DIT hierarchy until we find a referral. for (DN dn = targetDN.getParent(); dn != null; dn = dn.getParent()) { // Look for a record whose key matches the current DN. String normDN = dn.toNormalizedString(); key.setData(StaticUtils.getBytes(normDN)); OperationStatus status = cursor.getSearchKey(key, data, LockMode.DEFAULT); if (status == OperationStatus.SUCCESS) { // Construct a set of all the labeled URIs in the referral. Set labeledURIs = new LinkedHashSet(cursor.count()); do { String labeledURI = new String(data.getData(), "UTF-8"); labeledURIs.add(labeledURI); status = cursor.getNextDup(key, data, LockMode.DEFAULT); } while (status == OperationStatus.SUCCESS); throwReferralException(targetDN, dn, labeledURIs, searchScope); } } } finally { cursor.close(); } } catch (DatabaseException e) { assert debugException(CLASS_NAME, "targetEntryReferrals", e); } catch (UnsupportedEncodingException e) { assert debugException(CLASS_NAME, "targetEntryReferrals", e); } } /** * Return search result references for a search operation using the referral * database to find all referral entries within scope of the search. * @param searchOp The search operation for which search result references * should be returned. * @return true if the caller should continue processing the * search request and sending additional entries and references, or * false if not for some reason (e.g., the size limit * has been reached or the search has been abandoned). * @throws DirectoryException If a Directory Server error occurs. */ public boolean returnSearchReferences(SearchOperation searchOp) throws DirectoryException { Transaction txn = null; CursorConfig cursorConfig = null; /* * We will iterate forwards through a range of the keys to * find subordinates of the base entry from the top of the tree * downwards. */ DN baseDN = searchOp.getBaseDN(); String normBaseDN = baseDN.toNormalizedString(); byte[] suffix = StaticUtils.getBytes("," + normBaseDN); /* * Set the ending value to a value of equal length but slightly * greater than the suffix. Since keys are compared in * reverse order we must set the first byte (the comma). * No possibility of overflow here. */ byte[] end = suffix.clone(); end[0] = (byte) (end[0] + 1); DatabaseEntry data = new DatabaseEntry(); DatabaseEntry key = new DatabaseEntry(suffix); try { Cursor cursor = getDatabase().openCursor(txn, cursorConfig); try { // Initialize the cursor very close to the starting value then // step forward until we pass the ending value. for (OperationStatus status = cursor.getSearchKeyRange(key, data, LockMode.DEFAULT); status == OperationStatus.SUCCESS; status = cursor.getNextNoDup(key, data, LockMode.DEFAULT)) { int cmp = comparator.compare(key.getData(), end); if (cmp >= 0) { // We have gone past the ending value. break; } // We have found a subordinate referral. DN dn = DN.decode(new ASN1OctetString(key.getData())); // Make sure the referral is within scope. if (searchOp.getScope() == SearchScope.SINGLE_LEVEL) { if ((dn.getRDNComponents().length != baseDN.getRDNComponents().length + 1)) { continue; } } // Construct a list of all the URIs in the referral. ArrayList URIList = new ArrayList(cursor.count()); do { // Remove the label part of the labeled URI if there is a label. String labeledURI = new String(data.getData(), "UTF-8"); String uri = labeledURI; int i = labeledURI.indexOf(' '); if (i != -1) { uri = labeledURI.substring(0, i); } // From RFC 3296 section 5.4: // If the URI component is not a LDAP URL, it should be returned as // is. If the LDAP URL's DN part is absent or empty, the DN part // must be modified to contain the DN of the referral object. If // the URI component is a LDAP URL, the URI SHOULD be modified to // add an explicit scope specifier. try { LDAPURL ldapurl = LDAPURL.decode(uri, false); if (ldapurl.getScheme().equalsIgnoreCase("ldap")) { if (ldapurl.getBaseDN().isNullDN()) { ldapurl.setBaseDN(dn); } ldapurl.getAttributes().clear(); if (searchOp.getScope() == SearchScope.SINGLE_LEVEL) { ldapurl.setScope(SearchScope.BASE_OBJECT); } else { ldapurl.setScope(SearchScope.WHOLE_SUBTREE); } ldapurl.setFilter(null); uri = ldapurl.toString(); } } catch (DirectoryException e) { assert debugException(CLASS_NAME, "returnSearchReferences", e); // Return the non-LDAP URI as is. } URIList.add(uri); status = cursor.getNextDup(key, data, LockMode.DEFAULT); } while (status == OperationStatus.SUCCESS); SearchResultReference reference = new SearchResultReference(URIList); if (!searchOp.returnReference(reference)) { return false; } } } finally { cursor.close(); } } catch (DatabaseException e) { assert debugException(CLASS_NAME, "returnSearchReferences", e); } catch (UnsupportedEncodingException e) { assert debugException(CLASS_NAME, "returnSearchReferences", e); } return true; } }