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  /**
058   * The list of (domain service id, ServerState).
059   */
060  private final ConcurrentMap<DN, ServerState> list;
061
062  /**
063   * Creates a new empty object.
064   */
065  public MultiDomainServerState()
066  {
067    list = new ConcurrentSkipListMap<>();
068  }
069
070  /**
071   * Create an object from a string representation.
072   * @param mdss The provided string representation of the state.
073   * @throws DirectoryException when the string has an invalid format
074   */
075  public MultiDomainServerState(String mdss) throws DirectoryException
076  {
077    list = new ConcurrentSkipListMap<>(splitGenStateToServerStates(mdss));
078  }
079
080  /**
081   * Empty the object..
082   * After this call the object will be in the same state as if it
083   * was just created.
084   */
085  public void clear()
086  {
087    list.clear();
088  }
089
090  /**
091   * Update the ServerState of the provided baseDN with the replication
092   * {@link CSN} provided.
093   *
094   * @param baseDN       The provided baseDN.
095   * @param csn          The provided CSN.
096   *
097   * @return a boolean indicating if the update was meaningful.
098   */
099  public boolean update(DN baseDN, CSN csn)
100  {
101    if (csn == null)
102    {
103      return false;
104    }
105
106    ServerState serverState = list.get(baseDN);
107    if (serverState == null)
108    {
109      serverState = new ServerState();
110      final ServerState existingSS = list.putIfAbsent(baseDN, serverState);
111      if (existingSS != null)
112      {
113        serverState = existingSS;
114      }
115    }
116    return serverState.update(csn);
117  }
118
119  /**
120   * Update the ServerState of the provided baseDN with the provided server
121   * state.
122   *
123   * @param baseDN
124   *          The provided baseDN.
125   * @param serverState
126   *          The provided serverState.
127   */
128  public void update(DN baseDN, ServerState serverState)
129  {
130    for (CSN csn : serverState)
131    {
132      update(baseDN, csn);
133    }
134  }
135
136  /**
137   * Replace the ServerState of the provided baseDN with the provided server
138   * state. The provided server state will be owned by this instance, so care
139   * must be taken by calling code to duplicate it if needed.
140   *
141   * @param baseDN
142   *          The provided baseDN.
143   * @param serverState
144   *          The provided serverState.
145   */
146  public void replace(DN baseDN, ServerState serverState)
147  {
148    if (serverState == null)
149    {
150      throw new IllegalArgumentException("ServerState must not be null");
151    }
152    list.put(baseDN, serverState);
153  }
154
155  /**
156   * Update the current object with the provided multi domain server state.
157   *
158   * @param state
159   *          The provided multi domain server state.
160   */
161  public void update(MultiDomainServerState state)
162  {
163    for (Entry<DN, ServerState> entry : state.list.entrySet())
164    {
165      update(entry.getKey(), entry.getValue());
166    }
167  }
168
169  /**
170   * Returns a snapshot of this object.
171   *
172   * @return an unmodifiable Map representing a snapshot of this object.
173   */
174  public Map<DN, List<CSN>> getSnapshot()
175  {
176    if (list.isEmpty())
177    {
178      return Collections.emptyMap();
179    }
180    final Map<DN, List<CSN>> map = new HashMap<>();
181    for (Entry<DN, ServerState> entry : list.entrySet())
182    {
183      final List<CSN> l = entry.getValue().getSnapshot();
184      if (!l.isEmpty())
185      {
186        map.put(entry.getKey(), l);
187      }
188    }
189    return Collections.unmodifiableMap(map);
190  }
191
192  /**
193   * Returns a string representation of this object.
194   *
195   * @return The string representation.
196   */
197  @Override
198  public String toString()
199  {
200    final StringBuilder res = new StringBuilder();
201    if (list != null && !list.isEmpty())
202    {
203      for (Entry<DN, ServerState> entry : list.entrySet())
204      {
205        res.append(entry.getKey()).append(":")
206           .append(entry.getValue()).append(";");
207      }
208    }
209    return res.toString();
210  }
211
212  /**
213   * Dump a string representation in the provided buffer.
214   * @param buffer The provided buffer.
215   */
216  public void toString(StringBuilder buffer)
217  {
218    buffer.append(this);
219  }
220
221  /**
222   * Tests if the state is empty.
223   *
224   * @return True if the state is empty.
225   */
226  public boolean isEmpty()
227  {
228    return list.isEmpty();
229  }
230
231  /** {@inheritDoc} */
232  @Override
233  public Iterator<DN> iterator()
234  {
235    return list.keySet().iterator();
236  }
237
238  /**
239   * Returns the ServerState associated to the provided replication domain's
240   * baseDN.
241   *
242   * @param baseDN
243   *          the replication domain's baseDN
244   * @return the associated ServerState
245   */
246  public ServerState getServerState(DN baseDN)
247  {
248    return list.get(baseDN);
249  }
250
251  /**
252   * Returns the CSN associated to the provided replication domain's baseDN and
253   * serverId.
254   *
255   * @param baseDN
256   *          the replication domain's baseDN
257   * @param serverId
258   *          the serverId
259   * @return the associated CSN
260   */
261  public CSN getCSN(DN baseDN, int serverId)
262  {
263    final ServerState ss = list.get(baseDN);
264    if (ss != null)
265    {
266      return ss.getCSN(serverId);
267    }
268    return null;
269  }
270
271  /**
272   * Returns the oldest Pair&lt;DN, CSN&gt; held in current object, excluding
273   * the provided CSNs. Said otherwise, the value returned is the oldest
274   * Pair&lt;DN, CSN&gt; included in the current object, that is not part of the
275   * excludedCSNs.
276   *
277   * @param excludedCSNs
278   *          the CSNs that cannot be returned
279   * @return the oldest Pair&lt;DN, CSN&gt; included in the current object that
280   *         is not part of the excludedCSNs, or {@link Pair#EMPTY} if no such
281   *         older CSN exists.
282   */
283  public Pair<DN, CSN> getOldestCSNExcluding(MultiDomainServerState excludedCSNs)
284  {
285    Pair<DN, CSN> oldest = Pair.empty();
286    for (Entry<DN, ServerState> entry : list.entrySet())
287    {
288      final DN baseDN = entry.getKey();
289      final ServerState value = entry.getValue();
290      for (Entry<Integer, CSN> entry2 : value.getServerIdToCSNMap().entrySet())
291      {
292        final CSN csn = entry2.getValue();
293        if (!isReplicaExcluded(excludedCSNs, baseDN, csn)
294            && (oldest == Pair.EMPTY || csn.isOlderThan(oldest.getSecond())))
295        {
296          oldest = Pair.of(baseDN, csn);
297        }
298      }
299    }
300    return oldest;
301  }
302
303  private boolean isReplicaExcluded(MultiDomainServerState excluded, DN baseDN,
304      CSN csn)
305  {
306    return excluded != null
307        && csn.equals(excluded.getCSN(baseDN, csn.getServerId()));
308  }
309
310  /**
311   * Removes the mapping to the provided CSN if it is present in this
312   * MultiDomainServerState.
313   *
314   * @param baseDN
315   *          the replication domain's baseDN
316   * @param expectedCSN
317   *          the CSN to be removed
318   * @return true if the CSN could be removed, false otherwise.
319   */
320  public boolean removeCSN(DN baseDN, CSN expectedCSN)
321  {
322    final ServerState ss = list.get(baseDN);
323    return ss != null && ss.removeCSN(expectedCSN);
324  }
325
326  /**
327   * Test if this object equals the provided other object.
328   * @param other The other object with which we want to test equality.
329   * @return      Returns True if this equals other, else return false.
330   */
331  public boolean equalsTo(MultiDomainServerState other)
332  {
333    return cover(other) && other.cover(this);
334  }
335
336  /**
337   * Test if this object covers the provided covered object.
338   * @param  covered The provided object.
339   * @return true when this covers the provided object.
340   */
341  public boolean cover(MultiDomainServerState covered)
342  {
343    for (DN baseDN : covered.list.keySet())
344    {
345      ServerState state = list.get(baseDN);
346      ServerState coveredState = covered.list.get(baseDN);
347      if (state == null || coveredState == null || !state.cover(coveredState))
348      {
349        return false;
350      }
351    }
352    return true;
353  }
354
355  /**
356   * Test if this object covers the provided CSN for the provided baseDN.
357   *
358   * @param baseDN
359   *          The provided baseDN.
360   * @param csn
361   *          The provided CSN.
362   * @return true when this object covers the provided CSN for the provided
363   *         baseDN.
364   */
365  public boolean cover(DN baseDN, CSN csn)
366  {
367    final ServerState state = list.get(baseDN);
368    return state != null && state.cover(csn);
369  }
370
371  /**
372   * Splits the provided generalizedServerState being a String with the
373   * following syntax: "domain1:state1;domain2:state2;..." to a Map of (domain
374   * DN, domain ServerState).
375   *
376   * @param multiDomainServerState
377   *          the provided multi domain server state also known as cookie
378   * @exception DirectoryException
379   *              when an error occurs
380   * @return the split state.
381   */
382  private static Map<DN, ServerState> splitGenStateToServerStates(
383      String multiDomainServerState) throws DirectoryException
384  {
385    Map<DN, ServerState> startStates = new TreeMap<>();
386    if (multiDomainServerState != null && multiDomainServerState.length() > 0)
387    {
388      try
389      {
390        // Split the provided multiDomainServerState into domains
391        String[] domains = multiDomainServerState.split(";");
392        for (String domain : domains)
393        {
394          // For each domain, split the CSNs by server
395          // and build a server state (SHOULD BE OPTIMIZED)
396          final ServerState serverStateByDomain = new ServerState();
397
398          final String[] fields = domain.split(":");
399          if (fields.length == 0)
400          {
401            throw new DirectoryException(ResultCode.PROTOCOL_ERROR,
402                ERR_INVALID_COOKIE_SYNTAX.get(multiDomainServerState));
403          }
404          final String domainBaseDN = fields[0];
405          if (fields.length > 1)
406          {
407            final String serverStateStr = fields[1];
408            for (String csnStr : serverStateStr.split(" "))
409            {
410              final CSN csn = new CSN(csnStr);
411              serverStateByDomain.update(csn);
412            }
413          }
414          startStates.put(DN.valueOf(domainBaseDN), serverStateByDomain);
415        }
416      }
417      catch (DirectoryException de)
418      {
419        throw de;
420      }
421      catch (Exception e)
422      {
423        throw new DirectoryException(
424            ResultCode.PROTOCOL_ERROR,
425            LocalizableMessage.raw("Exception raised: " + e),
426            e);
427      }
428    }
429    return startStates;
430  }
431}