001/*
002 * Copyright 2009-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2009-2017 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 */
021package com.unboundid.ldap.sdk.persist;
022
023
024
025import java.io.Serializable;
026import java.lang.reflect.Constructor;
027import java.lang.reflect.Field;
028import java.lang.reflect.InvocationTargetException;
029import java.lang.reflect.Method;
030import java.lang.reflect.Modifier;
031import java.util.ArrayList;
032import java.util.Arrays;
033import java.util.Iterator;
034import java.util.LinkedHashMap;
035import java.util.LinkedList;
036import java.util.Collections;
037import java.util.HashSet;
038import java.util.List;
039import java.util.Map;
040import java.util.TreeMap;
041import java.util.TreeSet;
042import java.util.concurrent.atomic.AtomicBoolean;
043
044import com.unboundid.asn1.ASN1OctetString;
045import com.unboundid.ldap.sdk.Attribute;
046import com.unboundid.ldap.sdk.DN;
047import com.unboundid.ldap.sdk.Entry;
048import com.unboundid.ldap.sdk.Filter;
049import com.unboundid.ldap.sdk.LDAPException;
050import com.unboundid.ldap.sdk.Modification;
051import com.unboundid.ldap.sdk.ModificationType;
052import com.unboundid.ldap.sdk.RDN;
053import com.unboundid.ldap.sdk.ReadOnlyEntry;
054import com.unboundid.ldap.sdk.schema.ObjectClassDefinition;
055import com.unboundid.ldap.sdk.schema.ObjectClassType;
056import com.unboundid.util.NotMutable;
057import com.unboundid.util.ThreadSafety;
058import com.unboundid.util.ThreadSafetyLevel;
059
060import static com.unboundid.ldap.sdk.persist.PersistMessages.*;
061import static com.unboundid.util.Debug.*;
062import static com.unboundid.util.StaticUtils.*;
063
064
065
066/**
067 * This class provides a mechanism for validating, encoding, and decoding
068 * objects marked with the {@link LDAPObject} annotation type.
069 *
070 * @param  <T>  The type of object handled by this class.
071 */
072@NotMutable()
073@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
074public final class LDAPObjectHandler<T>
075       implements Serializable
076{
077  /**
078   * The serial version UID for this serializable class.
079   */
080  private static final long serialVersionUID = -1480360011153517161L;
081
082
083
084  // The object class attribute to include in entries that are created.
085  private final Attribute objectClassAttribute;
086
087  // The type of object handled by this class.
088  private final Class<T> type;
089
090  // The constructor to use to create a new instance of the class.
091  private final Constructor<T> constructor;
092
093  // The default parent DN for entries created from objects of the associated
094  //  type.
095  private final DN defaultParentDN;
096
097  // The field that will be used to hold the DN of the entry.
098  private final Field dnField;
099
100  // The field that will be used to hold the entry contents.
101  private final Field entryField;
102
103  // The LDAPObject annotation for the associated object.
104  private final LDAPObject ldapObject;
105
106  // The LDAP object handler for the superclass, if applicable.
107  private final LDAPObjectHandler<? super T> superclassHandler;
108
109  // The list of fields for with a filter usage of ALWAYS_ALLOWED.
110  private final List<FieldInfo> alwaysAllowedFilterFields;
111
112  // The list of fields for with a filter usage of CONDITIONALLY_ALLOWED.
113  private final List<FieldInfo> conditionallyAllowedFilterFields;
114
115  // The list of fields for with a filter usage of REQUIRED.
116  private final List<FieldInfo> requiredFilterFields;
117
118  // The list of fields for this class that should be used to construct the RDN.
119  private final List<FieldInfo> rdnFields;
120
121  // The list of getter methods for with a filter usage of ALWAYS_ALLOWED.
122  private final List<GetterInfo> alwaysAllowedFilterGetters;
123
124  // The list of getter methods for with a filter usage of
125  // CONDITIONALLY_ALLOWED.
126  private final List<GetterInfo> conditionallyAllowedFilterGetters;
127
128  // The list of getter methods for with a filter usage of REQUIRED.
129  private final List<GetterInfo> requiredFilterGetters;
130
131  // The list of getters for this class that should be used to construct the
132  // RDN.
133  private final List<GetterInfo> rdnGetters;
134
135  // The map of attribute names to their corresponding fields.
136  private final Map<String,FieldInfo> fieldMap;
137
138  // The map of attribute names to their corresponding getter methods.
139  private final Map<String,GetterInfo> getterMap;
140
141  // The map of attribute names to their corresponding setter methods.
142  private final Map<String,SetterInfo> setterMap;
143
144  // The method that should be invoked on an object after all other decode
145  // processing has been performed.
146  private final Method postDecodeMethod;
147
148  // The method that should be invoked on an object after all other encode
149  // processing has been performed.
150  private final Method postEncodeMethod;
151
152  // The structural object class that should be used for entries created from
153  // objects of the associated type.
154  private final String structuralClass;
155
156  // The set of attributes that should be requested when performing a search.
157  // It will not include lazily-loaded attributes.
158  private final String[] attributesToRequest;
159
160  // The auxiliary object classes that should should used for entries created
161  // from objects of the associated type.
162  private final String[] auxiliaryClasses;
163
164  // The set of attributes that will be requested if @LDAPObject has
165  // requestAllAttributes is false.  Even if requestAllAttributes is true, this
166  // may be used if a subclass has requestAllAttributes set to false.
167  private final String[] explicitAttributesToRequest;
168
169  // The set of attributes that should be lazily loaded.
170  private final String[] lazilyLoadedAttributes;
171
172  // The superior object classes that should should used for entries created
173  // from objects of the associated type.
174  private final String[] superiorClasses;
175
176
177
178  /**
179   * Creates a new instance of this handler that will handle objects of the
180   * specified type.
181   *
182   * @param  type  The type of object that will be handled by this class.
183   *
184   * @throws  LDAPPersistException  If there is a problem with the provided
185   *                                class that makes it unsuitable for use with
186   *                                the persistence framework.
187   */
188  @SuppressWarnings({"unchecked", "rawtypes"})
189  LDAPObjectHandler(final Class<T> type)
190       throws LDAPPersistException
191  {
192    this.type = type;
193
194    final Class<? super T> superclassType = type.getSuperclass();
195    if (superclassType == null)
196    {
197      superclassHandler = null;
198    }
199    else
200    {
201      final LDAPObject superclassAnnotation =
202           superclassType.getAnnotation(LDAPObject.class);
203      if (superclassAnnotation == null)
204      {
205        superclassHandler = null;
206      }
207      else
208      {
209        superclassHandler = new LDAPObjectHandler(superclassType);
210      }
211    }
212
213    final TreeMap<String,FieldInfo>  fields  = new TreeMap<String,FieldInfo>();
214    final TreeMap<String,GetterInfo> getters = new TreeMap<String,GetterInfo>();
215    final TreeMap<String,SetterInfo> setters = new TreeMap<String,SetterInfo>();
216
217    ldapObject = type.getAnnotation(LDAPObject.class);
218    if (ldapObject == null)
219    {
220      throw new LDAPPersistException(
221           ERR_OBJECT_HANDLER_OBJECT_NOT_ANNOTATED.get(type.getName()));
222    }
223
224    final LinkedHashMap<String,String> objectClasses =
225         new LinkedHashMap<String,String>(10);
226
227    final String oc = ldapObject.structuralClass();
228    if (oc.length() == 0)
229    {
230      structuralClass = getUnqualifiedClassName(type);
231    }
232    else
233    {
234      structuralClass = oc;
235    }
236
237    final StringBuilder invalidReason = new StringBuilder();
238    if (PersistUtils.isValidLDAPName(structuralClass, invalidReason))
239    {
240      objectClasses.put(toLowerCase(structuralClass), structuralClass);
241    }
242    else
243    {
244      throw new LDAPPersistException(
245           ERR_OBJECT_HANDLER_INVALID_STRUCTURAL_CLASS.get(type.getName(),
246                structuralClass, invalidReason.toString()));
247    }
248
249    auxiliaryClasses = ldapObject.auxiliaryClass();
250    for (final String auxiliaryClass : auxiliaryClasses)
251    {
252      if (PersistUtils.isValidLDAPName(auxiliaryClass, invalidReason))
253      {
254        objectClasses.put(toLowerCase(auxiliaryClass), auxiliaryClass);
255      }
256      else
257      {
258        throw new LDAPPersistException(
259             ERR_OBJECT_HANDLER_INVALID_AUXILIARY_CLASS.get(type.getName(),
260                  auxiliaryClass, invalidReason.toString()));
261      }
262    }
263
264    superiorClasses = ldapObject.superiorClass();
265    for (final String superiorClass : superiorClasses)
266    {
267      if (PersistUtils.isValidLDAPName(superiorClass, invalidReason))
268      {
269        objectClasses.put(toLowerCase(superiorClass), superiorClass);
270      }
271      else
272      {
273        throw new LDAPPersistException(
274             ERR_OBJECT_HANDLER_INVALID_SUPERIOR_CLASS.get(type.getName(),
275                  superiorClass, invalidReason.toString()));
276      }
277    }
278
279    if (superclassHandler != null)
280    {
281      for (final String s : superclassHandler.objectClassAttribute.getValues())
282      {
283        objectClasses.put(toLowerCase(s), s);
284      }
285    }
286
287    objectClassAttribute = new Attribute("objectClass", objectClasses.values());
288
289
290    final String parentDNStr = ldapObject.defaultParentDN();
291    try
292    {
293      if ((parentDNStr.length() == 0) && (superclassHandler != null))
294      {
295        defaultParentDN = superclassHandler.getDefaultParentDN();
296      }
297      else
298      {
299        defaultParentDN = new DN(parentDNStr);
300      }
301    }
302    catch (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}