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