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