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