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