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 2007-2008 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS
026 */
027package org.opends.server.backends;
028
029import static org.forgerock.util.Reject.*;
030import static org.opends.messages.BackendMessages.*;
031import static org.opends.server.util.ServerConstants.*;
032import static org.opends.server.util.StaticUtils.*;
033
034import java.io.File;
035import java.util.*;
036import java.util.concurrent.locks.ReentrantReadWriteLock;
037
038import org.forgerock.i18n.LocalizableMessage;
039import org.forgerock.i18n.slf4j.LocalizedLogger;
040import org.forgerock.opendj.config.server.ConfigChangeResult;
041import org.forgerock.opendj.config.server.ConfigException;
042import org.forgerock.opendj.ldap.ConditionResult;
043import org.forgerock.opendj.ldap.ResultCode;
044import org.forgerock.opendj.ldap.SearchScope;
045import org.opends.server.admin.server.ConfigurationChangeListener;
046import org.opends.server.admin.std.server.LDIFBackendCfg;
047import org.opends.server.api.AlertGenerator;
048import org.opends.server.api.Backend;
049import org.opends.server.controls.SubtreeDeleteControl;
050import org.opends.server.core.*;
051import org.opends.server.types.*;
052import org.opends.server.util.LDIFException;
053import org.opends.server.util.LDIFReader;
054import org.opends.server.util.LDIFWriter;
055import org.opends.server.util.StaticUtils;
056
057/**
058 * This class provides a backend implementation that stores the underlying data
059 * in an LDIF file.  When the backend is initialized, the contents of the
060 * backend are read into memory and all read operations are performed purely
061 * from memory.  Write operations cause the underlying LDIF file to be
062 * re-written on disk.
063 */
064public class LDIFBackend
065       extends Backend<LDIFBackendCfg>
066       implements ConfigurationChangeListener<LDIFBackendCfg>, AlertGenerator
067{
068  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
069
070
071
072  /** The base DNs for this backend. */
073  private DN[] baseDNs;
074
075  /** The mapping between parent DNs and their immediate children. */
076  private final Map<DN, Set<DN>> childDNs = new HashMap<>();
077
078  /** The base DNs for this backend, in a hash set. */
079  private Set<DN> baseDNSet;
080
081  /** The set of supported controls for this backend. */
082  private final Set<String> supportedControls =
083      Collections.singleton(OID_SUBTREE_DELETE_CONTROL);
084
085  /** The current configuration for this backend. */
086  private LDIFBackendCfg currentConfig;
087
088  /** The mapping between entry DNs and the corresponding entries. */
089  private final Map<DN, Entry> entryMap = new LinkedHashMap<>();
090
091  /** A read-write lock used to protect access to this backend. */
092  private final ReentrantReadWriteLock backendLock = new ReentrantReadWriteLock();
093
094  /** The path to the LDIF file containing the data for this backend. */
095  private String ldifFilePath;
096
097  /**
098   * Creates a new backend with the provided information.  All backend
099   * implementations must implement a default constructor that use
100   * <CODE>super()</CODE> to invoke this constructor.
101   */
102  public LDIFBackend()
103  {
104  }
105
106  /** {@inheritDoc} */
107  @Override
108  public void openBackend()
109         throws ConfigException, InitializationException
110  {
111    // We won't support anything other than exactly one base DN in this
112    // implementation.  If we were to add such support in the future, we would
113    // likely want to separate the data for each base DN into a separate entry
114    // map.
115    if (baseDNs == null || baseDNs.length != 1)
116    {
117      throw new ConfigException(ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS.get(currentConfig.dn()));
118    }
119
120    for (DN dn : baseDNs)
121    {
122      try
123      {
124        DirectoryServer.registerBaseDN(dn, this,
125                                       currentConfig.isIsPrivateBackend());
126      }
127      catch (Exception e)
128      {
129        logger.traceException(e);
130
131        LocalizableMessage message = ERR_BACKEND_CANNOT_REGISTER_BASEDN.get(
132            dn, getExceptionMessage(e));
133        throw new InitializationException(message, e);
134      }
135    }
136
137    DirectoryServer.registerAlertGenerator(this);
138
139    readLDIF();
140  }
141
142
143
144  /**
145   * Reads the contents of the LDIF backing file into memory.
146   *
147   * @throws  InitializationException  If a problem occurs while reading the
148   *                                   LDIF file.
149   */
150  private void readLDIF()
151          throws InitializationException
152  {
153    File ldifFile = getFileForPath(ldifFilePath);
154    if (! ldifFile.exists())
155    {
156      // This is fine.  We will just start with an empty backend.
157      if (logger.isTraceEnabled())
158      {
159        logger.trace("LDIF backend starting empty because LDIF file " +
160                         ldifFilePath + " does not exist");
161      }
162
163      entryMap.clear();
164      childDNs.clear();
165      return;
166    }
167
168
169    try
170    {
171      importLDIF(new LDIFImportConfig(ldifFile.getAbsolutePath()), false);
172    }
173    catch (DirectoryException de)
174    {
175      throw new InitializationException(de.getMessageObject(), de);
176    }
177  }
178
179
180
181  /**
182   * Writes the current set of entries to the target LDIF file.  The new LDIF
183   * will first be created as a temporary file and then renamed into place.  The
184   * caller must either hold the write lock for this backend, or must ensure
185   * that it's in some other state that guarantees exclusive access to the data.
186   *
187   * @throws  DirectoryException  If a problem occurs that prevents the updated
188   *                              LDIF from being written.
189   */
190  private void writeLDIF()
191          throws DirectoryException
192  {
193    File ldifFile = getFileForPath(ldifFilePath);
194    File tempFile = new File(ldifFile.getAbsolutePath() + ".new");
195    File oldFile  = new File(ldifFile.getAbsolutePath() + ".old");
196
197
198    // Write the new data to a temporary file.
199    LDIFWriter writer;
200    try
201    {
202      LDIFExportConfig exportConfig =
203           new LDIFExportConfig(tempFile.getAbsolutePath(),
204                                ExistingFileBehavior.OVERWRITE);
205      writer = new LDIFWriter(exportConfig);
206    }
207    catch (Exception e)
208    {
209      logger.traceException(e);
210
211      LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_CREATING_FILE.get(
212                       tempFile.getAbsolutePath(),
213                       currentConfig.dn(),
214                       stackTraceToSingleLineString(e));
215      DirectoryServer.sendAlertNotification(this,
216                           ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
217      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
218                                   m, e);
219    }
220
221
222    for (Entry entry : entryMap.values())
223    {
224      try
225      {
226        writer.writeEntry(entry);
227      }
228      catch (Exception e)
229      {
230        logger.traceException(e);
231
232        StaticUtils.close(writer);
233
234        LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_WRITING_FILE.get(
235                         tempFile.getAbsolutePath(),
236                         currentConfig.dn(),
237                         stackTraceToSingleLineString(e));
238        DirectoryServer.sendAlertNotification(this,
239                             ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
240        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
241                                     m, e);
242      }
243    }
244
245    // On Linux the final write() on a file can actually fail but not throw an Exception.
246    // The close() will throw an Exception in this case so we MUST check for Exceptions
247    // here.
248    try
249    {
250        writer.close();
251    }
252    catch (Exception e)
253    {
254      logger.traceException(e);
255      LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_CLOSING_FILE.get(
256                       tempFile.getAbsolutePath(),
257                       currentConfig.dn(),
258                       stackTraceToSingleLineString(e));
259      DirectoryServer.sendAlertNotification(this,
260                           ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
261      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
262                                   m, e);
263    }
264
265    // Extra sanity check
266    if (!entryMap.isEmpty() && tempFile.exists() && tempFile.length() == 0)
267    {
268      LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_EMPTY_FILE.get(
269                       tempFile.getAbsolutePath(),
270                       currentConfig.dn());
271      DirectoryServer.sendAlertNotification(this,
272                           ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
273      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), m);
274    }
275
276    if (tempFile.exists())
277    {
278      // Rename the existing "live" file out of the way and move the new file
279      // into place.
280      try
281      {
282        oldFile.delete();
283      }
284      catch (Exception e)
285      {
286        logger.traceException(e);
287      }
288    }
289
290    try
291    {
292      if (ldifFile.exists())
293      {
294        ldifFile.renameTo(oldFile);
295      }
296    }
297    catch (Exception e)
298    {
299      logger.traceException(e);
300    }
301
302    try
303    {
304      tempFile.renameTo(ldifFile);
305    }
306    catch (Exception e)
307    {
308      logger.traceException(e);
309
310      LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_RENAMING_FILE.get(
311                       tempFile.getAbsolutePath(),
312                       ldifFile.getAbsolutePath(),
313                       currentConfig.dn(),
314                       stackTraceToSingleLineString(e));
315      DirectoryServer.sendAlertNotification(this,
316                           ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
317      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
318                                   m, e);
319    }
320  }
321
322  /** {@inheritDoc} */
323  @Override
324  public void closeBackend()
325  {
326    backendLock.writeLock().lock();
327
328    try
329    {
330      currentConfig.removeLDIFChangeListener(this);
331      DirectoryServer.deregisterAlertGenerator(this);
332
333      for (DN dn : baseDNs)
334      {
335        try
336        {
337          DirectoryServer.deregisterBaseDN(dn);
338        }
339        catch (Exception e)
340        {
341          logger.traceException(e);
342        }
343      }
344    }
345    finally
346    {
347      backendLock.writeLock().unlock();
348    }
349  }
350
351  /** {@inheritDoc} */
352  @Override
353  public DN[] getBaseDNs()
354  {
355    return baseDNs;
356  }
357
358  /** {@inheritDoc} */
359  @Override
360  public long getEntryCount()
361  {
362    backendLock.readLock().lock();
363
364    try
365    {
366      if (entryMap != null)
367      {
368        return entryMap.size();
369      }
370
371      return -1;
372    }
373    finally
374    {
375      backendLock.readLock().unlock();
376    }
377  }
378
379  /** {@inheritDoc} */
380  @Override
381  public boolean isIndexed(AttributeType attributeType, IndexType indexType)
382  {
383    // All searches in this backend will always be considered indexed.
384    return true;
385  }
386
387  /** {@inheritDoc} */
388  @Override
389  public ConditionResult hasSubordinates(DN entryDN)
390         throws DirectoryException
391  {
392    backendLock.readLock().lock();
393
394    try
395    {
396      Set<DN> childDNSet = childDNs.get(entryDN);
397      if (childDNSet == null || childDNSet.isEmpty())
398      {
399        // It could be that the entry doesn't exist, in which case we should
400        // throw an exception.
401        if (entryMap.containsKey(entryDN))
402        {
403          return ConditionResult.FALSE;
404        }
405        else
406        {
407          throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
408              ERR_LDIF_BACKEND_HAS_SUBORDINATES_NO_SUCH_ENTRY.get(entryDN));
409        }
410      }
411      else
412      {
413        return ConditionResult.TRUE;
414      }
415    }
416    finally
417    {
418      backendLock.readLock().unlock();
419    }
420  }
421
422  /** {@inheritDoc} */
423  @Override
424  public long getNumberOfChildren(DN parentDN) throws DirectoryException
425  {
426    checkNotNull(parentDN, "parentDN must not be null");
427    return getNumberOfSubordinates(parentDN, false);
428  }
429
430  /** {@inheritDoc} */
431  @Override
432  public long getNumberOfEntriesInBaseDN(DN baseDN) throws DirectoryException
433  {
434    checkNotNull(baseDN, "baseDN must not be null");
435    if (!Arrays.asList(baseDNs).contains(baseDN))
436    {
437      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_LDIF_BACKEND_NUM_SUBORDINATES_NO_SUCH_ENTRY
438          .get(baseDN));
439    }
440    final int baseDNIfExists = childDNs.containsKey(baseDN) ? 1 : 0;
441    return getNumberOfSubordinates(baseDN, true) + baseDNIfExists;
442  }
443
444  private long getNumberOfSubordinates(DN entryDN, boolean includeSubtree) throws DirectoryException
445  {
446    backendLock.readLock().lock();
447
448    try
449    {
450      Set<DN> childDNSet = childDNs.get(entryDN);
451      if (childDNSet == null || childDNSet.isEmpty())
452      {
453        // It could be that the entry doesn't exist, in which case we should
454        // throw an exception.
455        if (entryMap.containsKey(entryDN))
456        {
457          return 0L;
458        }
459        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_LDIF_BACKEND_NUM_SUBORDINATES_NO_SUCH_ENTRY
460            .get(entryDN));
461      }
462
463      if (!includeSubtree)
464      {
465        return childDNSet.size();
466      }
467
468      long count = 0;
469      for (DN childDN : childDNSet)
470      {
471        count += getNumberOfSubordinates(childDN, true);
472        count++;
473      }
474      return count;
475    }
476    finally
477    {
478      backendLock.readLock().unlock();
479    }
480  }
481
482  /** {@inheritDoc} */
483  @Override
484  public Entry getEntry(DN entryDN)
485  {
486    backendLock.readLock().lock();
487
488    try
489    {
490      return entryMap.get(entryDN);
491    }
492    finally
493    {
494      backendLock.readLock().unlock();
495    }
496  }
497
498  /** {@inheritDoc} */
499  @Override
500  public boolean entryExists(DN entryDN)
501  {
502    backendLock.readLock().lock();
503
504    try
505    {
506      return entryMap.containsKey(entryDN);
507    }
508    finally
509    {
510      backendLock.readLock().unlock();
511    }
512  }
513
514  /** {@inheritDoc} */
515  @Override
516  public void addEntry(Entry entry, AddOperation addOperation)
517         throws DirectoryException
518  {
519    backendLock.writeLock().lock();
520
521    try
522    {
523      // Make sure that the target entry does not already exist, but that its
524      // parent does exist (or that the entry being added is the base DN).
525      DN entryDN = entry.getName();
526      if (entryMap.containsKey(entryDN))
527      {
528        LocalizableMessage m = ERR_LDIF_BACKEND_ADD_ALREADY_EXISTS.get(entryDN);
529        throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, m);
530      }
531
532      if (baseDNSet.contains(entryDN))
533      {
534        entryMap.put(entryDN, entry.duplicate(false));
535        writeLDIF();
536        return;
537      }
538      else
539      {
540        DN parentDN = entryDN.getParentDNInSuffix();
541        if (parentDN != null && entryMap.containsKey(parentDN))
542        {
543          entryMap.put(entryDN, entry.duplicate(false));
544
545          Set<DN> childDNSet = childDNs.get(parentDN);
546          if (childDNSet == null)
547          {
548            childDNSet = new HashSet<>();
549            childDNs.put(parentDN, childDNSet);
550          }
551          childDNSet.add(entryDN);
552          writeLDIF();
553          return;
554        }
555        else
556        {
557          DN matchedDN = null;
558          if (parentDN != null)
559          {
560            while (true)
561            {
562              parentDN = parentDN.getParentDNInSuffix();
563              if (parentDN == null)
564              {
565                break;
566              }
567
568              if (entryMap.containsKey(parentDN))
569              {
570                matchedDN = parentDN;
571                break;
572              }
573            }
574          }
575
576          LocalizableMessage m = ERR_LDIF_BACKEND_ADD_MISSING_PARENT.get(entryDN);
577          throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN, null);
578        }
579      }
580    }
581    finally
582    {
583      backendLock.writeLock().unlock();
584    }
585  }
586
587  /** {@inheritDoc} */
588  @Override
589  public void deleteEntry(DN entryDN, DeleteOperation deleteOperation)
590         throws DirectoryException
591  {
592    backendLock.writeLock().lock();
593
594    try
595    {
596      // Get the DN of the target entry's parent, if it exists.  We'll need to
597      // also remove the reference to the target entry from the parent's set of
598      // children.
599      DN parentDN = entryDN.getParentDNInSuffix();
600
601      // Make sure that the target entry exists.  If not, then fail.
602      if (! entryMap.containsKey(entryDN))
603      {
604        DN matchedDN = null;
605        while (parentDN != null)
606        {
607          if (entryMap.containsKey(parentDN))
608          {
609            matchedDN = parentDN;
610            break;
611          }
612
613          parentDN = parentDN.getParentDNInSuffix();
614        }
615
616        LocalizableMessage m = ERR_LDIF_BACKEND_DELETE_NO_SUCH_ENTRY.get(entryDN);
617        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN, null);
618      }
619
620
621      // See if the target entry has any children.  If so, then we'll only
622      // delete it if the request contains the subtree delete control (in
623      // which case we'll delete the entire subtree).
624      Set<DN> childDNSet = childDNs.get(entryDN);
625      if (childDNSet == null || childDNSet.isEmpty())
626      {
627        entryMap.remove(entryDN);
628        childDNs.remove(entryDN);
629
630        if (parentDN != null)
631        {
632          Set<DN> parentChildren = childDNs.get(parentDN);
633          if (parentChildren != null)
634          {
635            parentChildren.remove(entryDN);
636            if (parentChildren.isEmpty())
637            {
638              childDNs.remove(parentDN);
639            }
640          }
641        }
642      }
643      else
644      {
645        boolean subtreeDelete = deleteOperation != null
646            && deleteOperation
647                .getRequestControl(SubtreeDeleteControl.DECODER) != null;
648
649        if (! subtreeDelete)
650        {
651          LocalizableMessage m = ERR_LDIF_BACKEND_DELETE_NONLEAF.get(entryDN);
652          throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_NONLEAF, m);
653        }
654
655        entryMap.remove(entryDN);
656        childDNs.remove(entryDN);
657
658        if (parentDN != null)
659        {
660          Set<DN> parentChildren = childDNs.get(parentDN);
661          if (parentChildren != null)
662          {
663            parentChildren.remove(entryDN);
664            if (parentChildren.isEmpty())
665            {
666              childDNs.remove(parentDN);
667            }
668          }
669        }
670
671        for (DN childDN : childDNSet)
672        {
673          subtreeDelete(childDN);
674        }
675      }
676
677      writeLDIF();
678    }
679    finally
680    {
681      backendLock.writeLock().unlock();
682    }
683  }
684
685
686
687  /**
688   * Removes the specified entry and any subordinates that it may have from
689   * the backend.  This method assumes that the caller holds the backend write
690   * lock.
691   *
692   * @param  entryDN  The DN of the entry to remove, along with all of its
693   *                  subordinate entries.
694   */
695  private void subtreeDelete(DN entryDN)
696  {
697    entryMap.remove(entryDN);
698    Set<DN> childDNSet = childDNs.remove(entryDN);
699    if (childDNSet != null)
700    {
701      for (DN childDN : childDNSet)
702      {
703        subtreeDelete(childDN);
704      }
705    }
706  }
707
708  /** {@inheritDoc} */
709  @Override
710  public void replaceEntry(Entry oldEntry, Entry newEntry,
711      ModifyOperation modifyOperation) throws DirectoryException
712  {
713    backendLock.writeLock().lock();
714
715    try
716    {
717      // Make sure that the target entry exists.  If not, then fail.
718      DN entryDN = newEntry.getName();
719      if (! entryMap.containsKey(entryDN))
720      {
721        DN matchedDN = null;
722        DN parentDN = entryDN.getParentDNInSuffix();
723        while (parentDN != null)
724        {
725          if (entryMap.containsKey(parentDN))
726          {
727            matchedDN = parentDN;
728            break;
729          }
730
731          parentDN = parentDN.getParentDNInSuffix();
732        }
733
734        LocalizableMessage m = ERR_LDIF_BACKEND_MODIFY_NO_SUCH_ENTRY.get(entryDN);
735        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN, null);
736      }
737
738      entryMap.put(entryDN, newEntry.duplicate(false));
739      writeLDIF();
740      return;
741    }
742    finally
743    {
744      backendLock.writeLock().unlock();
745    }
746  }
747
748  /** {@inheritDoc} */
749  @Override
750  public void renameEntry(DN currentDN, Entry entry,
751                          ModifyDNOperation modifyDNOperation)
752         throws DirectoryException
753  {
754    backendLock.writeLock().lock();
755
756    try
757    {
758      // Make sure that the original entry exists and that the new entry doesn't
759      // exist but its parent does.
760      DN newDN = entry.getName();
761      if (! entryMap.containsKey(currentDN))
762      {
763        DN matchedDN = null;
764        DN parentDN = currentDN.getParentDNInSuffix();
765        while (parentDN != null)
766        {
767          if (entryMap.containsKey(parentDN))
768          {
769            matchedDN = parentDN;
770            break;
771          }
772
773          parentDN = parentDN.getParentDNInSuffix();
774        }
775
776        LocalizableMessage m = ERR_LDIF_BACKEND_MODDN_NO_SUCH_SOURCE_ENTRY.get(currentDN);
777        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN, null);
778      }
779
780      if (entryMap.containsKey(newDN))
781      {
782        LocalizableMessage m = ERR_LDIF_BACKEND_MODDN_TARGET_ENTRY_ALREADY_EXISTS.get(newDN);
783        throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, m);
784      }
785
786      DN newParentDN = newDN.getParentDNInSuffix();
787      if (! entryMap.containsKey(newParentDN))
788      {
789        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
790            ERR_LDIF_BACKEND_MODDN_NEW_PARENT_DOESNT_EXIST.get(newParentDN));
791      }
792
793      // Remove the entry from the list of children for the old parent and
794      // add the new entry DN to the set of children for the new parent.
795      DN oldParentDN = currentDN.getParentDNInSuffix();
796      Set<DN> parentChildDNs = childDNs.get(oldParentDN);
797      if (parentChildDNs != null)
798      {
799        parentChildDNs.remove(currentDN);
800        if (parentChildDNs.isEmpty()
801            && modifyDNOperation.getNewSuperior() != null)
802        {
803          childDNs.remove(oldParentDN);
804        }
805      }
806
807      parentChildDNs = childDNs.get(newParentDN);
808      if (parentChildDNs == null)
809      {
810        parentChildDNs = new HashSet<>();
811        childDNs.put(newParentDN, parentChildDNs);
812      }
813      parentChildDNs.add(newDN);
814
815
816      // If the entry has children, then we'll need to work on the whole
817      // subtree.  Otherwise, just work on the target entry.
818      Set<DN> childDNSet = childDNs.remove(currentDN);
819      entryMap.remove(currentDN);
820      entryMap.put(newDN, entry.duplicate(false));
821      if (childDNSet != null && !childDNSet.isEmpty())
822      {
823        for (DN childDN : childDNSet)
824        {
825          subtreeRename(childDN, newDN);
826        }
827      }
828      writeLDIF();
829    }
830    finally
831    {
832      backendLock.writeLock().unlock();
833    }
834  }
835
836
837
838  /**
839   * Moves the specified entry and all of its children so that they are
840   * appropriately placed below the given new parent DN.  This method assumes
841   * that the caller holds the backend write lock.
842   *
843   * @param  entryDN      The DN of the entry to move/rename.
844   * @param  newParentDN  The DN of the new parent under which the entry should
845   *                      be placed.
846   */
847  private void subtreeRename(DN entryDN, DN newParentDN)
848  {
849    Set<DN> childDNSet = childDNs.remove(entryDN);
850    DN newEntryDN = new DN(entryDN.rdn(), newParentDN);
851
852    Entry oldEntry = entryMap.remove(entryDN);
853    if (oldEntry == null)
854    {
855      // This should never happen.
856      if (logger.isTraceEnabled())
857      {
858        logger.trace("Subtree rename encountered entry DN " +
859                            entryDN + " for nonexistent entry.");
860      }
861      return;
862    }
863
864    Entry newEntry = oldEntry.duplicate(false);
865    newEntry.setDN(newEntryDN);
866    entryMap.put(newEntryDN, newEntry);
867
868    Set<DN> parentChildren = childDNs.get(newParentDN);
869    if (parentChildren == null)
870    {
871      parentChildren = new HashSet<>();
872      childDNs.put(newParentDN, parentChildren);
873    }
874    parentChildren.add(newEntryDN);
875
876    if (childDNSet != null)
877    {
878      for (DN childDN : childDNSet)
879      {
880        subtreeRename(childDN, newEntryDN);
881      }
882    }
883  }
884
885  /** {@inheritDoc} */
886  @Override
887  public void search(SearchOperation searchOperation)
888         throws DirectoryException
889  {
890    backendLock.readLock().lock();
891
892    try
893    {
894      // Get the base DN, scope, and filter for the search.
895      DN           baseDN = searchOperation.getBaseDN();
896      SearchScope  scope  = searchOperation.getScope();
897      SearchFilter filter = searchOperation.getFilter();
898
899
900      // Make sure the base entry exists if it's supposed to be in this backend.
901      Entry baseEntry = entryMap.get(baseDN);
902      if (baseEntry == null && handlesEntry(baseDN))
903      {
904        DN matchedDN = baseDN.getParentDNInSuffix();
905        while (matchedDN != null)
906        {
907          if (entryMap.containsKey(matchedDN))
908          {
909            break;
910          }
911
912          matchedDN = matchedDN.getParentDNInSuffix();
913        }
914
915        LocalizableMessage m = ERR_LDIF_BACKEND_SEARCH_NO_SUCH_BASE.get(baseDN);
916        throw new DirectoryException(
917                ResultCode.NO_SUCH_OBJECT, m, matchedDN, null);
918      }
919
920      if (baseEntry != null)
921      {
922        baseEntry = baseEntry.duplicate(true);
923      }
924
925      // If it's a base-level search, then just get that entry and return it if
926      // it matches the filter.
927      if (scope == SearchScope.BASE_OBJECT)
928      {
929        if (filter.matchesEntry(baseEntry))
930        {
931          searchOperation.returnEntry(baseEntry, new LinkedList<Control>());
932        }
933      }
934      else
935      {
936        // Walk through all entries and send the ones that match.
937        for (Entry e : entryMap.values())
938        {
939          e = e.duplicate(true);
940          if (e.matchesBaseAndScope(baseDN, scope) && filter.matchesEntry(e))
941          {
942            searchOperation.returnEntry(e, new LinkedList<Control>());
943          }
944        }
945      }
946    }
947    finally
948    {
949      backendLock.readLock().unlock();
950    }
951  }
952
953  /** {@inheritDoc} */
954  @Override
955  public Set<String> getSupportedControls()
956  {
957    return supportedControls;
958  }
959
960  /** {@inheritDoc} */
961  @Override
962  public Set<String> getSupportedFeatures()
963  {
964    return Collections.emptySet();
965  }
966
967  /** {@inheritDoc} */
968  @Override
969  public boolean supports(BackendOperation backendOperation)
970  {
971    switch (backendOperation)
972    {
973    case LDIF_EXPORT:
974    case LDIF_IMPORT:
975      return true;
976
977    default:
978      return false;
979    }
980  }
981
982  /** {@inheritDoc} */
983  @Override
984  public void exportLDIF(LDIFExportConfig exportConfig)
985         throws DirectoryException
986  {
987    backendLock.readLock().lock();
988
989    try
990    {
991      // Create the LDIF writer.
992      LDIFWriter ldifWriter;
993      try
994      {
995        ldifWriter = new LDIFWriter(exportConfig);
996      }
997      catch (Exception e)
998      {
999        logger.traceException(e);
1000
1001        LocalizableMessage m = ERR_LDIF_BACKEND_CANNOT_CREATE_LDIF_WRITER.get(
1002                         stackTraceToSingleLineString(e));
1003        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
1004                                     m, e);
1005      }
1006
1007
1008      // Walk through all the entries and write them to LDIF.
1009      DN entryDN = null;
1010      try
1011      {
1012        for (Entry entry : entryMap.values())
1013        {
1014          entryDN = entry.getName();
1015          ldifWriter.writeEntry(entry);
1016        }
1017      }
1018      catch (Exception e)
1019      {
1020        LocalizableMessage m = ERR_LDIF_BACKEND_CANNOT_WRITE_ENTRY_TO_LDIF.get(
1021            entryDN, stackTraceToSingleLineString(e));
1022        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
1023                                     m, e);
1024      }
1025      finally
1026      {
1027        StaticUtils.close(ldifWriter);
1028      }
1029    }
1030    finally
1031    {
1032      backendLock.readLock().unlock();
1033    }
1034  }
1035
1036  /** {@inheritDoc} */
1037  @Override
1038  public LDIFImportResult importLDIF(LDIFImportConfig importConfig, ServerContext serverContext)
1039      throws DirectoryException
1040  {
1041    return importLDIF(importConfig, true);
1042  }
1043
1044  /**
1045   * Processes an LDIF import operation, optionally writing the resulting LDIF
1046   * to disk.
1047   *
1048   * @param  importConfig  The LDIF import configuration.
1049   * @param  writeLDIF     Indicates whether the LDIF backing file for this
1050   *                       backend should be updated when the import is
1051   *                       complete.  This should only be {@code false} when
1052   *                       reading the LDIF as the backend is coming online.
1053   */
1054  private LDIFImportResult importLDIF(LDIFImportConfig importConfig,
1055                                     boolean writeLDIF)
1056         throws DirectoryException
1057  {
1058    backendLock.writeLock().lock();
1059
1060    try
1061    {
1062      LDIFReader reader;
1063      try
1064      {
1065        reader = new LDIFReader(importConfig);
1066      }
1067      catch (Exception e)
1068      {
1069        LocalizableMessage m = ERR_LDIF_BACKEND_CANNOT_CREATE_LDIF_READER.get(
1070                         stackTraceToSingleLineString(e));
1071        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
1072                                     m, e);
1073      }
1074
1075      entryMap.clear();
1076      childDNs.clear();
1077
1078
1079      try
1080      {
1081        while (true)
1082        {
1083          Entry e = null;
1084          try
1085          {
1086            e = reader.readEntry();
1087            if (e == null)
1088            {
1089              break;
1090            }
1091          }
1092          catch (LDIFException le)
1093          {
1094            if (! le.canContinueReading())
1095            {
1096              LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_READING_LDIF.get(
1097                               stackTraceToSingleLineString(le));
1098              throw new DirectoryException(
1099                             DirectoryServer.getServerErrorResultCode(), m, le);
1100            }
1101            else
1102            {
1103              continue;
1104            }
1105          }
1106
1107          // Make sure that we don't already have an entry with the same DN.  If
1108          // a duplicate is encountered, then log a message and continue.
1109          DN entryDN = e.getName();
1110          if (entryMap.containsKey(entryDN))
1111          {
1112            LocalizableMessage m =
1113                ERR_LDIF_BACKEND_DUPLICATE_ENTRY.get(ldifFilePath, currentConfig.dn(), entryDN);
1114            logger.error(m);
1115            reader.rejectLastEntry(m);
1116            continue;
1117          }
1118
1119
1120          // If the entry DN is a base DN, then add it with no more processing.
1121          if (baseDNSet.contains(entryDN))
1122          {
1123            entryMap.put(entryDN, e);
1124            continue;
1125          }
1126
1127
1128          // Make sure that the parent exists.  If not, then reject the entry.
1129          boolean isBelowBaseDN = false;
1130          for (DN baseDN : baseDNs)
1131          {
1132            if (baseDN.isAncestorOf(entryDN))
1133            {
1134              isBelowBaseDN = true;
1135              break;
1136            }
1137          }
1138
1139          if (! isBelowBaseDN)
1140          {
1141            LocalizableMessage m = ERR_LDIF_BACKEND_ENTRY_OUT_OF_SCOPE.get(
1142                ldifFilePath, currentConfig.dn(), entryDN);
1143            logger.error(m);
1144            reader.rejectLastEntry(m);
1145            continue;
1146          }
1147
1148          DN parentDN = entryDN.getParentDNInSuffix();
1149          if (parentDN == null || !entryMap.containsKey(parentDN))
1150          {
1151            LocalizableMessage m = ERR_LDIF_BACKEND_MISSING_PARENT.get(
1152                ldifFilePath, currentConfig.dn(), entryDN);
1153            logger.error(m);
1154            reader.rejectLastEntry(m);
1155            continue;
1156          }
1157
1158
1159          // The entry does not exist but its parent does, so add it and update
1160          // the set of children for the parent.
1161          entryMap.put(entryDN, e);
1162
1163          Set<DN> childDNSet = childDNs.get(parentDN);
1164          if (childDNSet == null)
1165          {
1166            childDNSet = new HashSet<>();
1167            childDNs.put(parentDN, childDNSet);
1168          }
1169
1170          childDNSet.add(entryDN);
1171        }
1172
1173
1174        if (writeLDIF)
1175        {
1176          writeLDIF();
1177        }
1178
1179        return new LDIFImportResult(reader.getEntriesRead(),
1180                                    reader.getEntriesRejected(),
1181                                    reader.getEntriesIgnored());
1182      }
1183      catch (DirectoryException de)
1184      {
1185        throw de;
1186      }
1187      catch (Exception e)
1188      {
1189        LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_READING_LDIF.get(
1190                         stackTraceToSingleLineString(e));
1191        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
1192                                     m, e);
1193      }
1194      finally
1195      {
1196        StaticUtils.close(reader);
1197      }
1198    }
1199    finally
1200    {
1201      backendLock.writeLock().unlock();
1202    }
1203  }
1204
1205  /** {@inheritDoc} */
1206  @Override
1207  public void createBackup(BackupConfig backupConfig)
1208         throws DirectoryException
1209  {
1210    LocalizableMessage message = ERR_LDIF_BACKEND_BACKUP_RESTORE_NOT_SUPPORTED.get();
1211    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
1212  }
1213
1214  /** {@inheritDoc} */
1215  @Override
1216  public void removeBackup(BackupDirectory backupDirectory, String backupID)
1217         throws DirectoryException
1218  {
1219    LocalizableMessage message = ERR_LDIF_BACKEND_BACKUP_RESTORE_NOT_SUPPORTED.get();
1220    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
1221  }
1222
1223  /** {@inheritDoc} */
1224  @Override
1225  public void restoreBackup(RestoreConfig restoreConfig)
1226         throws DirectoryException
1227  {
1228    LocalizableMessage message = ERR_LDIF_BACKEND_BACKUP_RESTORE_NOT_SUPPORTED.get();
1229    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
1230  }
1231
1232  /** {@inheritDoc} */
1233  @Override
1234  public void configureBackend(LDIFBackendCfg config, ServerContext serverContext) throws ConfigException
1235  {
1236    if (config != null)
1237    {
1238      currentConfig = config;
1239      currentConfig.addLDIFChangeListener(this);
1240
1241      baseDNs = new DN[currentConfig.getBaseDN().size()];
1242      currentConfig.getBaseDN().toArray(baseDNs);
1243      if (baseDNs.length != 1)
1244      {
1245        throw new ConfigException(ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS.get(currentConfig.dn()));
1246      }
1247
1248      baseDNSet = new HashSet<>();
1249      Collections.addAll(baseDNSet, baseDNs);
1250
1251      ldifFilePath = currentConfig.getLDIFFile();
1252    }
1253  }
1254
1255  /** {@inheritDoc} */
1256  @Override
1257  public boolean isConfigurationChangeAcceptable(LDIFBackendCfg configuration,
1258                      List<LocalizableMessage> unacceptableReasons)
1259  {
1260    boolean configAcceptable = true;
1261
1262    // Make sure that there is only a single base DN.
1263    if (configuration.getBaseDN().size() != 1)
1264    {
1265      unacceptableReasons.add(ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS.get(configuration.dn()));
1266      configAcceptable = false;
1267    }
1268
1269    return configAcceptable;
1270  }
1271
1272  /** {@inheritDoc} */
1273  @Override
1274  public ConfigChangeResult applyConfigurationChange(
1275                                 LDIFBackendCfg configuration)
1276  {
1277    // We don't actually need to do anything in response to this.  However, if
1278    // the base DNs or LDIF file are different from what we're currently using
1279    // then indicate that admin action is required.
1280    final ConfigChangeResult ccr = new ConfigChangeResult();
1281
1282    if (ldifFilePath != null)
1283    {
1284      File currentLDIF = getFileForPath(ldifFilePath);
1285      File newLDIF     = getFileForPath(configuration.getLDIFFile());
1286      if (! currentLDIF.equals(newLDIF))
1287      {
1288        ccr.addMessage(INFO_LDIF_BACKEND_LDIF_FILE_CHANGED.get());
1289        ccr.setAdminActionRequired(true);
1290      }
1291    }
1292
1293    if (baseDNSet != null && !baseDNSet.equals(configuration.getBaseDN()))
1294    {
1295      ccr.addMessage(INFO_LDIF_BACKEND_BASE_DN_CHANGED.get());
1296      ccr.setAdminActionRequired(true);
1297    }
1298
1299    currentConfig = configuration;
1300    return ccr;
1301  }
1302
1303  /** {@inheritDoc} */
1304  @Override
1305  public DN getComponentEntryDN()
1306  {
1307    return currentConfig.dn();
1308  }
1309
1310  /** {@inheritDoc} */
1311  @Override
1312  public String getClassName()
1313  {
1314    return LDIFBackend.class.getName();
1315  }
1316
1317  /** {@inheritDoc} */
1318  @Override
1319  public Map<String,String> getAlerts()
1320  {
1321    Map<String,String> alerts = new LinkedHashMap<>();
1322    alerts.put(ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE,
1323               ALERT_DESCRIPTION_LDIF_BACKEND_CANNOT_WRITE_UPDATE);
1324    return alerts;
1325  }
1326}