001/* 002 * Copyright 2007-2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2018 Ping Identity Corporation 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.ldap.sdk; 022 023 024 025import java.io.Serializable; 026import java.util.ArrayList; 027import java.util.List; 028 029import com.unboundid.asn1.ASN1Buffer; 030import com.unboundid.asn1.ASN1BufferSequence; 031import com.unboundid.asn1.ASN1BufferSet; 032import com.unboundid.asn1.ASN1Element; 033import com.unboundid.asn1.ASN1Enumerated; 034import com.unboundid.asn1.ASN1Exception; 035import com.unboundid.asn1.ASN1OctetString; 036import com.unboundid.asn1.ASN1Sequence; 037import com.unboundid.asn1.ASN1Set; 038import com.unboundid.asn1.ASN1StreamReader; 039import com.unboundid.asn1.ASN1StreamReaderSet; 040import com.unboundid.ldap.matchingrules.CaseIgnoreStringMatchingRule; 041import com.unboundid.util.Base64; 042import com.unboundid.util.Debug; 043import com.unboundid.util.NotMutable; 044import com.unboundid.util.StaticUtils; 045import com.unboundid.util.ThreadSafety; 046import com.unboundid.util.ThreadSafetyLevel; 047import com.unboundid.util.Validator; 048 049import static com.unboundid.ldap.sdk.LDAPMessages.*; 050 051 052 053/** 054 * This class provides a data structure for holding information about an LDAP 055 * modification, which describes a change to apply to an attribute. A 056 * modification includes the following elements: 057 * <UL> 058 * <LI>A modification type, which describes the type of change to apply.</LI> 059 * <LI>An attribute name, which specifies which attribute should be 060 * updated.</LI> 061 * <LI>An optional set of values to use for the modification.</LI> 062 * </UL> 063 */ 064@NotMutable() 065@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 066public final class Modification 067 implements Serializable 068{ 069 /** 070 * The value array that will be used when the modification should not have any 071 * values. 072 */ 073 private static final ASN1OctetString[] NO_VALUES = new ASN1OctetString[0]; 074 075 076 077 /** 078 * The byte array value array that will be used when the modification does not 079 * have any values. 080 */ 081 private static final byte[][] NO_BYTE_VALUES = new byte[0][]; 082 083 084 085 /** 086 * The serial version UID for this serializable class. 087 */ 088 private static final long serialVersionUID = 5170107037390858876L; 089 090 091 092 // The set of values for this modification. 093 private final ASN1OctetString[] values; 094 095 // The modification type for this modification. 096 private final ModificationType modificationType; 097 098 // The name of the attribute to target with this modification. 099 private final String attributeName; 100 101 102 103 /** 104 * Creates a new LDAP modification with the provided modification type and 105 * attribute name. It will not have any values. 106 * 107 * @param modificationType The modification type for this modification. 108 * @param attributeName The name of the attribute to target with this 109 * modification. It must not be {@code null}. 110 */ 111 public Modification(final ModificationType modificationType, 112 final String attributeName) 113 { 114 Validator.ensureNotNull(attributeName); 115 116 this.modificationType = modificationType; 117 this.attributeName = attributeName; 118 119 values = NO_VALUES; 120 } 121 122 123 124 /** 125 * Creates a new LDAP modification with the provided information. 126 * 127 * @param modificationType The modification type for this modification. 128 * @param attributeName The name of the attribute to target with this 129 * modification. It must not be {@code null}. 130 * @param attributeValue The attribute value for this modification. It 131 * must not be {@code null}. 132 */ 133 public Modification(final ModificationType modificationType, 134 final String attributeName, final String attributeValue) 135 { 136 Validator.ensureNotNull(attributeName, attributeValue); 137 138 this.modificationType = modificationType; 139 this.attributeName = attributeName; 140 141 values = new ASN1OctetString[] { new ASN1OctetString(attributeValue) }; 142 } 143 144 145 146 /** 147 * Creates a new LDAP modification with the provided information. 148 * 149 * @param modificationType The modification type for this modification. 150 * @param attributeName The name of the attribute to target with this 151 * modification. It must not be {@code null}. 152 * @param attributeValue The attribute value for this modification. It 153 * must not be {@code null}. 154 */ 155 public Modification(final ModificationType modificationType, 156 final String attributeName, final byte[] attributeValue) 157 { 158 Validator.ensureNotNull(attributeName, attributeValue); 159 160 this.modificationType = modificationType; 161 this.attributeName = attributeName; 162 163 values = new ASN1OctetString[] { new ASN1OctetString(attributeValue) }; 164 } 165 166 167 168 /** 169 * Creates a new LDAP modification with the provided information. 170 * 171 * @param modificationType The modification type for this modification. 172 * @param attributeName The name of the attribute to target with this 173 * modification. It must not be {@code null}. 174 * @param attributeValues The set of attribute value for this modification. 175 * It must not be {@code null}. 176 */ 177 public Modification(final ModificationType modificationType, 178 final String attributeName, 179 final String... attributeValues) 180 { 181 Validator.ensureNotNull(attributeName, attributeValues); 182 183 this.modificationType = modificationType; 184 this.attributeName = attributeName; 185 186 values = new ASN1OctetString[attributeValues.length]; 187 for (int i=0; i < values.length; i++) 188 { 189 values[i] = new ASN1OctetString(attributeValues[i]); 190 } 191 } 192 193 194 195 /** 196 * Creates a new LDAP modification with the provided information. 197 * 198 * @param modificationType The modification type for this modification. 199 * @param attributeName The name of the attribute to target with this 200 * modification. It must not be {@code null}. 201 * @param attributeValues The set of attribute value for this modification. 202 * It must not be {@code null}. 203 */ 204 public Modification(final ModificationType modificationType, 205 final String attributeName, 206 final byte[]... attributeValues) 207 { 208 Validator.ensureNotNull(attributeName, attributeValues); 209 210 this.modificationType = modificationType; 211 this.attributeName = attributeName; 212 213 values = new ASN1OctetString[attributeValues.length]; 214 for (int i=0; i < values.length; i++) 215 { 216 values[i] = new ASN1OctetString(attributeValues[i]); 217 } 218 } 219 220 221 222 /** 223 * Creates a new LDAP modification with the provided information. 224 * 225 * @param modificationType The modification type for this modification. 226 * @param attributeName The name of the attribute to target with this 227 * modification. It must not be {@code null}. 228 * @param attributeValues The set of attribute value for this modification. 229 * It must not be {@code null}. 230 */ 231 public Modification(final ModificationType modificationType, 232 final String attributeName, 233 final ASN1OctetString[] attributeValues) 234 { 235 this.modificationType = modificationType; 236 this.attributeName = attributeName; 237 values = attributeValues; 238 } 239 240 241 242 /** 243 * Retrieves the modification type for this modification. 244 * 245 * @return The modification type for this modification. 246 */ 247 public ModificationType getModificationType() 248 { 249 return modificationType; 250 } 251 252 253 254 /** 255 * Retrieves the attribute for this modification. 256 * 257 * @return The attribute for this modification. 258 */ 259 public Attribute getAttribute() 260 { 261 return new Attribute(attributeName, 262 CaseIgnoreStringMatchingRule.getInstance(), values); 263 } 264 265 266 267 /** 268 * Retrieves the name of the attribute to target with this modification. 269 * 270 * @return The name of the attribute to target with this modification. 271 */ 272 public String getAttributeName() 273 { 274 return attributeName; 275 } 276 277 278 279 /** 280 * Indicates whether this modification has at least one value. 281 * 282 * @return {@code true} if this modification has one or more values, or 283 * {@code false} if not. 284 */ 285 public boolean hasValue() 286 { 287 return (values.length > 0); 288 } 289 290 291 292 /** 293 * Retrieves the set of values for this modification as an array of strings. 294 * 295 * @return The set of values for this modification as an array of strings. 296 */ 297 public String[] getValues() 298 { 299 if (values.length == 0) 300 { 301 return StaticUtils.NO_STRINGS; 302 } 303 else 304 { 305 final String[] stringValues = new String[values.length]; 306 for (int i=0; i < values.length; i++) 307 { 308 stringValues[i] = values[i].stringValue(); 309 } 310 311 return stringValues; 312 } 313 } 314 315 316 317 /** 318 * Retrieves the set of values for this modification as an array of byte 319 * arrays. 320 * 321 * @return The set of values for this modification as an array of byte 322 * arrays. 323 */ 324 public byte[][] getValueByteArrays() 325 { 326 if (values.length == 0) 327 { 328 return NO_BYTE_VALUES; 329 } 330 else 331 { 332 final byte[][] byteValues = new byte[values.length][]; 333 for (int i=0; i < values.length; i++) 334 { 335 byteValues[i] = values[i].getValue(); 336 } 337 338 return byteValues; 339 } 340 } 341 342 343 344 /** 345 * Retrieves the set of values for this modification as an array of ASN.1 346 * octet strings. 347 * 348 * @return The set of values for this modification as an array of ASN.1 octet 349 * strings. 350 */ 351 public ASN1OctetString[] getRawValues() 352 { 353 return values; 354 } 355 356 357 358 /** 359 * Writes an ASN.1-encoded representation of this modification to the provided 360 * ASN.1 buffer. 361 * 362 * @param buffer The ASN.1 buffer to which the encoded representation should 363 * be written. 364 */ 365 public void writeTo(final ASN1Buffer buffer) 366 { 367 final ASN1BufferSequence modSequence = buffer.beginSequence(); 368 buffer.addEnumerated(modificationType.intValue()); 369 370 final ASN1BufferSequence attrSequence = buffer.beginSequence(); 371 buffer.addOctetString(attributeName); 372 373 final ASN1BufferSet valueSet = buffer.beginSet(); 374 for (final ASN1OctetString v : values) 375 { 376 buffer.addElement(v); 377 } 378 valueSet.end(); 379 attrSequence.end(); 380 modSequence.end(); 381 } 382 383 384 385 /** 386 * Encodes this modification to an ASN.1 sequence suitable for use in the LDAP 387 * protocol. 388 * 389 * @return An ASN.1 sequence containing the encoded value. 390 */ 391 public ASN1Sequence encode() 392 { 393 final ASN1Element[] attrElements = 394 { 395 new ASN1OctetString(attributeName), 396 new ASN1Set(values) 397 }; 398 399 final ASN1Element[] modificationElements = 400 { 401 new ASN1Enumerated(modificationType.intValue()), 402 new ASN1Sequence(attrElements) 403 }; 404 405 return new ASN1Sequence(modificationElements); 406 } 407 408 409 410 /** 411 * Reads and decodes an LDAP modification from the provided ASN.1 stream 412 * reader. 413 * 414 * @param reader The ASN.1 stream reader from which to read the 415 * modification. 416 * 417 * @return The decoded modification. 418 * 419 * @throws LDAPException If a problem occurs while trying to read or decode 420 * the modification. 421 */ 422 public static Modification readFrom(final ASN1StreamReader reader) 423 throws LDAPException 424 { 425 try 426 { 427 Validator.ensureNotNull(reader.beginSequence()); 428 final ModificationType modType = 429 ModificationType.valueOf(reader.readEnumerated()); 430 431 Validator.ensureNotNull(reader.beginSequence()); 432 final String attrName = reader.readString(); 433 434 final ArrayList<ASN1OctetString> valueList = new ArrayList<>(5); 435 final ASN1StreamReaderSet valueSet = reader.beginSet(); 436 while (valueSet.hasMoreElements()) 437 { 438 valueList.add(new ASN1OctetString(reader.readBytes())); 439 } 440 441 final ASN1OctetString[] values = new ASN1OctetString[valueList.size()]; 442 valueList.toArray(values); 443 444 return new Modification(modType, attrName, values); 445 } 446 catch (final Exception e) 447 { 448 Debug.debugException(e); 449 throw new LDAPException(ResultCode.DECODING_ERROR, 450 ERR_MOD_CANNOT_DECODE.get(StaticUtils.getExceptionMessage(e)), e); 451 } 452 } 453 454 455 456 /** 457 * Decodes the provided ASN.1 sequence as an LDAP modification. 458 * 459 * @param modificationSequence The ASN.1 sequence to decode as an LDAP 460 * modification. It must not be {@code null}. 461 * 462 * @return The decoded LDAP modification. 463 * 464 * @throws LDAPException If a problem occurs while trying to decode the 465 * provided ASN.1 sequence as an LDAP modification. 466 */ 467 public static Modification decode(final ASN1Sequence modificationSequence) 468 throws LDAPException 469 { 470 Validator.ensureNotNull(modificationSequence); 471 472 final ASN1Element[] modificationElements = modificationSequence.elements(); 473 if (modificationElements.length != 2) 474 { 475 throw new LDAPException(ResultCode.DECODING_ERROR, 476 ERR_MOD_DECODE_INVALID_ELEMENT_COUNT.get( 477 modificationElements.length)); 478 } 479 480 final int modType; 481 try 482 { 483 final ASN1Enumerated typeEnumerated = 484 ASN1Enumerated.decodeAsEnumerated(modificationElements[0]); 485 modType = typeEnumerated.intValue(); 486 } 487 catch (final ASN1Exception ae) 488 { 489 Debug.debugException(ae); 490 throw new LDAPException(ResultCode.DECODING_ERROR, 491 ERR_MOD_DECODE_CANNOT_PARSE_MOD_TYPE.get( 492 StaticUtils.getExceptionMessage(ae)), 493 ae); 494 } 495 496 final ASN1Sequence attrSequence; 497 try 498 { 499 attrSequence = ASN1Sequence.decodeAsSequence(modificationElements[1]); 500 } 501 catch (final ASN1Exception ae) 502 { 503 Debug.debugException(ae); 504 throw new LDAPException(ResultCode.DECODING_ERROR, 505 ERR_MOD_DECODE_CANNOT_PARSE_ATTR.get( 506 StaticUtils.getExceptionMessage(ae)), 507 ae); 508 } 509 510 final ASN1Element[] attrElements = attrSequence.elements(); 511 if (attrElements.length != 2) 512 { 513 throw new LDAPException(ResultCode.DECODING_ERROR, 514 ERR_MOD_DECODE_INVALID_ATTR_ELEMENT_COUNT.get(attrElements.length)); 515 } 516 517 final String attrName = 518 ASN1OctetString.decodeAsOctetString(attrElements[0]).stringValue(); 519 520 final ASN1Set valueSet; 521 try 522 { 523 valueSet = ASN1Set.decodeAsSet(attrElements[1]); 524 } 525 catch (final ASN1Exception ae) 526 { 527 Debug.debugException(ae); 528 throw new LDAPException(ResultCode.DECODING_ERROR, 529 ERR_MOD_DECODE_CANNOT_PARSE_ATTR_VALUE_SET.get( 530 StaticUtils.getExceptionMessage(ae)), ae); 531 } 532 533 final ASN1Element[] valueElements = valueSet.elements(); 534 final ASN1OctetString[] values = new ASN1OctetString[valueElements.length]; 535 for (int i=0; i < values.length; i++) 536 { 537 values[i] = ASN1OctetString.decodeAsOctetString(valueElements[i]); 538 } 539 540 return new Modification(ModificationType.valueOf(modType), attrName, 541 values); 542 } 543 544 545 546 /** 547 * Calculates a hash code for this LDAP modification. 548 * 549 * @return The generated hash code for this LDAP modification. 550 */ 551 @Override() 552 public int hashCode() 553 { 554 int hashCode = modificationType.intValue() + 555 StaticUtils.toLowerCase(attributeName).hashCode(); 556 557 for (final ASN1OctetString value : values) 558 { 559 hashCode += value.hashCode(); 560 } 561 562 return hashCode; 563 } 564 565 566 567 /** 568 * Indicates whether the provided object is equal to this LDAP modification. 569 * The provided object will only be considered equal if it is an LDAP 570 * modification with the same modification type, attribute name, and set of 571 * values as this LDAP modification. 572 * 573 * @param o The object for which to make the determination. 574 * 575 * @return {@code true} if the provided object is equal to this modification, 576 * or {@code false} if not. 577 */ 578 @Override() 579 public boolean equals(final Object o) 580 { 581 if (o == null) 582 { 583 return false; 584 } 585 586 if (o == this) 587 { 588 return true; 589 } 590 591 if (! (o instanceof Modification)) 592 { 593 return false; 594 } 595 596 final Modification mod = (Modification) o; 597 if (modificationType != mod.modificationType) 598 { 599 return false; 600 } 601 602 if (! attributeName.equalsIgnoreCase(mod.attributeName)) 603 { 604 return false; 605 } 606 607 if (values.length != mod.values.length) 608 { 609 return false; 610 } 611 612 // Look at the values using a byte-for-byte matching. 613 for (final ASN1OctetString value : values) 614 { 615 boolean found = false; 616 for (int j = 0; j < mod.values.length; j++) 617 { 618 if (value.equalsIgnoreType(mod.values[j])) 619 { 620 found = true; 621 break; 622 } 623 } 624 625 if (!found) 626 { 627 return false; 628 } 629 } 630 631 // If we've gotten here, then we can consider the object equal to this LDAP 632 // modification. 633 return true; 634 } 635 636 637 638 /** 639 * Retrieves a string representation of this LDAP modification. 640 * 641 * @return A string representation of this LDAP modification. 642 */ 643 @Override() 644 public String toString() 645 { 646 final StringBuilder buffer = new StringBuilder(); 647 toString(buffer); 648 return buffer.toString(); 649 } 650 651 652 653 /** 654 * Appends a string representation of this LDAP modification to the provided 655 * buffer. 656 * 657 * @param buffer The buffer to which to append the string representation of 658 * this LDAP modification. 659 */ 660 public void toString(final StringBuilder buffer) 661 { 662 buffer.append("LDAPModification(type="); 663 664 switch (modificationType.intValue()) 665 { 666 case 0: 667 buffer.append("add"); 668 break; 669 case 1: 670 buffer.append("delete"); 671 break; 672 case 2: 673 buffer.append("replace"); 674 break; 675 case 3: 676 buffer.append("increment"); 677 break; 678 default: 679 buffer.append(modificationType); 680 break; 681 } 682 683 buffer.append(", attr="); 684 buffer.append(attributeName); 685 686 if (values.length == 0) 687 { 688 buffer.append(", values={"); 689 } 690 else if (needsBase64Encoding()) 691 { 692 buffer.append(", base64Values={'"); 693 694 for (int i=0; i < values.length; i++) 695 { 696 if (i > 0) 697 { 698 buffer.append("', '"); 699 } 700 701 buffer.append(Base64.encode(values[i].getValue())); 702 } 703 704 buffer.append('\''); 705 } 706 else 707 { 708 buffer.append(", values={'"); 709 710 for (int i=0; i < values.length; i++) 711 { 712 if (i > 0) 713 { 714 buffer.append("', '"); 715 } 716 717 buffer.append(values[i].stringValue()); 718 } 719 720 buffer.append('\''); 721 } 722 723 buffer.append("})"); 724 } 725 726 727 728 /** 729 * Indicates whether this modification needs to be base64-encoded when 730 * represented as LDIF. 731 * 732 * @return {@code true} if this modification needs to be base64-encoded when 733 * represented as LDIF, or {@code false} if not. 734 */ 735 private boolean needsBase64Encoding() 736 { 737 for (final ASN1OctetString s : values) 738 { 739 if (Attribute.needsBase64Encoding(s.getValue())) 740 { 741 return true; 742 } 743 } 744 745 return false; 746 } 747 748 749 750 /** 751 * Appends a number of lines comprising the Java source code that can be used 752 * to recreate this modification to the given list. Note that unless a first 753 * line prefix and/or last line suffix are provided, this will just include 754 * the code for the constructor, starting with "new Modification(" and ending 755 * with the closing parenthesis for that constructor. 756 * 757 * @param lineList The list to which the source code lines should be 758 * added. 759 * @param indentSpaces The number of spaces that should be used to indent 760 * the generated code. It must not be negative. 761 * @param firstLinePrefix An optional string that should precede 762 * "new Modification(" on the first line of the 763 * generated code (e.g., it could be used for an 764 * attribute assignment, like "Modification m = "). 765 * It may be {@code null} or empty if there should be 766 * no first line prefix. 767 * @param lastLineSuffix An optional suffix that should follow the closing 768 * parenthesis of the constructor (e.g., it could be 769 * a semicolon to represent the end of a Java 770 * statement or a comma to separate it from another 771 * element in an array). It may be {@code null} or 772 * empty if there should be no last line suffix. 773 */ 774 public void toCode(final List<String> lineList, final int indentSpaces, 775 final String firstLinePrefix, final String lastLineSuffix) 776 { 777 // Generate a string with the appropriate indent. 778 final StringBuilder buffer = new StringBuilder(); 779 for (int i=0; i < indentSpaces; i++) 780 { 781 buffer.append(' '); 782 } 783 final String indent = buffer.toString(); 784 785 786 // Start the constructor. 787 buffer.setLength(0); 788 buffer.append(indent); 789 if (firstLinePrefix != null) 790 { 791 buffer.append(firstLinePrefix); 792 } 793 buffer.append("new Modification("); 794 lineList.add(buffer.toString()); 795 796 // There will always be a modification type. 797 buffer.setLength(0); 798 buffer.append(indent); 799 buffer.append(" \"ModificationType."); 800 buffer.append(modificationType.getName()); 801 buffer.append(','); 802 lineList.add(buffer.toString()); 803 804 805 // There will always be an attribute name. 806 buffer.setLength(0); 807 buffer.append(indent); 808 buffer.append(" \""); 809 buffer.append(attributeName); 810 buffer.append('"'); 811 812 813 // If the attribute has any values, then include each on its own line. 814 // If possible, represent the values as strings, but fall back to using 815 // byte arrays if necessary. But if this is something we might consider a 816 // sensitive attribute (like a password), then use fake values in the form 817 // "---redacted-value-N---" to indicate that the actual value has been 818 // hidden but to still show the correct number of values. 819 if (values.length > 0) 820 { 821 boolean allPrintable = true; 822 823 final ASN1OctetString[] attrValues; 824 if (StaticUtils.isSensitiveToCodeAttribute(attributeName)) 825 { 826 attrValues = new ASN1OctetString[values.length]; 827 for (int i=0; i < values.length; i++) 828 { 829 attrValues[i] = 830 new ASN1OctetString("---redacted-value-" + (i+1) + "---"); 831 } 832 } 833 else 834 { 835 attrValues = values; 836 for (final ASN1OctetString v : values) 837 { 838 if (! StaticUtils.isPrintableString(v.getValue())) 839 { 840 allPrintable = false; 841 break; 842 } 843 } 844 } 845 846 for (final ASN1OctetString v : attrValues) 847 { 848 buffer.append(','); 849 lineList.add(buffer.toString()); 850 851 buffer.setLength(0); 852 buffer.append(indent); 853 buffer.append(" "); 854 if (allPrintable) 855 { 856 buffer.append('"'); 857 buffer.append(v.stringValue()); 858 buffer.append('"'); 859 } 860 else 861 { 862 StaticUtils.byteArrayToCode(v.getValue(), buffer); 863 } 864 } 865 } 866 867 868 // Append the closing parenthesis and any last line suffix. 869 buffer.append(')'); 870 if (lastLineSuffix != null) 871 { 872 buffer.append(lastLineSuffix); 873 } 874 lineList.add(buffer.toString()); 875 } 876}