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