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}