001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS
026 */
027package org.opends.server.replication.common;
028
029import static org.opends.messages.ReplicationMessages.*;
030
031import java.util.Collections;
032import java.util.HashMap;
033import java.util.Iterator;
034import java.util.List;
035import java.util.Map;
036import java.util.Map.Entry;
037import java.util.TreeMap;
038import java.util.concurrent.ConcurrentMap;
039import java.util.concurrent.ConcurrentSkipListMap;
040
041import org.forgerock.i18n.LocalizableMessage;
042import org.forgerock.opendj.ldap.ResultCode;
043import org.forgerock.util.Pair;
044import org.opends.server.types.DN;
045import org.opends.server.types.DirectoryException;
046
047/**
048 * This object is used to store a list of ServerState object, one by replication
049 * domain. Globally, it is the generalization of ServerState (that applies to
050 * one domain) to a list of domains.
051 * <p>
052 * MultiDomainServerState is also known as "cookie" and is used with the
053 * cookie-based changelog.
054 */
055public class MultiDomainServerState implements Iterable<DN>
056{
057  /** The list of (domain service id, ServerState). */
058  private final ConcurrentMap<DN, ServerState> list;
059
060  /** Creates a new empty object. */
061  public MultiDomainServerState()
062  {
063    list = new ConcurrentSkipListMap<>();
064  }
065
066  /**
067   * Copy constructor.
068   *
069   * @param cookie
070   *          the cookie to copy
071   */
072  public MultiDomainServerState(MultiDomainServerState cookie)
073  {
074    list = new ConcurrentSkipListMap<>();
075
076    for (Map.Entry<DN, ServerState> mapEntry : cookie.list.entrySet())
077    {
078      DN dn = mapEntry.getKey();
079      ServerState state = mapEntry.getValue();
080      list.put(dn, state.duplicate());
081    }
082  }
083
084  /**
085   * Create an object from a string representation.
086   * @param cookie The provided string representation of the state.
087   * @throws DirectoryException when the string has an invalid format
088   */
089  public MultiDomainServerState(String cookie) throws DirectoryException
090  {
091    list = new ConcurrentSkipListMap<>(splitGenStateToServerStates(cookie));
092  }
093
094  /**
095   * Empty the object..
096   * After this call the object will be in the same state as if it
097   * was just created.
098   */
099  public void clear()
100  {
101    list.clear();
102  }
103
104  /**
105   * Update the ServerState of the provided baseDN with the replication
106   * {@link CSN} provided.
107   *
108   * @param baseDN       The provided baseDN.
109   * @param csn          The provided CSN.
110   *
111   * @return a boolean indicating if the update was meaningful.
112   */
113  public boolean update(DN baseDN, CSN csn)
114  {
115    if (csn == null)
116    {
117      return false;
118    }
119
120    ServerState serverState = list.get(baseDN);
121    if (serverState == null)
122    {
123      serverState = new ServerState();
124      final ServerState existingSS = list.putIfAbsent(baseDN, serverState);
125      if (existingSS != null)
126      {
127        serverState = existingSS;
128      }
129    }
130    return serverState.update(csn);
131  }
132
133  /**
134   * Update the ServerState of the provided baseDN with the provided server
135   * state.
136   *
137   * @param baseDN
138   *          The provided baseDN.
139   * @param serverState
140   *          The provided serverState.
141   */
142  public void update(DN baseDN, ServerState serverState)
143  {
144    for (CSN csn : serverState)
145    {
146      update(baseDN, csn);
147    }
148  }
149
150  /**
151   * Replace the ServerState of the provided baseDN with the provided server
152   * state. The provided server state will be owned by this instance, so care
153   * must be taken by calling code to duplicate it if needed.
154   *
155   * @param baseDN
156   *          The provided baseDN.
157   * @param serverState
158   *          The provided serverState.
159   */
160  public void replace(DN baseDN, ServerState serverState)
161  {
162    if (serverState == null)
163    {
164      throw new IllegalArgumentException("ServerState must not be null");
165    }
166    list.put(baseDN, serverState);
167  }
168
169  /**
170   * Update the current object with the provided multi domain server state.
171   *
172   * @param state
173   *          The provided multi domain server state.
174   */
175  public void update(MultiDomainServerState state)
176  {
177    for (Entry<DN, ServerState> entry : state.list.entrySet())
178    {
179      update(entry.getKey(), entry.getValue());
180    }
181  }
182
183  /**
184   * Returns a snapshot of this object.
185   *
186   * @return an unmodifiable Map representing a snapshot of this object.
187   */
188  public Map<DN, List<CSN>> getSnapshot()
189  {
190    if (list.isEmpty())
191    {
192      return Collections.emptyMap();
193    }
194    final Map<DN, List<CSN>> map = new HashMap<>();
195    for (Entry<DN, ServerState> entry : list.entrySet())
196    {
197      final List<CSN> l = entry.getValue().getSnapshot();
198      if (!l.isEmpty())
199      {
200        map.put(entry.getKey(), l);
201      }
202    }
203    return Collections.unmodifiableMap(map);
204  }
205
206  /**
207   * Returns a string representation of this object.
208   *
209   * @return The string representation.
210   */
211  @Override
212  public String toString()
213  {
214    final StringBuilder buffer = new StringBuilder();
215    toString(buffer);
216    return buffer.toString();
217  }
218
219  /**
220   * Dump a string representation in the provided buffer.
221   * @param buffer The provided buffer.
222   */
223  public void toString(StringBuilder buffer)
224  {
225    if (list != null && !list.isEmpty())
226    {
227      for (Entry<DN, ServerState> entry : list.entrySet())
228      {
229        entry.getKey().toString(buffer);
230        buffer.append(":");
231        entry.getValue().toString(buffer);
232        buffer.append(";");
233      }
234    }
235  }
236
237  /**
238   * Tests if the state is empty.
239   *
240   * @return True if the state is empty.
241   */
242  public boolean isEmpty()
243  {
244    return list.isEmpty();
245  }
246
247  /** {@inheritDoc} */
248  @Override
249  public Iterator<DN> iterator()
250  {
251    return list.keySet().iterator();
252  }
253
254  /**
255   * Returns the ServerState associated to the provided replication domain's
256   * baseDN.
257   *
258   * @param baseDN
259   *          the replication domain's baseDN
260   * @return the associated ServerState
261   */
262  public ServerState getServerState(DN baseDN)
263  {
264    return list.get(baseDN);
265  }
266
267  /**
268   * Returns the CSN associated to the provided replication domain's baseDN and
269   * serverId.
270   *
271   * @param baseDN
272   *          the replication domain's baseDN
273   * @param serverId
274   *          the serverId
275   * @return the associated CSN
276   */
277  public CSN getCSN(DN baseDN, int serverId)
278  {
279    final ServerState ss = list.get(baseDN);
280    if (ss != null)
281    {
282      return ss.getCSN(serverId);
283    }
284    return null;
285  }
286
287  /**
288   * Returns the oldest Pair&lt;DN, CSN&gt; held in current object, excluding
289   * the provided CSNs. Said otherwise, the value returned is the oldest
290   * Pair&lt;DN, CSN&gt; included in the current object, that is not part of the
291   * excludedCSNs.
292   *
293   * @param excludedCSNs
294   *          the CSNs that cannot be returned
295   * @return the oldest Pair&lt;DN, CSN&gt; included in the current object that
296   *         is not part of the excludedCSNs, or {@link Pair#EMPTY} if no such
297   *         older CSN exists.
298   */
299  public Pair<DN, CSN> getOldestCSNExcluding(MultiDomainServerState excludedCSNs)
300  {
301    Pair<DN, CSN> oldest = Pair.empty();
302    for (Entry<DN, ServerState> entry : list.entrySet())
303    {
304      final DN baseDN = entry.getKey();
305      final ServerState value = entry.getValue();
306      for (Entry<Integer, CSN> entry2 : value.getServerIdToCSNMap().entrySet())
307      {
308        final CSN csn = entry2.getValue();
309        if (!isReplicaExcluded(excludedCSNs, baseDN, csn)
310            && (oldest == Pair.EMPTY || csn.isOlderThan(oldest.getSecond())))
311        {
312          oldest = Pair.of(baseDN, csn);
313        }
314      }
315    }
316    return oldest;
317  }
318
319  private boolean isReplicaExcluded(MultiDomainServerState excluded, DN baseDN,
320      CSN csn)
321  {
322    return excluded != null
323        && csn.equals(excluded.getCSN(baseDN, csn.getServerId()));
324  }
325
326  /**
327   * Removes the mapping to the provided CSN if it is present in this
328   * MultiDomainServerState.
329   *
330   * @param baseDN
331   *          the replication domain's baseDN
332   * @param expectedCSN
333   *          the CSN to be removed
334   * @return true if the CSN could be removed, false otherwise.
335   */
336  public boolean removeCSN(DN baseDN, CSN expectedCSN)
337  {
338    final ServerState ss = list.get(baseDN);
339    return ss != null && ss.removeCSN(expectedCSN);
340  }
341
342  /**
343   * Test if this object equals the provided other object.
344   * @param other The other object with which we want to test equality.
345   * @return      Returns True if this equals other, else return false.
346   */
347  public boolean equalsTo(MultiDomainServerState other)
348  {
349    return cover(other) && other.cover(this);
350  }
351
352  /**
353   * Test if this object covers the provided covered object.
354   * @param  covered The provided object.
355   * @return true when this covers the provided object.
356   */
357  public boolean cover(MultiDomainServerState covered)
358  {
359    for (DN baseDN : covered.list.keySet())
360    {
361      ServerState state = list.get(baseDN);
362      ServerState coveredState = covered.list.get(baseDN);
363      if (state == null || coveredState == null || !state.cover(coveredState))
364      {
365        return false;
366      }
367    }
368    return true;
369  }
370
371  /**
372   * Test if this object covers the provided CSN for the provided baseDN.
373   *
374   * @param baseDN
375   *          The provided baseDN.
376   * @param csn
377   *          The provided CSN.
378   * @return true when this object covers the provided CSN for the provided
379   *         baseDN.
380   */
381  public boolean cover(DN baseDN, CSN csn)
382  {
383    final ServerState state = list.get(baseDN);
384    return state != null && state.cover(csn);
385  }
386
387  /**
388   * Splits the provided generalizedServerState being a String with the
389   * following syntax: "domain1:state1;domain2:state2;..." to a Map of (domain
390   * DN, domain ServerState).
391   *
392   * @param multiDomainServerState
393   *          the provided multi domain server state also known as cookie
394   * @exception DirectoryException
395   *              when an error occurs
396   * @return the split state.
397   */
398  private static Map<DN, ServerState> splitGenStateToServerStates(
399      String multiDomainServerState) throws DirectoryException
400  {
401    Map<DN, ServerState> startStates = new TreeMap<>();
402    if (multiDomainServerState != null && multiDomainServerState.length() > 0)
403    {
404      try
405      {
406        // Split the provided multiDomainServerState into domains
407        String[] domains = multiDomainServerState.split(";");
408        for (String domain : domains)
409        {
410          // For each domain, split the CSNs by server
411          // and build a server state (SHOULD BE OPTIMIZED)
412          final ServerState serverStateByDomain = new ServerState();
413
414          final String[] fields = domain.split(":");
415          if (fields.length == 0)
416          {
417            throw new DirectoryException(ResultCode.PROTOCOL_ERROR,
418                ERR_INVALID_COOKIE_SYNTAX.get(multiDomainServerState));
419          }
420          final String domainBaseDN = fields[0];
421          if (fields.length > 1)
422          {
423            final String serverStateStr = fields[1];
424            for (String csnStr : serverStateStr.split(" "))
425            {
426              final CSN csn = new CSN(csnStr);
427              serverStateByDomain.update(csn);
428            }
429          }
430          startStates.put(DN.valueOf(domainBaseDN), serverStateByDomain);
431        }
432      }
433      catch (DirectoryException de)
434      {
435        throw de;
436      }
437      catch (Exception e)
438      {
439        throw new DirectoryException(
440            ResultCode.PROTOCOL_ERROR,
441            LocalizableMessage.raw("Exception raised: " + e),
442            e);
443      }
444    }
445    return startStates;
446  }
447}