001/*
002 * Copyright 2007-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2018 Ping Identity Corporation
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.Debug;
043import com.unboundid.util.NotMutable;
044import com.unboundid.util.StaticUtils;
045import com.unboundid.util.ThreadSafety;
046import com.unboundid.util.ThreadSafetyLevel;
047import com.unboundid.util.Validator;
048
049import static com.unboundid.ldap.sdk.LDAPMessages.*;
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    Validator.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    Validator.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    Validator.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    Validator.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    Validator.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 StaticUtils.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      Validator.ensureNotNull(reader.beginSequence());
428      final ModificationType modType =
429           ModificationType.valueOf(reader.readEnumerated());
430
431      Validator.ensureNotNull(reader.beginSequence());
432      final String attrName = reader.readString();
433
434      final ArrayList<ASN1OctetString> valueList = new ArrayList<>(5);
435      final ASN1StreamReaderSet valueSet = reader.beginSet();
436      while (valueSet.hasMoreElements())
437      {
438        valueList.add(new ASN1OctetString(reader.readBytes()));
439      }
440
441      final ASN1OctetString[] values = new ASN1OctetString[valueList.size()];
442      valueList.toArray(values);
443
444      return new Modification(modType, attrName, values);
445    }
446    catch (final Exception e)
447    {
448      Debug.debugException(e);
449      throw new LDAPException(ResultCode.DECODING_ERROR,
450           ERR_MOD_CANNOT_DECODE.get(StaticUtils.getExceptionMessage(e)), e);
451    }
452  }
453
454
455
456  /**
457   * Decodes the provided ASN.1 sequence as an LDAP modification.
458   *
459   * @param  modificationSequence  The ASN.1 sequence to decode as an LDAP
460   *                               modification.  It must not be {@code null}.
461   *
462   * @return  The decoded LDAP modification.
463   *
464   * @throws  LDAPException  If a problem occurs while trying to decode the
465   *                         provided ASN.1 sequence as an LDAP modification.
466   */
467  public static Modification decode(final ASN1Sequence modificationSequence)
468         throws LDAPException
469  {
470    Validator.ensureNotNull(modificationSequence);
471
472    final ASN1Element[] modificationElements = modificationSequence.elements();
473    if (modificationElements.length != 2)
474    {
475      throw new LDAPException(ResultCode.DECODING_ERROR,
476                              ERR_MOD_DECODE_INVALID_ELEMENT_COUNT.get(
477                                   modificationElements.length));
478    }
479
480    final int modType;
481    try
482    {
483      final ASN1Enumerated typeEnumerated =
484           ASN1Enumerated.decodeAsEnumerated(modificationElements[0]);
485      modType = typeEnumerated.intValue();
486    }
487    catch (final ASN1Exception ae)
488    {
489      Debug.debugException(ae);
490      throw new LDAPException(ResultCode.DECODING_ERROR,
491           ERR_MOD_DECODE_CANNOT_PARSE_MOD_TYPE.get(
492                StaticUtils.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      Debug.debugException(ae);
504      throw new LDAPException(ResultCode.DECODING_ERROR,
505           ERR_MOD_DECODE_CANNOT_PARSE_ATTR.get(
506                StaticUtils.getExceptionMessage(ae)),
507           ae);
508    }
509
510    final ASN1Element[] attrElements = attrSequence.elements();
511    if (attrElements.length != 2)
512    {
513      throw new LDAPException(ResultCode.DECODING_ERROR,
514           ERR_MOD_DECODE_INVALID_ATTR_ELEMENT_COUNT.get(attrElements.length));
515    }
516
517    final String attrName =
518         ASN1OctetString.decodeAsOctetString(attrElements[0]).stringValue();
519
520    final ASN1Set valueSet;
521    try
522    {
523      valueSet = ASN1Set.decodeAsSet(attrElements[1]);
524    }
525    catch (final ASN1Exception ae)
526    {
527      Debug.debugException(ae);
528      throw new LDAPException(ResultCode.DECODING_ERROR,
529           ERR_MOD_DECODE_CANNOT_PARSE_ATTR_VALUE_SET.get(
530                StaticUtils.getExceptionMessage(ae)), ae);
531    }
532
533    final ASN1Element[] valueElements = valueSet.elements();
534    final ASN1OctetString[] values = new ASN1OctetString[valueElements.length];
535    for (int i=0; i < values.length; i++)
536    {
537      values[i] = ASN1OctetString.decodeAsOctetString(valueElements[i]);
538    }
539
540    return new Modification(ModificationType.valueOf(modType), attrName,
541                            values);
542  }
543
544
545
546  /**
547   * Calculates a hash code for this LDAP modification.
548   *
549   * @return  The generated hash code for this LDAP modification.
550   */
551  @Override()
552  public int hashCode()
553  {
554    int hashCode = modificationType.intValue() +
555         StaticUtils.toLowerCase(attributeName).hashCode();
556
557    for (final ASN1OctetString value : values)
558    {
559      hashCode += value.hashCode();
560    }
561
562    return hashCode;
563  }
564
565
566
567  /**
568   * Indicates whether the provided object is equal to this LDAP modification.
569   * The provided object will only be considered equal if it is an LDAP
570   * modification with the same modification type, attribute name, and set of
571   * values as this LDAP modification.
572   *
573   * @param  o  The object for which to make the determination.
574   *
575   * @return  {@code true} if the provided object is equal to this modification,
576   *          or {@code false} if not.
577   */
578  @Override()
579  public boolean equals(final Object o)
580  {
581    if (o == null)
582    {
583      return false;
584    }
585
586    if (o == this)
587    {
588      return true;
589    }
590
591    if (! (o instanceof Modification))
592    {
593      return false;
594    }
595
596    final Modification mod = (Modification) o;
597    if (modificationType != mod.modificationType)
598    {
599      return false;
600    }
601
602    if (! attributeName.equalsIgnoreCase(mod.attributeName))
603    {
604      return false;
605    }
606
607    if (values.length != mod.values.length)
608    {
609      return false;
610    }
611
612    // Look at the values using a byte-for-byte matching.
613    for (final ASN1OctetString value : values)
614    {
615      boolean found = false;
616      for (int j = 0; j < mod.values.length; j++)
617      {
618        if (value.equalsIgnoreType(mod.values[j]))
619        {
620          found = true;
621          break;
622        }
623      }
624
625      if (!found)
626      {
627        return false;
628      }
629    }
630
631    // If we've gotten here, then we can consider the object equal to this LDAP
632    // modification.
633    return true;
634  }
635
636
637
638  /**
639   * Retrieves a string representation of this LDAP modification.
640   *
641   * @return  A string representation of this LDAP modification.
642   */
643  @Override()
644  public String toString()
645  {
646    final StringBuilder buffer = new StringBuilder();
647    toString(buffer);
648    return buffer.toString();
649  }
650
651
652
653  /**
654   * Appends a string representation of this LDAP modification to the provided
655   * buffer.
656   *
657   * @param  buffer  The buffer to which to append the string representation of
658   *                 this LDAP modification.
659   */
660  public void toString(final StringBuilder buffer)
661  {
662    buffer.append("LDAPModification(type=");
663
664    switch (modificationType.intValue())
665    {
666      case 0:
667        buffer.append("add");
668        break;
669      case 1:
670        buffer.append("delete");
671        break;
672      case 2:
673        buffer.append("replace");
674        break;
675      case 3:
676        buffer.append("increment");
677        break;
678      default:
679        buffer.append(modificationType);
680        break;
681    }
682
683    buffer.append(", attr=");
684    buffer.append(attributeName);
685
686    if (values.length == 0)
687    {
688      buffer.append(", values={");
689    }
690    else if (needsBase64Encoding())
691    {
692      buffer.append(", base64Values={'");
693
694      for (int i=0; i < values.length; i++)
695      {
696        if (i > 0)
697        {
698          buffer.append("', '");
699        }
700
701        buffer.append(Base64.encode(values[i].getValue()));
702      }
703
704      buffer.append('\'');
705    }
706    else
707    {
708      buffer.append(", values={'");
709
710      for (int i=0; i < values.length; i++)
711      {
712        if (i > 0)
713        {
714          buffer.append("', '");
715        }
716
717        buffer.append(values[i].stringValue());
718      }
719
720      buffer.append('\'');
721    }
722
723    buffer.append("})");
724  }
725
726
727
728  /**
729   * Indicates whether this modification needs to be base64-encoded when
730   * represented as LDIF.
731   *
732   * @return  {@code true} if this modification needs to be base64-encoded when
733   *          represented as LDIF, or {@code false} if not.
734   */
735  private boolean needsBase64Encoding()
736  {
737    for (final ASN1OctetString s : values)
738    {
739      if (Attribute.needsBase64Encoding(s.getValue()))
740      {
741        return true;
742      }
743    }
744
745    return false;
746  }
747
748
749
750  /**
751   * Appends a number of lines comprising the Java source code that can be used
752   * to recreate this modification to the given list.  Note that unless a first
753   * line prefix and/or last line suffix are provided, this will just include
754   * the code for the constructor, starting with "new Modification(" and ending
755   * with the closing parenthesis for that constructor.
756   *
757   * @param  lineList         The list to which the source code lines should be
758   *                          added.
759   * @param  indentSpaces     The number of spaces that should be used to indent
760   *                          the generated code.  It must not be negative.
761   * @param  firstLinePrefix  An optional string that should precede
762   *                          "new Modification(" on the first line of the
763   *                          generated code (e.g., it could be used for an
764   *                          attribute assignment, like "Modification m = ").
765   *                          It may be {@code null} or empty if there should be
766   *                          no first line prefix.
767   * @param  lastLineSuffix   An optional suffix that should follow the closing
768   *                          parenthesis of the constructor (e.g., it could be
769   *                          a semicolon to represent the end of a Java
770   *                          statement or a comma to separate it from another
771   *                          element in an array).  It may be {@code null} or
772   *                          empty if there should be no last line suffix.
773   */
774  public void toCode(final List<String> lineList, final int indentSpaces,
775                     final String firstLinePrefix, final String lastLineSuffix)
776  {
777    // Generate a string with the appropriate indent.
778    final StringBuilder buffer = new StringBuilder();
779    for (int i=0; i < indentSpaces; i++)
780    {
781      buffer.append(' ');
782    }
783    final String indent = buffer.toString();
784
785
786    // Start the constructor.
787    buffer.setLength(0);
788    buffer.append(indent);
789    if (firstLinePrefix != null)
790    {
791      buffer.append(firstLinePrefix);
792    }
793    buffer.append("new Modification(");
794    lineList.add(buffer.toString());
795
796    // There will always be a modification type.
797    buffer.setLength(0);
798    buffer.append(indent);
799    buffer.append("     \"ModificationType.");
800    buffer.append(modificationType.getName());
801    buffer.append(',');
802    lineList.add(buffer.toString());
803
804
805    // There will always be an attribute name.
806    buffer.setLength(0);
807    buffer.append(indent);
808    buffer.append("     \"");
809    buffer.append(attributeName);
810    buffer.append('"');
811
812
813    // If the attribute has any values, then include each on its own line.
814    // If possible, represent the values as strings, but fall back to using
815    // byte arrays if necessary.  But if this is something we might consider a
816    // sensitive attribute (like a password), then use fake values in the form
817    // "---redacted-value-N---" to indicate that the actual value has been
818    // hidden but to still show the correct number of values.
819    if (values.length > 0)
820    {
821      boolean allPrintable = true;
822
823      final ASN1OctetString[] attrValues;
824      if (StaticUtils.isSensitiveToCodeAttribute(attributeName))
825      {
826        attrValues = new ASN1OctetString[values.length];
827        for (int i=0; i < values.length; i++)
828        {
829          attrValues[i] =
830               new ASN1OctetString("---redacted-value-" + (i+1) + "---");
831        }
832      }
833      else
834      {
835        attrValues = values;
836        for (final ASN1OctetString v : values)
837        {
838          if (! StaticUtils.isPrintableString(v.getValue()))
839          {
840            allPrintable = false;
841            break;
842          }
843        }
844      }
845
846      for (final ASN1OctetString v : attrValues)
847      {
848        buffer.append(',');
849        lineList.add(buffer.toString());
850
851        buffer.setLength(0);
852        buffer.append(indent);
853        buffer.append("     ");
854        if (allPrintable)
855        {
856          buffer.append('"');
857          buffer.append(v.stringValue());
858          buffer.append('"');
859        }
860        else
861        {
862          StaticUtils.byteArrayToCode(v.getValue(), buffer);
863        }
864      }
865    }
866
867
868    // Append the closing parenthesis and any last line suffix.
869    buffer.append(')');
870    if (lastLineSuffix != null)
871    {
872      buffer.append(lastLineSuffix);
873    }
874    lineList.add(buffer.toString());
875  }
876}