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 010 * trunk/opends/resource/legal-notices/OpenDS.LICENSE 011 * or https://OpenDS.dev.java.net/OpenDS.LICENSE. 012 * See the License for the specific language governing permissions 013 * and limitations under the License. 014 * 015 * When distributing Covered Code, include this CDDL HEADER in each 016 * file and include the License file at 017 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, 018 * add the following below this CDDL HEADER, with the fields enclosed 019 * by brackets "[]" replaced with your own identifying information: 020 * Portions Copyright [yyyy] [name of copyright owner] 021 * 022 * CDDL HEADER END 023 * 024 * 025 * Copyright 2006-2008 Sun Microsystems, Inc. 026 */ 027 package org.opends.server.replication.plugin; 028 import org.opends.messages.Message; 029 030 import static org.opends.server.loggers.ErrorLogger.logError; 031 import static org.opends.messages.ReplicationMessages.*; 032 033 import java.util.HashMap; 034 import java.util.Iterator; 035 import java.util.LinkedHashSet; 036 import java.util.List; 037 import java.util.Map; 038 import java.util.Set; 039 import java.util.TreeMap; 040 import java.util.HashSet; 041 042 import org.opends.server.core.DirectoryServer; 043 import org.opends.server.replication.common.ChangeNumber; 044 import org.opends.server.replication.protocol.OperationContext; 045 import org.opends.server.types.Attribute; 046 import org.opends.server.types.AttributeType; 047 import org.opends.server.types.AttributeValue; 048 import org.opends.server.types.Entry; 049 import org.opends.server.types.Modification; 050 import org.opends.server.types.ModificationType; 051 import org.opends.server.types.operation.PreOperationAddOperation; 052 import org.opends.server.types.operation.PreOperationModifyOperation; 053 054 /** 055 * This class is used to store historical information that is 056 * used to resolve modify conflicts 057 * 058 * It is assumed that the common case is not to have conflict and 059 * therefore is optimized (in order of importance) for : 060 * 1- detecting potential conflict 061 * 2- fast update of historical information for non-conflicting change 062 * 3- fast and efficient purge 063 * 4- compact 064 * 5- solve conflict. This should also be as fast as possible but 065 * not at the cost of any of the other previous objectives 066 * 067 * One Historical object is created for each entry in the entry cache 068 * each Historical Object contains a list of attribute historical information 069 */ 070 071 public class Historical 072 { 073 /** 074 * The name of the attribute used to store historical information. 075 */ 076 public static final String HISTORICALATTRIBUTENAME = "ds-sync-hist"; 077 078 /** 079 * Name used to store attachment of historical information in the 080 * operation. 081 */ 082 public static final String HISTORICAL = "ds-synch-historical"; 083 084 /** 085 * The name of the entryuuid attribute. 086 */ 087 public static final String ENTRYUIDNAME = "entryuuid"; 088 089 090 /* 091 * contains Historical information for each attribute sorted by attribute type 092 */ 093 private HashMap<AttributeType,AttrInfoWithOptions> attributesInfo 094 = new HashMap<AttributeType,AttrInfoWithOptions>(); 095 096 /** 097 * {@inheritDoc} 098 */ 099 @Override 100 public String toString() 101 { 102 StringBuilder builder = new StringBuilder(); 103 builder.append(encode()); 104 return builder.toString(); 105 } 106 107 /** 108 * Process an operation. 109 * This method is responsible for detecting and resolving conflict for 110 * modifyOperation. This is done by using the historical information. 111 * 112 * @param modifyOperation the operation to be processed 113 * @param modifiedEntry the entry that is being modified (before modification) 114 * @return true if the replayed operation was in conflict 115 */ 116 public boolean replayOperation(PreOperationModifyOperation modifyOperation, 117 Entry modifiedEntry) 118 { 119 boolean bConflict = false; 120 List<Modification> mods = modifyOperation.getModifications(); 121 ChangeNumber changeNumber = 122 OperationContext.getChangeNumber(modifyOperation); 123 124 for (Iterator<Modification> modsIterator = mods.iterator(); 125 modsIterator.hasNext(); ) 126 { 127 Modification m = modsIterator.next(); 128 129 AttributeInfo attrInfo = getAttrInfo(m); 130 131 if (attrInfo.replayOperation(modsIterator, changeNumber, 132 modifiedEntry, m)) 133 { 134 bConflict = true; 135 } 136 } 137 138 return bConflict; 139 } 140 141 /** 142 * Append replacement of state information to a given modification. 143 * 144 * @param modifyOperation the modification. 145 */ 146 public void generateState(PreOperationModifyOperation modifyOperation) 147 { 148 List<Modification> mods = modifyOperation.getModifications(); 149 Entry modifiedEntry = modifyOperation.getModifiedEntry(); 150 ChangeNumber changeNumber = 151 OperationContext.getChangeNumber(modifyOperation); 152 153 /* 154 * If this is a local operation we need first to update the historical 155 * information, then update the entry with the historical information 156 * If this is a replicated operation the historical information has 157 * already been set in the resolveConflict phase and we only need 158 * to update the entry 159 */ 160 if (!modifyOperation.isSynchronizationOperation()) 161 { 162 for (Modification mod : mods) 163 { 164 AttributeInfo attrInfo = getAttrInfo(mod); 165 if (attrInfo != null) 166 attrInfo.processLocalOrNonConflictModification(changeNumber, mod); 167 } 168 } 169 170 Attribute attr = encode(); 171 Modification mod; 172 mod = new Modification(ModificationType.REPLACE, attr); 173 mods.add(mod); 174 modifiedEntry.removeAttribute(attr.getAttributeType()); 175 modifiedEntry.addAttribute(attr, null); 176 } 177 178 /** 179 * Get the AttrInfo for a given Modification. 180 * The AttrInfo is the object that is used to store the historical 181 * information of a given attribute type. 182 * If there is no historical information for this attribute yet, a new 183 * empty AttrInfo is created and returned. 184 * 185 * @param mod The Modification that must be used. 186 * @return The AttrInfo corresponding to the given Modification. 187 */ 188 private AttributeInfo getAttrInfo(Modification mod) 189 { 190 Attribute modAttr = mod.getAttribute(); 191 if (isHistoricalAttribute(modAttr)) 192 { 193 // Don't keep historical information for the attribute that is 194 // used to store the historical information. 195 return null; 196 } 197 Set<String> options = modAttr.getOptions(); 198 AttributeType type = modAttr.getAttributeType(); 199 AttrInfoWithOptions attrInfoWithOptions = attributesInfo.get(type); 200 AttributeInfo attrInfo; 201 if (attrInfoWithOptions != null) 202 { 203 attrInfo = attrInfoWithOptions.get(options); 204 } 205 else 206 { 207 attrInfoWithOptions = new AttrInfoWithOptions(); 208 attributesInfo.put(type, attrInfoWithOptions); 209 attrInfo = null; 210 } 211 212 if (attrInfo == null) 213 { 214 attrInfo = AttributeInfo.createAttributeInfo(type); 215 attrInfoWithOptions.put(options, attrInfo); 216 } 217 return attrInfo; 218 } 219 220 /** 221 * Encode the historical information in an operational attribute. 222 * @return The historical information encoded in an operational attribute. 223 */ 224 public Attribute encode() 225 { 226 AttributeType historicalAttrType = 227 DirectoryServer.getSchema().getAttributeType(HISTORICALATTRIBUTENAME); 228 LinkedHashSet<AttributeValue> hist = new LinkedHashSet<AttributeValue>(); 229 230 for (Map.Entry<AttributeType, AttrInfoWithOptions> entryWithOptions : 231 attributesInfo.entrySet()) 232 233 { 234 AttributeType type = entryWithOptions.getKey(); 235 HashMap<Set<String> , AttributeInfo> attrwithoptions = 236 entryWithOptions.getValue().getAttributesInfo(); 237 238 for (Map.Entry<Set<String>, AttributeInfo> entry : 239 attrwithoptions.entrySet()) 240 { 241 boolean delAttr = false; 242 Set<String> options = entry.getKey(); 243 String optionsString = ""; 244 AttributeInfo info = entry.getValue(); 245 246 247 if (options != null) 248 { 249 StringBuilder optionsBuilder = new StringBuilder(); 250 for (String s : options) 251 { 252 optionsBuilder.append(';'); 253 optionsBuilder.append(s); 254 } 255 optionsString = optionsBuilder.toString(); 256 } 257 258 ChangeNumber deleteTime = info.getDeleteTime(); 259 /* generate the historical information for deleted attributes */ 260 if (deleteTime != null) 261 { 262 delAttr = true; 263 } 264 265 /* generate the historical information for modified attribute values */ 266 for (ValueInfo valInfo : info.getValuesInfo()) 267 { 268 String strValue; 269 if (valInfo.getValueDeleteTime() != null) 270 { 271 strValue = type.getNormalizedPrimaryName() + optionsString + ":" + 272 valInfo.getValueDeleteTime().toString() + 273 ":del:" + valInfo.getValue().toString(); 274 AttributeValue val = new AttributeValue(historicalAttrType, 275 strValue); 276 hist.add(val); 277 } 278 else if (valInfo.getValueUpdateTime() != null) 279 { 280 if ((delAttr && valInfo.getValueUpdateTime() == deleteTime) 281 && (valInfo.getValue() != null)) 282 { 283 strValue = type.getNormalizedPrimaryName() + optionsString + ":" + 284 valInfo.getValueUpdateTime().toString() + ":repl:" + 285 valInfo.getValue().toString(); 286 delAttr = false; 287 } 288 else 289 { 290 if (valInfo.getValue() == null) 291 { 292 strValue = type.getNormalizedPrimaryName() + optionsString 293 + ":" + valInfo.getValueUpdateTime().toString() + 294 ":add"; 295 } 296 else 297 { 298 strValue = type.getNormalizedPrimaryName() + optionsString 299 + ":" + valInfo.getValueUpdateTime().toString() + 300 ":add:" + valInfo.getValue().toString(); 301 } 302 } 303 304 AttributeValue val = new AttributeValue(historicalAttrType, 305 strValue); 306 hist.add(val); 307 } 308 } 309 310 if (delAttr) 311 { 312 String strValue = type.getNormalizedPrimaryName() 313 + optionsString + ":" + deleteTime.toString() 314 + ":attrDel"; 315 AttributeValue val = new AttributeValue(historicalAttrType, strValue); 316 hist.add(val); 317 } 318 } 319 } 320 321 Attribute attr; 322 323 if (hist.isEmpty()) 324 { 325 attr = new Attribute(historicalAttrType, HISTORICALATTRIBUTENAME, null); 326 } 327 else 328 { 329 attr = new Attribute(historicalAttrType, HISTORICALATTRIBUTENAME, hist); 330 } 331 return attr; 332 } 333 334 335 /** 336 * read the historical information from the entry attribute and 337 * load it into the Historical object attached to the entry. 338 * @param entry The entry which historical information must be loaded 339 * @return the generated Historical information 340 */ 341 public static Historical load(Entry entry) 342 { 343 List<Attribute> hist = getHistoricalAttr(entry); 344 Historical histObj = new Historical(); 345 AttributeType lastAttrType = null; 346 Set<String> lastOptions = new HashSet<String>(); 347 AttributeInfo attrInfo = null; 348 AttrInfoWithOptions attrInfoWithOptions = null; 349 350 if (hist == null) 351 { 352 return histObj; 353 } 354 355 try 356 { 357 for (Attribute attr : hist) 358 { 359 for (AttributeValue val : attr.getValues()) 360 { 361 HistVal histVal = new HistVal(val.getStringValue()); 362 AttributeType attrType = histVal.getAttrType(); 363 Set<String> options = histVal.getOptions(); 364 ChangeNumber cn = histVal.getCn(); 365 AttributeValue value = histVal.getAttributeValue(); 366 HistKey histKey = histVal.getHistKey(); 367 368 if (attrType == null) 369 { 370 /* 371 * This attribute is unknown from the schema 372 * Just skip it, the modification will be processed but no 373 * historical information is going to be kept. 374 * Log information for the repair tool. 375 */ 376 Message message = ERR_UNKNOWN_ATTRIBUTE_IN_HISTORICAL.get( 377 entry.getDN().toNormalizedString(), histVal.getAttrString()); 378 logError(message); 379 continue; 380 } 381 382 /* if attribute type does not match we create new 383 * AttrInfoWithOptions and AttrInfo 384 * we also add old AttrInfoWithOptions into histObj.attributesInfo 385 * if attribute type match but options does not match we create new 386 * AttrInfo that we add to AttrInfoWithOptions 387 * if both match we keep everything 388 */ 389 if (attrType != lastAttrType) 390 { 391 attrInfo = AttributeInfo.createAttributeInfo(attrType); 392 attrInfoWithOptions = new AttrInfoWithOptions(); 393 attrInfoWithOptions.put(options, attrInfo); 394 histObj.attributesInfo.put(attrType, attrInfoWithOptions); 395 396 lastAttrType = attrType; 397 lastOptions = options; 398 } 399 else 400 { 401 if (!options.equals(lastOptions)) 402 { 403 attrInfo = AttributeInfo.createAttributeInfo(attrType); 404 attrInfoWithOptions.put(options, attrInfo); 405 lastOptions = options; 406 } 407 } 408 409 attrInfo.load(histKey, value, cn); 410 } 411 } 412 } catch (Exception e) 413 { 414 // Any exception happening here means that the coding of the hsitorical 415 // information was wrong. 416 // Log an error and continue with an empty historical. 417 Message message = ERR_BAD_HISTORICAL.get(entry.getDN().toString()); 418 logError(message); 419 } 420 421 /* set the reference to the historical information in the entry */ 422 return histObj; 423 } 424 425 426 /** 427 * Use this historical information to generate fake operations that would 428 * result in this historical information. 429 * TODO : This is only implemented for modify operation, should implement ADD 430 * DELETE and MODRDN. 431 * @param entry The Entry to use to generate the FakeOperation Iterable. 432 * 433 * @return an Iterable of FakeOperation that would result in this historical 434 * information. 435 */ 436 public static Iterable<FakeOperation> generateFakeOperations(Entry entry) 437 { 438 TreeMap<ChangeNumber, FakeOperation> operations = 439 new TreeMap<ChangeNumber, FakeOperation>(); 440 List<Attribute> attrs = getHistoricalAttr(entry); 441 if (attrs != null) 442 { 443 for (Attribute attr : attrs) 444 { 445 for (AttributeValue val : attr.getValues()) 446 { 447 HistVal histVal = new HistVal(val.getStringValue()); 448 ChangeNumber cn = histVal.getCn(); 449 Modification mod = histVal.generateMod(); 450 ModifyFakeOperation modifyFakeOperation; 451 452 FakeOperation fakeOperation = operations.get(cn); 453 454 if (fakeOperation != null) 455 { 456 fakeOperation.addModification(mod); 457 } 458 else 459 { 460 String uuidString = getEntryUuid(entry); 461 if (uuidString != null) 462 { 463 modifyFakeOperation = new ModifyFakeOperation(entry.getDN(), 464 cn, uuidString); 465 466 modifyFakeOperation.addModification(mod); 467 operations.put(histVal.getCn(), modifyFakeOperation); 468 } 469 } 470 } 471 } 472 } 473 return operations.values(); 474 } 475 476 /** 477 * Get the Attribute used to store the historical information from 478 * the given Entry. 479 * 480 * @param entry The entry containing the historical information. 481 * 482 * @return The Attribute used to store the historical information. 483 */ 484 public static List<Attribute> getHistoricalAttr(Entry entry) 485 { 486 return entry.getAttribute(HISTORICALATTRIBUTENAME); 487 } 488 489 /** 490 * Get the entry unique Id in String form. 491 * 492 * @param entry The entry for which the unique id should be returned. 493 * 494 * @return The Unique Id of the entry if it has one. null, otherwise. 495 */ 496 public static String getEntryUuid(Entry entry) 497 { 498 String uuidString = null; 499 AttributeType entryuuidAttrType = 500 DirectoryServer.getSchema().getAttributeType(ENTRYUIDNAME); 501 List<Attribute> uuidAttrs = 502 entry.getOperationalAttribute(entryuuidAttrType); 503 if (uuidAttrs != null) 504 { 505 Attribute uuid = uuidAttrs.get(0); 506 if (uuid.hasValue()) 507 { 508 AttributeValue uuidVal = uuid.getValues().iterator().next(); 509 uuidString = uuidVal.getStringValue(); 510 } 511 } 512 return uuidString; 513 } 514 515 /** 516 * Get the Entry Unique Id from an add operation. 517 * This must be called after the entry uuid preop plugin (i.e no 518 * sooner than the replication provider pre-op) 519 * 520 * @param op The operation 521 * @return The Entry Unique Id String form. 522 */ 523 public static String getEntryUuid(PreOperationAddOperation op) 524 { 525 String uuidString = null; 526 Map<AttributeType, List<Attribute>> attrs = op.getOperationalAttributes(); 527 AttributeType entryuuidAttrType = 528 DirectoryServer.getSchema().getAttributeType(ENTRYUIDNAME); 529 List<Attribute> uuidAttrs = attrs.get(entryuuidAttrType); 530 531 if (uuidAttrs != null) 532 { 533 Attribute uuid = uuidAttrs.get(0); 534 if (uuid.hasValue()) 535 { 536 AttributeValue uuidVal = uuid.getValues().iterator().next(); 537 uuidString = uuidVal.getStringValue(); 538 } 539 } 540 return uuidString; 541 } 542 543 /** 544 * Check if a given attribute is an attribute used to store historical 545 * information. 546 * 547 * @param attr The attribute that needs to be checked. 548 * 549 * @return a boolean indicating if the given attribute is 550 * used to store historical information. 551 */ 552 public static boolean isHistoricalAttribute(Attribute attr) 553 { 554 AttributeType attrType = attr.getAttributeType(); 555 return attrType.getNameOrOID().equals(Historical.HISTORICALATTRIBUTENAME); 556 } 557 } 558