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