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}