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