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.Field;
042import java.lang.reflect.Modifier;
043import java.util.List;
044
045import com.unboundid.ldap.sdk.Attribute;
046import com.unboundid.ldap.sdk.Entry;
047import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
048import com.unboundid.util.Debug;
049import com.unboundid.util.NotMutable;
050import com.unboundid.util.NotNull;
051import com.unboundid.util.Nullable;
052import com.unboundid.util.StaticUtils;
053import com.unboundid.util.ThreadSafety;
054import com.unboundid.util.ThreadSafetyLevel;
055import com.unboundid.util.Validator;
056
057import static com.unboundid.ldap.sdk.persist.PersistMessages.*;
058
059
060
061/**
062 * This class provides a data structure that holds information about an
063 * annotated field.
064 */
065@NotMutable()
066@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
067public final class FieldInfo
068       implements Serializable
069{
070  /**
071   * The serial version UID for this serializable class.
072   */
073  private static final long serialVersionUID = -5715642176677596417L;
074
075
076
077  // Indicates whether attempts to populate the associated field should fail if
078  // the LDAP attribute has a value that is not valid for the data type of the
079  // field.
080  private final boolean failOnInvalidValue;
081
082  // Indicates whether attempts to populate the associated field should fail if
083  // the LDAP attribute has multiple values but the field can only hold a single
084  // value.
085  private final boolean failOnTooManyValues;
086
087  // Indicates whether the associated field should be included in the entry
088  // created for an add operation.
089  private final boolean includeInAdd;
090
091  // Indicates whether the associated field should be considered for inclusion
092  // in the set of modifications used for modify operations.
093  private final boolean includeInModify;
094
095  // Indicates whether the associated field is part of the RDN.
096  private final boolean includeInRDN;
097
098  // Indicates whether the associated field is required when decoding.
099  private final boolean isRequiredForDecode;
100
101  // Indicates whether the associated field is required when encoding.
102  private final boolean isRequiredForEncode;
103
104  // Indicates whether the associated field should be lazily-loaded.
105  private final boolean lazilyLoad;
106
107  // Indicates whether the associated field supports multiple values.
108  private final boolean supportsMultipleValues;
109
110  // The class that contains the associated field.
111  @NotNull private final Class<?> containingClass;
112
113  // The field with which this object is associated.
114  @NotNull private final Field field;
115
116  // The filter usage for the associated field.
117  @NotNull private final FilterUsage filterUsage;
118
119  // The encoder used for this field.
120  @NotNull private final ObjectEncoder encoder;
121
122  // The name of the associated attribute type.
123  @NotNull private final String attributeName;
124
125  // The default values for the field to use for object instantiation.
126  @NotNull private final String[] defaultDecodeValues;
127
128  // The default values for the field to use for add operations.
129  @NotNull private final String[] defaultEncodeValues;
130
131  // The names of the object classes for the associated attribute.
132  @NotNull private final String[] objectClasses;
133
134
135
136  /**
137   * Creates a new field info object from the provided field.
138   *
139   * @param  f  The field to use to create this object.  It must not be
140   *            {@code null} and it must be marked with the {@code LDAPField}
141   *            annotation.
142   * @param  c  The class which holds the field.  It must not be {@code null}
143   *            and it must be marked with the {@code LDAPObject} annotation.
144   *
145   * @throws  LDAPPersistException  If a problem occurs while processing the
146   *                                given field.
147   */
148  FieldInfo(@NotNull final Field f, @NotNull final Class<?> c)
149       throws LDAPPersistException
150  {
151    Validator.ensureNotNull(f, c);
152
153    field = f;
154    f.setAccessible(true);
155
156    final LDAPField  a = f.getAnnotation(LDAPField.class);
157    if (a == null)
158    {
159      throw new LDAPPersistException(ERR_FIELD_INFO_FIELD_NOT_ANNOTATED.get(
160           f.getName(), c.getName()));
161    }
162
163    final LDAPObject o = c.getAnnotation(LDAPObject.class);
164    if (o == null)
165    {
166      throw new LDAPPersistException(ERR_FIELD_INFO_CLASS_NOT_ANNOTATED.get(
167           c.getName()));
168    }
169
170    containingClass     = c;
171    failOnInvalidValue  = a.failOnInvalidValue();
172    includeInRDN        = a.inRDN();
173    includeInAdd        = (includeInRDN || a.inAdd());
174    includeInModify     = ((! includeInRDN) && a.inModify());
175    filterUsage         = a.filterUsage();
176    lazilyLoad          = a.lazilyLoad();
177    isRequiredForDecode = (a.requiredForDecode() && (! lazilyLoad));
178    isRequiredForEncode = (includeInRDN || a.requiredForEncode());
179    defaultDecodeValues = a.defaultDecodeValue();
180    defaultEncodeValues = a.defaultEncodeValue();
181
182    if (lazilyLoad)
183    {
184      if (defaultDecodeValues.length > 0)
185      {
186        throw new LDAPPersistException(
187             ERR_FIELD_INFO_LAZY_WITH_DEFAULT_DECODE.get(f.getName(),
188                  c.getName()));
189      }
190
191      if (defaultEncodeValues.length > 0)
192      {
193        throw new LDAPPersistException(
194             ERR_FIELD_INFO_LAZY_WITH_DEFAULT_ENCODE.get(f.getName(),
195                  c.getName()));
196      }
197
198      if (includeInRDN)
199      {
200        throw new LDAPPersistException(ERR_FIELD_INFO_LAZY_IN_RDN.get(
201             f.getName(), c.getName()));
202      }
203    }
204
205    final int modifiers = f.getModifiers();
206    if (Modifier.isFinal(modifiers))
207    {
208      throw new LDAPPersistException(ERR_FIELD_INFO_FIELD_FINAL.get(
209           f.getName(), c.getName()));
210    }
211
212    if (Modifier.isStatic(modifiers))
213    {
214      throw new LDAPPersistException(ERR_FIELD_INFO_FIELD_STATIC.get(
215           f.getName(), c.getName()));
216    }
217
218    try
219    {
220      encoder = a.encoderClass().newInstance();
221    }
222    catch (final Exception e)
223    {
224      Debug.debugException(e);
225      throw new LDAPPersistException(ERR_FIELD_INFO_CANNOT_GET_ENCODER.get(
226           a.encoderClass().getName(), f.getName(), c.getName(),
227           StaticUtils.getExceptionMessage(e)), e);
228    }
229
230    if (! encoder.supportsType(f.getGenericType()))
231    {
232      throw new LDAPPersistException(
233           ERR_FIELD_INFO_ENCODER_UNSUPPORTED_TYPE.get(
234                encoder.getClass().getName(), f.getName(), c.getName(),
235                f.getGenericType()));
236    }
237
238    supportsMultipleValues = encoder.supportsMultipleValues(f);
239    if (supportsMultipleValues)
240    {
241      failOnTooManyValues = false;
242    }
243    else
244    {
245      failOnTooManyValues = a.failOnTooManyValues();
246      if (defaultDecodeValues.length > 1)
247      {
248        throw new LDAPPersistException(
249             ERR_FIELD_INFO_UNSUPPORTED_MULTIPLE_DEFAULT_DECODE_VALUES.get(
250                  f.getName(), c.getName()));
251      }
252
253      if (defaultEncodeValues.length > 1)
254      {
255        throw new LDAPPersistException(
256             ERR_FIELD_INFO_UNSUPPORTED_MULTIPLE_DEFAULT_ENCODE_VALUES.get(
257                  f.getName(), c.getName()));
258      }
259    }
260
261    final String attrName = a.attribute();
262    if ((attrName == null) || attrName.isEmpty())
263    {
264      attributeName = f.getName();
265    }
266    else
267    {
268      attributeName = attrName;
269    }
270
271    final StringBuilder invalidReason = new StringBuilder();
272    if (! PersistUtils.isValidLDAPName(attributeName, true, invalidReason))
273    {
274      throw new LDAPPersistException(ERR_FIELD_INFO_INVALID_ATTR_NAME.get(
275           f.getName(), c.getName(), invalidReason.toString()));
276    }
277
278    final String structuralClass;
279    if (o.structuralClass().isEmpty())
280    {
281      structuralClass = StaticUtils.getUnqualifiedClassName(c);
282    }
283    else
284    {
285      structuralClass = o.structuralClass();
286    }
287
288    final String[] ocs = a.objectClass();
289    if ((ocs == null) || (ocs.length == 0))
290    {
291      objectClasses = new String[] { structuralClass };
292    }
293    else
294    {
295      objectClasses = ocs;
296    }
297
298    for (final String s : objectClasses)
299    {
300      if (! s.equalsIgnoreCase(structuralClass))
301      {
302        boolean found = false;
303        for (final String oc : o.auxiliaryClass())
304        {
305          if (s.equalsIgnoreCase(oc))
306          {
307            found = true;
308            break;
309          }
310        }
311
312        if (! found)
313        {
314          throw new LDAPPersistException(ERR_FIELD_INFO_INVALID_OC.get(
315               f.getName(), c.getName(), s));
316        }
317      }
318    }
319  }
320
321
322
323  /**
324   * Retrieves the field with which this object is associated.
325   *
326   * @return  The field with which this object is associated.
327   */
328  @NotNull()
329  public Field getField()
330  {
331    return field;
332  }
333
334
335
336  /**
337   * Retrieves the class that is marked with the {@link LDAPObject} annotation
338   * and contains the associated field.
339   *
340   * @return  The class that contains the associated field.
341   */
342  @NotNull()
343  public Class<?> getContainingClass()
344  {
345    return containingClass;
346  }
347
348
349
350  /**
351   * Indicates whether attempts to initialize an object should fail if the LDAP
352   * attribute has a value that cannot be stored in the associated field.
353   *
354   * @return  {@code true} if an exception should be thrown if an LDAP attribute
355   *          has a value that cannot be assigned to the associated field, or
356   *          {@code false} if the field should remain uninitialized.
357   */
358  public boolean failOnInvalidValue()
359  {
360    return failOnInvalidValue;
361  }
362
363
364
365  /**
366   * Indicates whether attempts to initialize an object should fail if the
367   * LDAP attribute has multiple values but the associated field can only hold a
368   * single value.  Note that the value returned from this method may be
369   * {@code false} even when the annotation has a value of {@code true} if the
370   * associated field supports multiple values.
371   *
372   * @return  {@code true} if an exception should be thrown if an attribute has
373   *          too many values to hold in the associated field, or {@code false}
374   *          if the first value returned should be assigned to the field.
375   */
376  public boolean failOnTooManyValues()
377  {
378    return failOnTooManyValues;
379  }
380
381
382
383  /**
384   * Indicates whether the associated field should be included in entries
385   * generated for add operations.  Note that the value returned from this
386   * method may be {@code true} even when the annotation has a value of
387   * {@code false} if the associated field is to be included in entry RDNs.
388   *
389   * @return  {@code true} if the associated field should be included in entries
390   *         generated for add operations, or {@code false} if not.
391   */
392  public boolean includeInAdd()
393  {
394    return includeInAdd;
395  }
396
397
398
399  /**
400   * Indicates whether the associated field should be considered for inclusion
401   * in the set of modifications generated for modify operations.  Note that the
402   * value returned from this method may be {@code false} even when the
403   * annotation has a value of {@code true} for the {@code inModify} element if
404   * the associated field is to be included in entry RDNs.
405   *
406   * @return  {@code true} if the associated field should be considered for
407   *          inclusion in the set of modifications generated for modify
408   *          operations, or {@code false} if not.
409   */
410  public boolean includeInModify()
411  {
412    return includeInModify;
413  }
414
415
416
417  /**
418   * Indicates whether the associated field should be used to generate entry
419   * RDNs.
420   *
421   * @return  {@code true} if the associated field should be used to generate
422   *          entry RDNs, or {@code false} if not.
423   */
424  public boolean includeInRDN()
425  {
426    return includeInRDN;
427  }
428
429
430
431  /**
432   * Retrieves the filter usage for the associated field.
433   *
434   * @return  The filter usage for the associated field.
435   */
436  @NotNull()
437  public FilterUsage getFilterUsage()
438  {
439    return filterUsage;
440  }
441
442
443
444  /**
445   * Indicates whether the associated field should be considered required for
446   * decode operations.
447   *
448   * @return  {@code true} if the associated field should be considered required
449   *          for decode operations, or {@code false} if not.
450   */
451  public boolean isRequiredForDecode()
452  {
453    return isRequiredForDecode;
454  }
455
456
457
458  /**
459   * Indicates whether the associated field should be considered required for
460   * encode operations.  Note that the value returned from this method may be
461   * {@code true} even when the annotation has a value of {@code true} for the
462   * {@code requiredForEncode} element if the associated field is to be included
463   * in entry RDNs.
464   *
465   * @return  {@code true} if the associated field should be considered required
466   *          for encode operations, or {@code false} if not.
467   */
468  public boolean isRequiredForEncode()
469  {
470    return isRequiredForEncode;
471  }
472
473
474
475  /**
476   * Indicates whether the associated field should be lazily-loaded.
477   *
478   * @return  {@code true} if the associated field should be lazily-loaded, or
479   *          {@code false} if not.
480   */
481  public boolean lazilyLoad()
482  {
483    return lazilyLoad;
484  }
485
486
487
488  /**
489   * Retrieves the encoder that should be used for the associated field.
490   *
491   * @return  The encoder that should be used for the associated field.
492   */
493  @NotNull()
494  public ObjectEncoder getEncoder()
495  {
496    return encoder;
497  }
498
499
500
501  /**
502   * Retrieves the name of the LDAP attribute used to hold values for the
503   * associated field.
504   *
505   * @return  The name of the LDAP attribute used to hold values for the
506   *          associated field.
507   */
508  @NotNull()
509  public String getAttributeName()
510  {
511    return attributeName;
512  }
513
514
515
516  /**
517   * Retrieves the set of default values that should be assigned to the
518   * associated field if there are no values for the corresponding attribute in
519   * the LDAP entry.
520   *
521   * @return  The set of default values for use when instantiating the object,
522   *          or an empty array if no default values are defined.
523   */
524  @NotNull()
525  public String[] getDefaultDecodeValues()
526  {
527    return defaultDecodeValues;
528  }
529
530
531
532  /**
533   * Retrieves the set of default values that should be used when creating an
534   * entry for an add operation if the associated field does not itself have any
535   * values.
536   *
537   * @return  The set of default values for use in add operations, or an empty
538   *          array if no default values are defined.
539   */
540  @NotNull()
541  public String[] getDefaultEncodeValues()
542  {
543    return defaultEncodeValues;
544  }
545
546
547
548  /**
549   * Retrieves the names of the object classes containing the associated
550   * attribute.
551   *
552   * @return  The names of the object classes containing the associated
553   *          attribute.
554   */
555  @NotNull()
556  public String[] getObjectClasses()
557  {
558    return objectClasses;
559  }
560
561
562
563  /**
564   * Indicates whether the associated field can hold multiple values.
565   *
566   * @return  {@code true} if the associated field can hold multiple values, or
567   *          {@code false} if not.
568   */
569  public boolean supportsMultipleValues()
570  {
571    return supportsMultipleValues;
572  }
573
574
575
576  /**
577   * Constructs a definition for an LDAP attribute type which may be added to
578   * the directory server schema to allow it to hold the value of the associated
579   * field.  Note that the object identifier used for the constructed attribute
580   * type definition is not required to be valid or unique.
581   *
582   * @return  The constructed attribute type definition.
583   *
584   * @throws  LDAPPersistException  If the object encoder does not support
585   *                                encoding values for the associated field
586   *                                type.
587   */
588  @NotNull()
589  AttributeTypeDefinition constructAttributeType()
590       throws LDAPPersistException
591  {
592    return constructAttributeType(DefaultOIDAllocator.getInstance());
593  }
594
595
596
597  /**
598   * Constructs a definition for an LDAP attribute type which may be added to
599   * the directory server schema to allow it to hold the value of the associated
600   * field.  Note that the object identifier used for the constructed attribute
601   * type definition is not required to be valid or unique.
602   *
603   * @param  a  The OID allocator to use to generate the object identifier.  It
604   *            must not be {@code null}.
605   *
606   * @return  The constructed attribute type definition.
607   *
608   * @throws  LDAPPersistException  If the object encoder does not support
609   *                                encoding values for the associated field
610   *                                type.
611   */
612  @NotNull()
613  AttributeTypeDefinition constructAttributeType(@NotNull final OIDAllocator a)
614       throws LDAPPersistException
615  {
616    return encoder.constructAttributeType(field, a);
617  }
618
619
620
621  /**
622   * Encodes the value for the associated field from the provided object to an
623   * attribute.
624   *
625   * @param  o                   The object containing the field to be encoded.
626   * @param  ignoreRequiredFlag  Indicates whether to ignore the value of the
627   *                             {@code requiredForEncode} setting.  If this is
628   *                             {@code true}, then this method will always
629   *                             return {@code null} if the field does not have
630   *                             a value even if this field is marked as
631   *                             required for encode processing.
632   *
633   * @return  The attribute containing the encoded representation of the field
634   *          value if it is non-{@code null}, an encoded representation of the
635   *          default add values if the associated field is {@code null} but
636   *          default values are defined, or {@code null} if the associated
637   *          field is {@code null} and there are no default values.
638   *
639   * @throws  LDAPPersistException  If a problem occurs while encoding the
640   *                                value of the associated field for the
641   *                                provided object, or if the field is marked
642   *                                as required but is {@code null} and does not
643   *                                have any default add values.
644   */
645  @Nullable()
646  Attribute encode(@NotNull final Object o, final boolean ignoreRequiredFlag)
647            throws LDAPPersistException
648  {
649    try
650    {
651      final Object fieldValue = field.get(o);
652      if (fieldValue == null)
653      {
654        if (defaultEncodeValues.length > 0)
655        {
656          return new Attribute(attributeName, defaultEncodeValues);
657        }
658
659        if (isRequiredForEncode && (! ignoreRequiredFlag))
660        {
661          throw new LDAPPersistException(
662               ERR_FIELD_INFO_MISSING_REQUIRED_VALUE.get(field.getName(),
663                    containingClass.getName()));
664        }
665
666        return null;
667      }
668
669      return encoder.encodeFieldValue(field, fieldValue, attributeName);
670    }
671    catch (final LDAPPersistException lpe)
672    {
673      Debug.debugException(lpe);
674      throw lpe;
675    }
676    catch (final Exception e)
677    {
678      Debug.debugException(e);
679      throw new LDAPPersistException(
680           ERR_FIELD_INFO_CANNOT_ENCODE.get(field.getName(),
681                containingClass.getName(), StaticUtils.getExceptionMessage(e)),
682           e);
683    }
684  }
685
686
687
688  /**
689   * Sets the value of the associated field in the given object from the
690   * information contained in the provided attribute.
691   *
692   * @param  o               The object for which to update the associated
693   *                         field.
694   * @param  e               The entry being decoded.
695   * @param  failureReasons  A list to which information about any failures
696   *                         may be appended.
697   *
698   * @return  {@code true} if the decode process was completely successful, or
699   *          {@code false} if there were one or more failures.
700   */
701  boolean decode(@NotNull final Object o, @NotNull final Entry e,
702                 @NotNull final List<String> failureReasons)
703  {
704    boolean successful = true;
705
706    Attribute a = e.getAttribute(attributeName);
707    if ((a == null) || (! a.hasValue()))
708    {
709      if (defaultDecodeValues.length > 0)
710      {
711        a = new Attribute(attributeName, defaultDecodeValues);
712      }
713      else
714      {
715        if (isRequiredForDecode)
716        {
717          successful = false;
718          failureReasons.add(ERR_FIELD_INFO_MISSING_REQUIRED_ATTRIBUTE.get(
719               containingClass.getName(), e.getDN(), attributeName,
720               field.getName()));
721        }
722
723        try
724        {
725          encoder.setNull(field, o);
726        }
727        catch (final LDAPPersistException lpe)
728        {
729          Debug.debugException(lpe);
730          successful = false;
731          failureReasons.add(lpe.getMessage());
732        }
733
734        return successful;
735      }
736    }
737
738    if (failOnTooManyValues && (a.size() > 1))
739    {
740      successful = false;
741      failureReasons.add(ERR_FIELD_INFO_FIELD_NOT_MULTIVALUED.get(a.getName(),
742           field.getName(), containingClass.getName()));
743    }
744
745    try
746    {
747      encoder.decodeField(field, o, a);
748    }
749    catch (final LDAPPersistException lpe)
750    {
751      Debug.debugException(lpe);
752      if (failOnInvalidValue)
753      {
754        successful = false;
755        failureReasons.add(lpe.getMessage());
756      }
757    }
758
759    return successful;
760  }
761}