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