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