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