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