/* * 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 ForgeRock AS */ package org.opends.server.backends.jeb; import com.sleepycat.je.*; import org.opends.messages.Message; import org.opends.server.core.DirectoryServer; import org.opends.server.core.SearchOperation; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.types.*; import org.opends.server.util.StaticUtils; import java.io.UnsupportedEncodingException; import java.util.*; import static org.opends.server.util.ServerConstants.ATTR_REFERRAL_URL; import static org.opends.server.loggers.debug.DebugLogger.*; import static org.opends.messages.JebMessages. NOTE_JEB_REFERRAL_RESULT_MESSAGE; /** * 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 extends DatabaseContainer { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); /** * The key comparator used for the DN database. */ private final Comparator<byte[]> dn2uriComparator; private final int prefixRDNComponents; /** * The standard attribute type that is used to specify the set of referral * URLs in a referral entry. */ private final AttributeType referralType = DirectoryServer.getAttributeType(ATTR_REFERRAL_URL); /** * A flag that indicates whether there are any referrals contained in this * database. It should only be set to {@code false} when it is known that * there are no referrals. */ private volatile ConditionResult containsReferrals = ConditionResult.UNDEFINED; /** * Create a new object representing a referral database in a given * entryContainer. * * @param name The name of the referral database. * @param env The JE environment. * @param entryContainer The entryContainer of the DN database. * @throws DatabaseException If an error occurs in the JE database. */ @SuppressWarnings("unchecked") DN2URI(String name, Environment env, EntryContainer entryContainer) throws DatabaseException { super(name, env, entryContainer); dn2uriComparator = new AttributeIndex.KeyComparator(); prefixRDNComponents = entryContainer.getBaseDN().getNumComponents(); DatabaseConfig dn2uriConfig = new DatabaseConfig(); if(env.getConfig().getReadOnly()) { dn2uriConfig.setReadOnly(true); dn2uriConfig.setSortedDuplicates(true); dn2uriConfig.setAllowCreate(false); dn2uriConfig.setTransactional(false); } else if(!env.getConfig().getTransactional()) { dn2uriConfig.setSortedDuplicates(true); dn2uriConfig.setAllowCreate(true); dn2uriConfig.setTransactional(false); dn2uriConfig.setDeferredWrite(true); } else { dn2uriConfig.setSortedDuplicates(true); dn2uriConfig.setAllowCreate(true); dn2uriConfig.setTransactional(true); } this.dbConfig = dn2uriConfig; //This line causes an unchecked cast error if the SuppressWarnings //annotation is removed at the beginning of this method. this.dbConfig.setBtreeComparator((Class<? extends Comparator<byte[]>>) dn2uriComparator.getClass()); } /** * 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. */ private boolean insert(Transaction txn, DN dn, String labeledURI) throws DatabaseException { byte[] normDN = JebFormat.dnToDNKey(dn, prefixRDNComponents); 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 = put(txn, key, data); if (status != OperationStatus.SUCCESS) { return false; } containsReferrals = ConditionResult.TRUE; 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 = JebFormat.dnToDNKey(dn, prefixRDNComponents); DatabaseEntry key = new DatabaseEntry(normDN); OperationStatus status; status = delete(txn, key); if (status != OperationStatus.SUCCESS) { return false; } containsReferrals = containsReferrals(txn); 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 = JebFormat.dnToDNKey(dn, prefixRDNComponents); byte[] URIBytes = StaticUtils.getBytes(labeledURI); DatabaseEntry key = new DatabaseEntry(normDN); DatabaseEntry data = new DatabaseEntry(URIBytes); OperationStatus status; Cursor cursor = 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; } containsReferrals = containsReferrals(txn); return true; } /** * Indicates whether the underlying database contains any referrals. * * @param txn The transaction to use when making the determination. * * @return {@code true} if it is believed that the underlying database may * contain at least one referral, or {@code false} if it is certain * that it doesn't. */ private ConditionResult containsReferrals(Transaction txn) { try { Cursor cursor = openCursor(txn, null); DatabaseEntry key = new DatabaseEntry(); DatabaseEntry data = new DatabaseEntry(); OperationStatus status = cursor.getFirst(key, data, null); cursor.close(); if (status == OperationStatus.SUCCESS) { return ConditionResult.TRUE; } else if (status == OperationStatus.NOTFOUND) { return ConditionResult.FALSE; } else { return ConditionResult.UNDEFINED; } } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } return ConditionResult.UNDEFINED; } } /** * 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<Modification> 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) { insert(txn, entryDN, v.getValue().toString()); } } break; case DELETE: if (a == null || a.isEmpty()) { delete(txn, entryDN); } else { for (AttributeValue v : a) { delete(txn, entryDN, v.getValue().toString()); } } break; case INCREMENT: // Nonsensical. break; case REPLACE: delete(txn, entryDN); if (a != null) { for (AttributeValue v : a) { insert(txn, entryDN, v.getValue().toString()); } } 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. * @return True if the entry was added successfully or False otherwise. * @throws DatabaseException If an error occurs in the JE database. */ public boolean addEntry(Transaction txn, Entry entry) throws DatabaseException { boolean success = true; Set<String> labeledURIs = entry.getReferralURLs(); if (labeledURIs != null) { DN dn = entry.getDN(); for (String labeledURI : labeledURIs) { if(!insert(txn, dn, labeledURI)) { success = false; } } } return success; } /** * 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<String> 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<String> 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<String> labeledURIs, SearchScope searchScope) throws DirectoryException { ArrayList<String> URIList = new ArrayList<String>(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.getNumComponents(), 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) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } // Return the non-LDAP URI as is. } URIList.add(uri); } // Throw a directory referral exception containing the URIs. Message msg = NOTE_JEB_REFERRAL_RESULT_MESSAGE.get(String.valueOf(referralDN)); throw new DirectoryException( ResultCode.REFERRAL, msg, 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 { if (containsReferrals == ConditionResult.UNDEFINED) { containsReferrals = containsReferrals(null); } if (containsReferrals == ConditionResult.FALSE) { return; } Transaction txn = null; CursorConfig cursorConfig = null; try { Cursor cursor = 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 = entryContainer.getParentWithinBase(targetDN); dn != null; dn = entryContainer.getParentWithinBase(dn)) { // Look for a record whose key matches the current DN. key.setData(JebFormat.dnToDNKey(dn, prefixRDNComponents)); OperationStatus status = cursor.getSearchKey(key, data, LockMode.DEFAULT); if (status == OperationStatus.SUCCESS) { // Construct a set of all the labeled URIs in the referral. Set<String> labeledURIs = new LinkedHashSet<String>(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) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } catch (UnsupportedEncodingException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, 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 <CODE>true</CODE> if the caller should continue processing the * search request and sending additional entries and references, or * <CODE>false</CODE> 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 { if (containsReferrals == ConditionResult.UNDEFINED) { containsReferrals = containsReferrals(null); } if (containsReferrals == ConditionResult.FALSE) { return true; } 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. */ byte[] baseDN = JebFormat.dnToDNKey(searchOp.getBaseDN(), prefixRDNComponents); byte[] suffix = Arrays.copyOf(baseDN, baseDN.length+1); suffix[suffix.length-1] = 0x00; byte[] end = suffix.clone(); end[end.length-1] = (byte) (end[end.length-1] + 1); /* * 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. */ DatabaseEntry data = new DatabaseEntry(); DatabaseEntry key = new DatabaseEntry(suffix); try { Cursor cursor = 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 = dn2uriComparator.compare(key.getData(), end); if (cmp >= 0) { // We have gone past the ending value. break; } // We have found a subordinate referral. DN dn = JebFormat.dnFromDNKey(key.getData(), 0, key.getSize(), entryContainer.getBaseDN()); // Make sure the referral is within scope. if (searchOp.getScope() == SearchScope.SINGLE_LEVEL) { if(JebFormat.findDNKeyParent(key.getData(), 0, key.getSize()) != baseDN.length) { continue; } } // Construct a list of all the URIs in the referral. ArrayList<String> URIList = new ArrayList<String>(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) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, 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(dn, reference)) { return false; } } } finally { cursor.close(); } } catch (DatabaseException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } catch (UnsupportedEncodingException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } return true; } }