Entry encode(final T o, final String parentDN) throws LDAPPersistException {
    // Get the attributes that should be included in the entry.
    final LinkedHashMap<String, Attribute> attrMap = new LinkedHashMap<String, Attribute>();
    attrMap.put("objectClass", objectClassAttribute);

    for (final Map.Entry<String, FieldInfo> e : fieldMap.entrySet()) {
      final FieldInfo i = e.getValue();
      if (!i.includeInAdd()) {
        continue;
      }

      final Attribute a = i.encode(o, false);
      if (a != null) {
        attrMap.put(e.getKey(), a);
      }
    }

    for (final Map.Entry<String, GetterInfo> e : getterMap.entrySet()) {
      final GetterInfo i = e.getValue();
      if (!i.includeInAdd()) {
        continue;
      }

      final Attribute a = i.encode(o);
      if (a != null) {
        attrMap.put(e.getKey(), a);
      }
    }

    final String dn = constructDN(o, parentDN, attrMap);
    final Entry entry = new Entry(dn, attrMap.values());

    if (postEncodeMethod != null) {
      try {
        postEncodeMethod.invoke(o, entry);
      } catch (Throwable t) {
        debugException(t);

        if (t instanceof InvocationTargetException) {
          t = ((InvocationTargetException) t).getTargetException();
        }

        throw new LDAPPersistException(
            ERR_OBJECT_HANDLER_ERROR_INVOKING_POST_ENCODE_METHOD.get(
                postEncodeMethod.getName(), type.getName(), getExceptionMessage(t)),
            t);
      }
    }

    setDNAndEntryFields(o, entry);

    if (superclassHandler != null) {
      final Entry e = superclassHandler.encode(o, parentDN);
      for (final Attribute a : e.getAttributes()) {
        entry.addAttribute(a);
      }
    }

    return entry;
  }
  String constructDN(final T o, final String parentDN, final Map<String, Attribute> attrMap)
      throws LDAPPersistException {
    final String existingDN = getEntryDN(o);
    if (existingDN != null) {
      return existingDN;
    }

    final ArrayList<String> rdnNameList = new ArrayList<String>(1);
    final ArrayList<byte[]> rdnValueList = new ArrayList<byte[]>(1);
    for (final FieldInfo i : rdnFields) {
      final Attribute a = attrMap.get(toLowerCase(i.getAttributeName()));
      if (a == null) {
        throw new LDAPPersistException(
            ERR_OBJECT_HANDLER_RDN_FIELD_MISSING_VALUE.get(type.getName(), i.getField().getName()));
      }

      rdnNameList.add(a.getName());
      rdnValueList.add(a.getValueByteArray());
    }

    for (final GetterInfo i : rdnGetters) {
      final Attribute a = attrMap.get(toLowerCase(i.getAttributeName()));
      if (a == null) {
        throw new LDAPPersistException(
            ERR_OBJECT_HANDLER_RDN_GETTER_MISSING_VALUE.get(
                type.getName(), i.getMethod().getName()));
      }

      rdnNameList.add(a.getName());
      rdnValueList.add(a.getValueByteArray());
    }

    final String[] rdnNames = new String[rdnNameList.size()];
    rdnNameList.toArray(rdnNames);

    final byte[][] rdnValues = new byte[rdnNames.length][];
    rdnValueList.toArray(rdnValues);

    final RDN rdn = new RDN(rdnNames, rdnValues);

    if (parentDN == null) {
      return new DN(rdn, defaultParentDN).toString();
    } else {
      try {
        final DN parsedParentDN = new DN(parentDN);
        return new DN(rdn, parsedParentDN).toString();
      } catch (LDAPException le) {
        debugException(le);
        throw new LDAPPersistException(
            ERR_OBJECT_HANDLER_INVALID_PARENT_DN.get(type.getName(), parentDN, le.getMessage()),
            le);
      }
    }
  }
  void decode(final T o, final Entry e) throws LDAPPersistException {
    if (superclassHandler != null) {
      superclassHandler.decode(o, e);
    }

    setDNAndEntryFields(o, e);

    final ArrayList<String> failureReasons = new ArrayList<String>(5);
    boolean successful = true;

    for (final FieldInfo i : fieldMap.values()) {
      successful &= i.decode(o, e, failureReasons);
    }

    for (final SetterInfo i : setterMap.values()) {
      successful &= i.invokeSetter(o, e, failureReasons);
    }

    Throwable cause = null;
    if (postDecodeMethod != null) {
      try {
        postDecodeMethod.invoke(o);
      } catch (final Throwable t) {
        debugException(t);

        if (t instanceof InvocationTargetException) {
          cause = ((InvocationTargetException) t).getTargetException();
        } else {
          cause = t;
        }

        successful = false;
        failureReasons.add(
            ERR_OBJECT_HANDLER_ERROR_INVOKING_POST_DECODE_METHOD.get(
                postDecodeMethod.getName(), type.getName(), getExceptionMessage(t)));
      }
    }

    if (!successful) {
      throw new LDAPPersistException(concatenateStrings(failureReasons), o, cause);
    }
  }
  List<Modification> getModifications(
      final T o, final boolean deleteNullValues, final String... attributes)
      throws LDAPPersistException {
    final ReadOnlyEntry originalEntry;
    if (entryField != null) {
      originalEntry = getEntry(o);
    } else {
      originalEntry = null;
    }
    if (originalEntry != null) {
      try {
        final T decodedOrig = decode(originalEntry);
        final Entry reEncodedOriginal = encode(decodedOrig, originalEntry.getParentDNString());

        final Entry newEntry = encode(o, originalEntry.getParentDNString());
        final List<Modification> mods =
            Entry.diff(reEncodedOriginal, newEntry, true, false, attributes);
        if (!deleteNullValues) {
          final Iterator<Modification> iterator = mods.iterator();
          while (iterator.hasNext()) {
            final Modification m = iterator.next();
            if (m.getRawValues().length == 0) {
              iterator.remove();
            }
          }
        }

        HashSet<String> stripAttrs = null;
        for (final FieldInfo i : fieldMap.values()) {
          if (!i.includeInModify()) {
            if (stripAttrs == null) {
              stripAttrs = new HashSet<String>(10);
            }
            stripAttrs.add(toLowerCase(i.getAttributeName()));
          }
        }

        for (final GetterInfo i : getterMap.values()) {
          if (!i.includeInModify()) {
            if (stripAttrs == null) {
              stripAttrs = new HashSet<String>(10);
            }
            stripAttrs.add(toLowerCase(i.getAttributeName()));
          }
        }

        if (stripAttrs != null) {
          final Iterator<Modification> iterator = mods.iterator();
          while (iterator.hasNext()) {
            final Modification m = iterator.next();
            if (stripAttrs.contains(toLowerCase(m.getAttributeName()))) {
              iterator.remove();
            }
          }
        }

        return mods;
      } catch (final Exception e) {
        debugException(e);
      } finally {
        setDNAndEntryFields(o, originalEntry);
      }
    }

    final HashSet<String> attrSet;
    if ((attributes == null) || (attributes.length == 0)) {
      attrSet = null;
    } else {
      attrSet = new HashSet<String>(attributes.length);
      for (final String s : attributes) {
        attrSet.add(toLowerCase(s));
      }
    }

    final ArrayList<Modification> mods = new ArrayList<Modification>(5);

    for (final Map.Entry<String, FieldInfo> e : fieldMap.entrySet()) {
      final String attrName = toLowerCase(e.getKey());
      if ((attrSet != null) && (!attrSet.contains(attrName))) {
        continue;
      }

      final FieldInfo i = e.getValue();
      if (!i.includeInModify()) {
        continue;
      }

      final Attribute a = i.encode(o, false);
      if (a == null) {
        if (!deleteNullValues) {
          continue;
        }

        if ((originalEntry != null) && (!originalEntry.hasAttribute(attrName))) {
          continue;
        }

        mods.add(new Modification(ModificationType.REPLACE, i.getAttributeName()));
        continue;
      }

      if (originalEntry != null) {
        final Attribute originalAttr = originalEntry.getAttribute(attrName);
        if ((originalAttr != null) && originalAttr.equals(a)) {
          continue;
        }
      }

      mods.add(new Modification(ModificationType.REPLACE, i.getAttributeName(), a.getRawValues()));
    }

    for (final Map.Entry<String, GetterInfo> e : getterMap.entrySet()) {
      final String attrName = toLowerCase(e.getKey());
      if ((attrSet != null) && (!attrSet.contains(attrName))) {
        continue;
      }

      final GetterInfo i = e.getValue();
      if (!i.includeInModify()) {
        continue;
      }

      final Attribute a = i.encode(o);
      if (a == null) {
        if (!deleteNullValues) {
          continue;
        }

        if ((originalEntry != null) && (!originalEntry.hasAttribute(attrName))) {
          continue;
        }

        mods.add(new Modification(ModificationType.REPLACE, i.getAttributeName()));
        continue;
      }

      if (originalEntry != null) {
        final Attribute originalAttr = originalEntry.getAttribute(attrName);
        if ((originalAttr != null) && originalAttr.equals(a)) {
          continue;
        }
      }

      mods.add(new Modification(ModificationType.REPLACE, i.getAttributeName(), a.getRawValues()));
    }

    if (superclassHandler != null) {
      final List<Modification> superMods =
          superclassHandler.getModifications(o, deleteNullValues, attributes);
      final ArrayList<Modification> modsToAdd = new ArrayList<Modification>(superMods.size());
      for (final Modification sm : superMods) {
        boolean add = true;
        for (final Modification m : mods) {
          if (m.getAttributeName().equalsIgnoreCase(sm.getAttributeName())) {
            add = false;
            break;
          }
        }
        if (add) {
          modsToAdd.add(sm);
        }
      }
      mods.addAll(modsToAdd);
    }

    return Collections.unmodifiableList(mods);
  }
  ObjectClassDefinition constructObjectClass(
      final String name, final String sup, final ObjectClassType type, final OIDAllocator a) {
    final TreeMap<String, String> requiredAttrs = new TreeMap<String, String>();
    final TreeMap<String, String> optionalAttrs = new TreeMap<String, String>();

    for (final FieldInfo i : fieldMap.values()) {
      boolean found = false;
      for (final String s : i.getObjectClasses()) {
        if (name.equalsIgnoreCase(s)) {
          found = true;
          break;
        }
      }

      if (!found) {
        continue;
      }

      final String attrName = i.getAttributeName();
      final String lowerName = toLowerCase(attrName);
      if (i.includeInRDN() || (i.isRequiredForDecode() && i.isRequiredForEncode())) {
        requiredAttrs.put(lowerName, attrName);
      } else {
        optionalAttrs.put(lowerName, attrName);
      }
    }

    for (final GetterInfo i : getterMap.values()) {
      boolean found = false;
      for (final String s : i.getObjectClasses()) {
        if (name.equalsIgnoreCase(s)) {
          found = true;
          break;
        }
      }

      if (!found) {
        continue;
      }

      final String attrName = i.getAttributeName();
      final String lowerName = toLowerCase(attrName);
      if (i.includeInRDN()) {
        requiredAttrs.put(lowerName, attrName);
      } else {
        optionalAttrs.put(lowerName, attrName);
      }
    }

    if (name.equalsIgnoreCase(structuralClass)) {
      for (final SetterInfo i : setterMap.values()) {
        final String attrName = i.getAttributeName();
        final String lowerName = toLowerCase(attrName);
        if (requiredAttrs.containsKey(lowerName) || optionalAttrs.containsKey(lowerName)) {
          continue;
        }

        optionalAttrs.put(lowerName, attrName);
      }
    }

    final String[] reqArray = new String[requiredAttrs.size()];
    requiredAttrs.values().toArray(reqArray);

    final String[] optArray = new String[optionalAttrs.size()];
    optionalAttrs.values().toArray(optArray);

    return new ObjectClassDefinition(
        a.allocateObjectClassOID(name),
        new String[] {name},
        null,
        false,
        new String[] {sup},
        type,
        reqArray,
        optArray,
        null);
  }