public String constructDN(final T o, final String parentDN) throws LDAPPersistException {
    final String existingDN = getEntryDN(o);
    if (existingDN != null) {
      return existingDN;
    }

    final LinkedHashMap<String, Attribute> attrMap = new LinkedHashMap<String, Attribute>(1);

    for (final FieldInfo i : rdnFields) {
      final Attribute a = i.encode(o, true);
      if (a == null) {
        throw new LDAPPersistException(
            ERR_OBJECT_HANDLER_RDN_FIELD_MISSING_VALUE.get(type.getName(), i.getField().getName()));
      }

      attrMap.put(toLowerCase(i.getAttributeName()), a);
    }

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

      attrMap.put(toLowerCase(i.getAttributeName()), a);
    }

    return constructDN(o, parentDN, attrMap);
  }
  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;
  }
  List<ObjectClassDefinition> constructObjectClasses(final OIDAllocator a)
      throws LDAPPersistException {
    final LinkedHashMap<String, ObjectClassDefinition> ocMap =
        new LinkedHashMap<String, ObjectClassDefinition>(1 + auxiliaryClasses.length);

    if (superclassHandler != null) {
      for (final ObjectClassDefinition d : superclassHandler.constructObjectClasses(a)) {
        ocMap.put(toLowerCase(d.getNameOrOID()), d);
      }
    }

    final String lowerStructuralClass = toLowerCase(structuralClass);
    if (!ocMap.containsKey(lowerStructuralClass)) {
      if (superclassHandler == null) {
        ocMap.put(
            lowerStructuralClass,
            constructObjectClass(structuralClass, "top", ObjectClassType.STRUCTURAL, a));
      } else {
        ocMap.put(
            lowerStructuralClass,
            constructObjectClass(
                structuralClass,
                superclassHandler.getStructuralClass(),
                ObjectClassType.STRUCTURAL,
                a));
      }
    }

    for (final String s : auxiliaryClasses) {
      final String lowerName = toLowerCase(s);
      if (!ocMap.containsKey(lowerName)) {
        ocMap.put(lowerName, constructObjectClass(s, "top", ObjectClassType.AUXILIARY, a));
      }
    }

    return Collections.unmodifiableList(new ArrayList<ObjectClassDefinition>(ocMap.values()));
  }
  @SuppressWarnings("unchecked")
  LDAPObjectHandler(final Class<T> type) throws LDAPPersistException {
    this.type = type;

    final Class<? super T> superclassType = type.getSuperclass();
    if (superclassType == null) {
      superclassHandler = null;
    } else {
      final LDAPObject superclassAnnotation = superclassType.getAnnotation(LDAPObject.class);
      if (superclassAnnotation == null) {
        superclassHandler = null;
      } else {
        superclassHandler = new LDAPObjectHandler(superclassType);
      }
    }

    final TreeMap<String, FieldInfo> fields = new TreeMap<String, FieldInfo>();
    final TreeMap<String, GetterInfo> getters = new TreeMap<String, GetterInfo>();
    final TreeMap<String, SetterInfo> setters = new TreeMap<String, SetterInfo>();

    ldapObject = type.getAnnotation(LDAPObject.class);
    if (ldapObject == null) {
      throw new LDAPPersistException(ERR_OBJECT_HANDLER_OBJECT_NOT_ANNOTATED.get(type.getName()));
    }

    final LinkedHashMap<String, String> objectClasses = new LinkedHashMap<String, String>(10);

    final String oc = ldapObject.structuralClass();
    if (oc.length() == 0) {
      structuralClass = getUnqualifiedClassName(type);
    } else {
      structuralClass = oc;
    }

    final StringBuilder invalidReason = new StringBuilder();
    if (PersistUtils.isValidLDAPName(structuralClass, invalidReason)) {
      objectClasses.put(toLowerCase(structuralClass), structuralClass);
    } else {
      throw new LDAPPersistException(
          ERR_OBJECT_HANDLER_INVALID_STRUCTURAL_CLASS.get(
              type.getName(), structuralClass, invalidReason.toString()));
    }

    auxiliaryClasses = ldapObject.auxiliaryClass();
    for (final String auxiliaryClass : auxiliaryClasses) {
      if (PersistUtils.isValidLDAPName(auxiliaryClass, invalidReason)) {
        objectClasses.put(toLowerCase(auxiliaryClass), auxiliaryClass);
      } else {
        throw new LDAPPersistException(
            ERR_OBJECT_HANDLER_INVALID_AUXILIARY_CLASS.get(
                type.getName(), auxiliaryClass, invalidReason.toString()));
      }
    }

    superiorClasses = ldapObject.superiorClass();
    for (final String superiorClass : superiorClasses) {
      if (PersistUtils.isValidLDAPName(superiorClass, invalidReason)) {
        objectClasses.put(toLowerCase(superiorClass), superiorClass);
      } else {
        throw new LDAPPersistException(
            ERR_OBJECT_HANDLER_INVALID_SUPERIOR_CLASS.get(
                type.getName(), superiorClass, invalidReason.toString()));
      }
    }

    if (superclassHandler != null) {
      for (final String s : superclassHandler.objectClassAttribute.getValues()) {
        objectClasses.put(toLowerCase(s), s);
      }
    }

    objectClassAttribute = new Attribute("objectClass", objectClasses.values());

    final String parentDNStr = ldapObject.defaultParentDN();
    try {
      defaultParentDN = new DN(parentDNStr);
    } catch (LDAPException le) {
      throw new LDAPPersistException(
          ERR_OBJECT_HANDLER_INVALID_DEFAULT_PARENT.get(
              type.getName(), parentDNStr, le.getMessage()),
          le);
    }

    final String postDecodeMethodName = ldapObject.postDecodeMethod();
    if (postDecodeMethodName.length() > 0) {
      try {
        postDecodeMethod = type.getDeclaredMethod(postDecodeMethodName);
        postDecodeMethod.setAccessible(true);
      } catch (Exception e) {
        debugException(e);
        throw new LDAPPersistException(
            ERR_OBJECT_HANDLER_INVALID_POST_DECODE_METHOD.get(
                type.getName(), postDecodeMethodName, getExceptionMessage(e)),
            e);
      }
    } else {
      postDecodeMethod = null;
    }

    final String postEncodeMethodName = ldapObject.postEncodeMethod();
    if (postEncodeMethodName.length() > 0) {
      try {
        postEncodeMethod = type.getDeclaredMethod(postEncodeMethodName, Entry.class);
        postEncodeMethod.setAccessible(true);
      } catch (Exception e) {
        debugException(e);
        throw new LDAPPersistException(
            ERR_OBJECT_HANDLER_INVALID_POST_ENCODE_METHOD.get(
                type.getName(), postEncodeMethodName, getExceptionMessage(e)),
            e);
      }
    } else {
      postEncodeMethod = null;
    }

    try {
      constructor = type.getDeclaredConstructor();
      constructor.setAccessible(true);
    } catch (Exception e) {
      debugException(e);
      throw new LDAPPersistException(
          ERR_OBJECT_HANDLER_NO_DEFAULT_CONSTRUCTOR.get(type.getName()), e);
    }

    Field tmpDNField = null;
    Field tmpEntryField = null;
    final LinkedList<FieldInfo> tmpRFilterFields = new LinkedList<FieldInfo>();
    final LinkedList<FieldInfo> tmpAAFilterFields = new LinkedList<FieldInfo>();
    final LinkedList<FieldInfo> tmpCAFilterFields = new LinkedList<FieldInfo>();
    final LinkedList<FieldInfo> tmpRDNFields = new LinkedList<FieldInfo>();
    for (final Field f : type.getDeclaredFields()) {
      final LDAPField fieldAnnotation = f.getAnnotation(LDAPField.class);
      final LDAPDNField dnFieldAnnotation = f.getAnnotation(LDAPDNField.class);
      final LDAPEntryField entryFieldAnnotation = f.getAnnotation(LDAPEntryField.class);

      if (fieldAnnotation != null) {
        f.setAccessible(true);

        final FieldInfo fieldInfo = new FieldInfo(f, type);
        final String attrName = toLowerCase(fieldInfo.getAttributeName());
        if (fields.containsKey(attrName)) {
          throw new LDAPPersistException(
              ERR_OBJECT_HANDLER_ATTR_CONFLICT.get(type.getName(), fieldInfo.getAttributeName()));
        } else {
          fields.put(attrName, fieldInfo);
        }

        switch (fieldInfo.getFilterUsage()) {
          case REQUIRED:
            tmpRFilterFields.add(fieldInfo);
            break;
          case ALWAYS_ALLOWED:
            tmpAAFilterFields.add(fieldInfo);
            break;
          case CONDITIONALLY_ALLOWED:
            tmpCAFilterFields.add(fieldInfo);
            break;
          case EXCLUDED:
          default:
            break;
        }

        if (fieldInfo.includeInRDN()) {
          tmpRDNFields.add(fieldInfo);
        }
      }

      if (dnFieldAnnotation != null) {
        f.setAccessible(true);

        if (fieldAnnotation != null) {
          throw new LDAPPersistException(
              ERR_OBJECT_HANDLER_CONFLICTING_FIELD_ANNOTATIONS.get(
                  type.getName(), "LDAPField", "LDAPDNField", f.getName()));
        }

        if (tmpDNField != null) {
          throw new LDAPPersistException(ERR_OBJECT_HANDLER_MULTIPLE_DN_FIELDS.get(type.getName()));
        }

        final int modifiers = f.getModifiers();
        if (Modifier.isFinal(modifiers)) {
          throw new LDAPPersistException(
              ERR_OBJECT_HANDLER_DN_FIELD_FINAL.get(f.getName(), type.getName()));
        } else if (Modifier.isStatic(modifiers)) {
          throw new LDAPPersistException(
              ERR_OBJECT_HANDLER_DN_FIELD_STATIC.get(f.getName(), type.getName()));
        }

        final Class<?> fieldType = f.getType();
        if (fieldType.equals(String.class)) {
          tmpDNField = f;
        } else {
          throw new LDAPPersistException(
              ERR_OBJECT_HANDLER_INVALID_DN_FIELD_TYPE.get(
                  type.getName(), f.getName(), fieldType.getName()));
        }
      }

      if (entryFieldAnnotation != null) {
        f.setAccessible(true);

        if (fieldAnnotation != null) {
          throw new LDAPPersistException(
              ERR_OBJECT_HANDLER_CONFLICTING_FIELD_ANNOTATIONS.get(
                  type.getName(), "LDAPField", "LDAPEntryField", f.getName()));
        }

        if (tmpEntryField != null) {
          throw new LDAPPersistException(
              ERR_OBJECT_HANDLER_MULTIPLE_ENTRY_FIELDS.get(type.getName()));
        }

        final int modifiers = f.getModifiers();
        if (Modifier.isFinal(modifiers)) {
          throw new LDAPPersistException(
              ERR_OBJECT_HANDLER_ENTRY_FIELD_FINAL.get(f.getName(), type.getName()));
        } else if (Modifier.isStatic(modifiers)) {
          throw new LDAPPersistException(
              ERR_OBJECT_HANDLER_ENTRY_FIELD_STATIC.get(f.getName(), type.getName()));
        }

        final Class<?> fieldType = f.getType();
        if (fieldType.equals(ReadOnlyEntry.class)) {
          tmpEntryField = f;
        } else {
          throw new LDAPPersistException(
              ERR_OBJECT_HANDLER_INVALID_ENTRY_FIELD_TYPE.get(
                  type.getName(), f.getName(), fieldType.getName()));
        }
      }
    }

    dnField = tmpDNField;
    entryField = tmpEntryField;
    requiredFilterFields = Collections.unmodifiableList(tmpRFilterFields);
    alwaysAllowedFilterFields = Collections.unmodifiableList(tmpAAFilterFields);
    conditionallyAllowedFilterFields = Collections.unmodifiableList(tmpCAFilterFields);
    rdnFields = Collections.unmodifiableList(tmpRDNFields);

    final LinkedList<GetterInfo> tmpRFilterGetters = new LinkedList<GetterInfo>();
    final LinkedList<GetterInfo> tmpAAFilterGetters = new LinkedList<GetterInfo>();
    final LinkedList<GetterInfo> tmpCAFilterGetters = new LinkedList<GetterInfo>();
    final LinkedList<GetterInfo> tmpRDNGetters = new LinkedList<GetterInfo>();
    for (final Method m : type.getDeclaredMethods()) {
      final LDAPGetter getter = m.getAnnotation(LDAPGetter.class);
      final LDAPSetter setter = m.getAnnotation(LDAPSetter.class);

      if (getter != null) {
        m.setAccessible(true);

        if (setter != null) {
          throw new LDAPPersistException(
              ERR_OBJECT_HANDLER_CONFLICTING_METHOD_ANNOTATIONS.get(
                  type.getName(), "LDAPGetter", "LDAPSetter", m.getName()));
        }

        final GetterInfo methodInfo = new GetterInfo(m, type);
        final String attrName = toLowerCase(methodInfo.getAttributeName());
        if (fields.containsKey(attrName) || getters.containsKey(attrName)) {
          throw new LDAPPersistException(
              ERR_OBJECT_HANDLER_ATTR_CONFLICT.get(type.getName(), methodInfo.getAttributeName()));
        } else {
          getters.put(attrName, methodInfo);
        }

        switch (methodInfo.getFilterUsage()) {
          case REQUIRED:
            tmpRFilterGetters.add(methodInfo);
            break;
          case ALWAYS_ALLOWED:
            tmpAAFilterGetters.add(methodInfo);
            break;
          case CONDITIONALLY_ALLOWED:
            tmpCAFilterGetters.add(methodInfo);
            break;
          case EXCLUDED:
          default:
            // No action required.
            break;
        }

        if (methodInfo.includeInRDN()) {
          tmpRDNGetters.add(methodInfo);
        }
      }

      if (setter != null) {
        m.setAccessible(true);

        final SetterInfo methodInfo = new SetterInfo(m, type);
        final String attrName = toLowerCase(methodInfo.getAttributeName());
        if (fields.containsKey(attrName) || setters.containsKey(attrName)) {
          throw new LDAPPersistException(
              ERR_OBJECT_HANDLER_ATTR_CONFLICT.get(type.getName(), methodInfo.getAttributeName()));
        } else {
          setters.put(attrName, methodInfo);
        }
      }
    }

    requiredFilterGetters = Collections.unmodifiableList(tmpRFilterGetters);
    alwaysAllowedFilterGetters = Collections.unmodifiableList(tmpAAFilterGetters);
    conditionallyAllowedFilterGetters = Collections.unmodifiableList(tmpCAFilterGetters);

    rdnGetters = Collections.unmodifiableList(tmpRDNGetters);
    if (rdnFields.isEmpty() && rdnGetters.isEmpty()) {
      throw new LDAPPersistException(ERR_OBJECT_HANDLER_NO_RDN_DEFINED.get(type.getName()));
    }

    fieldMap = Collections.unmodifiableMap(fields);
    getterMap = Collections.unmodifiableMap(getters);
    setterMap = Collections.unmodifiableMap(setters);

    final TreeSet<String> attrSet = new TreeSet<String>();
    final TreeSet<String> lazySet = new TreeSet<String>();
    if (ldapObject.requestAllAttributes()) {
      attrSet.add("*");
      attrSet.add("+");
    } else {
      for (final FieldInfo i : fields.values()) {
        if (i.lazilyLoad()) {
          lazySet.add(i.getAttributeName());
        } else {
          attrSet.add(i.getAttributeName());
        }
      }

      for (final SetterInfo i : setters.values()) {
        attrSet.add(i.getAttributeName());
      }
    }
    attributesToRequest = new String[attrSet.size()];
    attrSet.toArray(attributesToRequest);

    lazilyLoadedAttributes = new String[lazySet.size()];
    lazySet.toArray(lazilyLoadedAttributes);
  }