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