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