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<DN, CSN> held in current object, excluding 273 * the provided CSNs. Said otherwise, the value returned is the oldest 274 * Pair<DN, CSN> 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<DN, CSN> 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}