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