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