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 2014-2016 ForgeRock AS.
015 */
016package org.opends.server.backends;
017
018import static org.opends.messages.BackendMessages.*;
019import static org.opends.messages.ReplicationMessages.*;
020import static org.opends.server.config.ConfigConstants.*;
021import static org.opends.server.core.DirectoryServer.*;
022import static org.opends.server.replication.plugin.MultimasterReplication.*;
023import static org.opends.server.replication.server.changelog.api.DBCursor.KeyMatchingStrategy.*;
024import static org.opends.server.replication.server.changelog.api.DBCursor.PositionStrategy.*;
025import static org.opends.server.util.LDIFWriter.*;
026import static org.opends.server.util.ServerConstants.*;
027import static org.opends.server.util.StaticUtils.*;
028
029import java.text.SimpleDateFormat;
030import java.util.Collection;
031import java.util.Collections;
032import java.util.Date;
033import java.util.Iterator;
034import java.util.LinkedHashMap;
035import java.util.List;
036import java.util.Map;
037import java.util.Set;
038import java.util.TimeZone;
039import java.util.concurrent.ConcurrentLinkedQueue;
040import java.util.concurrent.ConcurrentSkipListMap;
041import java.util.concurrent.atomic.AtomicReference;
042
043import org.forgerock.i18n.LocalizableMessage;
044import org.forgerock.i18n.slf4j.LocalizedLogger;
045import org.forgerock.opendj.config.server.ConfigException;
046import org.forgerock.opendj.ldap.ByteString;
047import org.forgerock.opendj.ldap.ConditionResult;
048import org.forgerock.opendj.ldap.DN;
049import org.forgerock.opendj.ldap.ModificationType;
050import org.forgerock.opendj.ldap.RDN;
051import org.forgerock.opendj.ldap.ResultCode;
052import org.forgerock.opendj.ldap.SearchScope;
053import org.forgerock.opendj.ldap.schema.AttributeType;
054import org.opends.server.admin.Configuration;
055import org.opends.server.api.Backend;
056import org.opends.server.config.ConfigConstants;
057import org.opends.server.controls.EntryChangelogNotificationControl;
058import org.opends.server.controls.ExternalChangelogRequestControl;
059import org.opends.server.core.AddOperation;
060import org.opends.server.core.DeleteOperation;
061import org.opends.server.core.DirectoryServer;
062import org.opends.server.core.ModifyDNOperation;
063import org.opends.server.core.ModifyOperation;
064import org.opends.server.core.PersistentSearch;
065import org.opends.server.core.SearchOperation;
066import org.opends.server.core.ServerContext;
067import org.opends.server.replication.common.CSN;
068import org.opends.server.replication.common.MultiDomainServerState;
069import org.opends.server.replication.common.ServerState;
070import org.opends.server.replication.protocol.AddMsg;
071import org.opends.server.replication.protocol.DeleteMsg;
072import org.opends.server.replication.protocol.LDAPUpdateMsg;
073import org.opends.server.replication.protocol.ModifyCommonMsg;
074import org.opends.server.replication.protocol.ModifyDNMsg;
075import org.opends.server.replication.protocol.UpdateMsg;
076import org.opends.server.replication.server.ReplicationServer;
077import org.opends.server.replication.server.ReplicationServerDomain;
078import org.opends.server.replication.server.changelog.api.ChangeNumberIndexDB;
079import org.opends.server.replication.server.changelog.api.ChangeNumberIndexRecord;
080import org.opends.server.replication.server.changelog.api.ChangelogDB;
081import org.opends.server.replication.server.changelog.api.ChangelogException;
082import org.opends.server.replication.server.changelog.api.DBCursor;
083import org.opends.server.replication.server.changelog.api.DBCursor.CursorOptions;
084import org.opends.server.replication.server.changelog.api.ReplicaId;
085import org.opends.server.replication.server.changelog.api.ReplicationDomainDB;
086import org.opends.server.replication.server.changelog.file.ECLEnabledDomainPredicate;
087import org.opends.server.replication.server.changelog.file.ECLMultiDomainDBCursor;
088import org.opends.server.replication.server.changelog.file.MultiDomainDBCursor;
089import org.opends.server.types.Attribute;
090import org.opends.server.types.Attributes;
091import org.opends.server.types.BackupConfig;
092import org.opends.server.types.BackupDirectory;
093import org.opends.server.types.CanceledOperationException;
094import org.opends.server.types.Control;
095import org.opends.server.types.DirectoryException;
096import org.opends.server.types.Entry;
097import org.opends.server.types.FilterType;
098import org.opends.server.types.IndexType;
099import org.opends.server.types.InitializationException;
100import org.opends.server.types.LDIFExportConfig;
101import org.opends.server.types.LDIFImportConfig;
102import org.opends.server.types.LDIFImportResult;
103import org.opends.server.types.Modification;
104import org.opends.server.types.ObjectClass;
105import org.opends.server.types.Privilege;
106import org.opends.server.types.RawAttribute;
107import org.opends.server.types.RestoreConfig;
108import org.opends.server.types.SearchFilter;
109import org.opends.server.types.WritabilityMode;
110import org.opends.server.util.StaticUtils;
111
112/**
113 * A backend that provides access to the changelog, i.e. the "cn=changelog"
114 * suffix. It is a read-only backend that is created by a
115 * {@link ReplicationServer} and is not configurable.
116 * <p>
117 * There are two modes to search the changelog:
118 * <ul>
119 * <li>Cookie mode: when a "ECL Cookie Exchange Control" is provided with the
120 * request. The cookie provided in the control is used to retrieve entries from
121 * the ReplicaDBs. The <code>changeNumber</code> attribute is not returned with
122 * the entries.</li>
123 * <li>Change number mode: when no "ECL Cookie Exchange Control" is provided
124 * with the request. The entries are retrieved using the ChangeNumberIndexDB and
125 * their attributes are set with the information from the ReplicasDBs. The
126 * <code>changeNumber</code> attribute value is set from the content of
127 * ChangeNumberIndexDB.</li>
128 * </ul>
129 * <h3>Searches flow</h3>
130 * <p>
131 * Here is the flow of searches within the changelog backend APIs:
132 * <ul>
133 * <li>Normal searches only go through:
134 * <ol>
135 * <li>{@link ChangelogBackend#search(SearchOperation)} (once, single threaded)</li>
136 * </ol>
137 * </li>
138 * <li>Persistent searches with <code>changesOnly=false</code> go through:
139 * <ol>
140 * <li>{@link ChangelogBackend#registerPersistentSearch(PersistentSearch)}
141 * (once, single threaded),</li>
142 * <li>
143 * {@link ChangelogBackend#search(SearchOperation)} (once, single threaded)</li>
144 * <li>{@link ChangelogBackend#notify*EntryAdded()} (multiple times, multi
145 * threaded)</li>
146 * </ol>
147 * </li>
148 * <li>Persistent searches with <code>changesOnly=true</code> go through:
149 * <ol>
150 * <li>{@link ChangelogBackend#registerPersistentSearch(PersistentSearch)}
151 * (once, single threaded)</li>
152 * <li>
153 * {@link ChangelogBackend#notify*EntryAdded()} (multiple times, multi
154 * threaded)</li>
155 * </ol>
156 * </li>
157 * </ul>
158 *
159 * @see ReplicationServer
160 */
161public class ChangelogBackend extends Backend<Configuration>
162{
163  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
164
165  /** The id of this backend. */
166  public static final String BACKEND_ID = "changelog";
167
168  private static final long CHANGE_NUMBER_FOR_EMPTY_CURSOR = 0L;
169
170  private static final String CHANGE_NUMBER_ATTR = "changeNumber";
171  private static final String ENTRY_SENDER_ATTACHMENT = OID_ECL_COOKIE_EXCHANGE_CONTROL + ".entrySender";
172
173  /** The set of objectclasses that will be used in root entry. */
174  private static final Map<ObjectClass, String>
175    CHANGELOG_ROOT_OBJECT_CLASSES = new LinkedHashMap<>(2);
176  static
177  {
178    CHANGELOG_ROOT_OBJECT_CLASSES.put(DirectoryServer.getObjectClass(OC_TOP, true), OC_TOP);
179    CHANGELOG_ROOT_OBJECT_CLASSES.put(DirectoryServer.getObjectClass("container", true), "container");
180  }
181
182  /** The set of objectclasses that will be used in ECL entries. */
183  private static final Map<ObjectClass, String>
184    CHANGELOG_ENTRY_OBJECT_CLASSES = new LinkedHashMap<>(2);
185  static
186  {
187    CHANGELOG_ENTRY_OBJECT_CLASSES.put(DirectoryServer.getObjectClass(OC_TOP, true), OC_TOP);
188    CHANGELOG_ENTRY_OBJECT_CLASSES.put(DirectoryServer.getObjectClass(OC_CHANGELOG_ENTRY, true), OC_CHANGELOG_ENTRY);
189  }
190
191  /** The attribute type for the "creatorsName" attribute. */
192  private static final AttributeType CREATORS_NAME_TYPE = getAttributeType(OP_ATTR_CREATORS_NAME);
193  /** The attribute type for the "modifiersName" attribute. */
194  private static final AttributeType MODIFIERS_NAME_TYPE = getAttributeType(OP_ATTR_MODIFIERS_NAME);
195
196  /** The base DN for the external change log. */
197  public static final DN CHANGELOG_BASE_DN = DN.valueOf(DN_EXTERNAL_CHANGELOG_ROOT);
198
199  /** The set of base DNs for this backend. */
200  private DN[] baseDNs;
201  /** The set of supported controls for this backend. */
202  private final Set<String> supportedControls = Collections.singleton(OID_ECL_COOKIE_EXCHANGE_CONTROL);
203  /** Whether the base changelog entry has subordinates. */
204  private Boolean baseEntryHasSubordinates;
205
206  /** The replication server on which the changelog is read. */
207  private final ReplicationServer replicationServer;
208  private final ECLEnabledDomainPredicate domainPredicate;
209
210  /** The set of cookie-based persistent searches registered with this backend. */
211  private final ConcurrentLinkedQueue<PersistentSearch> cookieBasedPersistentSearches = new ConcurrentLinkedQueue<>();
212  /** The set of change number-based persistent searches registered with this backend. */
213  private final ConcurrentLinkedQueue<PersistentSearch> changeNumberBasedPersistentSearches =
214      new ConcurrentLinkedQueue<>();
215
216  /**
217   * Creates a new backend with the provided replication server.
218   *
219   * @param replicationServer
220   *          The replication server on which the changes are read.
221   * @param domainPredicate
222   *          Returns whether a domain is enabled for the external changelog.
223   */
224  public ChangelogBackend(final ReplicationServer replicationServer, final ECLEnabledDomainPredicate domainPredicate)
225  {
226    this.replicationServer = replicationServer;
227    this.domainPredicate = domainPredicate;
228    setBackendID(BACKEND_ID);
229    setWritabilityMode(WritabilityMode.DISABLED);
230    setPrivateBackend(true);
231  }
232
233  private ChangelogDB getChangelogDB()
234  {
235    return replicationServer.getChangelogDB();
236  }
237
238  /**
239   * Returns the ChangelogBackend configured for "cn=changelog" in this directory server.
240   *
241   * @return the ChangelogBackend configured for "cn=changelog" in this directory server
242   * @deprecated instead inject the required object where needed
243   */
244  @Deprecated
245  public static ChangelogBackend getInstance()
246  {
247    return (ChangelogBackend) DirectoryServer.getBackend(CHANGELOG_BASE_DN);
248  }
249
250  @Override
251  public void configureBackend(final Configuration config, ServerContext serverContext) throws ConfigException
252  {
253    throw new UnsupportedOperationException("The changelog backend is not configurable");
254  }
255
256  @Override
257  public void openBackend() throws InitializationException
258  {
259    baseDNs = new DN[] { CHANGELOG_BASE_DN };
260
261    try
262    {
263      DirectoryServer.registerBaseDN(CHANGELOG_BASE_DN, this, true);
264    }
265    catch (final DirectoryException e)
266    {
267      throw new InitializationException(
268          ERR_BACKEND_CANNOT_REGISTER_BASEDN.get(DN_EXTERNAL_CHANGELOG_ROOT, getExceptionMessage(e)), e);
269    }
270  }
271
272  @Override
273  public void closeBackend()
274  {
275    try
276    {
277      DirectoryServer.deregisterBaseDN(CHANGELOG_BASE_DN);
278    }
279    catch (final DirectoryException e)
280    {
281      logger.traceException(e);
282    }
283  }
284
285  @Override
286  public DN[] getBaseDNs()
287  {
288    return baseDNs;
289  }
290
291  @Override
292  public boolean isIndexed(final AttributeType attributeType, final IndexType indexType)
293  {
294    return true;
295  }
296
297  @Override
298  public Entry getEntry(final DN entryDN) throws DirectoryException
299  {
300    if (entryDN == null)
301    {
302      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
303          ERR_BACKEND_GET_ENTRY_NULL.get(getBackendID()));
304    }
305    throw new RuntimeException("Not implemented");
306  }
307
308  @Override
309  public ConditionResult hasSubordinates(final DN entryDN) throws DirectoryException
310  {
311    if (CHANGELOG_BASE_DN.equals(entryDN))
312    {
313      final Boolean hasSubs = baseChangelogHasSubordinates();
314      if (hasSubs == null)
315      {
316        return ConditionResult.UNDEFINED;
317      }
318      return ConditionResult.valueOf(hasSubs);
319    }
320    return ConditionResult.FALSE;
321  }
322
323  private Boolean baseChangelogHasSubordinates() throws DirectoryException
324  {
325    if (baseEntryHasSubordinates == null)
326    {
327      // compute its value
328      try
329      {
330        final ReplicationDomainDB replicationDomainDB = getChangelogDB().getReplicationDomainDB();
331        CursorOptions options = new CursorOptions(GREATER_THAN_OR_EQUAL_TO_KEY, ON_MATCHING_KEY);
332        try (final MultiDomainDBCursor cursor =
333            replicationDomainDB.getCursorFrom(new MultiDomainServerState(), options, getExcludedBaseDNs()))
334        {
335          baseEntryHasSubordinates = cursor.next();
336        }
337      }
338      catch (ChangelogException e)
339      {
340        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_CHANGELOG_BACKEND_ATTRIBUTE.get(
341            "hasSubordinates", DN_EXTERNAL_CHANGELOG_ROOT, stackTraceToSingleLineString(e)));
342      }
343    }
344    return baseEntryHasSubordinates;
345  }
346
347  @Override
348  public long getNumberOfEntriesInBaseDN(final DN baseDN) throws DirectoryException
349  {
350    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_NUM_SUBORDINATES_NOT_SUPPORTED.get());
351  }
352
353  @Override
354  public long getNumberOfChildren(final DN parentDN) throws DirectoryException
355  {
356    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_NUM_SUBORDINATES_NOT_SUPPORTED.get());
357  }
358
359  /**
360   * Notifies persistent searches of this backend that a new cookie entry was added to it.
361   * <p>
362   * Note: This method correspond to the "persistent search" phase.
363   * It is executed multiple times per persistent search, multi-threaded, until the persistent search is cancelled.
364   * <p>
365   * This method must only be called after the provided data have been persisted to disk.
366   *
367   * @param baseDN
368   *          the baseDN of the newly added entry.
369   * @param updateMsg
370   *          the update message of the newly added entry
371   * @throws ChangelogException
372   *           If a problem occurs while notifying of the newly added entry.
373   */
374  public void notifyCookieEntryAdded(DN baseDN, UpdateMsg updateMsg) throws ChangelogException
375  {
376    if (!(updateMsg instanceof LDAPUpdateMsg))
377    {
378      return;
379    }
380
381    try
382    {
383      for (PersistentSearch pSearch : cookieBasedPersistentSearches)
384      {
385        final SearchOperation searchOp = pSearch.getSearchOperation();
386        final CookieEntrySender entrySender = searchOp.getAttachment(ENTRY_SENDER_ATTACHMENT);
387        entrySender.persistentSearchSendEntry(baseDN, updateMsg);
388      }
389    }
390    catch (DirectoryException e)
391    {
392      throw new ChangelogException(e.getMessageObject(), e);
393    }
394  }
395
396  /**
397   * Notifies persistent searches of this backend that a new change number entry was added to it.
398   * <p>
399   * Note: This method correspond to the "persistent search" phase.
400   * It is executed multiple times per persistent search, multi-threaded, until the persistent search is cancelled.
401   * <p>
402   * This method must only be called after the provided data have been persisted to disk.
403   *
404   * @param baseDN
405   *          the baseDN of the newly added entry.
406   * @param changeNumber
407   *          the change number of the newly added entry. It will be greater
408   *          than zero for entries added to the change number index and less
409   *          than or equal to zero for entries added to any replica DB
410   * @param cookieString
411   *          a string representing the cookie of the newly added entry.
412   *          This is only meaningful for entries added to the change number index
413   * @param updateMsg
414   *          the update message of the newly added entry
415   * @throws ChangelogException
416   *           If a problem occurs while notifying of the newly added entry.
417   */
418  public void notifyChangeNumberEntryAdded(DN baseDN, long changeNumber, String cookieString, UpdateMsg updateMsg)
419      throws ChangelogException
420  {
421    if (!(updateMsg instanceof LDAPUpdateMsg)
422        || changeNumberBasedPersistentSearches.isEmpty())
423    {
424      return;
425    }
426
427    try
428    {
429      // changeNumber entry can be shared with multiple persistent searches
430      final Entry changeNumberEntry = createEntryFromMsg(baseDN, changeNumber, cookieString, updateMsg);
431      for (PersistentSearch pSearch : changeNumberBasedPersistentSearches)
432      {
433        final SearchOperation searchOp = pSearch.getSearchOperation();
434        final ChangeNumberEntrySender entrySender = searchOp.getAttachment(ENTRY_SENDER_ATTACHMENT);
435        entrySender.persistentSearchSendEntry(changeNumber, changeNumberEntry);
436      }
437    }
438    catch (DirectoryException e)
439    {
440      throw new ChangelogException(e.getMessageObject(), e);
441    }
442  }
443
444  private boolean isCookieBased(final SearchOperation searchOp)
445  {
446    for (Control c : searchOp.getRequestControls())
447    {
448      if (OID_ECL_COOKIE_EXCHANGE_CONTROL.equals(c.getOID()))
449      {
450        return true;
451      }
452    }
453    return false;
454  }
455
456  @Override
457  public void addEntry(Entry entry, AddOperation addOperation)
458      throws DirectoryException, CanceledOperationException
459  {
460    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
461        ERR_BACKEND_ADD_NOT_SUPPORTED.get(String.valueOf(entry.getName()), getBackendID()));
462  }
463
464  @Override
465  public void deleteEntry(DN entryDN, DeleteOperation deleteOperation)
466      throws DirectoryException, CanceledOperationException
467  {
468    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
469        ERR_BACKEND_DELETE_NOT_SUPPORTED.get(String.valueOf(entryDN), getBackendID()));
470  }
471
472  @Override
473  public void replaceEntry(Entry oldEntry, Entry newEntry,
474      ModifyOperation modifyOperation) throws DirectoryException,
475      CanceledOperationException
476  {
477    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
478        ERR_BACKEND_MODIFY_NOT_SUPPORTED.get(String.valueOf(newEntry.getName()), getBackendID()));
479  }
480
481  @Override
482  public void renameEntry(DN currentDN, Entry entry,
483      ModifyDNOperation modifyDNOperation) throws DirectoryException,
484      CanceledOperationException
485  {
486    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
487        ERR_BACKEND_MODIFY_DN_NOT_SUPPORTED.get(String.valueOf(currentDN), getBackendID()));
488  }
489
490  /**
491   * {@inheritDoc}
492   * <p>
493   * Runs the "initial search" phase (as opposed to a "persistent search"
494   * phase). The "initial search" phase is the only search run by normal
495   * searches, but it is also run by persistent searches with
496   * <code>changesOnly=false</code>. Persistent searches with
497   * <code>changesOnly=true</code> never execute this code.
498   * <p>
499   * Note: this method is executed only once per persistent search, single
500   * threaded.
501   */
502  @Override
503  public void search(final SearchOperation searchOperation) throws DirectoryException
504  {
505    checkChangelogReadPrivilege(searchOperation);
506
507    final Set<DN> excludedBaseDNs = getExcludedBaseDNs();
508    final MultiDomainServerState cookie = getCookieFromControl(searchOperation, excludedBaseDNs);
509
510    final ChangeNumberRange range = optimizeSearch(searchOperation.getBaseDN(), searchOperation.getFilter());
511    try
512    {
513      final boolean isPersistentSearch = isPersistentSearch(searchOperation);
514      if (cookie != null)
515      {
516        initialSearchFromCookie(
517            getCookieEntrySender(SearchPhase.INITIAL, searchOperation, cookie, excludedBaseDNs, isPersistentSearch));
518      }
519      else
520      {
521        initialSearchFromChangeNumber(
522            getChangeNumberEntrySender(SearchPhase.INITIAL, searchOperation, range, isPersistentSearch));
523      }
524    }
525    catch (ChangelogException e)
526    {
527      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_CHANGELOG_BACKEND_SEARCH.get(
528          searchOperation.getBaseDN(), searchOperation.getFilter(), stackTraceToSingleLineString(e)));
529    }
530  }
531
532  private MultiDomainServerState getCookieFromControl(final SearchOperation searchOperation, Set<DN> excludedBaseDNs)
533      throws DirectoryException
534  {
535    final ExternalChangelogRequestControl eclRequestControl =
536        searchOperation.getRequestControl(ExternalChangelogRequestControl.DECODER);
537    if (eclRequestControl != null)
538    {
539      final MultiDomainServerState cookie = eclRequestControl.getCookie();
540      validateProvidedCookie(cookie, excludedBaseDNs);
541      return cookie;
542    }
543    return null;
544  }
545
546  @Override
547  public Set<String> getSupportedControls()
548  {
549    return supportedControls;
550  }
551
552  @Override
553  public Set<String> getSupportedFeatures()
554  {
555    return Collections.emptySet();
556  }
557
558  @Override
559  public boolean supports(BackendOperation backendOperation)
560  {
561    return false;
562  }
563
564  @Override
565  public void exportLDIF(final LDIFExportConfig exportConfig)
566      throws DirectoryException
567  {
568    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
569        ERR_BACKEND_IMPORT_AND_EXPORT_NOT_SUPPORTED.get(getBackendID()));
570  }
571
572  @Override
573  public LDIFImportResult importLDIF(LDIFImportConfig importConfig, ServerContext serverContext)
574      throws DirectoryException
575  {
576    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
577        ERR_BACKEND_IMPORT_AND_EXPORT_NOT_SUPPORTED.get(getBackendID()));
578  }
579
580  @Override
581  public void createBackup(BackupConfig backupConfig) throws DirectoryException
582  {
583    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
584        ERR_BACKEND_BACKUP_AND_RESTORE_NOT_SUPPORTED.get(getBackendID()));
585  }
586
587  @Override
588  public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException
589  {
590      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
591          ERR_BACKEND_BACKUP_AND_RESTORE_NOT_SUPPORTED.get(getBackendID()));
592  }
593
594  @Override
595  public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException
596  {
597      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
598          ERR_BACKEND_BACKUP_AND_RESTORE_NOT_SUPPORTED.get(getBackendID()));
599  }
600
601  @Override
602  public long getEntryCount()
603  {
604    try
605    {
606      return getNumberOfEntriesInBaseDN(CHANGELOG_BASE_DN) + 1;
607    }
608    catch (DirectoryException e)
609    {
610      logger.traceException(e);
611      return -1;
612    }
613  }
614
615  /**
616   * Represent the change number range targeted by a search operation.
617   * <p>
618   * This class should be visible for tests.
619   */
620  static final class ChangeNumberRange
621  {
622    private long lowerBound = -1;
623    private long upperBound = -1;
624
625    /**
626     * Returns the lowest change number to retrieve (inclusive).
627     *
628     * @return the lowest change number
629     */
630    long getLowerBound()
631    {
632      return lowerBound;
633    }
634
635    /**
636     * Returns the highest change number to retrieve (inclusive).
637     *
638     * @return the highest change number
639     */
640    long getUpperBound()
641    {
642      return upperBound;
643    }
644  }
645
646  /**
647   * Returns the set of DNs to exclude from the search.
648   *
649   * @return the DNs corresponding to domains to exclude from the search.
650   * @throws DirectoryException
651   *           If a DN can't be decoded.
652   */
653  private static Set<DN> getExcludedBaseDNs() throws DirectoryException
654  {
655    return getExcludedChangelogDomains();
656  }
657
658  /**
659   * Optimize the search parameters by analyzing the DN and filter.
660   * It also performs validation on some search parameters
661   * for both cookie and change number based changelogs.
662   *
663   * @param baseDN the provided search baseDN.
664   * @param userFilter the provided search filter.
665   * @return the optimized change number range
666   * @throws DirectoryException when an exception occurs.
667   */
668  ChangeNumberRange optimizeSearch(final DN baseDN, final SearchFilter userFilter) throws DirectoryException
669  {
670    SearchFilter equalityFilter = null;
671    switch (baseDN.size())
672    {
673    case 1:
674      // "cn=changelog" : use user-provided search filter.
675      break;
676    case 2:
677      // It is probably "changeNumber=xxx,cn=changelog", use equality filter
678      // But it also could be "<service-id>,cn=changelog" so need to check on attribute
679      equalityFilter = buildSearchFilterFrom(baseDN, CHANGE_NUMBER_ATTR);
680      break;
681    default:
682      // "replicationCSN=xxx,<service-id>,cn=changelog" : use equality filter
683      equalityFilter = buildSearchFilterFrom(baseDN, "replicationCSN");
684      break;
685    }
686
687    return optimizeSearchUsingFilter(equalityFilter != null ? equalityFilter : userFilter);
688  }
689
690  /**
691   * Build a search filter from given DN and attribute.
692   *
693   * @return the search filter or {@code null} if attribute is not present in
694   *         the provided DN
695   */
696  private SearchFilter buildSearchFilterFrom(final DN baseDN, final String attrName)
697  {
698    final RDN rdn = baseDN.rdn();
699    AttributeType attrType = DirectoryServer.getAttributeType(attrName);
700    final ByteString attrValue = rdn.getAttributeValue(attrType);
701    if (attrValue != null)
702    {
703      return SearchFilter.createEqualityFilter(attrType, attrValue);
704    }
705    return null;
706  }
707
708  private ChangeNumberRange optimizeSearchUsingFilter(final SearchFilter filter) throws DirectoryException
709  {
710    final ChangeNumberRange range = new ChangeNumberRange();
711    if (filter == null)
712    {
713      return range;
714    }
715
716    if (matches(filter, FilterType.GREATER_OR_EQUAL, CHANGE_NUMBER_ATTR))
717    {
718      range.lowerBound = decodeChangeNumber(filter.getAssertionValue());
719    }
720    else if (matches(filter, FilterType.LESS_OR_EQUAL, CHANGE_NUMBER_ATTR))
721    {
722      range.upperBound = decodeChangeNumber(filter.getAssertionValue());
723    }
724    else if (matches(filter, FilterType.EQUALITY, CHANGE_NUMBER_ATTR))
725    {
726      final long number = decodeChangeNumber(filter.getAssertionValue());
727      range.lowerBound = number;
728      range.upperBound = number;
729    }
730    else if (matches(filter, FilterType.EQUALITY, "replicationcsn"))
731    {
732      // == exact CSN
733      // validate provided CSN is correct
734      new CSN(filter.getAssertionValue().toString());
735    }
736    else if (filter.getFilterType() == FilterType.AND)
737    {
738      // TODO: it looks like it could be generalized to N components, not only two
739      final Collection<SearchFilter> components = filter.getFilterComponents();
740      final SearchFilter filters[] = components.toArray(new SearchFilter[0]);
741      long upper1 = -1;
742      long lower1 = -1;
743      long upper2 = -1;
744      long lower2 = -1;
745      if (filters.length > 0)
746      {
747        ChangeNumberRange range1 = optimizeSearchUsingFilter(filters[0]);
748        upper1 = range1.upperBound;
749        lower1 = range1.lowerBound;
750      }
751      if (filters.length > 1)
752      {
753        ChangeNumberRange range2 = optimizeSearchUsingFilter(filters[1]);
754        upper2 = range2.upperBound;
755        lower2 = range2.lowerBound;
756      }
757      if (upper1 == -1)
758      {
759        range.upperBound = upper2;
760      }
761      else if (upper2 == -1)
762      {
763        range.upperBound = upper1;
764      }
765      else
766      {
767        range.upperBound = Math.min(upper1, upper2);
768      }
769
770      range.lowerBound = Math.max(lower1, lower2);
771    }
772    return range;
773  }
774
775  private static long decodeChangeNumber(final ByteString assertionValue)
776      throws DirectoryException
777  {
778    try
779    {
780      return Long.decode(assertionValue.toString());
781    }
782    catch (NumberFormatException e)
783    {
784      throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
785          LocalizableMessage.raw("Could not convert value '%s' to long", assertionValue));
786    }
787  }
788
789  private boolean matches(SearchFilter filter, FilterType filterType, String primaryName)
790  {
791    return filter.getFilterType() == filterType
792           && filter.getAttributeType() != null
793           && filter.getAttributeType().getNameOrOID().equalsIgnoreCase(primaryName);
794  }
795
796  /** Search the changelog when a cookie control is provided. */
797  private void initialSearchFromCookie(final CookieEntrySender entrySender)
798      throws DirectoryException, ChangelogException
799  {
800    if (!sendBaseChangelogEntry(entrySender.searchOp))
801    { // only return the base entry: stop here
802      return;
803    }
804
805    final ReplicationDomainDB replicationDomainDB = getChangelogDB().getReplicationDomainDB();
806    CursorOptions options = new CursorOptions(GREATER_THAN_OR_EQUAL_TO_KEY, AFTER_MATCHING_KEY);
807    try (final MultiDomainDBCursor cursor =
808        replicationDomainDB.getCursorFrom(entrySender.cookie, options, entrySender.excludedBaseDNs);
809        ECLMultiDomainDBCursor replicaUpdatesCursor = new ECLMultiDomainDBCursor(domainPredicate, cursor))
810    {
811      if (sendCookieEntriesFromCursor(entrySender, replicaUpdatesCursor))
812      {
813        entrySender.transitioningToPersistentSearchPhase();
814        sendCookieEntriesFromCursor(entrySender, replicaUpdatesCursor);
815      }
816    }
817    finally
818    {
819      entrySender.finalizeInitialSearch();
820    }
821  }
822
823  private CookieEntrySender getCookieEntrySender(SearchPhase startPhase, final SearchOperation searchOperation,
824      MultiDomainServerState cookie, Set<DN> excludedBaseDNs, boolean isPersistentSearch)
825  {
826    if (isPersistentSearch && SearchPhase.INITIAL.equals(startPhase))
827    {
828      return searchOperation.getAttachment(ENTRY_SENDER_ATTACHMENT);
829    }
830    return new CookieEntrySender(searchOperation, startPhase, cookie, excludedBaseDNs);
831  }
832
833  private boolean sendCookieEntriesFromCursor(final CookieEntrySender entrySender,
834      final ECLMultiDomainDBCursor replicaUpdatesCursor) throws ChangelogException, DirectoryException
835  {
836    boolean continueSearch = true;
837    while (continueSearch && replicaUpdatesCursor.next())
838    {
839      final UpdateMsg updateMsg = replicaUpdatesCursor.getRecord();
840      final DN domainBaseDN = replicaUpdatesCursor.getData();
841      continueSearch = entrySender.initialSearchSendEntry(updateMsg, domainBaseDN);
842    }
843    return continueSearch;
844  }
845
846  private boolean isPersistentSearch(SearchOperation op)
847  {
848    for (PersistentSearch pSearch : getPersistentSearches())
849    {
850      if (op == pSearch.getSearchOperation())
851      {
852        return true;
853      }
854    }
855    return false;
856  }
857
858  @Override
859  public void registerPersistentSearch(PersistentSearch pSearch) throws DirectoryException
860  {
861    initializePersistentSearch(pSearch);
862
863    if (isCookieBased(pSearch.getSearchOperation()))
864    {
865      cookieBasedPersistentSearches.add(pSearch);
866    }
867    else
868    {
869      changeNumberBasedPersistentSearches.add(pSearch);
870    }
871    super.registerPersistentSearch(pSearch);
872  }
873
874  private void initializePersistentSearch(PersistentSearch pSearch) throws DirectoryException
875  {
876    final SearchOperation searchOp = pSearch.getSearchOperation();
877
878    // Validation must be done during registration for changes only persistent searches.
879    // Otherwise, when there is an initial search phase,
880    // validation is performed by the search() method.
881    if (pSearch.isChangesOnly())
882    {
883      checkChangelogReadPrivilege(searchOp);
884    }
885    final ChangeNumberRange range = optimizeSearch(searchOp.getBaseDN(), searchOp.getFilter());
886
887    final SearchPhase startPhase = pSearch.isChangesOnly() ? SearchPhase.PERSISTENT : SearchPhase.INITIAL;
888    if (isCookieBased(searchOp))
889    {
890      final Set<DN> excludedBaseDNs = getExcludedBaseDNs();
891      final MultiDomainServerState cookie = getCookie(pSearch.isChangesOnly(), searchOp, excludedBaseDNs);
892      searchOp.setAttachment(ENTRY_SENDER_ATTACHMENT,
893          new CookieEntrySender(searchOp, startPhase, cookie, excludedBaseDNs));
894    }
895    else
896    {
897      searchOp.setAttachment(ENTRY_SENDER_ATTACHMENT,
898          new ChangeNumberEntrySender(searchOp, startPhase, range));
899    }
900  }
901
902  private MultiDomainServerState getCookie(boolean isChangesOnly, SearchOperation searchOp, Set<DN> excludedBaseDNs)
903      throws DirectoryException
904  {
905    if (isChangesOnly)
906    {
907      // this changesOnly persistent search will not go through #initialSearch()
908      // so we must initialize the cookie here
909      return getNewestCookie(searchOp);
910    }
911    return getCookieFromControl(searchOp, excludedBaseDNs);
912  }
913
914  private MultiDomainServerState getNewestCookie(SearchOperation searchOp)
915  {
916    if (!isCookieBased(searchOp))
917    {
918      return null;
919    }
920
921    final MultiDomainServerState cookie = new MultiDomainServerState();
922    for (final Iterator<ReplicationServerDomain> it =
923        replicationServer.getDomainIterator(); it.hasNext();)
924    {
925      final DN baseDN = it.next().getBaseDN();
926      final ServerState state = getChangelogDB().getReplicationDomainDB().getDomainNewestCSNs(baseDN);
927      cookie.update(baseDN, state);
928    }
929    return cookie;
930  }
931
932  /**
933   * Validates the cookie contained in search parameters by checking its content
934   * with the actual replication server state.
935   *
936   * @throws DirectoryException
937   *           If the state is not valid
938   */
939  private void validateProvidedCookie(final MultiDomainServerState cookie, Set<DN> excludedBaseDNs)
940      throws DirectoryException
941  {
942    if (cookie != null && !cookie.isEmpty())
943    {
944      replicationServer.validateCookie(cookie, excludedBaseDNs);
945    }
946  }
947
948  /** Search the changelog using change number(s). */
949  private void initialSearchFromChangeNumber(final ChangeNumberEntrySender entrySender)
950      throws ChangelogException, DirectoryException
951  {
952    if (!sendBaseChangelogEntry(entrySender.searchOp))
953    { // only return the base entry: stop here
954      return;
955    }
956
957    final AtomicReference<MultiDomainDBCursor> replicaUpdatesCursor = new AtomicReference<>();
958    try (DBCursor<ChangeNumberIndexRecord> cnIndexDBCursor = getCNIndexDBCursor(entrySender.lowestChangeNumber))
959    {
960      final MultiDomainServerState cookie = new MultiDomainServerState();
961
962      if (sendChangeNumberEntriesFromCursors(entrySender, cnIndexDBCursor, replicaUpdatesCursor, cookie))
963      {
964        entrySender.transitioningToPersistentSearchPhase();
965        sendChangeNumberEntriesFromCursors(entrySender, cnIndexDBCursor, replicaUpdatesCursor, cookie);
966      }
967    }
968    finally
969    {
970      entrySender.finalizeInitialSearch();
971      StaticUtils.close(replicaUpdatesCursor.get());
972    }
973  }
974
975  private ChangeNumberEntrySender getChangeNumberEntrySender(SearchPhase startPhase,
976      final SearchOperation searchOperation, ChangeNumberRange range, boolean isPersistentSearch)
977  {
978    if (isPersistentSearch && SearchPhase.INITIAL.equals(startPhase))
979    {
980      return searchOperation.getAttachment(ENTRY_SENDER_ATTACHMENT);
981    }
982    return new ChangeNumberEntrySender(searchOperation, SearchPhase.INITIAL, range);
983  }
984
985  private boolean sendChangeNumberEntriesFromCursors(final ChangeNumberEntrySender entrySender,
986      DBCursor<ChangeNumberIndexRecord> cnIndexDBCursor, AtomicReference<MultiDomainDBCursor> replicaUpdatesCursor,
987      MultiDomainServerState cookie) throws ChangelogException, DirectoryException
988  {
989    boolean continueSearch = true;
990    while (continueSearch && cnIndexDBCursor.next())
991    {
992      // Handle the current cnIndex record
993      final ChangeNumberIndexRecord cnIndexRecord = cnIndexDBCursor.getRecord();
994      if (replicaUpdatesCursor.get() == null)
995      {
996        replicaUpdatesCursor.set(initializeReplicaUpdatesCursor(cnIndexRecord));
997        initializeCookieForChangeNumberMode(cookie, cnIndexRecord);
998      }
999      else
1000      {
1001        cookie.update(cnIndexRecord.getBaseDN(), cnIndexRecord.getCSN());
1002      }
1003      continueSearch = entrySender.changeNumberIsInRange(cnIndexRecord.getChangeNumber());
1004      if (continueSearch)
1005      {
1006        final UpdateMsg updateMsg = findReplicaUpdateMessage(replicaUpdatesCursor.get(), cnIndexRecord.getCSN());
1007        if (updateMsg != null)
1008        {
1009          continueSearch = entrySender.initialSearchSendEntry(cnIndexRecord, updateMsg, cookie);
1010          replicaUpdatesCursor.get().next();
1011        }
1012      }
1013    }
1014    return continueSearch;
1015  }
1016
1017  /** Initialize the provided cookie from the provided change number index record. */
1018  private void initializeCookieForChangeNumberMode(
1019      MultiDomainServerState cookie, final ChangeNumberIndexRecord cnIndexRecord) throws ChangelogException
1020  {
1021    // Initialize the multi domain cursor only from the change number index record.
1022    // The cookie is always empty at this stage.
1023    CursorOptions options = new CursorOptions(LESS_THAN_OR_EQUAL_TO_KEY, ON_MATCHING_KEY, cnIndexRecord.getCSN());
1024    MultiDomainServerState unused = new MultiDomainServerState();
1025    MultiDomainDBCursor cursor = getChangelogDB().getReplicationDomainDB().getCursorFrom(unused, options);
1026    try (ECLMultiDomainDBCursor eclCursor = new ECLMultiDomainDBCursor(domainPredicate, cursor))
1027    {
1028      updateCookieToMediumConsistencyPoint(cookie, eclCursor, cnIndexRecord);
1029    }
1030  }
1031
1032  /**
1033   * Rebuilds the changelogcookie starting at the newest change number index record.
1034   * <p>
1035   * It updates the provided cookie with the changes from the provided ECL cursor,
1036   * up to (and including) the provided change number index record.
1037   * <p>
1038   * Therefore, after calling this method, the cursor is positioned
1039   * to the change immediately following the provided change number index record.
1040   *
1041   * @param cookie the cookie to update
1042   * @param cursor the cursor where to read changes from
1043   * @param cnIndexRecord the change number index record to go right after
1044   * @throws ChangelogException if any problem occurs
1045   */
1046  public static void updateCookieToMediumConsistencyPoint(
1047      MultiDomainServerState cookie, ECLMultiDomainDBCursor cursor, ChangeNumberIndexRecord cnIndexRecord)
1048          throws ChangelogException
1049  {
1050    if (cnIndexRecord == null)
1051    {
1052      return;
1053    }
1054
1055    while (cursor.next())
1056    {
1057      UpdateMsg updateMsg = cursor.getRecord();
1058      if (updateMsg.getCSN().compareTo(cnIndexRecord.getCSN()) > 0)
1059      {
1060        break;
1061      }
1062      cookie.update(cursor.getData(), updateMsg.getCSN());
1063    }
1064  }
1065
1066  private MultiDomainDBCursor initializeReplicaUpdatesCursor(
1067      final ChangeNumberIndexRecord cnIndexRecord) throws ChangelogException
1068  {
1069    final MultiDomainServerState state = new MultiDomainServerState();
1070    state.update(cnIndexRecord.getBaseDN(), cnIndexRecord.getCSN());
1071
1072    // No need for ECLMultiDomainDBCursor in this case
1073    // as updateMsg will be matched with cnIndexRecord
1074    CursorOptions options = new CursorOptions(GREATER_THAN_OR_EQUAL_TO_KEY, ON_MATCHING_KEY);
1075    final MultiDomainDBCursor replicaUpdatesCursor =
1076        getChangelogDB().getReplicationDomainDB().getCursorFrom(state, options);
1077    replicaUpdatesCursor.next();
1078    return replicaUpdatesCursor;
1079  }
1080
1081  /**
1082   * Returns the replica update message corresponding to the provided
1083   * cnIndexRecord.
1084   *
1085   * @return the update message, which may be {@code null} if the update message
1086   *         could not be found because it was purged or because corresponding
1087   *         baseDN was removed from the changelog
1088   * @throws DirectoryException
1089   *           If inconsistency is detected between the available update
1090   *           messages and the provided cnIndexRecord
1091   */
1092  private UpdateMsg findReplicaUpdateMessage(final MultiDomainDBCursor replicaUpdatesCursor, CSN csn)
1093      throws ChangelogException, DirectoryException
1094  {
1095    while (true)
1096    {
1097      final UpdateMsg updateMsg = replicaUpdatesCursor.getRecord();
1098      final int compareIndexWithUpdateMsg = csn.compareTo(updateMsg.getCSN());
1099      if (compareIndexWithUpdateMsg < 0) {
1100        // Either update message has been purged or baseDN has been removed from changelogDB,
1101        // ignore current index record and go to the next one
1102        return null;
1103      }
1104      else if (compareIndexWithUpdateMsg == 0)
1105      {
1106        // Found the matching update message
1107        return updateMsg;
1108      }
1109      // Case compareIndexWithUpdateMsg > 0 : the update message has not bean reached yet
1110      if (!replicaUpdatesCursor.next())
1111      {
1112        // Should never happen, as it means some messages have disappeared
1113        // TODO : put the correct I18N message
1114        throw new DirectoryException(ResultCode.OPERATIONS_ERROR,
1115            LocalizableMessage.raw("Could not find replica update message matching index record. " +
1116                "No more replica update messages with a csn newer than " + updateMsg.getCSN() + " exist."));
1117      }
1118    }
1119  }
1120
1121  /** Returns a cursor on CNIndexDB for the provided first change number. */
1122  private DBCursor<ChangeNumberIndexRecord> getCNIndexDBCursor(
1123      final long firstChangeNumber) throws ChangelogException
1124  {
1125    final ChangeNumberIndexDB cnIndexDB = getChangelogDB().getChangeNumberIndexDB();
1126    long changeNumberToUse = firstChangeNumber;
1127    if (changeNumberToUse <= 1)
1128    {
1129      final ChangeNumberIndexRecord oldestRecord = cnIndexDB.getOldestRecord();
1130      changeNumberToUse = oldestRecord == null ? CHANGE_NUMBER_FOR_EMPTY_CURSOR : oldestRecord.getChangeNumber();
1131    }
1132    return cnIndexDB.getCursorFrom(changeNumberToUse);
1133  }
1134
1135  /** Creates a changelog entry. */
1136  private static Entry createEntryFromMsg(final DN baseDN, final long changeNumber, final String cookie,
1137      final UpdateMsg msg) throws DirectoryException
1138  {
1139    if (msg instanceof AddMsg)
1140    {
1141      return createAddMsg(baseDN, changeNumber, cookie, msg);
1142    }
1143    else if (msg instanceof ModifyCommonMsg)
1144    {
1145      return createModifyMsg(baseDN, changeNumber, cookie, msg);
1146    }
1147    else if (msg instanceof DeleteMsg)
1148    {
1149      final DeleteMsg delMsg = (DeleteMsg) msg;
1150      return createChangelogEntry(baseDN, changeNumber, cookie, delMsg, null, "delete", delMsg.getInitiatorsName());
1151    }
1152    throw new DirectoryException(ResultCode.OPERATIONS_ERROR,
1153        LocalizableMessage.raw("Unexpected message type when trying to create changelog entry for dn %s : %s", baseDN,
1154            msg.getClass()));
1155  }
1156
1157  /**
1158   * Creates an entry from an add message.
1159   * <p>
1160   * Map addMsg to an LDIF string for the 'changes' attribute, and pull out
1161   * change initiators name if available which is contained in the creatorsName
1162   * attribute.
1163   */
1164  private static Entry createAddMsg(final DN baseDN, final long changeNumber, final String cookie, final UpdateMsg msg)
1165      throws DirectoryException
1166  {
1167    final AddMsg addMsg = (AddMsg) msg;
1168    String changeInitiatorsName = null;
1169    String ldifChanges = null;
1170    try
1171    {
1172      final StringBuilder builder = new StringBuilder(256);
1173      for (Attribute attr : addMsg.getAttributes())
1174      {
1175        if (attr.getAttributeDescription().getAttributeType().equals(CREATORS_NAME_TYPE) && !attr.isEmpty())
1176        {
1177          // This attribute is not multi-valued.
1178          changeInitiatorsName = attr.iterator().next().toString();
1179        }
1180        final String attrName = attr.getNameWithOptions();
1181        for (ByteString value : attr)
1182        {
1183          builder.append(attrName);
1184          appendLDIFSeparatorAndValue(builder, value);
1185          builder.append('\n');
1186        }
1187      }
1188      ldifChanges = builder.toString();
1189    }
1190    catch (Exception e)
1191    {
1192      logEncodingMessageError("add", addMsg.getDN(), e);
1193    }
1194
1195    return createChangelogEntry(baseDN, changeNumber, cookie, addMsg, ldifChanges, "add", changeInitiatorsName);
1196  }
1197
1198  /**
1199   * Creates an entry from a modify message.
1200   * <p>
1201   * Map the modifyMsg to an LDIF string for the 'changes' attribute, and pull
1202   * out change initiators name if available which is contained in the
1203   * modifiersName attribute.
1204   */
1205  private static Entry createModifyMsg(final DN baseDN, final long changeNumber, final String cookie,
1206      final UpdateMsg msg) throws DirectoryException
1207  {
1208    final ModifyCommonMsg modifyMsg = (ModifyCommonMsg) msg;
1209    String changeInitiatorsName = null;
1210    String ldifChanges = null;
1211    try
1212    {
1213      final StringBuilder builder = new StringBuilder(128);
1214      for (Modification mod : modifyMsg.getMods())
1215      {
1216        final Attribute attr = mod.getAttribute();
1217        if (mod.getModificationType() == ModificationType.REPLACE
1218            && attr.getAttributeDescription().getAttributeType().equals(MODIFIERS_NAME_TYPE)
1219            && !attr.isEmpty())
1220        {
1221          // This attribute is not multi-valued.
1222          changeInitiatorsName = attr.iterator().next().toString();
1223        }
1224        final String attrName = attr.getNameWithOptions();
1225        builder.append(mod.getModificationType());
1226        builder.append(": ");
1227        builder.append(attrName);
1228        builder.append('\n');
1229
1230        for (ByteString value : attr)
1231        {
1232          builder.append(attrName);
1233          appendLDIFSeparatorAndValue(builder, value);
1234          builder.append('\n');
1235        }
1236        builder.append("-\n");
1237      }
1238      ldifChanges = builder.toString();
1239    }
1240    catch (Exception e)
1241    {
1242      logEncodingMessageError("modify", modifyMsg.getDN(), e);
1243    }
1244
1245    final boolean isModifyDNMsg = modifyMsg instanceof ModifyDNMsg;
1246    final Entry entry = createChangelogEntry(baseDN, changeNumber, cookie, modifyMsg, ldifChanges,
1247        isModifyDNMsg ? "modrdn" : "modify", changeInitiatorsName);
1248
1249    if (isModifyDNMsg)
1250    {
1251      final ModifyDNMsg modDNMsg = (ModifyDNMsg) modifyMsg;
1252      addAttribute(entry, "newrdn", modDNMsg.getNewRDN());
1253      if (modDNMsg.getNewSuperior() != null)
1254      {
1255        addAttribute(entry, "newsuperior", modDNMsg.getNewSuperior());
1256      }
1257      addAttribute(entry, "deleteoldrdn", String.valueOf(modDNMsg.deleteOldRdn()));
1258    }
1259    return entry;
1260  }
1261
1262  /**
1263   * Log an encoding message error.
1264   *
1265   * @param messageType
1266   *            String identifying type of message. Should be "add" or "modify".
1267   * @param entryDN
1268   *            DN of original entry
1269   */
1270  private static void logEncodingMessageError(String messageType, DN entryDN, Exception exception)
1271  {
1272    logger.traceException(exception);
1273    logger.error(LocalizableMessage.raw(
1274        "An exception was encountered while trying to encode a replication " + messageType + " message for entry \""
1275        + entryDN + "\" into an External Change Log entry: " + exception.getMessage()));
1276  }
1277
1278  private void checkChangelogReadPrivilege(SearchOperation searchOp) throws DirectoryException
1279  {
1280    if (!searchOp.getClientConnection().hasPrivilege(Privilege.CHANGELOG_READ, searchOp))
1281    {
1282      throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
1283          NOTE_SEARCH_CHANGELOG_INSUFFICIENT_PRIVILEGES.get());
1284    }
1285  }
1286
1287  /**
1288   * Create a changelog entry from a set of provided information. This is the part of
1289   * entry creation common to all types of msgs (ADD, DEL, MOD, MODDN).
1290   */
1291  private static Entry createChangelogEntry(final DN baseDN, final long changeNumber, final String cookie,
1292      final LDAPUpdateMsg msg, final String ldifChanges, final String changeType,
1293      final String changeInitiatorsName) throws DirectoryException
1294  {
1295    final CSN csn = msg.getCSN();
1296    String dnString;
1297    if (changeNumber > 0)
1298    {
1299      // change number mode
1300      dnString = "changeNumber=" + changeNumber + "," + DN_EXTERNAL_CHANGELOG_ROOT;
1301    }
1302    else
1303    {
1304      // Cookie mode
1305      dnString = "replicationCSN=" + csn + "," + baseDN + "," + DN_EXTERNAL_CHANGELOG_ROOT;
1306    }
1307
1308    final Map<AttributeType, List<Attribute>> userAttrs = new LinkedHashMap<>();
1309    final Map<AttributeType, List<Attribute>> opAttrs = new LinkedHashMap<>();
1310
1311    // Operational standard attributes
1312    addAttributeByType(ATTR_SUBSCHEMA_SUBENTRY_LC, ATTR_SUBSCHEMA_SUBENTRY_LC,
1313        ConfigConstants.DN_DEFAULT_SCHEMA_ROOT, userAttrs, opAttrs);
1314    addAttributeByType("numsubordinates", "numSubordinates", "0", userAttrs, opAttrs);
1315    addAttributeByType("hassubordinates", "hasSubordinates", "false", userAttrs, opAttrs);
1316    addAttributeByType("entrydn", "entryDN", dnString, userAttrs, opAttrs);
1317
1318    // REQUIRED attributes
1319    if (changeNumber > 0)
1320    {
1321      addAttributeByType("changenumber", "changeNumber", String.valueOf(changeNumber), userAttrs, opAttrs);
1322    }
1323    SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_GMT_TIME);
1324    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); // ??
1325    final String format = dateFormat.format(new Date(csn.getTime()));
1326    addAttributeByType("changetime", "changeTime", format, userAttrs, opAttrs);
1327    addAttributeByType("changetype", "changeType", changeType, userAttrs, opAttrs);
1328    addAttributeByType("targetdn", "targetDN", msg.getDN().toString(), userAttrs, opAttrs);
1329
1330    // NON REQUESTED attributes
1331    addAttributeByType("replicationcsn", "replicationCSN", csn.toString(), userAttrs, opAttrs);
1332    addAttributeByType("replicaidentifier", "replicaIdentifier", Integer.toString(csn.getServerId()),
1333        userAttrs, opAttrs);
1334
1335    if (ldifChanges != null)
1336    {
1337      addAttributeByType("changes", "changes", ldifChanges, userAttrs, opAttrs);
1338    }
1339    if (changeInitiatorsName != null)
1340    {
1341      addAttributeByType("changeinitiatorsname", "changeInitiatorsName", changeInitiatorsName, userAttrs, opAttrs);
1342    }
1343
1344    final String targetUUID = msg.getEntryUUID();
1345    if (targetUUID != null)
1346    {
1347      addAttributeByType("targetentryuuid", "targetEntryUUID", targetUUID, userAttrs, opAttrs);
1348    }
1349    final String cookie2 = cookie != null ? cookie : "";
1350    addAttributeByType("changelogcookie", "changeLogCookie", cookie2, userAttrs, opAttrs);
1351
1352    final List<RawAttribute> includedAttributes = msg.getEclIncludes();
1353    if (includedAttributes != null && !includedAttributes.isEmpty())
1354    {
1355      final StringBuilder builder = new StringBuilder(256);
1356      for (final RawAttribute includedAttribute : includedAttributes)
1357      {
1358        final String name = includedAttribute.getAttributeType();
1359        for (final ByteString value : includedAttribute.getValues())
1360        {
1361          builder.append(name);
1362          appendLDIFSeparatorAndValue(builder, value);
1363          builder.append('\n');
1364        }
1365      }
1366      final String includedAttributesLDIF = builder.toString();
1367      addAttributeByType("includedattributes", "includedAttributes", includedAttributesLDIF, userAttrs, opAttrs);
1368    }
1369
1370    return new Entry(DN.valueOf(dnString), CHANGELOG_ENTRY_OBJECT_CLASSES, userAttrs, opAttrs);
1371  }
1372
1373  /**
1374   * Sends the entry if it matches the base, scope and filter of the current search operation.
1375   * It will also send the base changelog entry if it needs to be sent and was not sent before.
1376   *
1377   * @return {@code true} if search should continue, {@code false} otherwise
1378   */
1379  private static boolean sendEntryIfMatches(SearchOperation searchOp, Entry entry, String cookie)
1380      throws DirectoryException
1381  {
1382    if (matchBaseAndScopeAndFilter(searchOp, entry))
1383    {
1384      return searchOp.returnEntry(entry, getControls(cookie));
1385    }
1386    // maybe the next entry will match?
1387    return true;
1388  }
1389
1390  /** Indicates if the provided entry matches the filter, base and scope. */
1391  private static boolean matchBaseAndScopeAndFilter(SearchOperation searchOp, Entry entry) throws DirectoryException
1392  {
1393    return entry.matchesBaseAndScope(searchOp.getBaseDN(), searchOp.getScope())
1394        && searchOp.getFilter().matchesEntry(entry);
1395  }
1396
1397  private static List<Control> getControls(String cookie)
1398  {
1399    if (cookie != null)
1400    {
1401      final Control c = new EntryChangelogNotificationControl(true, cookie);
1402      return Collections.singletonList(c);
1403    }
1404    return Collections.emptyList();
1405  }
1406
1407  /**
1408   * Create and returns the base changelog entry to the underlying search operation.
1409   * <p>
1410   * "initial search" phase must return the base entry immediately.
1411   *
1412   * @return {@code true} if search should continue, {@code false} otherwise
1413   */
1414  private boolean sendBaseChangelogEntry(SearchOperation searchOp) throws DirectoryException
1415  {
1416    final DN baseDN = searchOp.getBaseDN();
1417    final SearchFilter filter = searchOp.getFilter();
1418    final SearchScope scope = searchOp.getScope();
1419
1420    if (ChangelogBackend.CHANGELOG_BASE_DN.isInScopeOf(baseDN, scope))
1421    {
1422      final Entry entry = buildBaseChangelogEntry();
1423      if (filter.matchesEntry(entry) && !searchOp.returnEntry(entry, null))
1424      {
1425        // Abandon, size limit reached.
1426        return false;
1427      }
1428    }
1429    return !baseDN.equals(ChangelogBackend.CHANGELOG_BASE_DN)
1430        || !scope.equals(SearchScope.BASE_OBJECT);
1431  }
1432
1433  private Entry buildBaseChangelogEntry() throws DirectoryException
1434  {
1435    final String hasSubordinatesStr = Boolean.toString(baseChangelogHasSubordinates());
1436
1437    final Map<AttributeType, List<Attribute>> userAttrs = new LinkedHashMap<>();
1438    final Map<AttributeType, List<Attribute>> operationalAttrs = new LinkedHashMap<>();
1439
1440    // We never return the numSubordinates attribute for the base changelog entry
1441    // and there is a very good reason for that:
1442    // - Either we compute it before sending the entries,
1443    // -- then we risk returning more entries if new entries come in after we computed numSubordinates
1444    // --   or we risk returning less entries if purge kicks in      after we computed numSubordinates
1445    // - Or we accumulate all the entries that must be returned before sending them => OutOfMemoryError
1446
1447    addAttributeByUppercaseName(ATTR_COMMON_NAME, ATTR_COMMON_NAME, BACKEND_ID, userAttrs, operationalAttrs);
1448    addAttributeByUppercaseName(ATTR_SUBSCHEMA_SUBENTRY_LC, ATTR_SUBSCHEMA_SUBENTRY,
1449        ConfigConstants.DN_DEFAULT_SCHEMA_ROOT, userAttrs, operationalAttrs);
1450    addAttributeByUppercaseName("hassubordinates", "hasSubordinates", hasSubordinatesStr, userAttrs, operationalAttrs);
1451    addAttributeByUppercaseName("entrydn", "entryDN", DN_EXTERNAL_CHANGELOG_ROOT, userAttrs, operationalAttrs);
1452    return new Entry(CHANGELOG_BASE_DN, CHANGELOG_ROOT_OBJECT_CLASSES, userAttrs, operationalAttrs);
1453  }
1454
1455  private static void addAttribute(final Entry e, final String attrType, final String attrValue)
1456  {
1457    e.addAttribute(Attributes.create(attrType, attrValue), null);
1458  }
1459
1460  private static void addAttributeByType(String attrNameLowercase,
1461      String attrNameUppercase, String attrValue,
1462      Map<AttributeType, List<Attribute>> userAttrs,
1463      Map<AttributeType, List<Attribute>> operationalAttrs)
1464  {
1465    addAttribute(attrNameLowercase, attrNameUppercase, attrValue, userAttrs, operationalAttrs, true);
1466  }
1467
1468  private static void addAttributeByUppercaseName(String attrNameLowercase,
1469      String attrNameUppercase,  String attrValue,
1470      Map<AttributeType, List<Attribute>> userAttrs,
1471      Map<AttributeType, List<Attribute>> operationalAttrs)
1472  {
1473    addAttribute(attrNameLowercase, attrNameUppercase, attrValue, userAttrs, operationalAttrs, false);
1474  }
1475
1476  private static void addAttribute(final String attrNameLowercase,
1477      final String attrNameUppercase, final String attrValue,
1478      final Map<AttributeType, List<Attribute>> userAttrs,
1479      final Map<AttributeType, List<Attribute>> operationalAttrs, final boolean addByType)
1480  {
1481    AttributeType attrType = DirectoryServer.getAttributeType(attrNameUppercase);
1482    final Attribute a = addByType
1483        ? Attributes.create(attrType, attrValue)
1484        : Attributes.create(attrNameUppercase, attrValue);
1485    final List<Attribute> attrList = Collections.singletonList(a);
1486    if (attrType.isOperational())
1487    {
1488      operationalAttrs.put(attrType, attrList);
1489    }
1490    else
1491    {
1492      userAttrs.put(attrType, attrList);
1493    }
1494  }
1495
1496  /** Describes the current search phase. */
1497  private enum SearchPhase
1498  {
1499    /**
1500     * "Initial search" phase. The "initial search" phase is running
1501     * concurrently. All update notifications are ignored.
1502     */
1503    INITIAL,
1504    /**
1505     * Transitioning from the "initial search" phase to the "persistent search"
1506     * phase. "Initial search" phase has finished reading from the DB. It now
1507     * verifies if any more updates have been persisted to the DB since stopping
1508     * and send them. All update notifications are blocked.
1509     */
1510    TRANSITIONING,
1511    /**
1512     * "Persistent search" phase. "Initial search" phase has completed. All
1513     * update notifications are published.
1514     */
1515    PERSISTENT;
1516  }
1517
1518  /**
1519   * Contains data to ensure that the same change is not sent twice to clients
1520   * because of race conditions between the "initial search" phase and the
1521   * "persistent search" phase.
1522   */
1523  private static class SendEntryData<K extends Comparable<K>>
1524  {
1525    private final AtomicReference<SearchPhase> searchPhase = new AtomicReference<>(SearchPhase.INITIAL);
1526    private final Object transitioningLock = new Object();
1527    private volatile K lastKeySentByInitialSearch;
1528
1529    private SendEntryData(SearchPhase startPhase)
1530    {
1531      searchPhase.set(startPhase);
1532    }
1533
1534    private void finalizeInitialSearch()
1535    {
1536      searchPhase.set(SearchPhase.PERSISTENT);
1537      synchronized (transitioningLock)
1538      { // initial search phase has completed, release all persistent searches
1539        transitioningLock.notifyAll();
1540      }
1541    }
1542
1543    public void transitioningToPersistentSearchPhase()
1544    {
1545      searchPhase.set(SearchPhase.TRANSITIONING);
1546    }
1547
1548    private void initialSearchSendsEntry(final K key)
1549    {
1550      lastKeySentByInitialSearch = key;
1551    }
1552
1553    private boolean persistentSearchCanSendEntry(K key)
1554    {
1555      final SearchPhase stateValue = searchPhase.get();
1556      switch (stateValue)
1557      {
1558      case INITIAL:
1559        return false;
1560      case TRANSITIONING:
1561        synchronized (transitioningLock)
1562        {
1563          while (SearchPhase.TRANSITIONING.equals(searchPhase.get()))
1564          {
1565            // "initial search" phase is over, and is now verifying whether new
1566            // changes have been published to the DB.
1567            // Wait for this check to complete
1568            try
1569            {
1570              transitioningLock.wait();
1571            }
1572            catch (InterruptedException e)
1573            {
1574              Thread.currentThread().interrupt();
1575              // Shutdown must have been called. Stop sending entries.
1576              return false;
1577            }
1578          }
1579        }
1580        return key.compareTo(lastKeySentByInitialSearch) > 0;
1581      case PERSISTENT:
1582        return true;
1583      default:
1584        throw new RuntimeException("Not implemented for " + stateValue);
1585      }
1586    }
1587  }
1588
1589  /** Sends entries to clients for change number searches. */
1590  private static class ChangeNumberEntrySender
1591  {
1592    private final SearchOperation searchOp;
1593    private final long lowestChangeNumber;
1594    private final long highestChangeNumber;
1595    private final SendEntryData<Long> sendEntryData;
1596
1597    private ChangeNumberEntrySender(SearchOperation searchOp, SearchPhase startPhase, ChangeNumberRange range)
1598    {
1599      this.searchOp = searchOp;
1600      this.sendEntryData = new SendEntryData<>(startPhase);
1601      this.lowestChangeNumber = range.lowerBound;
1602      this.highestChangeNumber = range.upperBound;
1603    }
1604
1605    /**
1606     * Indicates if provided change number is compatible with last change
1607     * number.
1608     *
1609     * @param changeNumber
1610     *          The change number to test.
1611     * @return {@code true} if and only if the provided change number is in the
1612     *         range of the last change number.
1613     */
1614    boolean changeNumberIsInRange(long changeNumber)
1615    {
1616      return highestChangeNumber == -1 || changeNumber <= highestChangeNumber;
1617    }
1618
1619    private void finalizeInitialSearch()
1620    {
1621      sendEntryData.finalizeInitialSearch();
1622    }
1623
1624    private void transitioningToPersistentSearchPhase()
1625    {
1626      sendEntryData.transitioningToPersistentSearchPhase();
1627    }
1628
1629    /**
1630     * @return {@code true} if search should continue, {@code false} otherwise
1631     */
1632    private boolean initialSearchSendEntry(ChangeNumberIndexRecord cnIndexRecord, UpdateMsg updateMsg,
1633        MultiDomainServerState cookie) throws DirectoryException
1634    {
1635      final DN baseDN = cnIndexRecord.getBaseDN();
1636      sendEntryData.initialSearchSendsEntry(cnIndexRecord.getChangeNumber());
1637      final Entry entry = createEntryFromMsg(baseDN, cnIndexRecord.getChangeNumber(), cookie.toString(), updateMsg);
1638      return sendEntryIfMatches(searchOp, entry, null);
1639    }
1640
1641    private void persistentSearchSendEntry(long changeNumber, Entry entry) throws DirectoryException
1642    {
1643      if (sendEntryData.persistentSearchCanSendEntry(changeNumber))
1644      {
1645        sendEntryIfMatches(searchOp, entry, null);
1646      }
1647    }
1648  }
1649
1650  /** Sends entries to clients for cookie-based searches. */
1651  private static class CookieEntrySender {
1652    private final SearchOperation searchOp;
1653    private final SearchPhase startPhase;
1654    private final Set<DN> excludedBaseDNs;
1655    private final MultiDomainServerState cookie;
1656    private final ConcurrentSkipListMap<ReplicaId, SendEntryData<CSN>> replicaIdToSendEntryData =
1657        new ConcurrentSkipListMap<>();
1658
1659    private CookieEntrySender(SearchOperation searchOp, SearchPhase startPhase, MultiDomainServerState cookie,
1660        Set<DN> excludedBaseDNs)
1661    {
1662      this.searchOp = searchOp;
1663      this.startPhase = startPhase;
1664      this.cookie = cookie;
1665      this.excludedBaseDNs = excludedBaseDNs;
1666    }
1667
1668    private void finalizeInitialSearch()
1669    {
1670      for (SendEntryData<CSN> sendEntryData : replicaIdToSendEntryData.values())
1671      {
1672        sendEntryData.finalizeInitialSearch();
1673      }
1674    }
1675
1676    private void transitioningToPersistentSearchPhase()
1677    {
1678      for (SendEntryData<CSN> sendEntryData : replicaIdToSendEntryData.values())
1679      {
1680        sendEntryData.transitioningToPersistentSearchPhase();
1681      }
1682    }
1683
1684    private SendEntryData<CSN> getSendEntryData(DN baseDN, CSN csn)
1685    {
1686      final ReplicaId replicaId = ReplicaId.of(baseDN, csn.getServerId());
1687      SendEntryData<CSN> data = replicaIdToSendEntryData.get(replicaId);
1688      if (data == null)
1689      {
1690        final SendEntryData<CSN> newData = new SendEntryData<>(startPhase);
1691        data = replicaIdToSendEntryData.putIfAbsent(replicaId, newData);
1692        return data == null ? newData : data;
1693      }
1694      return data;
1695    }
1696
1697    private boolean initialSearchSendEntry(final UpdateMsg updateMsg, final DN baseDN) throws DirectoryException
1698    {
1699      final CSN csn = updateMsg.getCSN();
1700      final SendEntryData<CSN> sendEntryData = getSendEntryData(baseDN, csn);
1701      sendEntryData.initialSearchSendsEntry(csn);
1702      final String cookieString = updateCookie(baseDN, updateMsg.getCSN());
1703      final Entry entry = createEntryFromMsg(baseDN, 0, cookieString, updateMsg);
1704      return sendEntryIfMatches(searchOp, entry, cookieString);
1705    }
1706
1707    private void persistentSearchSendEntry(DN baseDN, UpdateMsg updateMsg)
1708        throws DirectoryException
1709    {
1710      final CSN csn = updateMsg.getCSN();
1711      final SendEntryData<CSN> sendEntryData = getSendEntryData(baseDN, csn);
1712      if (sendEntryData.persistentSearchCanSendEntry(csn))
1713      {
1714        // multi threaded case: wait for the "initial search" phase to set the cookie
1715        final String cookieString = updateCookie(baseDN, updateMsg.getCSN());
1716        final Entry cookieEntry = createEntryFromMsg(baseDN, 0, cookieString, updateMsg);
1717        // FIXME JNR use this instead of previous line:
1718        // entry.replaceAttribute(Attributes.create("changelogcookie", cookieString));
1719        sendEntryIfMatches(searchOp, cookieEntry, cookieString);
1720      }
1721    }
1722
1723    private String updateCookie(DN baseDN, final CSN csn)
1724    {
1725      synchronized (cookie)
1726      { // forbid concurrent updates to the cookie
1727        cookie.update(baseDN, csn);
1728        return cookie.toString();
1729      }
1730    }
1731  }
1732}