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.LockMode.*;
030import static com.sleepycat.je.OperationStatus.*;
031
032import static org.opends.messages.BackendMessages.*;
033import static org.opends.server.backends.jeb.JebFormat.*;
034import static org.opends.server.util.ServerConstants.*;
035
036import java.util.*;
037
038import org.forgerock.i18n.LocalizableMessage;
039import org.forgerock.i18n.slf4j.LocalizedLogger;
040import org.forgerock.opendj.ldap.ByteSequenceReader;
041import org.forgerock.opendj.ldap.ByteString;
042import org.forgerock.opendj.ldap.ByteStringBuilder;
043import org.forgerock.opendj.ldap.ConditionResult;
044import org.forgerock.opendj.ldap.ResultCode;
045import org.forgerock.opendj.ldap.SearchScope;
046import org.forgerock.util.Pair;
047import org.opends.server.core.DirectoryServer;
048import org.opends.server.core.SearchOperation;
049import org.opends.server.types.*;
050import org.opends.server.util.StaticUtils;
051
052import com.sleepycat.je.*;
053
054/**
055 * This class represents the referral database which contains URIs from referral
056 * entries.
057 * <p>
058 * The key is the DN of the referral entry and the value is that of a pair
059 * (labeled URI in the ref attribute for that entry, DN). The DN must be
060 * duplicated in the value because the key is suitable for comparisons but is
061 * not reversible to a valid DN. Duplicate keys are permitted since a referral
062 * entry can contain multiple values of the ref attribute. Key order is the same
063 * as in the DN database so that all referrals in a subtree can be retrieved by
064 * cursoring through a range of the records.
065 */
066public class DN2URI extends DatabaseContainer
067{
068  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
069
070  private static final byte STRING_SEPARATOR = 0x00;
071
072  /**
073   * The key comparator used for the DN database.
074   */
075  private final Comparator<byte[]> dn2uriComparator;
076
077
078  private final int prefixRDNComponents;
079
080
081  /**
082   * The standard attribute type that is used to specify the set of referral
083   * URLs in a referral entry.
084   */
085  private final AttributeType referralType =
086       DirectoryServer.getAttributeType(ATTR_REFERRAL_URL);
087
088  /**
089   * A flag that indicates whether there are any referrals contained in this
090   * database.  It should only be set to {@code false} when it is known that
091   * there are no referrals.
092   */
093  private volatile ConditionResult containsReferrals =
094       ConditionResult.UNDEFINED;
095
096
097  /**
098   * Create a new object representing a referral database in a given
099   * entryContainer.
100   *
101   * @param name The name of the referral database.
102   * @param env The JE environment.
103   * @param entryContainer The entryContainer of the DN database.
104   * @throws DatabaseException If an error occurs in the JE database.
105   */
106  @SuppressWarnings("unchecked")
107  DN2URI(String name, Environment env,
108        EntryContainer entryContainer)
109      throws DatabaseException
110  {
111    super(name, env, entryContainer);
112
113    dn2uriComparator = new AttributeIndex.KeyComparator();
114    prefixRDNComponents = entryContainer.getBaseDN().size();
115
116    this.dbConfig = JEBUtils.toDatabaseConfigAllowDuplicates(env);
117    this.dbConfig.setBtreeComparator((Class<? extends Comparator<byte[]>>)
118                                  dn2uriComparator.getClass());
119  }
120
121  /**
122   * Insert a URI value in the referral database.
123   *
124   * @param txn A database transaction used for the update, or null if none is
125   * required.
126   * @param dn The DN of the referral entry.
127   * @param labeledURI The labeled URI value of the ref attribute.
128   * @return true if the record was inserted, false if it was not.
129   * @throws DatabaseException If an error occurs in the JE database.
130   */
131  private boolean insert(Transaction txn, DN dn, String labeledURI)
132       throws DatabaseException
133  {
134    byte[] normDN = JebFormat.dnToDNKey(dn, prefixRDNComponents);
135    DatabaseEntry key = new DatabaseEntry(normDN);
136    DatabaseEntry data = new DatabaseEntry(encodeURIAndDN(labeledURI, dn));
137
138    // The JE insert method does not permit duplicate keys so we must use the
139    // put method.
140    if (put(txn, key, data) == SUCCESS)
141    {
142      containsReferrals = ConditionResult.TRUE;
143      return true;
144    }
145    return false;
146  }
147
148  private byte[] encodeURIAndDN(String labeledURI, DN dn)
149  {
150    return new ByteStringBuilder()
151      .append(labeledURI)
152      .append(STRING_SEPARATOR)
153      .append(dn.toString())
154      .toByteArray();
155  }
156
157  private Pair<String, DN> decodeURIAndDN(byte[] data) throws DirectoryException {
158    try {
159      final ByteSequenceReader reader = ByteString.valueOf(data).asReader();
160      final String labeledURI = reader.getString(getNextStringLength(reader));
161      // skip the string separator
162      reader.skip(1);
163      final DN dn = DN.valueOf(reader.getString(reader.remaining()));
164      return Pair.of(labeledURI, dn);
165    }
166    catch (Exception e) {
167       throw new DirectoryException(ResultCode.OPERATIONS_ERROR, ERR_DATABASE_EXCEPTION.get(e));
168    }
169  }
170
171  /** Returns the length of next string by looking for the zero byte used as separator. */
172  private int getNextStringLength(ByteSequenceReader reader)
173  {
174    int length = 0;
175    while (reader.peek(length) != STRING_SEPARATOR)
176    {
177      length++;
178    }
179    return length;
180  }
181
182  /**
183   * Delete URI values for a given referral entry from the referral database.
184   *
185   * @param txn A database transaction used for the update, or null if none is
186   * required.
187   * @param dn The DN of the referral entry for which URI values are to be
188   * deleted.
189   * @return true if the values were deleted, false if not.
190   * @throws DatabaseException If an error occurs in the JE database.
191   */
192  boolean delete(Transaction txn, DN dn) throws DatabaseException
193  {
194    byte[] normDN = JebFormat.dnToDNKey(dn, prefixRDNComponents);
195    DatabaseEntry key = new DatabaseEntry(normDN);
196
197    if (delete(txn, key) == SUCCESS)
198    {
199      containsReferrals = containsReferrals(txn);
200      return true;
201    }
202    return false;
203  }
204
205  /**
206   * Delete a single URI value from the referral database.
207   * @param txn A database transaction used for the update, or null if none is
208   * required.
209   * @param dn The DN of the referral entry.
210   * @param labeledURI The URI value to be deleted.
211   * @return true if the value was deleted, false if not.
212   * @throws DatabaseException If an error occurs in the JE database.
213   */
214  private boolean delete(Transaction txn, DN dn, String labeledURI) throws DatabaseException
215  {
216    CursorConfig cursorConfig = null;
217    byte[] normDN = JebFormat.dnToDNKey(dn, prefixRDNComponents);
218    byte[] URIBytes = StaticUtils.getBytes(labeledURI);
219    DatabaseEntry key = new DatabaseEntry(normDN);
220    DatabaseEntry data = new DatabaseEntry(URIBytes);
221
222    Cursor cursor = openCursor(txn, cursorConfig);
223    try
224    {
225      OperationStatus status = cursor.getSearchBoth(key, data, null);
226      if (status == OperationStatus.SUCCESS)
227      {
228        status = cursor.delete();
229      }
230
231      if (status == OperationStatus.SUCCESS)
232      {
233        containsReferrals = containsReferrals(txn);
234        return true;
235      }
236      return false;
237    }
238    finally
239    {
240      cursor.close();
241    }
242  }
243
244  /**
245   * Indicates whether the underlying database contains any referrals.
246   *
247   * @param  txn  The transaction to use when making the determination.
248   *
249   * @return  {@code true} if it is believed that the underlying database may
250   *          contain at least one referral, or {@code false} if it is certain
251   *          that it doesn't.
252   */
253  private ConditionResult containsReferrals(Transaction txn)
254  {
255    try
256    {
257      Cursor cursor = openCursor(txn, null);
258      DatabaseEntry key  = new DatabaseEntry();
259      DatabaseEntry data = new DatabaseEntry();
260
261      OperationStatus status = cursor.getFirst(key, data, null);
262      cursor.close();
263
264      if (status == OperationStatus.SUCCESS)
265      {
266        return ConditionResult.TRUE;
267      }
268      else if (status == OperationStatus.NOTFOUND)
269      {
270        return ConditionResult.FALSE;
271      }
272      else
273      {
274        return ConditionResult.UNDEFINED;
275      }
276    }
277    catch (Exception e)
278    {
279      logger.traceException(e);
280
281      return ConditionResult.UNDEFINED;
282    }
283  }
284
285  /**
286   * Update the referral database for an entry that has been modified.  Does
287   * not do anything unless the entry before the modification or the entry after
288   * the modification is a referral entry.
289   *
290   * @param txn A database transaction used for the update, or null if none is
291   * required.
292   * @param before The entry before the modifications have been applied.
293   * @param after The entry after the modifications have been applied.
294   * @param mods The sequence of modifications made to the entry.
295   * @throws DatabaseException If an error occurs in the JE database.
296   */
297  void modifyEntry(Transaction txn, Entry before, Entry after, List<Modification> mods)
298       throws DatabaseException
299  {
300    DN entryDN = before.getName();
301    for (Modification mod : mods)
302    {
303      Attribute modAttr = mod.getAttribute();
304      AttributeType modAttrType = modAttr.getAttributeType();
305      if (modAttrType.equals(referralType))
306      {
307        Attribute a = mod.getAttribute();
308        switch (mod.getModificationType().asEnum())
309        {
310          case ADD:
311            if (a != null)
312            {
313              for (ByteString v : a)
314              {
315                insert(txn, entryDN, v.toString());
316              }
317            }
318            break;
319
320          case DELETE:
321            if (a == null || a.isEmpty())
322            {
323              delete(txn, entryDN);
324            }
325            else
326            {
327              for (ByteString v : a)
328              {
329                delete(txn, entryDN, v.toString());
330              }
331            }
332            break;
333
334          case INCREMENT:
335            // Nonsensical.
336            break;
337
338          case REPLACE:
339            delete(txn, entryDN);
340            if (a != null)
341            {
342              for (ByteString v : a)
343              {
344                insert(txn, entryDN, v.toString());
345              }
346            }
347            break;
348        }
349      }
350    }
351  }
352
353  /**
354   * Update the referral database for an entry that has been replaced.  Does
355   * not do anything unless the entry before it was replaced or the entry after
356   * it was replaced is a referral entry.
357   *
358   * @param txn A database transaction used for the update, or null if none is
359   * required.
360   * @param before The entry before it was replaced.
361   * @param after The entry after it was replaced.
362   * @throws DatabaseException If an error occurs in the JE database.
363   */
364  public void replaceEntry(Transaction txn, Entry before, Entry after)
365       throws DatabaseException
366  {
367    deleteEntry(txn, before);
368    addEntry(txn, after);
369  }
370
371  /**
372   * Update the referral database for a new entry. Does nothing if the entry
373   * is not a referral entry.
374   * @param txn A database transaction used for the update, or null if none is
375   * required.
376   * @param entry The entry to be added.
377   * @return True if the entry was added successfully or False otherwise.
378   * @throws DatabaseException If an error occurs in the JE database.
379   */
380  public boolean addEntry(Transaction txn, Entry entry)
381       throws DatabaseException
382  {
383    boolean success = true;
384    Set<String> labeledURIs = entry.getReferralURLs();
385    if (labeledURIs != null)
386    {
387      DN dn = entry.getName();
388      for (String labeledURI : labeledURIs)
389      {
390        if(!insert(txn, dn, labeledURI))
391        {
392          success = false;
393        }
394      }
395    }
396    return success;
397  }
398
399  /**
400   * Update the referral database for a deleted entry. Does nothing if the entry
401   * was not a referral entry.
402   * @param txn A database transaction used for the update, or null if none is
403   * required.
404   * @param entry The entry to be deleted.
405   * @throws DatabaseException If an error occurs in the JE database.
406   */
407  void deleteEntry(Transaction txn, Entry entry) throws DatabaseException
408  {
409    Set<String> labeledURIs = entry.getReferralURLs();
410    if (labeledURIs != null)
411    {
412      delete(txn, entry.getName());
413    }
414  }
415
416  /**
417   * Checks whether the target of an operation is a referral entry and throws
418   * a Directory referral exception if it is.
419   * @param entry The target entry of the operation, or the base entry of a
420   * search operation.
421   * @param searchScope The scope of the search operation, or null if the
422   * operation is not a search operation.
423   * @throws DirectoryException If a referral is found at or above the target
424   * DN.  The referral URLs will be set appropriately for the references found
425   * in the referral entry.
426   */
427  void checkTargetForReferral(Entry entry, SearchScope searchScope) throws DirectoryException
428  {
429    Set<String> referralURLs = entry.getReferralURLs();
430    if (referralURLs != null)
431    {
432      throwReferralException(entry.getName(), entry.getName(), referralURLs,
433                             searchScope);
434    }
435  }
436
437  /**
438   * Throws a Directory referral exception for the case where a referral entry
439   * exists at or above the target DN of an operation.
440   * @param targetDN The target DN of the operation, or the base object of a
441   * search operation.
442   * @param referralDN The DN of the referral entry.
443   * @param labeledURIs The set of labeled URIs in the referral entry.
444   * @param searchScope The scope of the search operation, or null if the
445   * operation is not a search operation.
446   * @throws DirectoryException If a referral is found at or above the target
447   * DN.  The referral URLs will be set appropriately for the references found
448   * in the referral entry.
449   */
450  private void throwReferralException(DN targetDN, DN referralDN, Set<String> labeledURIs, SearchScope searchScope)
451       throws DirectoryException
452  {
453    ArrayList<String> URIList = new ArrayList<>(labeledURIs.size());
454    for (String labeledURI : labeledURIs)
455    {
456      // Remove the label part of the labeled URI if there is a label.
457      String uri = labeledURI;
458      int i = labeledURI.indexOf(' ');
459      if (i != -1)
460      {
461        uri = labeledURI.substring(0, i);
462      }
463
464      try
465      {
466        LDAPURL ldapurl = LDAPURL.decode(uri, false);
467
468        if ("ldap".equalsIgnoreCase(ldapurl.getScheme()))
469        {
470          DN urlBaseDN = targetDN;
471          if (!referralDN.equals(ldapurl.getBaseDN()))
472          {
473            urlBaseDN =
474                 EntryContainer.modDN(targetDN,
475                                      referralDN.size(),
476                                      ldapurl.getBaseDN());
477          }
478          ldapurl.setBaseDN(urlBaseDN);
479          if (searchScope == null)
480          {
481            // RFC 3296, 5.2.  Target Object Considerations:
482            // In cases where the URI to be returned is a LDAP URL, the server
483            // SHOULD trim any present scope, filter, or attribute list from the
484            // URI before returning it.  Critical extensions MUST NOT be trimmed
485            // or modified.
486            StringBuilder builder = new StringBuilder(uri.length());
487            ldapurl.toString(builder, true);
488            uri = builder.toString();
489          }
490          else
491          {
492            // RFC 3296, 5.3.  Base Object Considerations:
493            // In cases where the URI to be returned is a LDAP URL, the server
494            // MUST provide an explicit scope specifier from the LDAP URL prior
495            // to returning it.
496            ldapurl.getAttributes().clear();
497            ldapurl.setScope(searchScope);
498            ldapurl.setFilter(null);
499            uri = ldapurl.toString();
500          }
501        }
502      }
503      catch (DirectoryException e)
504      {
505        logger.traceException(e);
506        // Return the non-LDAP URI as is.
507      }
508
509      URIList.add(uri);
510    }
511
512    // Throw a directory referral exception containing the URIs.
513    LocalizableMessage msg = NOTE_REFERRAL_RESULT_MESSAGE.get(referralDN);
514    throw new DirectoryException(
515            ResultCode.REFERRAL, msg, referralDN, URIList, null);
516  }
517
518  /**
519   * Process referral entries that are above the target DN of an operation.
520   * @param targetDN The target DN of the operation, or the base object of a
521   * search operation.
522   * @param searchScope The scope of the search operation, or null if the
523   * operation is not a search operation.
524   * @throws DirectoryException If a referral is found at or above the target
525   * DN.  The referral URLs will be set appropriately for the references found
526   * in the referral entry.
527   */
528  void targetEntryReferrals(DN targetDN, SearchScope searchScope) throws DirectoryException
529  {
530    if (containsReferrals == ConditionResult.UNDEFINED)
531    {
532      containsReferrals = containsReferrals(null);
533    }
534
535    if (containsReferrals == ConditionResult.FALSE)
536    {
537      return;
538    }
539
540    Transaction txn = null;
541    CursorConfig cursorConfig = null;
542
543    try
544    {
545      Cursor cursor = openCursor(txn, cursorConfig);
546      try
547      {
548        DatabaseEntry key = new DatabaseEntry();
549        DatabaseEntry data = new DatabaseEntry();
550
551        // Go up through the DIT hierarchy until we find a referral.
552        for (DN dn = entryContainer.getParentWithinBase(targetDN); dn != null;
553             dn = entryContainer.getParentWithinBase(dn))
554        {
555          // Look for a record whose key matches the current DN.
556          key.setData(JebFormat.dnToDNKey(dn, prefixRDNComponents));
557          OperationStatus status = cursor.getSearchKey(key, data, DEFAULT);
558          if (status == OperationStatus.SUCCESS)
559          {
560            // Construct a set of all the labeled URIs in the referral.
561            Set<String> labeledURIs = new LinkedHashSet<>(cursor.count());
562            do
563            {
564              final Pair<String, DN> uriAndDN = decodeURIAndDN(data.getData());
565              final String labeledURI = uriAndDN.getFirst();
566              labeledURIs.add(labeledURI);
567              status = cursor.getNextDup(key, data, DEFAULT);
568            } while (status == OperationStatus.SUCCESS);
569
570            throwReferralException(targetDN, dn, labeledURIs, searchScope);
571          }
572        }
573      }
574      finally
575      {
576        cursor.close();
577      }
578    }
579    catch (DatabaseException e)
580    {
581      logger.traceException(e);
582    }
583  }
584
585  /**
586   * Return search result references for a search operation using the referral
587   * database to find all referral entries within scope of the search.
588   * @param searchOp The search operation for which search result references
589   * should be returned.
590   * @return  <CODE>true</CODE> if the caller should continue processing the
591   *          search request and sending additional entries and references, or
592   *          <CODE>false</CODE> if not for some reason (e.g., the size limit
593   *          has been reached or the search has been abandoned).
594   * @throws DirectoryException If a Directory Server error occurs.
595   */
596  boolean returnSearchReferences(SearchOperation searchOp) throws DirectoryException
597  {
598    if (containsReferrals == ConditionResult.UNDEFINED)
599    {
600      containsReferrals = containsReferrals(null);
601    }
602
603    if (containsReferrals == ConditionResult.FALSE)
604    {
605      return true;
606    }
607
608    Transaction txn = null;
609    CursorConfig cursorConfig = null;
610
611    /*
612     * We will iterate forwards through a range of the keys to
613     * find subordinates of the base entry from the top of the tree
614     * downwards.
615     */
616    byte[] baseDN = JebFormat.dnToDNKey(searchOp.getBaseDN(), prefixRDNComponents);
617    final byte special = 0x00;
618    byte[] suffix = Arrays.copyOf(baseDN, baseDN.length+1);
619    suffix[suffix.length - 1] = special;
620    byte[] end = Arrays.copyOf(suffix, suffix.length);
621    end[end.length - 1] = special + 1;
622
623    /*
624     * Set the ending value to a value of equal length but slightly
625     * greater than the suffix. Since keys are compared in
626     * reverse order we must set the first byte (the comma).
627     * No possibility of overflow here.
628     */
629
630    DatabaseEntry data = new DatabaseEntry();
631    DatabaseEntry key = new DatabaseEntry(suffix);
632
633    try
634    {
635      Cursor cursor = openCursor(txn, cursorConfig);
636      try
637      {
638        // Initialize the cursor very close to the starting value then
639        // step forward until we pass the ending value.
640        for (OperationStatus status =
641             cursor.getSearchKeyRange(key, data, DEFAULT);
642             status == OperationStatus.SUCCESS;
643             status = cursor.getNextNoDup(key, data, DEFAULT))
644        {
645          int cmp = dn2uriComparator.compare(key.getData(), end);
646          if (cmp >= 0)
647          {
648            // We have gone past the ending value.
649            break;
650          }
651
652          // We have found a subordinate referral.
653          final Pair<String, DN> uriAndDN = decodeURIAndDN(data.getData());
654          final String labeledURI = uriAndDN.getFirst();
655          final DN dn = uriAndDN.getSecond();
656
657          // Make sure the referral is within scope.
658          if (searchOp.getScope() == SearchScope.SINGLE_LEVEL
659              && findDNKeyParent(key.getData()) != baseDN.length)
660          {
661            continue;
662          }
663
664          // Construct a list of all the URIs in the referral.
665          ArrayList<String> URIList = new ArrayList<>(cursor.count());
666          do
667          {
668            // Remove the label part of the labeled URI if there is a label.
669            String uri = labeledURI;
670            int i = labeledURI.indexOf(' ');
671            if (i != -1)
672            {
673              uri = labeledURI.substring(0, i);
674            }
675
676            // From RFC 3296 section 5.4:
677            // If the URI component is not a LDAP URL, it should be returned as
678            // is.  If the LDAP URL's DN part is absent or empty, the DN part
679            // must be modified to contain the DN of the referral object.  If
680            // the URI component is a LDAP URL, the URI SHOULD be modified to
681            // add an explicit scope specifier.
682            try
683            {
684              LDAPURL ldapurl = LDAPURL.decode(uri, false);
685
686              if ("ldap".equalsIgnoreCase(ldapurl.getScheme()))
687              {
688                if (ldapurl.getBaseDN().isRootDN())
689                {
690                  ldapurl.setBaseDN(dn);
691                }
692                ldapurl.getAttributes().clear();
693                if (searchOp.getScope() == SearchScope.SINGLE_LEVEL)
694                {
695                  ldapurl.setScope(SearchScope.BASE_OBJECT);
696                }
697                else
698                {
699                  ldapurl.setScope(SearchScope.WHOLE_SUBTREE);
700                }
701                ldapurl.setFilter(null);
702                uri = ldapurl.toString();
703              }
704            }
705            catch (DirectoryException e)
706            {
707              logger.traceException(e);
708              // Return the non-LDAP URI as is.
709            }
710
711            URIList.add(uri);
712            status = cursor.getNextDup(key, data, DEFAULT);
713          } while (status == OperationStatus.SUCCESS);
714
715          SearchResultReference reference = new SearchResultReference(URIList);
716          if (!searchOp.returnReference(dn, reference))
717          {
718            return false;
719          }
720        }
721      }
722      finally
723      {
724        cursor.close();
725      }
726    }
727    catch (DatabaseException e)
728    {
729      logger.traceException(e);
730    }
731
732    return true;
733  }
734}