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