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