001/*
002 * Copyright 2009-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2009-2018 Ping Identity Corporation
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk.persist;
022
023
024
025import java.io.Serializable;
026import java.lang.reflect.Constructor;
027import java.lang.reflect.Field;
028import java.lang.reflect.InvocationTargetException;
029import java.lang.reflect.Method;
030import java.lang.reflect.Modifier;
031import java.util.ArrayList;
032import java.util.Arrays;
033import java.util.Iterator;
034import java.util.LinkedHashMap;
035import java.util.LinkedList;
036import java.util.Collections;
037import java.util.HashSet;
038import java.util.List;
039import java.util.Map;
040import java.util.TreeMap;
041import java.util.TreeSet;
042import java.util.concurrent.atomic.AtomicBoolean;
043
044import com.unboundid.asn1.ASN1OctetString;
045import com.unboundid.ldap.sdk.Attribute;
046import com.unboundid.ldap.sdk.DN;
047import com.unboundid.ldap.sdk.Entry;
048import com.unboundid.ldap.sdk.Filter;
049import com.unboundid.ldap.sdk.LDAPException;
050import com.unboundid.ldap.sdk.Modification;
051import com.unboundid.ldap.sdk.ModificationType;
052import com.unboundid.ldap.sdk.RDN;
053import com.unboundid.ldap.sdk.ReadOnlyEntry;
054import com.unboundid.ldap.sdk.schema.ObjectClassDefinition;
055import com.unboundid.ldap.sdk.schema.ObjectClassType;
056import com.unboundid.util.NotMutable;
057import com.unboundid.util.ThreadSafety;
058import com.unboundid.util.ThreadSafetyLevel;
059
060import static com.unboundid.ldap.sdk.persist.PersistMessages.*;
061import static com.unboundid.util.Debug.*;
062import static com.unboundid.util.StaticUtils.*;
063
064
065
066/**
067 * This class provides a mechanism for validating, encoding, and decoding
068 * objects marked with the {@link LDAPObject} annotation type.
069 *
070 * @param  <T>  The type of object handled by this class.
071 */
072@NotMutable()
073@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
074public final class LDAPObjectHandler<T>
075       implements Serializable
076{
077  /**
078   * The serial version UID for this serializable class.
079   */
080  private static final long serialVersionUID = -1480360011153517161L;
081
082
083
084  // The object class attribute to include in entries that are created.
085  private final Attribute objectClassAttribute;
086
087  // The type of object handled by this class.
088  private final Class<T> type;
089
090  // The constructor to use to create a new instance of the class.
091  private final Constructor<T> constructor;
092
093  // The default parent DN for entries created from objects of the associated
094  //  type.
095  private final DN defaultParentDN;
096
097  // The field that will be used to hold the DN of the entry.
098  private final Field dnField;
099
100  // The field that will be used to hold the entry contents.
101  private final Field entryField;
102
103  // The LDAPObject annotation for the associated object.
104  private final LDAPObject ldapObject;
105
106  // The LDAP object handler for the superclass, if applicable.
107  private final LDAPObjectHandler<? super T> superclassHandler;
108
109  // The list of fields for with a filter usage of ALWAYS_ALLOWED.
110  private final List<FieldInfo> alwaysAllowedFilterFields;
111
112  // The list of fields for with a filter usage of CONDITIONALLY_ALLOWED.
113  private final List<FieldInfo> conditionallyAllowedFilterFields;
114
115  // The list of fields for with a filter usage of REQUIRED.
116  private final List<FieldInfo> requiredFilterFields;
117
118  // The list of fields for this class that should be used to construct the RDN.
119  private final List<FieldInfo> rdnFields;
120
121  // The list of getter methods for with a filter usage of ALWAYS_ALLOWED.
122  private final List<GetterInfo> alwaysAllowedFilterGetters;
123
124  // The list of getter methods for with a filter usage of
125  // CONDITIONALLY_ALLOWED.
126  private final List<GetterInfo> conditionallyAllowedFilterGetters;
127
128  // The list of getter methods for with a filter usage of REQUIRED.
129  private final List<GetterInfo> requiredFilterGetters;
130
131  // The list of getters for this class that should be used to construct the
132  // RDN.
133  private final List<GetterInfo> rdnGetters;
134
135  // The map of attribute names to their corresponding fields.
136  private final Map<String,FieldInfo> fieldMap;
137
138  // The map of attribute names to their corresponding getter methods.
139  private final Map<String,GetterInfo> getterMap;
140
141  // The map of attribute names to their corresponding setter methods.
142  private final Map<String,SetterInfo> setterMap;
143
144  // The method that should be invoked on an object after all other decode
145  // processing has been performed.
146  private final Method postDecodeMethod;
147
148  // The method that should be invoked on an object after all other encode
149  // processing has been performed.
150  private final Method postEncodeMethod;
151
152  // The structural object class that should be used for entries created from
153  // objects of the associated type.
154  private final String structuralClass;
155
156  // The set of attributes that should be requested when performing a search.
157  // It will not include lazily-loaded attributes.
158  private final String[] attributesToRequest;
159
160  // The auxiliary object classes that should should used for entries created
161  // from objects of the associated type.
162  private final String[] auxiliaryClasses;
163
164  // The set of attributes that will be requested if @LDAPObject has
165  // requestAllAttributes is false.  Even if requestAllAttributes is true, this
166  // may be used if a subclass has requestAllAttributes set to false.
167  private final String[] explicitAttributesToRequest;
168
169  // The set of attributes that should be lazily loaded.
170  private final String[] lazilyLoadedAttributes;
171
172  // The superior object classes that should should used for entries created
173  // from objects of the associated type.
174  private final String[] superiorClasses;
175
176
177
178  /**
179   * Creates a new instance of this handler that will handle objects of the
180   * specified type.
181   *
182   * @param  type  The type of object that will be handled by this class.
183   *
184   * @throws  LDAPPersistException  If there is a problem with the provided
185   *                                class that makes it unsuitable for use with
186   *                                the persistence framework.
187   */
188  @SuppressWarnings({"unchecked", "rawtypes"})
189  LDAPObjectHandler(final Class<T> type)
190       throws LDAPPersistException
191  {
192    this.type = type;
193
194    final Class<? super T> superclassType = type.getSuperclass();
195    if (superclassType == null)
196    {
197      superclassHandler = null;
198    }
199    else
200    {
201      final LDAPObject superclassAnnotation =
202           superclassType.getAnnotation(LDAPObject.class);
203      if (superclassAnnotation == null)
204      {
205        superclassHandler = null;
206      }
207      else
208      {
209        superclassHandler = new LDAPObjectHandler(superclassType);
210      }
211    }
212
213    final TreeMap<String,FieldInfo>  fields  = new TreeMap<String,FieldInfo>();
214    final TreeMap<String,GetterInfo> getters = new TreeMap<String,GetterInfo>();
215    final TreeMap<String,SetterInfo> setters = new TreeMap<String,SetterInfo>();
216
217    ldapObject = type.getAnnotation(LDAPObject.class);
218    if (ldapObject == null)
219    {
220      throw new LDAPPersistException(
221           ERR_OBJECT_HANDLER_OBJECT_NOT_ANNOTATED.get(type.getName()));
222    }
223
224    final LinkedHashMap<String,String> objectClasses =
225         new LinkedHashMap<String,String>(10);
226
227    final String oc = ldapObject.structuralClass();
228    if (oc.length() == 0)
229    {
230      structuralClass = getUnqualifiedClassName(type);
231    }
232    else
233    {
234      structuralClass = oc;
235    }
236
237    final StringBuilder invalidReason = new StringBuilder();
238    if (PersistUtils.isValidLDAPName(structuralClass, invalidReason))
239    {
240      objectClasses.put(toLowerCase(structuralClass), structuralClass);
241    }
242    else
243    {
244      throw new LDAPPersistException(
245           ERR_OBJECT_HANDLER_INVALID_STRUCTURAL_CLASS.get(type.getName(),
246                structuralClass, invalidReason.toString()));
247    }
248
249    auxiliaryClasses = ldapObject.auxiliaryClass();
250    for (final String auxiliaryClass : auxiliaryClasses)
251    {
252      if (PersistUtils.isValidLDAPName(auxiliaryClass, invalidReason))
253      {
254        objectClasses.put(toLowerCase(auxiliaryClass), auxiliaryClass);
255      }
256      else
257      {
258        throw new LDAPPersistException(
259             ERR_OBJECT_HANDLER_INVALID_AUXILIARY_CLASS.get(type.getName(),
260                  auxiliaryClass, invalidReason.toString()));
261      }
262    }
263
264    superiorClasses = ldapObject.superiorClass();
265    for (final String superiorClass : superiorClasses)
266    {
267      if (PersistUtils.isValidLDAPName(superiorClass, invalidReason))
268      {
269        objectClasses.put(toLowerCase(superiorClass), superiorClass);
270      }
271      else
272      {
273        throw new LDAPPersistException(
274             ERR_OBJECT_HANDLER_INVALID_SUPERIOR_CLASS.get(type.getName(),
275                  superiorClass, invalidReason.toString()));
276      }
277    }
278
279    if (superclassHandler != null)
280    {
281      for (final String s : superclassHandler.objectClassAttribute.getValues())
282      {
283        objectClasses.put(toLowerCase(s), s);
284      }
285    }
286
287    objectClassAttribute = new Attribute("objectClass", objectClasses.values());
288
289
290    final String parentDNStr = ldapObject.defaultParentDN();
291    try
292    {
293      if ((parentDNStr.length() == 0) && (superclassHandler != null))
294      {
295        defaultParentDN = superclassHandler.getDefaultParentDN();
296      }
297      else
298      {
299        defaultParentDN = new DN(parentDNStr);
300      }
301    }
302    catch (final LDAPException le)
303    {
304      throw new LDAPPersistException(
305           ERR_OBJECT_HANDLER_INVALID_DEFAULT_PARENT.get(type.getName(),
306                parentDNStr, le.getMessage()), le);
307    }
308
309
310    final String postDecodeMethodName = ldapObject.postDecodeMethod();
311    if (postDecodeMethodName.length() > 0)
312    {
313      try
314      {
315        postDecodeMethod = type.getDeclaredMethod(postDecodeMethodName);
316        postDecodeMethod.setAccessible(true);
317      }
318      catch (final Exception e)
319      {
320        debugException(e);
321        throw new LDAPPersistException(
322             ERR_OBJECT_HANDLER_INVALID_POST_DECODE_METHOD.get(type.getName(),
323                  postDecodeMethodName, getExceptionMessage(e)), e);
324      }
325    }
326    else
327    {
328      postDecodeMethod = null;
329    }
330
331
332    final String postEncodeMethodName = ldapObject.postEncodeMethod();
333    if (postEncodeMethodName.length() > 0)
334    {
335      try
336      {
337        postEncodeMethod = type.getDeclaredMethod(postEncodeMethodName,
338             Entry.class);
339        postEncodeMethod.setAccessible(true);
340      }
341      catch (final Exception e)
342      {
343        debugException(e);
344        throw new LDAPPersistException(
345             ERR_OBJECT_HANDLER_INVALID_POST_ENCODE_METHOD.get(type.getName(),
346                  postEncodeMethodName, getExceptionMessage(e)), e);
347      }
348    }
349    else
350    {
351      postEncodeMethod = null;
352    }
353
354
355    try
356    {
357      constructor = type.getDeclaredConstructor();
358      constructor.setAccessible(true);
359    }
360    catch (final Exception e)
361    {
362      debugException(e);
363      throw new LDAPPersistException(
364           ERR_OBJECT_HANDLER_NO_DEFAULT_CONSTRUCTOR.get(type.getName()), e);
365    }
366
367    Field tmpDNField = null;
368    Field tmpEntryField = null;
369    final LinkedList<FieldInfo> tmpRFilterFields = new LinkedList<FieldInfo>();
370    final LinkedList<FieldInfo> tmpAAFilterFields = new LinkedList<FieldInfo>();
371    final LinkedList<FieldInfo> tmpCAFilterFields = new LinkedList<FieldInfo>();
372    final LinkedList<FieldInfo> tmpRDNFields = new LinkedList<FieldInfo>();
373    for (final Field f : type.getDeclaredFields())
374    {
375      final LDAPField fieldAnnotation = f.getAnnotation(LDAPField.class);
376      final LDAPDNField dnFieldAnnotation = f.getAnnotation(LDAPDNField.class);
377      final LDAPEntryField entryFieldAnnotation =
378           f.getAnnotation(LDAPEntryField.class);
379
380      if (fieldAnnotation != null)
381      {
382        f.setAccessible(true);
383
384        final FieldInfo fieldInfo = new FieldInfo(f, type);
385        final String attrName = toLowerCase(fieldInfo.getAttributeName());
386        if (fields.containsKey(attrName))
387        {
388          throw new LDAPPersistException(ERR_OBJECT_HANDLER_ATTR_CONFLICT.get(
389               type.getName(), fieldInfo.getAttributeName()));
390        }
391        else
392        {
393          fields.put(attrName, fieldInfo);
394        }
395
396        switch (fieldInfo.getFilterUsage())
397        {
398          case REQUIRED:
399            tmpRFilterFields.add(fieldInfo);
400            break;
401          case ALWAYS_ALLOWED:
402            tmpAAFilterFields.add(fieldInfo);
403            break;
404          case CONDITIONALLY_ALLOWED:
405            tmpCAFilterFields.add(fieldInfo);
406            break;
407          case EXCLUDED:
408          default:
409            // No action required.
410            break;
411        }
412
413        if (fieldInfo.includeInRDN())
414        {
415          tmpRDNFields.add(fieldInfo);
416        }
417      }
418
419      if (dnFieldAnnotation != null)
420      {
421        f.setAccessible(true);
422
423        if (fieldAnnotation != null)
424        {
425          throw new LDAPPersistException(
426               ERR_OBJECT_HANDLER_CONFLICTING_FIELD_ANNOTATIONS.get(
427                    type.getName(), "LDAPField", "LDAPDNField", f.getName()));
428        }
429
430        if (tmpDNField != null)
431        {
432          throw new LDAPPersistException(
433               ERR_OBJECT_HANDLER_MULTIPLE_DN_FIELDS.get(type.getName()));
434        }
435
436        final int modifiers = f.getModifiers();
437        if (Modifier.isFinal(modifiers))
438        {
439          throw new LDAPPersistException(ERR_OBJECT_HANDLER_DN_FIELD_FINAL.get(
440               f.getName(), type.getName()));
441        }
442        else if (Modifier.isStatic(modifiers))
443        {
444          throw new LDAPPersistException(ERR_OBJECT_HANDLER_DN_FIELD_STATIC.get(
445               f.getName(), type.getName()));
446        }
447
448        final Class<?> fieldType = f.getType();
449        if (fieldType.equals(String.class))
450        {
451          tmpDNField = f;
452        }
453        else
454        {
455          throw new LDAPPersistException(
456               ERR_OBJECT_HANDLER_INVALID_DN_FIELD_TYPE.get(type.getName(),
457                    f.getName(), fieldType.getName()));
458        }
459      }
460
461      if (entryFieldAnnotation != null)
462      {
463        f.setAccessible(true);
464
465        if (fieldAnnotation != null)
466        {
467          throw new LDAPPersistException(
468               ERR_OBJECT_HANDLER_CONFLICTING_FIELD_ANNOTATIONS.get(
469                    type.getName(), "LDAPField", "LDAPEntryField",
470                    f.getName()));
471        }
472
473        if (tmpEntryField != null)
474        {
475          throw new LDAPPersistException(
476               ERR_OBJECT_HANDLER_MULTIPLE_ENTRY_FIELDS.get(type.getName()));
477        }
478
479        final int modifiers = f.getModifiers();
480        if (Modifier.isFinal(modifiers))
481        {
482          throw new LDAPPersistException(
483               ERR_OBJECT_HANDLER_ENTRY_FIELD_FINAL.get(f.getName(),
484                    type.getName()));
485        }
486        else if (Modifier.isStatic(modifiers))
487        {
488          throw new LDAPPersistException(
489               ERR_OBJECT_HANDLER_ENTRY_FIELD_STATIC.get(f.getName(),
490                    type.getName()));
491        }
492
493        final Class<?> fieldType = f.getType();
494        if (fieldType.equals(ReadOnlyEntry.class))
495        {
496          tmpEntryField = f;
497        }
498        else
499        {
500          throw new LDAPPersistException(
501               ERR_OBJECT_HANDLER_INVALID_ENTRY_FIELD_TYPE.get(type.getName(),
502                    f.getName(), fieldType.getName()));
503        }
504      }
505    }
506
507    dnField = tmpDNField;
508    entryField = tmpEntryField;
509    requiredFilterFields = Collections.unmodifiableList(tmpRFilterFields);
510    alwaysAllowedFilterFields = Collections.unmodifiableList(tmpAAFilterFields);
511    conditionallyAllowedFilterFields =
512         Collections.unmodifiableList(tmpCAFilterFields);
513    rdnFields    = Collections.unmodifiableList(tmpRDNFields);
514
515    final LinkedList<GetterInfo> tmpRFilterGetters =
516         new LinkedList<GetterInfo>();
517    final LinkedList<GetterInfo> tmpAAFilterGetters =
518         new LinkedList<GetterInfo>();
519    final LinkedList<GetterInfo> tmpCAFilterGetters =
520         new LinkedList<GetterInfo>();
521    final LinkedList<GetterInfo> tmpRDNGetters = new LinkedList<GetterInfo>();
522    for (final Method m : type.getDeclaredMethods())
523    {
524      final LDAPGetter getter = m.getAnnotation(LDAPGetter.class);
525      final LDAPSetter setter = m.getAnnotation(LDAPSetter.class);
526
527      if (getter != null)
528      {
529        m.setAccessible(true);
530
531        if (setter != null)
532        {
533          throw new LDAPPersistException(
534               ERR_OBJECT_HANDLER_CONFLICTING_METHOD_ANNOTATIONS.get(
535                    type.getName(), "LDAPGetter", "LDAPSetter",
536                    m.getName()));
537        }
538
539        final GetterInfo methodInfo = new GetterInfo(m, type);
540        final String attrName = toLowerCase(methodInfo.getAttributeName());
541        if (fields.containsKey(attrName) || getters.containsKey(attrName))
542        {
543          throw new LDAPPersistException(ERR_OBJECT_HANDLER_ATTR_CONFLICT.get(
544               type.getName(), methodInfo.getAttributeName()));
545        }
546        else
547        {
548          getters.put(attrName, methodInfo);
549        }
550
551        switch (methodInfo.getFilterUsage())
552        {
553          case REQUIRED:
554            tmpRFilterGetters.add(methodInfo);
555            break;
556          case ALWAYS_ALLOWED:
557            tmpAAFilterGetters.add(methodInfo);
558            break;
559          case CONDITIONALLY_ALLOWED:
560            tmpCAFilterGetters.add(methodInfo);
561            break;
562          case EXCLUDED:
563          default:
564            // No action required.
565            break;
566        }
567
568        if (methodInfo.includeInRDN())
569        {
570          tmpRDNGetters.add(methodInfo);
571        }
572      }
573
574      if (setter != null)
575      {
576        m.setAccessible(true);
577
578        final SetterInfo methodInfo = new SetterInfo(m, type);
579        final String attrName = toLowerCase(methodInfo.getAttributeName());
580        if (fields.containsKey(attrName) || setters.containsKey(attrName))
581        {
582          throw new LDAPPersistException(ERR_OBJECT_HANDLER_ATTR_CONFLICT.get(
583               type.getName(), methodInfo.getAttributeName()));
584        }
585        else
586        {
587          setters.put(attrName, methodInfo);
588        }
589      }
590    }
591
592    requiredFilterGetters = Collections.unmodifiableList(tmpRFilterGetters);
593    alwaysAllowedFilterGetters =
594         Collections.unmodifiableList(tmpAAFilterGetters);
595    conditionallyAllowedFilterGetters =
596         Collections.unmodifiableList(tmpCAFilterGetters);
597
598    rdnGetters = Collections.unmodifiableList(tmpRDNGetters);
599    if (rdnFields.isEmpty() && rdnGetters.isEmpty() &&
600        (superclassHandler == null))
601    {
602      throw new LDAPPersistException(ERR_OBJECT_HANDLER_NO_RDN_DEFINED.get(
603           type.getName()));
604    }
605
606    fieldMap  = Collections.unmodifiableMap(fields);
607    getterMap = Collections.unmodifiableMap(getters);
608    setterMap = Collections.unmodifiableMap(setters);
609
610
611    final TreeSet<String> attrSet = new TreeSet<String>();
612    final TreeSet<String> lazySet = new TreeSet<String>();
613    for (final FieldInfo i : fields.values())
614    {
615      if (i.lazilyLoad())
616      {
617        lazySet.add(i.getAttributeName());
618      }
619      else
620      {
621        attrSet.add(i.getAttributeName());
622      }
623    }
624
625    for (final SetterInfo i : setters.values())
626    {
627      attrSet.add(i.getAttributeName());
628    }
629
630    if (superclassHandler != null)
631    {
632      attrSet.addAll(Arrays.asList(
633           superclassHandler.explicitAttributesToRequest));
634      lazySet.addAll(Arrays.asList(superclassHandler.lazilyLoadedAttributes));
635    }
636
637    explicitAttributesToRequest = new String[attrSet.size()];
638    attrSet.toArray(explicitAttributesToRequest);
639
640    if (requestAllAttributes())
641    {
642      attributesToRequest = new String[] { "*", "+" };
643    }
644    else
645    {
646      attributesToRequest = explicitAttributesToRequest;
647    }
648
649    lazilyLoadedAttributes = new String[lazySet.size()];
650    lazySet.toArray(lazilyLoadedAttributes);
651  }
652
653
654
655  /**
656   * Retrieves the type of object handled by this class.
657   *
658   * @return  The type of object handled by this class.
659   */
660  public Class<T> getType()
661  {
662    return type;
663  }
664
665
666
667  /**
668   * Retrieves the {@code LDAPObjectHandler} object for the superclass of the
669   * associated type, if it is marked with the {@code LDAPObject annotation}.
670   *
671   * @return  The {@code LDAPObjectHandler} object for the superclass of the
672   *          associated type, or {@code null} if the superclass is not marked
673   *          with the {@code LDAPObject} annotation.
674   */
675  public LDAPObjectHandler<?> getSuperclassHandler()
676  {
677    return superclassHandler;
678  }
679
680
681
682  /**
683   * Retrieves the {@link LDAPObject} annotation for the associated class.
684   *
685   * @return  The {@code LDAPObject} annotation for the associated class.
686   */
687  public LDAPObject getLDAPObjectAnnotation()
688  {
689    return ldapObject;
690  }
691
692
693
694  /**
695   * Retrieves the constructor used to create a new instance of the appropriate
696   * type.
697   *
698   * @return  The constructor used to create a new instance of the appropriate
699   *          type.
700   */
701  public Constructor<T> getConstructor()
702  {
703    return constructor;
704  }
705
706
707
708  /**
709   * Retrieves the field that will be used to hold the DN of the associated
710   * entry, if defined.
711   *
712   * @return  The field that will be used to hold the DN of the associated
713   *          entry, or {@code null} if no DN field is defined in the associated
714   *          object type.
715   */
716  public Field getDNField()
717  {
718    return dnField;
719  }
720
721
722
723  /**
724   * Retrieves the field that will be used to hold a read-only copy of the entry
725   * used to create the object instance, if defined.
726   *
727   * @return  The field that will be used to hold a read-only copy of the entry
728   *          used to create the object instance, or {@code null} if no entry
729   *          field is defined in the associated object type.
730   */
731  public Field getEntryField()
732  {
733    return entryField;
734  }
735
736
737
738  /**
739   * Retrieves the default parent DN for objects of the associated type.
740   *
741   * @return  The default parent DN for objects of the associated type.
742   */
743  public DN getDefaultParentDN()
744  {
745    return defaultParentDN;
746  }
747
748
749
750  /**
751   * Retrieves the name of the structural object class for objects of the
752   * associated type.
753   *
754   * @return  The name of the structural object class for objects of the
755   *          associated type.
756   */
757  public String getStructuralClass()
758  {
759    return structuralClass;
760  }
761
762
763
764  /**
765   * Retrieves the names of the auxiliary object classes for objects of the
766   * associated type.
767   *
768   * @return  The names of the auxiliary object classes for objects of the
769   *          associated type.  It may be empty if no auxiliary classes are
770   *          defined.
771   */
772  public String[] getAuxiliaryClasses()
773  {
774    return auxiliaryClasses;
775  }
776
777
778
779  /**
780   * Retrieves the names of the superior object classes for objects of the
781   * associated type.
782   *
783   * @return  The names of the superior object classes for objects of the
784   *          associated type.  It may be empty if no superior classes are
785   *          defined.
786   */
787  public String[] getSuperiorClasses()
788  {
789    return superiorClasses;
790  }
791
792
793
794  /**
795   * Indicates whether to request all attributes.  This will return {@code true}
796   * if the associated {@code LDAPObject}, or any {@code LDAPObject} for any
797   * superclass, has {@code requestAllAttributes} set to {@code true}.
798   *
799   * @return  {@code true} if {@code LDAPObject} has
800   *          {@code requestAllAttributes} set to {@code true} for any class in
801   *          the hierarchy, or {@code false} if not.
802   */
803  public boolean requestAllAttributes()
804  {
805    return (ldapObject.requestAllAttributes() ||
806            ((superclassHandler != null) &&
807             superclassHandler.requestAllAttributes()));
808  }
809
810
811
812  /**
813   * Retrieves the names of the attributes that should be requested when
814   * performing a search.  It will not include lazily-loaded attributes.
815   *
816   * @return  The names of the attributes that should be requested when
817   *          performing a search.
818   */
819  public String[] getAttributesToRequest()
820  {
821    return attributesToRequest;
822  }
823
824
825
826  /**
827   * Retrieves the names of the attributes that should be lazily loaded for
828   * objects of this type.
829   *
830   * @return  The names of the attributes that should be lazily loaded for
831   *          objects of this type.  It may be empty if no attributes should be
832   *          lazily-loaded.
833   */
834  public String[] getLazilyLoadedAttributes()
835  {
836    return lazilyLoadedAttributes;
837  }
838
839
840
841  /**
842   * Retrieves the DN of the entry in which the provided object is stored, if
843   * available.  The entry DN will not be available if the provided object was
844   * not retrieved using the persistence framework, or if the associated class
845   * (or one of its superclasses) does not have a field marked with either the
846   * {@link LDAPDNField} or {@link LDAPEntryField} annotation.
847   *
848   * @param  o  The object for which to retrieve the associated entry DN.
849   *
850   * @return  The DN of the entry in which the provided object is stored, or
851   *          {@code null} if that is not available.
852   *
853   * @throws  LDAPPersistException  If a problem occurred while attempting to
854   *                                obtain the entry DN.
855   */
856  public String getEntryDN(final T o)
857         throws LDAPPersistException
858  {
859    final String dnFieldValue = getDNFieldValue(o);
860    if (dnFieldValue != null)
861    {
862      return dnFieldValue;
863    }
864
865    final ReadOnlyEntry entry = getEntry(o);
866    if (entry != null)
867    {
868      return entry.getDN();
869    }
870
871    return null;
872  }
873
874
875
876  /**
877   * Retrieves the value of the DN field for the provided object.  If there is
878   * no DN field in this object handler but there is one defined for a handler
879   * for one of its superclasses, then it will be obtained recursively.
880   *
881   * @param  o  The object for which to retrieve the associated entry DN.
882   *
883   * @return  The value of the DN field for the provided object.
884   *
885   * @throws  LDAPPersistException  If a problem is encountered while attempting
886   *                                to access the value of the DN field.
887   */
888  private String getDNFieldValue(final T o)
889          throws LDAPPersistException
890  {
891    if (dnField != null)
892    {
893      try
894      {
895        final Object dnObject = dnField.get(o);
896        if (dnObject == null)
897        {
898          return null;
899        }
900        else
901        {
902          return String.valueOf(dnObject);
903        }
904      }
905      catch (final Exception e)
906      {
907        debugException(e);
908        throw new LDAPPersistException(
909             ERR_OBJECT_HANDLER_ERROR_ACCESSING_DN_FIELD.get(dnField.getName(),
910                  type.getName(), getExceptionMessage(e)), e);
911      }
912    }
913
914    if (superclassHandler != null)
915    {
916      return superclassHandler.getDNFieldValue(o);
917    }
918
919    return null;
920  }
921
922
923
924  /**
925   * Retrieves a read-only copy of the entry that was used to initialize the
926   * provided object, if available.  The entry will only be available if the
927   * object was retrieved from the directory using the persistence framework and
928   * the associated class (or one of its superclasses) has a field marked with
929   * the {@link LDAPEntryField} annotation.
930   *
931   * @param  o  The object for which to retrieve the read-only entry.
932   *
933   * @return  A read-only copy of the entry that was used to initialize the
934   *          provided object, or {@code null} if that is not available.
935   *
936   * @throws  LDAPPersistException  If a problem occurred while attempting to
937   *                                obtain the entry DN.
938   */
939  public ReadOnlyEntry getEntry(final T o)
940         throws LDAPPersistException
941  {
942    if (entryField != null)
943    {
944      try
945      {
946        final Object entryObject = entryField.get(o);
947        if (entryObject == null)
948        {
949          return null;
950        }
951        else
952        {
953          return (ReadOnlyEntry) entryObject;
954        }
955      }
956      catch (final Exception e)
957      {
958        debugException(e);
959        throw new LDAPPersistException(
960             ERR_OBJECT_HANDLER_ERROR_ACCESSING_ENTRY_FIELD.get(
961                  entryField.getName(), type.getName(), getExceptionMessage(e)),
962             e);
963      }
964    }
965
966    if (superclassHandler != null)
967    {
968      return superclassHandler.getEntry(o);
969    }
970
971    return null;
972  }
973
974
975
976  /**
977   * Retrieves a map of all fields in the class that should be persisted as LDAP
978   * attributes.  The keys in the map will be the lowercase names of the LDAP
979   * attributes used to persist the information, and the values will be
980   * information about the fields associated with those attributes.
981   *
982   * @return  A map of all fields in the class that should be persisted as LDAP
983   *          attributes.
984   */
985  public Map<String,FieldInfo> getFields()
986  {
987    return fieldMap;
988  }
989
990
991
992  /**
993   * Retrieves a map of all getter methods in the class whose values should be
994   * persisted as LDAP attributes.  The keys in the map will be the lowercase
995   * names of the LDAP attributes used to persist the information, and the
996   * values will be information about the getter methods associated with those
997   * attributes.
998   *
999   * @return  A map of all getter methods in the class whose values should be
1000   *          persisted as LDAP attributes.
1001   */
1002  public Map<String,GetterInfo> getGetters()
1003  {
1004    return getterMap;
1005  }
1006
1007
1008
1009  /**
1010   * Retrieves a map of all setter methods in the class that should be invoked
1011   * with information read from LDAP attributes.  The keys in the map will be
1012   * the lowercase names of the LDAP attributes with the information used to
1013   * invoke the setter, and the values will be information about the setter
1014   * methods associated with those attributes.
1015   *
1016   * @return  A map of all setter methods in the class that should be invoked
1017   *          with information read from LDAP attributes.
1018   */
1019  public Map<String,SetterInfo> getSetters()
1020  {
1021    return setterMap;
1022  }
1023
1024
1025
1026  /**
1027   * Constructs a list of LDAP object class definitions which may be added to
1028   * the directory server schema to allow it to hold objects of this type.  Note
1029   * that the object identifiers used for the constructed object class
1030   * definitions are not required to be valid or unique.
1031   *
1032   * @param  a  The OID allocator to use to generate the object identifiers for
1033   *            the constructed attribute types.  It must not be {@code null}.
1034   *
1035   * @return  A list of object class definitions that may be used to represent
1036   *          objects of the associated type in an LDAP directory.
1037   *
1038   * @throws  LDAPPersistException  If a problem occurs while attempting to
1039   *                                generate the list of object class
1040   *                                definitions.
1041   */
1042  List<ObjectClassDefinition> constructObjectClasses(final OIDAllocator a)
1043         throws LDAPPersistException
1044  {
1045    final LinkedHashMap<String,ObjectClassDefinition> ocMap =
1046         new LinkedHashMap<String,ObjectClassDefinition>(
1047              1 + auxiliaryClasses.length);
1048
1049    if (superclassHandler != null)
1050    {
1051      for (final ObjectClassDefinition d :
1052           superclassHandler.constructObjectClasses(a))
1053      {
1054        ocMap.put(toLowerCase(d.getNameOrOID()), d);
1055      }
1056    }
1057
1058    final String lowerStructuralClass = toLowerCase(structuralClass);
1059    if (! ocMap.containsKey(lowerStructuralClass))
1060    {
1061      if (superclassHandler == null)
1062      {
1063        ocMap.put(lowerStructuralClass, constructObjectClass(structuralClass,
1064             "top", ObjectClassType.STRUCTURAL, a));
1065      }
1066      else
1067      {
1068        ocMap.put(lowerStructuralClass, constructObjectClass(structuralClass,
1069             superclassHandler.getStructuralClass(), ObjectClassType.STRUCTURAL,
1070             a));
1071      }
1072    }
1073
1074    for (final String s : auxiliaryClasses)
1075    {
1076      final String lowerName = toLowerCase(s);
1077      if (! ocMap.containsKey(lowerName))
1078      {
1079        ocMap.put(lowerName,
1080             constructObjectClass(s, "top", ObjectClassType.AUXILIARY, a));
1081      }
1082    }
1083
1084    return Collections.unmodifiableList(new ArrayList<ObjectClassDefinition>(
1085         ocMap.values()));
1086  }
1087
1088
1089
1090  /**
1091   * Constructs an LDAP object class definition for the object class with the
1092   * specified name.
1093   *
1094   * @param  name  The name of the object class to create.  It must not be
1095   *               {@code null}.
1096   * @param  sup   The name of the superior object class.  It must not be
1097   *               {@code null}.
1098   * @param  type  The type of object class to create.  It must not be
1099   *               {@code null}.
1100   * @param  a     The OID allocator to use to generate the object identifiers
1101   *               for the constructed attribute types.  It must not be
1102   *               {@code null}.
1103   *
1104   * @return  The constructed object class definition.
1105   */
1106  ObjectClassDefinition constructObjectClass(final String name,
1107                                             final String sup,
1108                                             final ObjectClassType type,
1109                                             final OIDAllocator a)
1110  {
1111    final TreeMap<String,String> requiredAttrs = new TreeMap<String,String>();
1112    final TreeMap<String,String> optionalAttrs = new TreeMap<String,String>();
1113
1114
1115    // Extract the attributes for all of the fields.
1116    for (final FieldInfo i : fieldMap.values())
1117    {
1118      boolean found = false;
1119      for (final String s : i.getObjectClasses())
1120      {
1121        if (name.equalsIgnoreCase(s))
1122        {
1123          found = true;
1124          break;
1125        }
1126      }
1127
1128      if (! found)
1129      {
1130        continue;
1131      }
1132
1133      final String attrName  = i.getAttributeName();
1134      final String lowerName = toLowerCase(attrName);
1135      if (i.includeInRDN() ||
1136          (i.isRequiredForDecode() && i.isRequiredForEncode()))
1137      {
1138        requiredAttrs.put(lowerName, attrName);
1139      }
1140      else
1141      {
1142        optionalAttrs.put(lowerName, attrName);
1143      }
1144    }
1145
1146
1147    // Extract the attributes for all of the getter methods.
1148    for (final GetterInfo i : getterMap.values())
1149    {
1150      boolean found = false;
1151      for (final String s : i.getObjectClasses())
1152      {
1153        if (name.equalsIgnoreCase(s))
1154        {
1155          found = true;
1156          break;
1157        }
1158      }
1159
1160      if (! found)
1161      {
1162        continue;
1163      }
1164
1165      final String attrName  = i.getAttributeName();
1166      final String lowerName = toLowerCase(attrName);
1167      if (i.includeInRDN())
1168      {
1169        requiredAttrs.put(lowerName, attrName);
1170      }
1171      else
1172      {
1173        optionalAttrs.put(lowerName, attrName);
1174      }
1175    }
1176
1177
1178    // Extract the attributes for all of the setter methods.  We'll assume that
1179    // they are all part of the structural object class and all optional.
1180    if (name.equalsIgnoreCase(structuralClass))
1181    {
1182      for (final SetterInfo i : setterMap.values())
1183      {
1184        final String attrName  = i.getAttributeName();
1185        final String lowerName = toLowerCase(attrName);
1186        if (requiredAttrs.containsKey(lowerName) ||
1187             optionalAttrs.containsKey(lowerName))
1188        {
1189          continue;
1190        }
1191
1192        optionalAttrs.put(lowerName, attrName);
1193      }
1194    }
1195
1196    final String[] reqArray = new String[requiredAttrs.size()];
1197    requiredAttrs.values().toArray(reqArray);
1198
1199    final String[] optArray = new String[optionalAttrs.size()];
1200    optionalAttrs.values().toArray(optArray);
1201
1202    return new ObjectClassDefinition(a.allocateObjectClassOID(name),
1203         new String[] { name }, null, false, new String[] { sup }, type,
1204         reqArray, optArray, null);
1205  }
1206
1207
1208
1209  /**
1210   * Creates a new object based on the contents of the provided entry.
1211   *
1212   * @param  e  The entry to use to create and initialize the object.
1213   *
1214   * @return  The object created from the provided entry.
1215   *
1216   * @throws  LDAPPersistException  If an error occurs while creating or
1217   *                                initializing the object from the information
1218   *                                in the provided entry.
1219   */
1220  T decode(final Entry e)
1221    throws LDAPPersistException
1222  {
1223    final T o;
1224    try
1225    {
1226      o = constructor.newInstance();
1227    }
1228    catch (final Throwable t)
1229    {
1230      debugException(t);
1231
1232      if (t instanceof InvocationTargetException)
1233      {
1234        final Throwable targetException =
1235             ((InvocationTargetException) t).getTargetException();
1236        throw new LDAPPersistException(
1237             ERR_OBJECT_HANDLER_ERROR_INVOKING_CONSTRUCTOR.get(type.getName(),
1238                  getExceptionMessage(targetException)), targetException);
1239      }
1240      else
1241      {
1242        throw new LDAPPersistException(
1243             ERR_OBJECT_HANDLER_ERROR_INVOKING_CONSTRUCTOR.get(type.getName(),
1244                  getExceptionMessage(t)), t);
1245      }
1246    }
1247
1248    decode(o, e);
1249    return o;
1250  }
1251
1252
1253
1254  /**
1255   * Initializes the provided object from the contents of the provided entry.
1256   *
1257   * @param  o  The object to be initialized with the contents of the provided
1258   *            entry.
1259   * @param  e  The entry to use to initialize the object.
1260   *
1261   * @throws  LDAPPersistException  If an error occurs while initializing the
1262   *                                object from the information in the provided
1263   *                                entry.
1264   */
1265  void decode(final T o, final Entry e)
1266       throws LDAPPersistException
1267  {
1268    if (superclassHandler != null)
1269    {
1270      superclassHandler.decode(o, e);
1271    }
1272
1273    setDNAndEntryFields(o, e);
1274
1275    final ArrayList<String> failureReasons = new ArrayList<String>(5);
1276    boolean successful = true;
1277
1278    for (final FieldInfo i : fieldMap.values())
1279    {
1280      successful &= i.decode(o, e, failureReasons);
1281    }
1282
1283    for (final SetterInfo i : setterMap.values())
1284    {
1285      successful &= i.invokeSetter(o, e, failureReasons);
1286    }
1287
1288    Throwable cause = null;
1289    if (postDecodeMethod != null)
1290    {
1291      try
1292      {
1293        postDecodeMethod.invoke(o);
1294      }
1295      catch (final Throwable t)
1296      {
1297        debugException(t);
1298
1299        if (t instanceof InvocationTargetException)
1300        {
1301          cause = ((InvocationTargetException) t).getTargetException();
1302        }
1303        else
1304        {
1305          cause = t;
1306        }
1307
1308        successful = false;
1309        failureReasons.add(
1310             ERR_OBJECT_HANDLER_ERROR_INVOKING_POST_DECODE_METHOD.get(
1311                  postDecodeMethod.getName(), type.getName(),
1312                  getExceptionMessage(t)));
1313      }
1314    }
1315
1316    if (! successful)
1317    {
1318      throw new LDAPPersistException(concatenateStrings(failureReasons), o,
1319           cause);
1320    }
1321  }
1322
1323
1324
1325  /**
1326   * Encodes the provided object to an entry suitable for use in an add
1327   * operation.
1328   *
1329   * @param  o         The object to be encoded.
1330   * @param  parentDN  The parent DN to use by default for the entry that is
1331   *                   generated.  If the provided object was previously read
1332   *                   from a directory server and includes a DN field or an
1333   *                   entry field with the original DN used for the object,
1334   *                   then that original DN will be used even if it is not
1335   *                   an immediate subordinate of the provided parent.  This
1336   *                   may be {@code null} if the entry to create should not
1337   *                   have a parent but instead should have a DN consisting of
1338   *                   only a single RDN component.
1339   *
1340   * @return  The entry containing an encoded representation of the provided
1341   *          object.
1342   *
1343   * @throws  LDAPPersistException  If a problem occurs while encoding the
1344   *                                provided object.
1345   */
1346  Entry encode(final T o, final String parentDN)
1347        throws LDAPPersistException
1348  {
1349    // Get the attributes that should be included in the entry.
1350    final LinkedHashMap<String,Attribute> attrMap =
1351         new LinkedHashMap<String,Attribute>();
1352    attrMap.put("objectClass", objectClassAttribute);
1353
1354    for (final Map.Entry<String,FieldInfo> e : fieldMap.entrySet())
1355    {
1356      final FieldInfo i = e.getValue();
1357      if (! i.includeInAdd())
1358      {
1359        continue;
1360      }
1361
1362      final Attribute a = i.encode(o, false);
1363      if (a != null)
1364      {
1365        attrMap.put(e.getKey(), a);
1366      }
1367    }
1368
1369    for (final Map.Entry<String,GetterInfo> e : getterMap.entrySet())
1370    {
1371      final GetterInfo i = e.getValue();
1372      if (! i.includeInAdd())
1373      {
1374        continue;
1375      }
1376
1377      final Attribute a = i.encode(o);
1378      if (a != null)
1379      {
1380        attrMap.put(e.getKey(), a);
1381      }
1382    }
1383
1384
1385    // Get the DN to use for the entry.
1386    final String dn = constructDN(o, parentDN, attrMap);
1387    final Entry entry = new Entry(dn, attrMap.values());
1388
1389    if (postEncodeMethod != null)
1390    {
1391      try
1392      {
1393        postEncodeMethod.invoke(o, entry);
1394      }
1395      catch (final Throwable t)
1396      {
1397        debugException(t);
1398
1399        if (t instanceof InvocationTargetException)
1400        {
1401          final Throwable targetException =
1402               ((InvocationTargetException) t).getTargetException();
1403          throw new LDAPPersistException(
1404               ERR_OBJECT_HANDLER_ERROR_INVOKING_POST_ENCODE_METHOD.get(
1405                    postEncodeMethod.getName(), type.getName(),
1406                    getExceptionMessage(targetException)), targetException);
1407        }
1408        else
1409        {
1410          throw new LDAPPersistException(
1411               ERR_OBJECT_HANDLER_ERROR_INVOKING_POST_ENCODE_METHOD.get(
1412                    postEncodeMethod.getName(), type.getName(),
1413                    getExceptionMessage(t)), t);
1414        }
1415      }
1416    }
1417
1418    setDNAndEntryFields(o, entry);
1419
1420    if (superclassHandler != null)
1421    {
1422      final Entry e = superclassHandler.encode(o, parentDN);
1423      for (final Attribute a : e.getAttributes())
1424      {
1425        entry.addAttribute(a);
1426      }
1427    }
1428
1429    return entry;
1430  }
1431
1432
1433
1434  /**
1435   * Sets the DN and entry fields for the provided object, if appropriate.
1436   *
1437   * @param  o  The object to be updated.
1438   * @param  e  The entry with which the object is associated.
1439   *
1440   * @throws  LDAPPersistException  If a problem occurs while setting the value
1441   *                                of the DN or entry field.
1442   */
1443  private void setDNAndEntryFields(final T o, final Entry e)
1444          throws LDAPPersistException
1445  {
1446    if (dnField != null)
1447    {
1448      try
1449      {
1450        if (dnField.get(o) == null)
1451        {
1452          dnField.set(o, e.getDN());
1453        }
1454      }
1455      catch (final Exception ex)
1456      {
1457        debugException(ex);
1458        throw new LDAPPersistException(ERR_OBJECT_HANDLER_ERROR_SETTING_DN.get(
1459             type.getName(), e.getDN(), dnField.getName(),
1460             getExceptionMessage(ex)), ex);
1461      }
1462    }
1463
1464    if (entryField != null)
1465    {
1466      try
1467      {
1468        if (entryField.get(o) == null)
1469        {
1470          entryField.set(o, new ReadOnlyEntry(e));
1471        }
1472      }
1473      catch (final Exception ex)
1474      {
1475        debugException(ex);
1476        throw new LDAPPersistException(
1477             ERR_OBJECT_HANDLER_ERROR_SETTING_ENTRY.get(type.getName(),
1478                  entryField.getName(), getExceptionMessage(ex)), ex);
1479      }
1480    }
1481
1482    if (superclassHandler != null)
1483    {
1484      superclassHandler.setDNAndEntryFields(o, e);
1485    }
1486  }
1487
1488
1489
1490  /**
1491   * Determines the DN that should be used for the entry associated with the
1492   * given object.  If the provided object was retrieved from the directory
1493   * using the persistence framework and has a field with either the
1494   * {@link LDAPDNField} or {@link LDAPEntryField} annotation, then the actual
1495   * DN of the corresponding entry will be returned.  Otherwise, it will be
1496   * constructed using the fields and getter methods marked for inclusion in
1497   * the entry RDN.
1498   *
1499   * @param  o         The object for which to determine the appropriate DN.
1500   * @param  parentDN  The parent DN to use for the constructed DN.  If a
1501   *                   non-{@code null} value is provided, then that value will
1502   *                   be used as the parent DN (and the empty string will
1503   *                   indicate that the generated DN should not have a parent).
1504   *                   If the value is {@code null}, then the default parent DN
1505   *                   as defined in the {@link LDAPObject} annotation will be
1506   *                   used.  If the provided parent DN is {@code null} and the
1507   *                   {@code LDAPObject} annotation does not specify a default
1508   *                   parent DN, then the generated DN will not have a parent.
1509   *
1510   * @return  The entry DN for the provided object.
1511   *
1512   * @throws  LDAPPersistException  If a problem occurs while obtaining the
1513   *                                entry DN, or if the provided parent DN
1514   *                                represents an invalid DN.
1515   */
1516  public String constructDN(final T o, final String parentDN)
1517         throws LDAPPersistException
1518  {
1519    final String existingDN = getEntryDN(o);
1520    if (existingDN != null)
1521    {
1522      return existingDN;
1523    }
1524
1525    final int numRDNs = rdnFields.size() + rdnGetters.size();
1526    if (numRDNs == 0)
1527    {
1528      return superclassHandler.constructDN(o, parentDN);
1529    }
1530
1531    final LinkedHashMap<String,Attribute> attrMap =
1532         new LinkedHashMap<String,Attribute>(numRDNs);
1533
1534    for (final FieldInfo i : rdnFields)
1535    {
1536      final Attribute a = i.encode(o, true);
1537      if (a == null)
1538      {
1539        throw new LDAPPersistException(
1540             ERR_OBJECT_HANDLER_RDN_FIELD_MISSING_VALUE.get(type.getName(),
1541                  i.getField().getName()));
1542      }
1543
1544      attrMap.put(toLowerCase(i.getAttributeName()), a);
1545    }
1546
1547    for (final GetterInfo i : rdnGetters)
1548    {
1549      final Attribute a = i.encode(o);
1550      if (a == null)
1551      {
1552        throw new LDAPPersistException(
1553             ERR_OBJECT_HANDLER_RDN_GETTER_MISSING_VALUE.get(type.getName(),
1554                  i.getMethod().getName()));
1555      }
1556
1557      attrMap.put(toLowerCase(i.getAttributeName()), a);
1558    }
1559
1560    return constructDN(o, parentDN, attrMap);
1561  }
1562
1563
1564
1565  /**
1566   * Determines the DN that should be used for the entry associated with the
1567   * given object.  If the provided object was retrieved from the directory
1568   * using the persistence framework and has a field with either the
1569   * {@link LDAPDNField} or {@link LDAPEntryField} annotation, then the actual
1570   * DN of the corresponding entry will be returned.  Otherwise, it will be
1571   * constructed using the fields and getter methods marked for inclusion in
1572   * the entry RDN.
1573   *
1574   * @param  o         The object for which to determine the appropriate DN.
1575   * @param  parentDN  The parent DN to use for the constructed DN.  If a
1576   *                   non-{@code null} value is provided, then that value will
1577   *                   be used as the parent DN (and the empty string will
1578   *                   indicate that the generated DN should not have a parent).
1579   *                   If the value is {@code null}, then the default parent DN
1580   *                   as defined in the {@link LDAPObject} annotation will be
1581   *                   used.  If the provided parent DN is {@code null} and the
1582   *                   {@code LDAPObject} annotation does not specify a default
1583   *                   parent DN, then the generated DN will not have a parent.
1584   * @param  attrMap   A map of the attributes that will be included in the
1585   *                   entry and may be used to construct the RDN elements.
1586   *
1587   * @return  The entry DN for the provided object.
1588   *
1589   * @throws  LDAPPersistException  If a problem occurs while obtaining the
1590   *                                entry DN, or if the provided parent DN
1591   *                                represents an invalid DN.
1592   */
1593  String constructDN(final T o, final String parentDN,
1594                     final Map<String,Attribute> attrMap)
1595         throws LDAPPersistException
1596  {
1597    final String existingDN = getEntryDN(o);
1598    if (existingDN != null)
1599    {
1600      return existingDN;
1601    }
1602
1603    final int numRDNs = rdnFields.size() + rdnGetters.size();
1604    if (numRDNs == 0)
1605    {
1606      return superclassHandler.constructDN(o, parentDN);
1607    }
1608
1609    final ArrayList<String> rdnNameList  = new ArrayList<String>(numRDNs);
1610    final ArrayList<byte[]> rdnValueList = new ArrayList<byte[]>(numRDNs);
1611    for (final FieldInfo i : rdnFields)
1612    {
1613      final Attribute a = attrMap.get(toLowerCase(i.getAttributeName()));
1614      if (a == null)
1615      {
1616        throw new LDAPPersistException(
1617             ERR_OBJECT_HANDLER_RDN_FIELD_MISSING_VALUE.get(type.getName(),
1618                  i.getField().getName()));
1619      }
1620
1621      rdnNameList.add(a.getName());
1622      rdnValueList.add(a.getValueByteArray());
1623    }
1624
1625    for (final GetterInfo i : rdnGetters)
1626    {
1627      final Attribute a = attrMap.get(toLowerCase(i.getAttributeName()));
1628      if (a == null)
1629      {
1630        throw new LDAPPersistException(
1631             ERR_OBJECT_HANDLER_RDN_GETTER_MISSING_VALUE.get(type.getName(),
1632                  i.getMethod().getName()));
1633      }
1634
1635      rdnNameList.add(a.getName());
1636      rdnValueList.add(a.getValueByteArray());
1637    }
1638
1639    final String[] rdnNames = new String[rdnNameList.size()];
1640    rdnNameList.toArray(rdnNames);
1641
1642    final byte[][] rdnValues = new byte[rdnNames.length][];
1643    rdnValueList.toArray(rdnValues);
1644
1645    final RDN rdn = new RDN(rdnNames, rdnValues);
1646
1647    if (parentDN == null)
1648    {
1649      return new DN(rdn, defaultParentDN).toString();
1650    }
1651    else
1652    {
1653      try
1654      {
1655        final DN parsedParentDN = new DN(parentDN);
1656        return new DN(rdn, parsedParentDN).toString();
1657      }
1658      catch (final LDAPException le)
1659      {
1660        debugException(le);
1661        throw new LDAPPersistException(ERR_OBJECT_HANDLER_INVALID_PARENT_DN.get(
1662             type.getName(), parentDN, le.getMessage()), le);
1663      }
1664    }
1665  }
1666
1667
1668
1669  /**
1670   * Creates a list of modifications that can be used to update the stored
1671   * representation of the provided object in the directory.  If the provided
1672   * object was retrieved from the directory using the persistence framework and
1673   * includes a field with the {@link LDAPEntryField} annotation, then that
1674   * entry will be used to make the returned set of modifications as efficient
1675   * as possible.  Otherwise, the resulting modifications will include attempts
1676   * to replace every attribute which are associated with fields or getters
1677   * that should be used in modify operations.
1678   *
1679   * @param  o                 The object to be encoded.
1680   * @param  deleteNullValues  Indicates whether to include modifications that
1681   *                           may completely remove an attribute from the
1682   *                           entry if the corresponding field or getter method
1683   *                           has a value of {@code null}.
1684   * @param  byteForByte       Indicates whether to use a byte-for-byte
1685   *                           comparison to identify which attribute values
1686   *                           have changed.  Using byte-for-byte comparison
1687   *                           requires additional processing over using each
1688   *                           attribute's associated matching rule, but it can
1689   *                           detect changes that would otherwise be considered
1690   *                           logically equivalent (e.g., changing the
1691   *                           capitalization of a value that uses a
1692   *                           case-insensitive matching rule).
1693   * @param  attributes        The set of LDAP attributes for which to include
1694   *                           modifications.  If this is empty or {@code null},
1695   *                           then all attributes marked for inclusion in the
1696   *                           modification will be examined.
1697   *
1698   * @return  A list of modifications that can be used to update the stored
1699   *          representation of the provided object in the directory.  It may
1700   *          be empty if there are no differences identified in the attributes
1701   *          to be evaluated.
1702   *
1703   * @throws  LDAPPersistException  If a problem occurs while computing the set
1704   *                                of modifications.
1705   */
1706  List<Modification> getModifications(final T o, final boolean deleteNullValues,
1707                                      final boolean byteForByte,
1708                                      final String... attributes)
1709         throws LDAPPersistException
1710  {
1711    final ReadOnlyEntry originalEntry;
1712    if (entryField != null)
1713    {
1714      originalEntry = getEntry(o);
1715    }
1716    else
1717    {
1718      originalEntry = null;
1719    }
1720
1721    // If we have an original copy of the entry, then we can try encoding the
1722    // updated object to a new entry and diff the two entries.
1723    if (originalEntry != null)
1724    {
1725      try
1726      {
1727        final T decodedOrig = decode(originalEntry);
1728        final Entry reEncodedOriginal =
1729             encode(decodedOrig, originalEntry.getParentDNString());
1730
1731        final Entry newEntry = encode(o, originalEntry.getParentDNString());
1732        final List<Modification> mods = Entry.diff(reEncodedOriginal, newEntry,
1733             true, false, byteForByte, attributes);
1734        if (! deleteNullValues)
1735        {
1736          final Iterator<Modification> iterator = mods.iterator();
1737          while (iterator.hasNext())
1738          {
1739            final Modification m = iterator.next();
1740            if (m.getRawValues().length == 0)
1741            {
1742              iterator.remove();
1743            }
1744          }
1745        }
1746
1747        // If there are any attributes that should be excluded from
1748        // modifications, then strip them out.
1749        HashSet<String> stripAttrs = null;
1750        for (final FieldInfo i : fieldMap.values())
1751        {
1752          if (! i.includeInModify())
1753          {
1754            if (stripAttrs == null)
1755            {
1756              stripAttrs = new HashSet<String>(10);
1757            }
1758            stripAttrs.add(toLowerCase(i.getAttributeName()));
1759          }
1760        }
1761
1762        for (final GetterInfo i : getterMap.values())
1763        {
1764          if (! i.includeInModify())
1765          {
1766            if (stripAttrs == null)
1767            {
1768              stripAttrs = new HashSet<String>(10);
1769            }
1770            stripAttrs.add(toLowerCase(i.getAttributeName()));
1771          }
1772        }
1773
1774        if (stripAttrs != null)
1775        {
1776          final Iterator<Modification> iterator = mods.iterator();
1777          while (iterator.hasNext())
1778          {
1779            final Modification m = iterator.next();
1780            if (stripAttrs.contains(toLowerCase(m.getAttributeName())))
1781            {
1782              iterator.remove();
1783            }
1784          }
1785        }
1786
1787        return mods;
1788      }
1789      catch (final Exception e)
1790      {
1791        debugException(e);
1792      }
1793      finally
1794      {
1795        setDNAndEntryFields(o, originalEntry);
1796      }
1797    }
1798
1799    final HashSet<String> attrSet;
1800    if ((attributes == null) || (attributes.length == 0))
1801    {
1802      attrSet = null;
1803    }
1804    else
1805    {
1806      attrSet = new HashSet<String>(attributes.length);
1807      for (final String s : attributes)
1808      {
1809        attrSet.add(toLowerCase(s));
1810      }
1811    }
1812
1813    final ArrayList<Modification> mods = new ArrayList<Modification>(5);
1814
1815    for (final Map.Entry<String,FieldInfo> e : fieldMap.entrySet())
1816    {
1817      final String attrName = toLowerCase(e.getKey());
1818      if ((attrSet != null) && (! attrSet.contains(attrName)))
1819      {
1820        continue;
1821      }
1822
1823      final FieldInfo i = e.getValue();
1824      if (! i.includeInModify())
1825      {
1826        continue;
1827      }
1828
1829      final Attribute a = i.encode(o, false);
1830      if (a == null)
1831      {
1832        if (! deleteNullValues)
1833        {
1834          continue;
1835        }
1836
1837        if ((originalEntry != null) && (! originalEntry.hasAttribute(attrName)))
1838        {
1839          continue;
1840        }
1841
1842        mods.add(new Modification(ModificationType.REPLACE,
1843             i.getAttributeName()));
1844        continue;
1845      }
1846
1847      if (originalEntry != null)
1848      {
1849        final Attribute originalAttr = originalEntry.getAttribute(attrName);
1850        if ((originalAttr != null) && originalAttr.equals(a))
1851        {
1852        continue;
1853        }
1854      }
1855
1856      mods.add(new Modification(ModificationType.REPLACE, i.getAttributeName(),
1857           a.getRawValues()));
1858    }
1859
1860    for (final Map.Entry<String,GetterInfo> e : getterMap.entrySet())
1861    {
1862      final String attrName = toLowerCase(e.getKey());
1863      if ((attrSet != null) && (! attrSet.contains(attrName)))
1864      {
1865        continue;
1866      }
1867
1868      final GetterInfo i = e.getValue();
1869      if (! i.includeInModify())
1870      {
1871        continue;
1872      }
1873
1874      final Attribute a = i.encode(o);
1875      if (a == null)
1876      {
1877        if (! deleteNullValues)
1878        {
1879          continue;
1880        }
1881
1882        if ((originalEntry != null) && (! originalEntry.hasAttribute(attrName)))
1883        {
1884          continue;
1885        }
1886
1887        mods.add(new Modification(ModificationType.REPLACE,
1888             i.getAttributeName()));
1889        continue;
1890      }
1891
1892      if (originalEntry != null)
1893      {
1894        final Attribute originalAttr = originalEntry.getAttribute(attrName);
1895        if ((originalAttr != null) && originalAttr.equals(a))
1896        {
1897        continue;
1898        }
1899      }
1900
1901      mods.add(new Modification(ModificationType.REPLACE, i.getAttributeName(),
1902           a.getRawValues()));
1903    }
1904
1905    if (superclassHandler != null)
1906    {
1907      final List<Modification> superMods =
1908           superclassHandler.getModifications(o, deleteNullValues, byteForByte,
1909                attributes);
1910      final ArrayList<Modification> modsToAdd =
1911           new ArrayList<Modification>(superMods.size());
1912      for (final Modification sm : superMods)
1913      {
1914        boolean add = true;
1915        for (final Modification m : mods)
1916        {
1917          if (m.getAttributeName().equalsIgnoreCase(sm.getAttributeName()))
1918          {
1919            add = false;
1920            break;
1921          }
1922        }
1923        if (add)
1924        {
1925          modsToAdd.add(sm);
1926        }
1927      }
1928      mods.addAll(modsToAdd);
1929    }
1930
1931    return Collections.unmodifiableList(mods);
1932  }
1933
1934
1935
1936  /**
1937   * Retrieves a filter that will match any entry containing the structural and
1938   * auxiliary classes for this object type.
1939   *
1940   * @return  A filter that will match any entry containing the structural and
1941   *          auxiliary classes for this object type.
1942   */
1943  public Filter createBaseFilter()
1944  {
1945    if (auxiliaryClasses.length == 0)
1946    {
1947      return Filter.createEqualityFilter("objectClass", structuralClass);
1948    }
1949    else
1950    {
1951      final ArrayList<Filter> comps =
1952           new ArrayList<Filter>(1+auxiliaryClasses.length);
1953      comps.add(Filter.createEqualityFilter("objectClass", structuralClass));
1954      for (final String s : auxiliaryClasses)
1955      {
1956        comps.add(Filter.createEqualityFilter("objectClass", s));
1957      }
1958      return Filter.createANDFilter(comps);
1959    }
1960  }
1961
1962
1963
1964  /**
1965   * Retrieves a filter that can be used to search for entries matching the
1966   * provided object.  It will be constructed as an AND search using all fields
1967   * with a non-{@code null} value and that have a {@link LDAPField} annotation
1968   * with the {@code inFilter} element set to {@code true}, and all  getter
1969   * methods that return a non-{@code null} value and have a
1970   * {@link LDAPGetter} annotation with the {@code inFilter} element set to
1971   * {@code true}.
1972   *
1973   * @param  o  The object for which to create the search filter.
1974   *
1975   * @return  A filter that can be used to search for entries matching the
1976   *          provided object.
1977   *
1978   * @throws  LDAPPersistException  If it is not possible to construct a search
1979   *                                filter for some reason (e.g., because the
1980   *                                provided object does not have any
1981   *                                non-{@code null} fields or getters that are
1982   *                                marked for inclusion in filters).
1983   */
1984  public Filter createFilter(final T o)
1985         throws LDAPPersistException
1986  {
1987    final AtomicBoolean addedRequiredOrAllowed = new AtomicBoolean(false);
1988
1989    final Filter f = createFilter(o, addedRequiredOrAllowed);
1990    if (! addedRequiredOrAllowed.get())
1991    {
1992      throw new LDAPPersistException(
1993           ERR_OBJECT_HANDLER_FILTER_MISSING_REQUIRED_OR_ALLOWED.get());
1994    }
1995
1996    return f;
1997  }
1998
1999
2000
2001  /**
2002   * Retrieves a filter that can be used to search for entries matching the
2003   * provided object.  It will be constructed as an AND search using all fields
2004   * with a non-{@code null} value and that have a {@link LDAPField} annotation
2005   * with the {@code inFilter} element set to {@code true}, and all  getter
2006   * methods that return a non-{@code null} value and have a
2007   * {@link LDAPGetter} annotation with the {@code inFilter} element set to
2008   * {@code true}.
2009   *
2010   * @param  o                       The object for which to create the search
2011   *                                 filter.
2012   * @param  addedRequiredOrAllowed  Indicates whether any filter elements from
2013   *                                 required or allowed fields or getters have
2014   *                                 been added to the filter yet.
2015   *
2016   * @return  A filter that can be used to search for entries matching the
2017   *          provided object.
2018   *
2019   * @throws  LDAPPersistException  If it is not possible to construct a search
2020   *                                filter for some reason (e.g., because the
2021   *                                provided object does not have any
2022   *                                non-{@code null} fields or getters that are
2023   *                                marked for inclusion in filters).
2024   */
2025  private Filter createFilter(final T o,
2026                              final AtomicBoolean addedRequiredOrAllowed)
2027          throws LDAPPersistException
2028  {
2029    final ArrayList<Attribute> attrs = new ArrayList<Attribute>(5);
2030    attrs.add(objectClassAttribute);
2031
2032    for (final FieldInfo i : requiredFilterFields)
2033    {
2034      final Attribute a = i.encode(o, true);
2035      if (a == null)
2036      {
2037        throw new LDAPPersistException(
2038             ERR_OBJECT_HANDLER_FILTER_MISSING_REQUIRED_FIELD.get(
2039                  i.getField().getName()));
2040      }
2041      else
2042      {
2043        attrs.add(a);
2044        addedRequiredOrAllowed.set(true);
2045      }
2046    }
2047
2048    for (final GetterInfo i : requiredFilterGetters)
2049    {
2050      final Attribute a = i.encode(o);
2051      if (a == null)
2052      {
2053        throw new LDAPPersistException(
2054             ERR_OBJECT_HANDLER_FILTER_MISSING_REQUIRED_GETTER.get(
2055                  i.getMethod().getName()));
2056      }
2057      else
2058      {
2059        attrs.add(a);
2060        addedRequiredOrAllowed.set(true);
2061      }
2062    }
2063
2064    for (final FieldInfo i : alwaysAllowedFilterFields)
2065    {
2066      final Attribute a = i.encode(o, true);
2067      if (a != null)
2068      {
2069        attrs.add(a);
2070        addedRequiredOrAllowed.set(true);
2071      }
2072    }
2073
2074    for (final GetterInfo i : alwaysAllowedFilterGetters)
2075    {
2076      final Attribute a = i.encode(o);
2077      if (a != null)
2078      {
2079        attrs.add(a);
2080        addedRequiredOrAllowed.set(true);
2081      }
2082    }
2083
2084    for (final FieldInfo i : conditionallyAllowedFilterFields)
2085    {
2086      final Attribute a = i.encode(o, true);
2087      if (a != null)
2088      {
2089        attrs.add(a);
2090      }
2091    }
2092
2093    for (final GetterInfo i : conditionallyAllowedFilterGetters)
2094    {
2095      final Attribute a = i.encode(o);
2096      if (a != null)
2097      {
2098        attrs.add(a);
2099      }
2100    }
2101
2102    final ArrayList<Filter> comps = new ArrayList<Filter>(attrs.size());
2103    for (final Attribute a : attrs)
2104    {
2105      for (final ASN1OctetString v : a.getRawValues())
2106      {
2107        comps.add(Filter.createEqualityFilter(a.getName(), v.getValue()));
2108      }
2109    }
2110
2111    if (superclassHandler != null)
2112    {
2113      final Filter f =
2114           superclassHandler.createFilter(o, addedRequiredOrAllowed);
2115      if (f.getFilterType() == Filter.FILTER_TYPE_AND)
2116      {
2117        comps.addAll(Arrays.asList(f.getComponents()));
2118      }
2119      else
2120      {
2121        comps.add(f);
2122      }
2123    }
2124
2125    return Filter.createANDFilter(comps);
2126  }
2127}