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