/**
   * A utility method which may be used by implementations in order to obtain the value of the
   * specified attribute from the provided entry as a boolean.
   *
   * @param entry The entry whose attribute is to be parsed as a boolean.
   * @param attributeType The attribute type whose value should be parsed as a boolean.
   * @return The attribute's value represented as a ConditionResult value, or
   *     ConditionResult.UNDEFINED if the specified attribute does not exist in the entry.
   * @throws DirectoryException If the value cannot be decoded as a boolean.
   */
  protected static final ConditionResult getBoolean(
      final Entry entry, final AttributeType attributeType) throws DirectoryException {
    final List<Attribute> attrList = entry.getAttribute(attributeType);
    if (attrList != null) {
      for (final Attribute a : attrList) {
        if (a.isEmpty()) {
          continue;
        }

        final String valueString = toLowerCase(a.iterator().next().getValue().toString());

        if (valueString.equals("true")
            || valueString.equals("yes")
            || valueString.equals("on")
            || valueString.equals("1")) {
          if (debugEnabled()) {
            TRACER.debugInfo(
                "Attribute %s resolves to true for user entry " + "%s",
                attributeType.getNameOrOID(), entry.getDN().toString());
          }

          return ConditionResult.TRUE;
        }

        if (valueString.equals("false")
            || valueString.equals("no")
            || valueString.equals("off")
            || valueString.equals("0")) {
          if (debugEnabled()) {
            TRACER.debugInfo(
                "Attribute %s resolves to false for user " + "entry %s",
                attributeType.getNameOrOID(), entry.getDN().toString());
          }

          return ConditionResult.FALSE;
        }

        if (debugEnabled()) {
          TRACER.debugError(
              "Unable to resolve value %s for attribute %s " + "in user entry %s as a Boolean.",
              valueString, attributeType.getNameOrOID(), entry.getDN().toString());
        }

        final Message message =
            ERR_PWPSTATE_CANNOT_DECODE_BOOLEAN.get(
                valueString, attributeType.getNameOrOID(), entry.getDN().toString());
        throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
      }
    }

    if (debugEnabled()) {
      TRACER.debugInfo(
          "Returning %s because attribute %s does not exist " + "in user entry %s",
          ConditionResult.UNDEFINED.toString(),
          attributeType.getNameOrOID(),
          entry.getDN().toString());
    }

    return ConditionResult.UNDEFINED;
  }
예제 #2
0
  /**
   * Test entry.
   *
   * @throws Exception If the test failed unexpectedly.
   */
  @Test()
  public void testEntryToAndFromDatabase() throws Exception {
    // Make sure that the server is up and running.
    TestCaseUtils.startServer();

    // Convert the test LDIF string to a byte array
    byte[] originalLDIFBytes = StaticUtils.getBytes(ldifString);

    LDIFReader reader =
        new LDIFReader(new LDIFImportConfig(new ByteArrayInputStream(originalLDIFBytes)));

    Entry entryBefore, entryAfter;
    while ((entryBefore = reader.readEntry(false)) != null) {
      ByteString bytes = ID2Entry.entryToDatabase(entryBefore, new DataConfig(false, false, null));

      entryAfter = ID2Entry.entryFromDatabase(bytes, DirectoryServer.getDefaultCompressedSchema());

      // check DN and number of attributes
      assertEquals(entryBefore.getAttributes().size(), entryAfter.getAttributes().size());

      assertEquals(entryBefore.getDN(), entryAfter.getDN());

      // check the object classes were not changed
      for (String ocBefore : entryBefore.getObjectClasses().values()) {
        ObjectClass objectClass = DirectoryServer.getObjectClass(ocBefore.toLowerCase());
        if (objectClass == null) {
          objectClass = DirectoryServer.getDefaultObjectClass(ocBefore);
        }
        String ocAfter = entryAfter.getObjectClasses().get(objectClass);

        assertEquals(ocBefore, ocAfter);
      }

      // check the user attributes were not changed
      for (AttributeType attrType : entryBefore.getUserAttributes().keySet()) {
        List<Attribute> listBefore = entryBefore.getAttribute(attrType);
        List<Attribute> listAfter = entryAfter.getAttribute(attrType);

        assertTrue(listAfter != null);

        assertEquals(listBefore.size(), listAfter.size());

        for (Attribute attrBefore : listBefore) {
          boolean found = false;

          for (Attribute attrAfter : listAfter) {
            if (attrAfter.optionsEqual(attrBefore.getOptions())) {
              // Found the corresponding attribute

              assertEquals(attrBefore, attrAfter);
              found = true;
            }
          }

          assertTrue(found);
        }
      }
    }
    reader.close();
  }
 private boolean find(List<Attribute> certAttrList, ByteString certBytes) {
   for (Attribute a : certAttrList) {
     if (a.contains(certBytes)) {
       return true;
     }
   }
   return false;
 }
예제 #4
0
  private void encodeV2Attributes(
      ByteStringBuilder buffer,
      Map<AttributeType, List<Attribute>> attributes,
      EntryEncodeConfig config)
      throws DirectoryException {
    int numAttributes = 0;

    // First count how many attributes are there to encode.
    for (List<Attribute> attrList : attributes.values()) {
      for (Attribute a : attrList) {
        if (a.isVirtual() || a.isEmpty()) {
          continue;
        }

        numAttributes++;
      }
    }

    // Encoded one-to-five byte number of attributes
    buffer.appendBERLength(numAttributes);

    if (config.compressAttributeDescriptions()) {
      for (List<Attribute> attrList : attributes.values()) {
        for (Attribute a : attrList) {
          if (a.isVirtual() || a.isEmpty()) {
            continue;
          }

          ByteStringBuilder bsb = new ByteStringBuilder();
          config.getCompressedSchema().encodeAttribute(bsb, a);
          buffer.appendBERLength(bsb.length());
          buffer.append(bsb);
        }
      }
    } else {
      // The attributes will be encoded as a sequence of:
      // - A UTF-8 byte representation of the attribute name.
      // - A zero delimiter
      // - A one-to-five byte number of values for the attribute
      // - A sequence of:
      //   - A one-to-five byte length for the value
      //   - A UTF-8 byte representation for the value
      for (List<Attribute> attrList : attributes.values()) {
        for (Attribute a : attrList) {
          byte[] nameBytes = getBytes(a.getNameWithOptions());
          buffer.append(nameBytes);
          buffer.append((byte) 0x00);

          buffer.appendBERLength(a.size());
          for (AttributeValue v : a) {
            buffer.appendBERLength(v.getValue().length());
            buffer.append(v.getValue());
          }
        }
      }
    }
  }
  /**
   * A utility method which may be used by implementations in order to obtain the value of the
   * specified attribute from the provided entry as a time in generalized time format.
   *
   * @param entry The entry whose attribute is to be parsed as a boolean.
   * @param attributeType The attribute type whose value should be parsed as a generalized time
   *     value.
   * @return The requested time, or -1 if it could not be determined.
   * @throws DirectoryException If a problem occurs while attempting to decode the value as a
   *     generalized time.
   */
  protected static final long getGeneralizedTime(
      final Entry entry, final AttributeType attributeType) throws DirectoryException {
    long timeValue = -1;

    final List<Attribute> attrList = entry.getAttribute(attributeType);
    if (attrList != null) {
      for (final Attribute a : attrList) {
        if (a.isEmpty()) {
          continue;
        }

        final AttributeValue v = a.iterator().next();
        try {
          timeValue = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(v.getNormalizedValue());
        } catch (final Exception e) {
          if (debugEnabled()) {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);

            TRACER.debugWarning(
                "Unable to decode value %s for attribute %s " + "in user entry %s: %s",
                v.getValue().toString(),
                attributeType.getNameOrOID(),
                entry.getDN().toString(),
                stackTraceToSingleLineString(e));
          }

          final Message message =
              ERR_PWPSTATE_CANNOT_DECODE_GENERALIZED_TIME.get(
                  v.getValue().toString(),
                  attributeType.getNameOrOID(),
                  entry.getDN().toString(),
                  String.valueOf(e));
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message, e);
        }
        break;
      }
    }

    if (timeValue == -1) {
      if (debugEnabled()) {
        TRACER.debugInfo(
            "Returning -1 because attribute %s does not " + "exist in user entry %s",
            attributeType.getNameOrOID(), entry.getDN().toString());
      }
    }
    // FIXME: else to be consistent...

    return timeValue;
  }
예제 #6
0
  /**
   * The attributes will be encoded as a sequence of: - A UTF-8 byte representation of the attribute
   * name. - A zero delimiter - A one-to-five byte number of values for the attribute - A sequence
   * of: - A one-to-five byte length for the value - A UTF-8 byte representation for the value
   */
  private void append(ByteStringBuilder buffer, Map<AttributeType, List<Attribute>> attributes) {
    for (List<Attribute> attrList : attributes.values()) {
      for (Attribute a : attrList) {
        byte[] nameBytes = getBytes(a.getNameWithOptions());
        buffer.appendBytes(nameBytes);
        buffer.appendByte(0x00);

        buffer.appendBERLength(a.size());
        for (ByteString v : a) {
          buffer.appendBERLength(v.length());
          buffer.appendBytes(v);
        }
      }
    }
  }
예제 #7
0
 /**
  * Gathers all of the attribute types in an entry along with the "objectclass" attribute type in a
  * List. The "objectclass" attribute is added to the list first so it is evaluated first.
  *
  * @param e Entry to gather the attributes for.
  * @return List containing the attribute types.
  */
 private List<AttributeType> getAllAttrs(Entry e) {
   Map<AttributeType, List<Attribute>> attrMap = e.getUserAttributes();
   Map<AttributeType, List<Attribute>> opAttrMap = e.getOperationalAttributes();
   List<AttributeType> typeList = new LinkedList<AttributeType>();
   Attribute attr = e.getObjectClassAttribute();
   /*
    * When a search is not all attributes returned, the "objectclass"
    * attribute type is missing from the entry.
    */
   if (attr != null) {
     AttributeType ocType = attr.getAttributeType();
     typeList.add(ocType);
   }
   typeList.addAll(attrMap.keySet());
   typeList.addAll(opAttrMap.keySet());
   return typeList;
 }
  /**
   * This method calculates the historical information and update the hist attribute to store the
   * historical information for a modify operation that does not conflict with previous operation.
   * This is the usual path and should therefore be optimized.
   *
   * <p>It does not check if the operation to process is conflicting or not with previous
   * operations. The caller is responsible for this.
   *
   * @param csn The CSN of the operation to process
   * @param mod The modify operation to process.
   */
  @Override
  public void processLocalOrNonConflictModification(CSN csn, Modification mod) {
    /*
     * The operation is either a non-conflicting operation or a local
     * operation so there is no need to check the historical information
     * for conflicts.
     * If this is a local operation, then this code is run after
     * the pre-operation phase.
     * If this is a non-conflicting replicated operation, this code is run
     * during the handleConflictResolution().
     */

    Attribute modAttr = mod.getAttribute();
    AttributeType type = modAttr.getAttributeType();

    switch (mod.getModificationType().asEnum()) {
      case DELETE:
        if (modAttr.isEmpty()) {
          delete(csn);
        } else {
          delete(modAttr, csn);
        }
        break;

      case ADD:
        if (type.isSingleValue()) {
          delete(csn);
        }
        add(modAttr, csn);
        break;

      case REPLACE:
        /* TODO : can we replace specific attribute values ????? */
        delete(csn);
        add(modAttr, csn);
        break;

      case INCREMENT:
        /* FIXME : we should update CSN */
        break;
    }
  }
예제 #9
0
  private void encodeV1Attributes(
      ByteStringBuilder buffer, Map<AttributeType, List<Attribute>> attributes) {
    int numAttributes = 0;

    // First count how many attributes are there to encode.
    for (List<Attribute> attrList : attributes.values()) {
      for (Attribute a : attrList) {
        if (a.isVirtual() || a.isEmpty()) {
          continue;
        }

        numAttributes++;
      }
    }

    // Encoded one-to-five byte number of attributes
    buffer.appendBERLength(numAttributes);

    append(buffer, attributes);
  }
예제 #10
0
  /**
   * Test for escaped characters in templates, check LDIF output when the templates combines escaped
   * characters and variables.
   */
  @Test(dependsOnMethods = {"testParsingEscapeCharInTemplate"})
  public void testOutputCombineEscapeCharInTemplate() throws Exception {
    String[] lines = {
      "branch: dc=test",
      "subordinateTemplate: templateWithEscape:1",
      "",
      "template: templateWithEscape",
      "rdnAttr: uid",
      "objectclass: inetOrgPerson",
      "uid: testEntry",
      "sn: Bar",
      // The value below combines variable, randoms and escaped chars.
      // The resulting value is "Foo <?>{1}Bar" where ? is a letter from [A-Z].
      "cn: Foo \\<<random:chars:ABCDEFGHIJKLMNOPQRSTUVWXYZ:1>\\>\\{1\\}{sn}",
      "",
    };

    File tmpFile = File.createTempFile("combineEscapeChar", "out.ldif");
    tmpFile.deleteOnExit();
    String outLdifFilePath = tmpFile.getAbsolutePath();

    LdifFileWriter.makeLdif(outLdifFilePath, resourcePath, lines);

    LDIFImportConfig ldifConfig = new LDIFImportConfig(outLdifFilePath);
    ldifConfig.setValidateSchema(false);
    LDIFReader reader = new LDIFReader(ldifConfig);
    Entry top = reader.readEntry();
    Entry e = reader.readEntry();
    reader.close();

    assertNotNull(top);
    assertNotNull(e);
    List<Attribute> attrs = e.getAttribute("cn");
    assertFalse(attrs.isEmpty());
    Attribute a = attrs.get(0);
    assertTrue(
        a.iterator().next().toString().matches("Foo <[A-Z]>\\{1\\}Bar"),
        "cn value doesn't match the expected value");
  }
예제 #11
0
 private Integer getIntegerUserAttribute(
     Entry userEntry,
     String attributeTypeName,
     Arg1<Object> nonUniqueAttributeMessage,
     Arg2<Object, Object> cannotProcessAttributeMessage) {
   AttributeType attrType = DirectoryServer.getAttributeTypeOrDefault(attributeTypeName);
   List<Attribute> attrList = userEntry.getAttribute(attrType);
   if (attrList != null && attrList.size() == 1) {
     Attribute a = attrList.get(0);
     if (a.size() == 1) {
       ByteString v = a.iterator().next();
       try {
         return Integer.valueOf(v.toString());
       } catch (Exception e) {
         logger.traceException(e);
         logger.error(cannotProcessAttributeMessage.get(v, userEntry.getName()));
       }
     } else if (a.size() > 1) {
       logger.error(nonUniqueAttributeMessage.get(userEntry.getName()));
     }
   }
   return null;
 }
예제 #12
0
  private void encodeV2Attributes(
      ByteStringBuilder buffer,
      Map<AttributeType, List<Attribute>> attributes,
      EntryEncodeConfig config)
      throws DirectoryException {
    int numAttributes = 0;

    // First count how many attributes are there to encode.
    for (List<Attribute> attrList : attributes.values()) {
      for (Attribute a : attrList) {
        if (a.isVirtual() || a.isEmpty()) {
          continue;
        }

        numAttributes++;
      }
    }

    // Encoded one-to-five byte number of attributes
    buffer.appendBERLength(numAttributes);

    if (config.compressAttributeDescriptions()) {
      for (List<Attribute> attrList : attributes.values()) {
        for (Attribute a : attrList) {
          if (a.isVirtual() || a.isEmpty()) {
            continue;
          }

          ByteStringBuilder bsb = new ByteStringBuilder();
          config.getCompressedSchema().encodeAttribute(bsb, a);
          buffer.appendBERLength(bsb.length());
          buffer.appendBytes(bsb);
        }
      }
    } else {
      append(buffer, attributes);
    }
  }
예제 #13
0
  /**
   * Checks to see if a LDAP modification is allowed access.
   *
   * @param container The structure containing the LDAP modifications
   * @param operation The operation to check modify privileges on. operation to check and the
   *     evaluation context to apply the check against.
   * @param skipAccessCheck True if access checking should be skipped.
   * @return True if access is allowed.
   * @throws DirectoryException If a modified ACI could not be decoded.
   */
  private boolean aciCheckMods(
      AciLDAPOperationContainer container,
      LocalBackendModifyOperation operation,
      boolean skipAccessCheck)
      throws DirectoryException {
    Entry resourceEntry = container.getResourceEntry();
    DN dn = resourceEntry.getDN();
    List<Modification> modifications = operation.getModifications();

    for (Modification m : modifications) {
      Attribute modAttr = m.getAttribute();
      AttributeType modAttrType = modAttr.getAttributeType();

      if (modAttrType.equals(aciType)) {
        /*
         * Check that the operation has modify privileges if it contains
         * an "aci" attribute type.
         */
        if (!operation.getClientConnection().hasPrivilege(Privilege.MODIFY_ACL, operation)) {
          Message message =
              INFO_ACI_MODIFY_FAILED_PRIVILEGE.get(
                  String.valueOf(container.getResourceDN()),
                  String.valueOf(container.getClientDN()));
          logError(message);
          return false;
        }
      }
      // This access check handles the case where all attributes of this
      // type are being replaced or deleted. If only a subset is being
      // deleted than this access check is skipped.
      ModificationType modType = m.getModificationType();
      if (((modType == ModificationType.DELETE) && modAttr.isEmpty())
          || ((modType == ModificationType.REPLACE) || (modType == ModificationType.INCREMENT))) {
        /*
         * Check if we have rights to delete all values of an attribute
         * type in the resource entry.
         */
        if (resourceEntry.hasAttribute(modAttrType)) {
          container.setCurrentAttributeType(modAttrType);
          List<Attribute> attrList = resourceEntry.getAttribute(modAttrType, modAttr.getOptions());
          if (attrList != null) {
            for (Attribute a : attrList) {
              for (AttributeValue v : a) {
                container.setCurrentAttributeValue(v);
                container.setRights(ACI_WRITE_DELETE);
                if (!skipAccessCheck && !accessAllowed(container)) {
                  return false;
                }
              }
            }
          }
        }
      }

      if (!modAttr.isEmpty()) {
        for (AttributeValue v : modAttr) {
          container.setCurrentAttributeType(modAttrType);
          switch (m.getModificationType()) {
            case ADD:
            case REPLACE:
              container.setCurrentAttributeValue(v);
              container.setRights(ACI_WRITE_ADD);
              if (!skipAccessCheck && !accessAllowed(container)) {
                return false;
              }
              break;
            case DELETE:
              container.setCurrentAttributeValue(v);
              container.setRights(ACI_WRITE_DELETE);
              if (!skipAccessCheck && !accessAllowed(container)) {
                return false;
              }
              break;
            case INCREMENT:
              Entry modifiedEntry = operation.getModifiedEntry();
              List<Attribute> modifiedAttrs =
                  modifiedEntry.getAttribute(modAttrType, modAttr.getOptions());
              if (modifiedAttrs != null) {
                for (Attribute attr : modifiedAttrs) {
                  for (AttributeValue val : attr) {
                    container.setCurrentAttributeValue(val);
                    container.setRights(ACI_WRITE_ADD);
                    if (!skipAccessCheck && !accessAllowed(container)) {
                      return false;
                    }
                  }
                }
              }
              break;
          }
          /*
           * Check if the modification type has an "aci" attribute type.
           * If so, check the syntax of that attribute value. Fail the
           * the operation if the syntax check fails.
           */
          if (modAttrType.equals(aciType) || modAttrType.equals(globalAciType)) {
            try {
              // A global ACI needs a NULL DN, not the DN of the
              // modification.
              if (modAttrType.equals(globalAciType)) {
                dn = DN.nullDN();
              }
              Aci.decode(v.getValue(), dn);
            } catch (AciException ex) {
              Message message =
                  WARN_ACI_MODIFY_FAILED_DECODE.get(String.valueOf(dn), ex.getMessage());
              throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
            }
          }
        }
      }
    }
    return true;
  }
  /**
   * Process a add attribute values that is conflicting with a previous modification.
   *
   * @param csn the historical info associated to the entry
   * @param m the modification that is being processed
   * @param modsIterator iterator on the list of modification
   * @return false if operation becomes empty and must not be processed
   */
  private boolean conflictAdd(CSN csn, Modification m, Iterator<Modification> modsIterator) {
    /*
     * if historicalattributedelete is newer forget this mod else find
     * attr value if does not exist add historicalvalueadded timestamp
     * add real value in entry else if timestamp older and already was
     * historicalvalueadded update historicalvalueadded else if
     * timestamp older and was historicalvaluedeleted change
     * historicalvaluedeleted into historicalvalueadded add value in
     * real entry
     */

    if (csn.isOlderThan(getDeleteTime())) {
      /* A delete has been done more recently than this add
       * forget this MOD ADD
       */
      modsIterator.remove();
      return false;
    }

    AttributeBuilder builder = new AttributeBuilder(m.getAttribute());
    for (ByteString addVal : m.getAttribute()) {
      AttrValueHistorical valInfo = new AttrValueHistorical(addVal, csn, null);
      AttrValueHistorical oldValInfo = valuesHist.get(valInfo);
      if (oldValInfo == null) {
        /* this value does not exist yet
         * add it in the historical information
         * let the operation process normally
         */
        valuesHist.put(valInfo, valInfo);
      } else {
        if (oldValInfo.isUpdate()) {
          /* if the value is already present
           * check if the updateTime must be updated
           * in all cases suppress this value from the value list
           * as it is already present in the entry
           */
          if (csn.isNewerThan(oldValInfo.getValueUpdateTime())) {
            valuesHist.remove(oldValInfo);
            valuesHist.put(valInfo, valInfo);
          }
          builder.remove(addVal);
        } else { // it is a delete
          /* this value is marked as a deleted value
           * check if this mod is more recent the this delete
           */
          if (csn.isNewerThanOrEqualTo(oldValInfo.getValueDeleteTime())) {
            /* this add is more recent,
             * remove the old delete historical information
             * and add our more recent one
             * let the operation process
             */
            valuesHist.remove(oldValInfo);
            valuesHist.put(valInfo, valInfo);
          } else {
            /* the delete that is present in the historical information
             * is more recent so it must win,
             * remove this value from the list of values to add
             * don't update the historical information
             */
            builder.remove(addVal);
          }
        }
      }
    }

    Attribute attr = builder.toAttribute();
    m.setAttribute(attr);

    if (attr.isEmpty()) {
      modsIterator.remove();
    }

    if (csn.isNewerThan(getLastUpdateTime())) {
      lastUpdateTime = csn;
    }

    return true;
  }
  /**
   * Process a delete attribute values that is conflicting with a previous modification.
   *
   * @param csn The CSN of the currently processed change
   * @param m the modification that is being processed
   * @param modifiedEntry the entry that is modified (before current mod)
   * @return false if there is nothing to do
   */
  private boolean conflictDelete(CSN csn, Modification m, Entry modifiedEntry) {
    /*
     * We are processing a conflicting DELETE modification
     *
     * This code is written on the assumption that conflict are
     * rare. We therefore don't care much about the performance
     * However since it is rarely executed this code needs to be
     * as simple as possible to make sure that all paths are tested.
     * In this case the most simple seem to change the DELETE
     * in a REPLACE modification that keeps all values
     * more recent that the DELETE.
     * we are therefore going to change m into a REPLACE that will keep
     * all the values that have been updated after the DELETE time
     * If a value is present in the entry without any state information
     * it must be removed so we simply ignore them
     */

    Attribute modAttr = m.getAttribute();
    if (modAttr.isEmpty()) {
      /*
       * We are processing a DELETE attribute modification
       */
      m.setModificationType(ModificationType.REPLACE);
      AttributeBuilder builder = new AttributeBuilder(modAttr, true);

      Iterator<AttrValueHistorical> it = valuesHist.keySet().iterator();
      while (it.hasNext()) {
        AttrValueHistorical valInfo = it.next();

        if (csn.isOlderThan(valInfo.getValueUpdateTime())) {
          /*
           * this value has been updated after this delete, therefore
           * this value must be kept
           */
          builder.add(valInfo.getAttributeValue());
        } else {
          /*
           * this value is going to be deleted, remove it from historical
           * information unless it is a Deleted attribute value that is
           * more recent than this DELETE
           */
          if (csn.isNewerThanOrEqualTo(valInfo.getValueDeleteTime())) {
            it.remove();
          }
        }
      }

      m.setAttribute(builder.toAttribute());

      if (csn.isNewerThan(getDeleteTime())) {
        deleteTime = csn;
      }
      if (csn.isNewerThan(getLastUpdateTime())) {
        lastUpdateTime = csn;
      }
    } else {
      // we are processing DELETE of some attribute values
      AttributeBuilder builder = new AttributeBuilder(modAttr);

      for (ByteString val : modAttr) {
        boolean deleteIt = true; // true if the delete must be done
        boolean addedInCurrentOp = false;

        /* update historical information */
        AttrValueHistorical valInfo = new AttrValueHistorical(val, null, csn);
        AttrValueHistorical oldValInfo = valuesHist.get(valInfo);
        if (oldValInfo != null) {
          /* this value already exist in the historical information */
          if (csn.equals(oldValInfo.getValueUpdateTime())) {
            // This value was added earlier in the same operation
            // we need to keep the delete.
            addedInCurrentOp = true;
          }
          if (csn.isNewerThanOrEqualTo(oldValInfo.getValueDeleteTime())
              && csn.isNewerThanOrEqualTo(oldValInfo.getValueUpdateTime())) {
            valuesHist.remove(oldValInfo);
            valuesHist.put(valInfo, valInfo);
          } else if (oldValInfo.isUpdate()) {
            deleteIt = false;
          }
        } else {
          valuesHist.remove(oldValInfo);
          valuesHist.put(valInfo, valInfo);
        }

        /* if the attribute value is not to be deleted
         * or if attribute value is not present suppress it from the
         * MOD to make sure the delete is going to succeed
         */
        if (!deleteIt
            || (!modifiedEntry.hasValue(modAttr.getAttributeType(), modAttr.getOptions(), val)
                && !addedInCurrentOp)) {
          // this value was already deleted before and therefore
          // this should not be replayed.
          builder.remove(val);
          if (builder.isEmpty()) {
            // This was the last values in the set of values to be deleted.
            // this MOD must therefore be skipped.
            return false;
          }
        }
      }

      m.setAttribute(builder.toAttribute());

      if (csn.isNewerThan(getLastUpdateTime())) {
        lastUpdateTime = csn;
      }
    }

    return true;
  }