001    /*
002     * Copyright 2007-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2016 UnboundID Corp.
007     *
008     * This program is free software; you can redistribute it and/or modify
009     * it under the terms of the GNU General Public License (GPLv2 only)
010     * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011     * as published by the Free Software Foundation.
012     *
013     * This program is distributed in the hope that it will be useful,
014     * but WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program; if not, see <http://www.gnu.org/licenses>.
020     */
021    package com.unboundid.ldap.sdk;
022    
023    
024    
025    import java.io.Serializable;
026    import java.util.ArrayList;
027    import java.util.List;
028    
029    import com.unboundid.asn1.ASN1Buffer;
030    import com.unboundid.asn1.ASN1BufferSequence;
031    import com.unboundid.asn1.ASN1BufferSet;
032    import com.unboundid.asn1.ASN1Element;
033    import com.unboundid.asn1.ASN1Enumerated;
034    import com.unboundid.asn1.ASN1Exception;
035    import com.unboundid.asn1.ASN1OctetString;
036    import com.unboundid.asn1.ASN1Sequence;
037    import com.unboundid.asn1.ASN1Set;
038    import com.unboundid.asn1.ASN1StreamReader;
039    import com.unboundid.asn1.ASN1StreamReaderSet;
040    import com.unboundid.ldap.matchingrules.CaseIgnoreStringMatchingRule;
041    import com.unboundid.util.Base64;
042    import com.unboundid.util.NotMutable;
043    import com.unboundid.util.ThreadSafety;
044    import com.unboundid.util.ThreadSafetyLevel;
045    
046    import static com.unboundid.ldap.sdk.LDAPMessages.*;
047    import static com.unboundid.util.Debug.*;
048    import static com.unboundid.util.StaticUtils.*;
049    import 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)
066    public 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    }