001/*
002 * Copyright 2007-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2007-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2007-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldif;
037
038
039
040import java.util.ArrayList;
041import java.util.HashSet;
042import java.util.Iterator;
043import java.util.List;
044
045import com.unboundid.asn1.ASN1OctetString;
046import com.unboundid.ldap.sdk.ChangeType;
047import com.unboundid.ldap.sdk.Control;
048import com.unboundid.ldap.sdk.LDAPException;
049import com.unboundid.ldap.sdk.LDAPInterface;
050import com.unboundid.ldap.sdk.LDAPResult;
051import com.unboundid.ldap.sdk.Modification;
052import com.unboundid.ldap.sdk.ModifyRequest;
053import com.unboundid.util.ByteStringBuffer;
054import com.unboundid.util.Debug;
055import com.unboundid.util.NotMutable;
056import com.unboundid.util.NotNull;
057import com.unboundid.util.Nullable;
058import com.unboundid.util.StaticUtils;
059import com.unboundid.util.ThreadSafety;
060import com.unboundid.util.ThreadSafetyLevel;
061import com.unboundid.util.Validator;
062
063
064
065/**
066 * This class defines an LDIF modify change record, which can be used to
067 * represent an LDAP modify request.  See the documentation for the
068 * {@link LDIFChangeRecord} class for an example demonstrating the process for
069 * interacting with LDIF change records.
070 */
071@NotMutable()
072@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
073public final class LDIFModifyChangeRecord
074       extends LDIFChangeRecord
075{
076  /**
077   * The name of the system property that will be used to indicate whether
078   * to always include a trailing dash after the last change in the LDIF
079   * representation of a modify change record.  By default, the dash will always
080   * be included.
081   */
082  @NotNull public static final  String PROPERTY_ALWAYS_INCLUDE_TRAILING_DASH =
083       "com.unboundid.ldif.modify.alwaysIncludeTrailingDash";
084
085
086
087  /**
088   * Indicates whether to always include a trailing dash after the last change
089   * in the LDIF representation.
090   */
091  private static boolean alwaysIncludeTrailingDash = true;
092
093
094
095  static
096  {
097    final String propValue =
098         StaticUtils.getSystemProperty(PROPERTY_ALWAYS_INCLUDE_TRAILING_DASH);
099    if ((propValue != null) && (propValue.equalsIgnoreCase("false")))
100    {
101      alwaysIncludeTrailingDash = false;
102    }
103  }
104
105
106
107  /**
108   * The serial version UID for this serializable class.
109   */
110  private static final long serialVersionUID = -7558098319600288036L;
111
112
113
114  // The set of modifications for this modify change record.
115  @NotNull private final Modification[] modifications;
116
117
118
119  /**
120   * Creates a new LDIF modify change record with the provided DN and set of
121   * modifications.
122   *
123   * @param  dn             The DN for this LDIF add change record.  It must not
124   *                        be {@code null}.
125   * @param  modifications  The set of modifications for this LDIF modify change
126   *                        record.  It must not be {@code null} or empty.
127   */
128  public LDIFModifyChangeRecord(@NotNull final String dn,
129                                @NotNull final Modification... modifications)
130  {
131    this(dn, modifications, null);
132  }
133
134
135
136  /**
137   * Creates a new LDIF modify change record with the provided DN and set of
138   * modifications.
139   *
140   * @param  dn             The DN for this LDIF add change record.  It must not
141   *                        be {@code null}.
142   * @param  modifications  The set of modifications for this LDIF modify change
143   *                        record.  It must not be {@code null} or empty.
144   * @param  controls       The set of controls for this LDIF modify change
145   *                        record.  It may be {@code null} or empty if there
146   *                        are no controls.
147   */
148  public LDIFModifyChangeRecord(@NotNull final String dn,
149                                @NotNull final Modification[] modifications,
150                                @Nullable final List<Control> controls)
151  {
152    super(dn, controls);
153
154    Validator.ensureNotNull(modifications);
155    Validator.ensureTrue(modifications.length > 0,
156         "LDIFModifyChangeRecord.modifications must not be empty.");
157
158    this.modifications = modifications;
159  }
160
161
162
163  /**
164   * Creates a new LDIF modify change record with the provided DN and set of
165   * modifications.
166   *
167   * @param  dn             The DN for this LDIF add change record.  It must not
168   *                        be {@code null}.
169   * @param  modifications  The set of modifications for this LDIF modify change
170   *                        record.  It must not be {@code null} or empty.
171   */
172  public LDIFModifyChangeRecord(@NotNull final String dn,
173                                @NotNull final List<Modification> modifications)
174  {
175    this(dn, modifications, null);
176  }
177
178
179
180  /**
181   * Creates a new LDIF modify change record with the provided DN and set of
182   * modifications.
183   *
184   * @param  dn             The DN for this LDIF add change record.  It must not
185   *                        be {@code null}.
186   * @param  modifications  The set of modifications for this LDIF modify change
187   *                        record.  It must not be {@code null} or empty.
188   * @param  controls       The set of controls for this LDIF modify change
189   *                        record.  It may be {@code null} or empty if there
190   *                        are no controls.
191   */
192  public LDIFModifyChangeRecord(@NotNull final String dn,
193                                @NotNull final List<Modification> modifications,
194                                @Nullable final List<Control> controls)
195  {
196    super(dn, controls);
197
198    Validator.ensureNotNull(modifications);
199    Validator.ensureFalse(modifications.isEmpty(),
200         "LDIFModifyChangeRecord.modifications must not be empty.");
201
202    this.modifications = new Modification[modifications.size()];
203    modifications.toArray(this.modifications);
204  }
205
206
207
208  /**
209   * Creates a new LDIF modify change record from the provided modify request.
210   *
211   * @param  modifyRequest  The modify request to use to create this LDIF modify
212   *                        change record.  It must not be {@code null}.
213   */
214  public LDIFModifyChangeRecord(@NotNull final ModifyRequest modifyRequest)
215  {
216    super(modifyRequest.getDN(), modifyRequest.getControlList());
217
218    final List<Modification> mods = modifyRequest.getModifications();
219    modifications = new Modification[mods.size()];
220
221    final Iterator<Modification> iterator = mods.iterator();
222    for (int i=0; i < modifications.length; i++)
223    {
224      modifications[i] = iterator.next();
225    }
226  }
227
228
229
230  /**
231   * Indicates whether the LDIF representation of a modify change record should
232   * always include a trailing dash after the last (or only) change.
233   *
234   * @return  {@code true} if the LDIF representation of a modify change record
235   *          should always include a trailing dash after the last (or only)
236   *          change, or {@code false} if not.
237   */
238  public static boolean alwaysIncludeTrailingDash()
239  {
240    return alwaysIncludeTrailingDash;
241  }
242
243
244
245  /**
246   * Specifies whether the LDIF representation of a modify change record should
247   * always include a trailing dash after the last (or only) change.
248   *
249   * @param  alwaysIncludeTrailingDash  Indicates whether the LDIF
250   *                                    representation of a modify change record
251   *                                    should always include a trailing dash
252   *                                    after the last (or only) change.
253   */
254  public static void setAlwaysIncludeTrailingDash(
255                          final boolean alwaysIncludeTrailingDash)
256  {
257    LDIFModifyChangeRecord.alwaysIncludeTrailingDash =
258         alwaysIncludeTrailingDash;
259  }
260
261
262
263  /**
264   * Retrieves the set of modifications for this modify change record.
265   *
266   * @return  The set of modifications for this modify change record.
267   */
268  @NotNull()
269  public Modification[] getModifications()
270  {
271    return modifications;
272  }
273
274
275
276  /**
277   * Creates a modify request from this LDIF modify change record.  Any change
278   * record controls will be included in the request
279   *
280   * @return  The modify request created from this LDIF modify change record.
281   */
282  @NotNull()
283  public ModifyRequest toModifyRequest()
284  {
285    return toModifyRequest(true);
286  }
287
288
289
290  /**
291   * Creates a modify request from this LDIF modify change record, optionally
292   * including any change record controls in the request.
293   *
294   * @param  includeControls  Indicates whether to include any controls in the
295   *                          request.
296   *
297   * @return  The modify request created from this LDIF modify change record.
298   */
299  @NotNull()
300  public ModifyRequest toModifyRequest(final boolean includeControls)
301  {
302    final ModifyRequest modifyRequest =
303         new ModifyRequest(getDN(), modifications);
304    if (includeControls)
305    {
306      modifyRequest.setControls(getControls());
307    }
308
309    return modifyRequest;
310  }
311
312
313
314  /**
315   * {@inheritDoc}
316   */
317  @Override()
318  @NotNull()
319  public ChangeType getChangeType()
320  {
321    return ChangeType.MODIFY;
322  }
323
324
325
326  /**
327   * {@inheritDoc}
328   */
329  @Override()
330  @NotNull()
331  public LDIFModifyChangeRecord duplicate(@Nullable final Control... controls)
332  {
333    return new LDIFModifyChangeRecord(getDN(), modifications,
334         StaticUtils.toList(controls));
335  }
336
337
338
339  /**
340   * {@inheritDoc}
341   */
342  @Override()
343  @NotNull()
344  public LDAPResult processChange(@NotNull final LDAPInterface connection,
345                                  final boolean includeControls)
346         throws LDAPException
347  {
348    return connection.modify(toModifyRequest(includeControls));
349  }
350
351
352
353  /**
354   * {@inheritDoc}
355   */
356  @Override()
357  @NotNull()
358  public String[] toLDIF(final int wrapColumn)
359  {
360    List<String> ldifLines = new ArrayList<>(modifications.length*4);
361    encodeNameAndValue("dn", new ASN1OctetString(getDN()), ldifLines);
362
363    for (final Control c : getControls())
364    {
365      encodeNameAndValue("control", encodeControlString(c), ldifLines);
366    }
367
368    ldifLines.add("changetype: modify");
369
370    for (int i=0; i < modifications.length; i++)
371    {
372      final String attrName = modifications[i].getAttributeName();
373
374      switch (modifications[i].getModificationType().intValue())
375      {
376        case 0:
377          ldifLines.add("add: " + attrName);
378          break;
379        case 1:
380          ldifLines.add("delete: " + attrName);
381          break;
382        case 2:
383          ldifLines.add("replace: " + attrName);
384          break;
385        case 3:
386          ldifLines.add("increment: " + attrName);
387          break;
388        default:
389          // This should never happen.
390          continue;
391      }
392
393      for (final ASN1OctetString value : modifications[i].getRawValues())
394      {
395        encodeNameAndValue(attrName, value, ldifLines);
396      }
397
398      if (alwaysIncludeTrailingDash || (i < (modifications.length - 1)))
399      {
400        ldifLines.add("-");
401      }
402    }
403
404    if (wrapColumn > 2)
405    {
406      ldifLines = LDIFWriter.wrapLines(wrapColumn, ldifLines);
407    }
408
409    final String[] ldifArray = new String[ldifLines.size()];
410    ldifLines.toArray(ldifArray);
411    return ldifArray;
412  }
413
414
415
416  /**
417   * {@inheritDoc}
418   */
419  @Override()
420  public void toLDIF(@NotNull final ByteStringBuffer buffer,
421                     final int wrapColumn)
422  {
423    LDIFWriter.encodeNameAndValue("dn", new ASN1OctetString(getDN()), buffer,
424         wrapColumn);
425    buffer.append(StaticUtils.EOL_BYTES);
426
427    for (final Control c : getControls())
428    {
429      LDIFWriter.encodeNameAndValue("control", encodeControlString(c), buffer,
430           wrapColumn);
431      buffer.append(StaticUtils.EOL_BYTES);
432    }
433
434    LDIFWriter.encodeNameAndValue("changetype", new ASN1OctetString("modify"),
435                                  buffer, wrapColumn);
436    buffer.append(StaticUtils.EOL_BYTES);
437
438    for (int i=0; i < modifications.length; i++)
439    {
440      final String attrName = modifications[i].getAttributeName();
441
442      switch (modifications[i].getModificationType().intValue())
443      {
444        case 0:
445          LDIFWriter.encodeNameAndValue("add", new ASN1OctetString(attrName),
446                                        buffer, wrapColumn);
447          buffer.append(StaticUtils.EOL_BYTES);
448          break;
449        case 1:
450          LDIFWriter.encodeNameAndValue("delete", new ASN1OctetString(attrName),
451                                        buffer, wrapColumn);
452          buffer.append(StaticUtils.EOL_BYTES);
453          break;
454        case 2:
455          LDIFWriter.encodeNameAndValue("replace",
456                                        new ASN1OctetString(attrName), buffer,
457                                        wrapColumn);
458          buffer.append(StaticUtils.EOL_BYTES);
459          break;
460        case 3:
461          LDIFWriter.encodeNameAndValue("increment",
462                                        new ASN1OctetString(attrName), buffer,
463                                        wrapColumn);
464          buffer.append(StaticUtils.EOL_BYTES);
465          break;
466        default:
467          // This should never happen.
468          continue;
469      }
470
471      for (final ASN1OctetString value : modifications[i].getRawValues())
472      {
473        LDIFWriter.encodeNameAndValue(attrName, value, buffer, wrapColumn);
474        buffer.append(StaticUtils.EOL_BYTES);
475      }
476
477      if (alwaysIncludeTrailingDash || (i < (modifications.length - 1)))
478      {
479        buffer.append('-');
480        buffer.append(StaticUtils.EOL_BYTES);
481      }
482    }
483  }
484
485
486
487  /**
488   * {@inheritDoc}
489   */
490  @Override()
491  public void toLDIFString(@NotNull final StringBuilder buffer,
492                           final int wrapColumn)
493  {
494    LDIFWriter.encodeNameAndValue("dn", new ASN1OctetString(getDN()), buffer,
495         wrapColumn);
496    buffer.append(StaticUtils.EOL);
497
498    for (final Control c : getControls())
499    {
500      LDIFWriter.encodeNameAndValue("control", encodeControlString(c), buffer,
501           wrapColumn);
502      buffer.append(StaticUtils.EOL);
503    }
504
505    LDIFWriter.encodeNameAndValue("changetype", new ASN1OctetString("modify"),
506                                  buffer, wrapColumn);
507    buffer.append(StaticUtils.EOL);
508
509    for (int i=0; i < modifications.length; i++)
510    {
511      final String attrName = modifications[i].getAttributeName();
512
513      switch (modifications[i].getModificationType().intValue())
514      {
515        case 0:
516          LDIFWriter.encodeNameAndValue("add", new ASN1OctetString(attrName),
517                                        buffer, wrapColumn);
518          buffer.append(StaticUtils.EOL);
519          break;
520        case 1:
521          LDIFWriter.encodeNameAndValue("delete", new ASN1OctetString(attrName),
522                                        buffer, wrapColumn);
523          buffer.append(StaticUtils.EOL);
524          break;
525        case 2:
526          LDIFWriter.encodeNameAndValue("replace",
527                                        new ASN1OctetString(attrName), buffer,
528                                        wrapColumn);
529          buffer.append(StaticUtils.EOL);
530          break;
531        case 3:
532          LDIFWriter.encodeNameAndValue("increment",
533                                        new ASN1OctetString(attrName), buffer,
534                                        wrapColumn);
535          buffer.append(StaticUtils.EOL);
536          break;
537        default:
538          // This should never happen.
539          continue;
540      }
541
542      for (final ASN1OctetString value : modifications[i].getRawValues())
543      {
544        LDIFWriter.encodeNameAndValue(attrName, value, buffer, wrapColumn);
545        buffer.append(StaticUtils.EOL);
546      }
547
548      if (alwaysIncludeTrailingDash || (i < (modifications.length - 1)))
549      {
550        buffer.append('-');
551        buffer.append(StaticUtils.EOL);
552      }
553    }
554  }
555
556
557
558  /**
559   * {@inheritDoc}
560   */
561  @Override()
562  public int hashCode()
563  {
564    int hashCode;
565    try
566    {
567      hashCode = getParsedDN().hashCode();
568    }
569    catch (final Exception e)
570    {
571      Debug.debugException(e);
572      hashCode = StaticUtils.toLowerCase(getDN()).hashCode();
573    }
574
575    for (final Modification m : modifications)
576    {
577      hashCode += m.hashCode();
578    }
579
580    return hashCode;
581  }
582
583
584
585  /**
586   * {@inheritDoc}
587   */
588  @Override()
589  public boolean equals(@Nullable final Object o)
590  {
591    if (o == null)
592    {
593      return false;
594    }
595
596    if (o == this)
597    {
598      return true;
599    }
600
601    if (! (o instanceof LDIFModifyChangeRecord))
602    {
603      return false;
604    }
605
606    final LDIFModifyChangeRecord r = (LDIFModifyChangeRecord) o;
607
608    final HashSet<Control> c1 = new HashSet<>(getControls());
609    final HashSet<Control> c2 = new HashSet<>(r.getControls());
610    if (! c1.equals(c2))
611    {
612      return false;
613    }
614
615    try
616    {
617      if (! getParsedDN().equals(r.getParsedDN()))
618      {
619        return false;
620      }
621    }
622    catch (final Exception e)
623    {
624      Debug.debugException(e);
625      if (! StaticUtils.toLowerCase(getDN()).equals(
626           StaticUtils.toLowerCase(r.getDN())))
627      {
628        return false;
629      }
630    }
631
632    if (modifications.length != r.modifications.length)
633    {
634      return false;
635    }
636
637    for (int i=0; i < modifications.length; i++)
638    {
639      if (! modifications[i].equals(r.modifications[i]))
640      {
641        return false;
642      }
643    }
644
645    return true;
646  }
647
648
649
650  /**
651   * {@inheritDoc}
652   */
653  @Override()
654  public void toString(@NotNull final StringBuilder buffer)
655  {
656    buffer.append("LDIFModifyChangeRecord(dn='");
657    buffer.append(getDN());
658    buffer.append("', mods={");
659
660    for (int i=0; i < modifications.length; i++)
661    {
662      if (i > 0)
663      {
664        buffer.append(", ");
665      }
666      modifications[i].toString(buffer);
667    }
668    buffer.append('}');
669
670    final List<Control> controls = getControls();
671    if (! controls.isEmpty())
672    {
673      buffer.append(", controls={");
674
675      final Iterator<Control> iterator = controls.iterator();
676      while (iterator.hasNext())
677      {
678        iterator.next().toString(buffer);
679        if (iterator.hasNext())
680        {
681          buffer.append(',');
682        }
683      }
684
685      buffer.append('}');
686    }
687
688    buffer.append(')');
689  }
690}