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