001/*
002 * Copyright 2007-2022 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2007-2022 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-2022 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.ldap.sdk;
037
038
039
040import java.util.ArrayList;
041import java.util.Arrays;
042import java.util.Collections;
043import java.util.List;
044import java.util.Timer;
045import java.util.concurrent.LinkedBlockingQueue;
046import java.util.concurrent.TimeUnit;
047import java.util.logging.Level;
048
049import com.unboundid.asn1.ASN1Buffer;
050import com.unboundid.asn1.ASN1BufferSequence;
051import com.unboundid.asn1.ASN1Element;
052import com.unboundid.asn1.ASN1OctetString;
053import com.unboundid.asn1.ASN1Sequence;
054import com.unboundid.ldap.protocol.LDAPMessage;
055import com.unboundid.ldap.protocol.LDAPResponse;
056import com.unboundid.ldap.protocol.ProtocolOp;
057import com.unboundid.ldif.LDIFChangeRecord;
058import com.unboundid.ldif.LDIFException;
059import com.unboundid.ldif.LDIFModifyChangeRecord;
060import com.unboundid.ldif.LDIFReader;
061import com.unboundid.util.Debug;
062import com.unboundid.util.InternalUseOnly;
063import com.unboundid.util.Mutable;
064import com.unboundid.util.NotNull;
065import com.unboundid.util.Nullable;
066import com.unboundid.util.StaticUtils;
067import com.unboundid.util.ThreadSafety;
068import com.unboundid.util.ThreadSafetyLevel;
069import com.unboundid.util.Validator;
070
071import static com.unboundid.ldap.sdk.LDAPMessages.*;
072
073
074
075/**
076 * This class implements the processing necessary to perform an LDAPv3 modify
077 * operation, which can be used to update an entry in the directory server.  A
078 * modify request contains the DN of the entry to modify, as well as one or more
079 * changes to apply to that entry.  See the {@link Modification} class for more
080 * information about the types of modifications that may be processed.
081 * <BR><BR>
082 * A modify request can be created with a DN and set of modifications, but it
083 * can also be as a list of the lines that comprise the LDIF representation of
084 * the modification as described in
085 * <A HREF="http://www.ietf.org/rfc/rfc2849.txt">RFC 2849</A>.  For example, the
086 * following code demonstrates creating a modify request from the LDIF
087 * representation of the modification:
088 * <PRE>
089 *   ModifyRequest modifyRequest = new ModifyRequest(
090 *     "dn: dc=example,dc=com",
091 *     "changetype: modify",
092 *     "replace: description",
093 *     "description: This is the new description.");
094 * </PRE>
095 * <BR><BR>
096 * {@code ModifyRequest} objects are mutable and therefore can be altered and
097 * re-used for multiple requests.  Note, however, that {@code ModifyRequest}
098 * objects are not threadsafe and therefore a single {@code ModifyRequest}
099 * object instance should not be used to process multiple requests at the same
100 * time.
101 */
102@Mutable()
103@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
104public final class ModifyRequest
105       extends UpdatableLDAPRequest
106       implements ReadOnlyModifyRequest, ResponseAcceptor, ProtocolOp
107{
108  /**
109   * The serial version UID for this serializable class.
110   */
111  private static final long serialVersionUID = -4747622844001634758L;
112
113
114
115  // The queue that will be used to receive response messages from the server.
116  @NotNull private final LinkedBlockingQueue<LDAPResponse> responseQueue =
117       new LinkedBlockingQueue<>();
118
119  // The set of modifications to perform.
120  @NotNull private final ArrayList<Modification> modifications;
121
122  // The message ID from the last LDAP message sent from this request.
123  private int messageID = -1;
124
125  // The DN of the entry to modify.
126  @NotNull private String dn;
127
128
129
130  /**
131   * Creates a new modify request with the provided information.
132   *
133   * @param  dn   The DN of the entry to modify.  It must not be {@code null}.
134   * @param  mod  The modification to apply to the entry.  It must not be
135   *              {@code null}.
136   */
137  public ModifyRequest(@NotNull final String dn,
138                       @NotNull final Modification mod)
139  {
140    super(null);
141
142    Validator.ensureNotNull(dn, mod);
143
144    this.dn = dn;
145
146    modifications = new ArrayList<>(1);
147    modifications.add(mod);
148  }
149
150
151
152  /**
153   * Creates a new modify request with the provided information.
154   *
155   * @param  dn    The DN of the entry to modify.  It must not be {@code null}.
156   * @param  mods  The set of modifications to apply to the entry.  It must not
157   *               be {@code null} or empty.
158   */
159  public ModifyRequest(@NotNull final String dn,
160                       @NotNull final Modification... mods)
161  {
162    super(null);
163
164    Validator.ensureNotNull(dn, mods);
165    Validator.ensureFalse(mods.length == 0,
166         "ModifyRequest.mods must not be empty.");
167
168    this.dn = dn;
169
170    modifications = new ArrayList<>(mods.length);
171    modifications.addAll(Arrays.asList(mods));
172  }
173
174
175
176  /**
177   * Creates a new modify request with the provided information.
178   *
179   * @param  dn    The DN of the entry to modify.  It must not be {@code null}.
180   * @param  mods  The set of modifications to apply to the entry.  It must not
181   *               be {@code null} or empty.
182   */
183  public ModifyRequest(@NotNull final String dn,
184                       @NotNull final List<Modification> mods)
185  {
186    super(null);
187
188    Validator.ensureNotNull(dn, mods);
189    Validator.ensureFalse(mods.isEmpty(),
190         "ModifyRequest.mods must not be empty.");
191
192    this.dn = dn;
193
194    modifications = new ArrayList<>(mods);
195  }
196
197
198
199  /**
200   * Creates a new modify request with the provided information.
201   *
202   * @param  dn   The DN of the entry to modify.  It must not be {@code null}.
203   * @param  mod  The modification to apply to the entry.  It must not be
204   *              {@code null}.
205   */
206  public ModifyRequest(@NotNull final DN dn, @NotNull final Modification mod)
207  {
208    super(null);
209
210    Validator.ensureNotNull(dn, mod);
211
212    this.dn = dn.toString();
213
214    modifications = new ArrayList<>(1);
215    modifications.add(mod);
216  }
217
218
219
220  /**
221   * Creates a new modify request with the provided information.
222   *
223   * @param  dn    The DN of the entry to modify.  It must not be {@code null}.
224   * @param  mods  The set of modifications to apply to the entry.  It must not
225   *               be {@code null} or empty.
226   */
227  public ModifyRequest(@NotNull final DN dn,
228                       @NotNull final Modification... mods)
229  {
230    super(null);
231
232    Validator.ensureNotNull(dn, mods);
233    Validator.ensureFalse(mods.length == 0,
234         "ModifyRequest.mods must not be empty.");
235
236    this.dn = dn.toString();
237
238    modifications = new ArrayList<>(mods.length);
239    modifications.addAll(Arrays.asList(mods));
240  }
241
242
243
244  /**
245   * Creates a new modify request with the provided information.
246   *
247   * @param  dn    The DN of the entry to modify.  It must not be {@code null}.
248   * @param  mods  The set of modifications to apply to the entry.  It must not
249   *               be {@code null} or empty.
250   */
251  public ModifyRequest(@NotNull final DN dn,
252                       @NotNull final List<Modification> mods)
253  {
254    super(null);
255
256    Validator.ensureNotNull(dn, mods);
257    Validator.ensureFalse(mods.isEmpty(),
258         "ModifyRequest.mods must not be empty.");
259
260    this.dn = dn.toString();
261
262    modifications = new ArrayList<>(mods);
263  }
264
265
266
267  /**
268   * Creates a new modify request with the provided information.
269   *
270   * @param  dn        The DN of the entry to modify.  It must not be
271   *                   {@code null}.
272   * @param  mod       The modification to apply to the entry.  It must not be
273   *                   {@code null}.
274   * @param  controls  The set of controls to include in the request.
275   */
276  public ModifyRequest(@NotNull final String dn,
277                       @NotNull final Modification mod,
278                       @Nullable final Control[] controls)
279  {
280    super(controls);
281
282    Validator.ensureNotNull(dn, mod);
283
284    this.dn = dn;
285
286    modifications = new ArrayList<>(1);
287    modifications.add(mod);
288  }
289
290
291
292  /**
293   * Creates a new modify request with the provided information.
294   *
295   * @param  dn        The DN of the entry to modify.  It must not be
296   *                   {@code null}.
297   * @param  mods      The set of modifications to apply to the entry.  It must
298   *                   not be {@code null} or empty.
299   * @param  controls  The set of controls to include in the request.
300   */
301  public ModifyRequest(@NotNull final String dn,
302                       @NotNull final Modification[] mods,
303                       @Nullable final Control[] controls)
304  {
305    super(controls);
306
307    Validator.ensureNotNull(dn, mods);
308    Validator.ensureFalse(mods.length == 0,
309         "ModifyRequest.mods must not be empty.");
310
311    this.dn = dn;
312
313    modifications = new ArrayList<>(mods.length);
314    modifications.addAll(Arrays.asList(mods));
315  }
316
317
318
319  /**
320   * Creates a new modify request with the provided information.
321   *
322   * @param  dn        The DN of the entry to modify.  It must not be
323   *                   {@code null}.
324   * @param  mods      The set of modifications to apply to the entry.  It must
325   *                   not be {@code null} or empty.
326   * @param  controls  The set of controls to include in the request.
327   */
328  public ModifyRequest(@NotNull final String dn,
329                       @NotNull final List<Modification> mods,
330                       @Nullable final Control[] controls)
331  {
332    super(controls);
333
334    Validator.ensureNotNull(dn, mods);
335    Validator.ensureFalse(mods.isEmpty(),
336         "ModifyRequest.mods must not be empty.");
337
338    this.dn = dn;
339
340    modifications = new ArrayList<>(mods);
341  }
342
343
344
345  /**
346   * Creates a new modify request with the provided information.
347   *
348   * @param  dn        The DN of the entry to modify.  It must not be
349   *                   {@code null}.
350   * @param  mod       The modification to apply to the entry.  It must not be
351   *                   {@code null}.
352   * @param  controls  The set of controls to include in the request.
353   */
354  public ModifyRequest(@NotNull final DN dn, @NotNull final Modification mod,
355                       @Nullable final Control[] controls)
356  {
357    super(controls);
358
359    Validator.ensureNotNull(dn, mod);
360
361    this.dn = dn.toString();
362
363    modifications = new ArrayList<>(1);
364    modifications.add(mod);
365  }
366
367
368
369  /**
370   * Creates a new modify request with the provided information.
371   *
372   * @param  dn        The DN of the entry to modify.  It must not be
373   *                   {@code null}.
374   * @param  mods      The set of modifications to apply to the entry.  It must
375   *                   not be {@code null} or empty.
376   * @param  controls  The set of controls to include in the request.
377   */
378  public ModifyRequest(@NotNull final DN dn, @NotNull final Modification[] mods,
379                       @Nullable final Control[] controls)
380  {
381    super(controls);
382
383    Validator.ensureNotNull(dn, mods);
384    Validator.ensureFalse(mods.length == 0,
385         "ModifyRequest.mods must not be empty.");
386
387    this.dn = dn.toString();
388
389    modifications = new ArrayList<>(mods.length);
390    modifications.addAll(Arrays.asList(mods));
391  }
392
393
394
395  /**
396   * Creates a new modify request with the provided information.
397   *
398   * @param  dn        The DN of the entry to modify.  It must not be
399   *                   {@code null}.
400   * @param  mods      The set of modifications to apply to the entry.  It must
401   *                   not be {@code null} or empty.
402   * @param  controls  The set of controls to include in the request.
403   */
404  public ModifyRequest(@NotNull final DN dn,
405                       @NotNull final List<Modification> mods,
406                       @Nullable final Control[] controls)
407  {
408    super(controls);
409
410    Validator.ensureNotNull(dn, mods);
411    Validator.ensureFalse(mods.isEmpty(),
412         "ModifyRequest.mods must not be empty.");
413
414    this.dn = dn.toString();
415
416    modifications = new ArrayList<>(mods);
417  }
418
419
420
421  /**
422   * Creates a new modify request from the provided LDIF representation of the
423   * changes.
424   *
425   * @param  ldifModificationLines  The lines that comprise an LDIF
426   *                                representation of a modify change record.
427   *                                It must not be {@code null} or empty.
428   *
429   * @throws  LDIFException  If the provided set of lines cannot be parsed as an
430   *                         LDIF modify change record.
431   */
432  public ModifyRequest(@NotNull final String... ldifModificationLines)
433         throws LDIFException
434  {
435    super(null);
436
437    final LDIFChangeRecord changeRecord =
438         LDIFReader.decodeChangeRecord(ldifModificationLines);
439    if (! (changeRecord instanceof LDIFModifyChangeRecord))
440    {
441      throw new LDIFException(ERR_MODIFY_INVALID_LDIF.get(), 0, false,
442                              ldifModificationLines, null);
443    }
444
445    final LDIFModifyChangeRecord modifyRecord =
446         (LDIFModifyChangeRecord) changeRecord;
447    final ModifyRequest r = modifyRecord.toModifyRequest();
448
449    dn            = r.dn;
450    modifications = r.modifications;
451  }
452
453
454
455  /**
456   * {@inheritDoc}
457   */
458  @Override()
459  @NotNull()
460  public String getDN()
461  {
462    return dn;
463  }
464
465
466
467  /**
468   * Specifies the DN of the entry to modify.
469   *
470   * @param  dn  The DN of the entry to modify.  It must not be {@code null}.
471   */
472  public void setDN(@NotNull final String dn)
473  {
474    Validator.ensureNotNull(dn);
475
476    this.dn = dn;
477  }
478
479
480
481  /**
482   * Specifies the DN of the entry to modify.
483   *
484   * @param  dn  The DN of the entry to modify.  It must not be {@code null}.
485   */
486  public void setDN(@NotNull final DN dn)
487  {
488    Validator.ensureNotNull(dn);
489
490    this.dn = dn.toString();
491  }
492
493
494
495  /**
496   * {@inheritDoc}
497   */
498  @Override()
499  @NotNull()
500  public List<Modification> getModifications()
501  {
502    return Collections.unmodifiableList(modifications);
503  }
504
505
506
507  /**
508   * Adds the provided modification to the set of modifications for this modify
509   * request.
510   *
511   * @param  mod  The modification to be added.  It must not be {@code null}.
512   */
513  public void addModification(@NotNull final Modification mod)
514  {
515    Validator.ensureNotNull(mod);
516
517    modifications.add(mod);
518  }
519
520
521
522  /**
523   * Removes the provided modification from the set of modifications for this
524   * modify request.
525   *
526   * @param  mod  The modification to be removed.  It must not be {@code null}.
527   *
528   * @return  {@code true} if the specified modification was found and removed,
529   *          or {@code false} if not.
530   */
531  public boolean removeModification(@NotNull final Modification mod)
532  {
533    Validator.ensureNotNull(mod);
534
535    return modifications.remove(mod);
536  }
537
538
539
540  /**
541   * Replaces the existing set of modifications for this modify request with the
542   * provided modification.
543   *
544   * @param  mod  The modification to use for this modify request.  It must not
545   *              be {@code null}.
546   */
547  public void setModifications(@NotNull final Modification mod)
548  {
549    Validator.ensureNotNull(mod);
550
551    modifications.clear();
552    modifications.add(mod);
553  }
554
555
556
557  /**
558   * Replaces the existing set of modifications for this modify request with the
559   * provided modifications.
560   *
561   * @param  mods  The set of modification to use for this modify request.  It
562   *               must not be {@code null} or empty.
563   */
564  public void setModifications(@NotNull final Modification[] mods)
565  {
566    Validator.ensureNotNull(mods);
567    Validator.ensureFalse(mods.length == 0,
568         "ModifyRequest.setModifications.mods must not be empty.");
569
570    modifications.clear();
571    modifications.addAll(Arrays.asList(mods));
572  }
573
574
575
576  /**
577   * Replaces the existing set of modifications for this modify request with the
578   * provided modifications.
579   *
580   * @param  mods  The set of modification to use for this modify request.  It
581   *               must not be {@code null} or empty.
582   */
583  public void setModifications(@NotNull final List<Modification> mods)
584  {
585    Validator.ensureNotNull(mods);
586    Validator.ensureFalse(mods.isEmpty(),
587         "ModifyRequest.setModifications.mods must not be empty.");
588
589    modifications.clear();
590    modifications.addAll(mods);
591  }
592
593
594
595  /**
596   * {@inheritDoc}
597   */
598  @Override()
599  public byte getProtocolOpType()
600  {
601    return LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_REQUEST;
602  }
603
604
605
606  /**
607   * {@inheritDoc}
608   */
609  @Override()
610  public void writeTo(@NotNull final ASN1Buffer writer)
611  {
612    final ASN1BufferSequence requestSequence =
613         writer.beginSequence(LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_REQUEST);
614    writer.addOctetString(dn);
615
616    final ASN1BufferSequence modSequence = writer.beginSequence();
617    for (final Modification m : modifications)
618    {
619      m.writeTo(writer);
620    }
621    modSequence.end();
622    requestSequence.end();
623  }
624
625
626
627  /**
628   * Encodes the modify request protocol op to an ASN.1 element.
629   *
630   * @return  The ASN.1 element with the encoded modify request protocol op.
631   */
632  @Override()
633  @NotNull()
634  public ASN1Element encodeProtocolOp()
635  {
636    final ASN1Element[] modElements = new ASN1Element[modifications.size()];
637    for (int i=0; i < modElements.length; i++)
638    {
639      modElements[i] = modifications.get(i).encode();
640    }
641
642    final ASN1Element[] protocolOpElements =
643    {
644      new ASN1OctetString(dn),
645      new ASN1Sequence(modElements)
646    };
647
648
649
650    return new ASN1Sequence(LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_REQUEST,
651                            protocolOpElements);
652  }
653
654
655
656  /**
657   * Sends this modify request to the directory server over the provided
658   * connection and returns the associated response.
659   *
660   * @param  connection  The connection to use to communicate with the directory
661   *                     server.
662   * @param  depth       The current referral depth for this request.  It should
663   *                     always be one for the initial request, and should only
664   *                     be incremented when following referrals.
665   *
666   * @return  An LDAP result object that provides information about the result
667   *          of the modify processing.
668   *
669   * @throws  LDAPException  If a problem occurs while sending the request or
670   *                         reading the response.
671   */
672  @Override()
673  @NotNull()
674  protected LDAPResult process(@NotNull final LDAPConnection connection,
675                               final int depth)
676            throws LDAPException
677  {
678    if (connection.synchronousMode())
679    {
680      @SuppressWarnings("deprecation")
681      final boolean autoReconnect =
682           connection.getConnectionOptions().autoReconnect();
683      return processSync(connection, depth, autoReconnect);
684    }
685
686    final long requestTime = System.nanoTime();
687    processAsync(connection, null);
688
689    try
690    {
691      // Wait for and process the response.
692      final LDAPResponse response;
693      try
694      {
695        final long responseTimeout = getResponseTimeoutMillis(connection);
696        if (responseTimeout > 0)
697        {
698          response = responseQueue.poll(responseTimeout, TimeUnit.MILLISECONDS);
699        }
700        else
701        {
702          response = responseQueue.take();
703        }
704      }
705      catch (final InterruptedException ie)
706      {
707        Debug.debugException(ie);
708        Thread.currentThread().interrupt();
709        throw new LDAPException(ResultCode.LOCAL_ERROR,
710             ERR_MODIFY_INTERRUPTED.get(connection.getHostPort()), ie);
711      }
712
713      return handleResponse(connection, response, requestTime, depth, false);
714    }
715    finally
716    {
717      connection.deregisterResponseAcceptor(messageID);
718    }
719  }
720
721
722
723  /**
724   * Sends this modify request to the directory server over the provided
725   * connection and returns the message ID for the request.
726   *
727   * @param  connection      The connection to use to communicate with the
728   *                         directory server.
729   * @param  resultListener  The async result listener that is to be notified
730   *                         when the response is received.  It may be
731   *                         {@code null} only if the result is to be processed
732   *                         by this class.
733   *
734   * @return  The async request ID created for the operation, or {@code null} if
735   *          the provided {@code resultListener} is {@code null} and the
736   *          operation will not actually be processed asynchronously.
737   *
738   * @throws  LDAPException  If a problem occurs while sending the request.
739   */
740  @Nullable()
741  AsyncRequestID processAsync(@NotNull final LDAPConnection connection,
742                      @Nullable final AsyncResultListener resultListener)
743                 throws LDAPException
744  {
745    // Create the LDAP message.
746    messageID = connection.nextMessageID();
747    final LDAPMessage message = new LDAPMessage(messageID, this, getControls());
748
749
750    // If the provided async result listener is {@code null}, then we'll use
751    // this class as the message acceptor.  Otherwise, create an async helper
752    // and use it as the message acceptor.
753    final AsyncRequestID asyncRequestID;
754    final long timeout = getResponseTimeoutMillis(connection);
755    if (resultListener == null)
756    {
757      asyncRequestID = null;
758      connection.registerResponseAcceptor(messageID, this);
759    }
760    else
761    {
762      final AsyncHelper helper = new AsyncHelper(connection,
763           OperationType.MODIFY, messageID, resultListener,
764           getIntermediateResponseListener());
765      connection.registerResponseAcceptor(messageID, helper);
766      asyncRequestID = helper.getAsyncRequestID();
767
768      if (timeout > 0L)
769      {
770        final Timer timer = connection.getTimer();
771        final AsyncTimeoutTimerTask timerTask =
772             new AsyncTimeoutTimerTask(helper);
773        timer.schedule(timerTask, timeout);
774        asyncRequestID.setTimerTask(timerTask);
775      }
776    }
777
778
779    // Send the request to the server.
780    try
781    {
782      Debug.debugLDAPRequest(Level.INFO, this, messageID, connection);
783
784      final LDAPConnectionLogger logger =
785           connection.getConnectionOptions().getConnectionLogger();
786      if (logger != null)
787      {
788        logger.logModifyRequest(connection, messageID, this);
789      }
790
791      connection.getConnectionStatistics().incrementNumModifyRequests();
792      connection.sendMessage(message, timeout);
793      return asyncRequestID;
794    }
795    catch (final LDAPException le)
796    {
797      Debug.debugException(le);
798
799      connection.deregisterResponseAcceptor(messageID);
800      throw le;
801    }
802  }
803
804
805
806  /**
807   * Processes this modify operation in synchronous mode, in which the same
808   * thread will send the request and read the response.
809   *
810   * @param  connection  The connection to use to communicate with the directory
811   *                     server.
812   * @param  depth       The current referral depth for this request.  It should
813   *                     always be one for the initial request, and should only
814   *                     be incremented when following referrals.
815   * @param  allowRetry  Indicates whether the request may be re-tried on a
816   *                     re-established connection if the initial attempt fails
817   *                     in a way that indicates the connection is no longer
818   *                     valid and autoReconnect is true.
819   *
820   * @return  An LDAP result object that provides information about the result
821   *          of the modify processing.
822   *
823   * @throws  LDAPException  If a problem occurs while sending the request or
824   *                         reading the response.
825   */
826  @NotNull()
827  private LDAPResult processSync(@NotNull final LDAPConnection connection,
828                                 final int depth, final boolean allowRetry)
829          throws LDAPException
830  {
831    // Create the LDAP message.
832    messageID = connection.nextMessageID();
833    final LDAPMessage message =
834         new LDAPMessage(messageID,  this, getControls());
835
836
837    // Send the request to the server.
838    final long requestTime = System.nanoTime();
839    Debug.debugLDAPRequest(Level.INFO, this, messageID, connection);
840
841    final LDAPConnectionLogger logger =
842         connection.getConnectionOptions().getConnectionLogger();
843    if (logger != null)
844    {
845      logger.logModifyRequest(connection, messageID, this);
846    }
847
848    connection.getConnectionStatistics().incrementNumModifyRequests();
849    try
850    {
851      connection.sendMessage(message, getResponseTimeoutMillis(connection));
852    }
853    catch (final LDAPException le)
854    {
855      Debug.debugException(le);
856
857      if (allowRetry)
858      {
859        final LDAPResult retryResult = reconnectAndRetry(connection, depth,
860             le.getResultCode());
861        if (retryResult != null)
862        {
863          return retryResult;
864        }
865      }
866
867      throw le;
868    }
869
870    while (true)
871    {
872      final LDAPResponse response;
873      try
874      {
875        response = connection.readResponse(messageID);
876      }
877      catch (final LDAPException le)
878      {
879        Debug.debugException(le);
880
881        if ((le.getResultCode() == ResultCode.TIMEOUT) &&
882            connection.getConnectionOptions().abandonOnTimeout())
883        {
884          connection.abandon(messageID);
885        }
886
887        if (allowRetry)
888        {
889          final LDAPResult retryResult = reconnectAndRetry(connection, depth,
890               le.getResultCode());
891          if (retryResult != null)
892          {
893            return retryResult;
894          }
895        }
896
897        throw le;
898      }
899
900      if (response instanceof IntermediateResponse)
901      {
902        final IntermediateResponseListener listener =
903             getIntermediateResponseListener();
904        if (listener != null)
905        {
906          listener.intermediateResponseReturned(
907               (IntermediateResponse) response);
908        }
909      }
910      else
911      {
912        return handleResponse(connection, response, requestTime, depth,
913             allowRetry);
914      }
915    }
916  }
917
918
919
920  /**
921   * Performs the necessary processing for handling a response.
922   *
923   * @param  connection   The connection used to read the response.
924   * @param  response     The response to be processed.
925   * @param  requestTime  The time the request was sent to the server.
926   * @param  depth        The current referral depth for this request.  It
927   *                      should always be one for the initial request, and
928   *                      should only be incremented when following referrals.
929   * @param  allowRetry   Indicates whether the request may be re-tried on a
930   *                      re-established connection if the initial attempt fails
931   *                      in a way that indicates the connection is no longer
932   *                      valid and autoReconnect is true.
933   *
934   * @return  The modify result.
935   *
936   * @throws  LDAPException  If a problem occurs.
937   */
938  @NotNull()
939  private LDAPResult handleResponse(@NotNull final LDAPConnection connection,
940                                    @Nullable final LDAPResponse response,
941                                    final long requestTime, final int depth,
942                                    final boolean allowRetry)
943          throws LDAPException
944  {
945    if (response == null)
946    {
947      final long waitTime =
948           StaticUtils.nanosToMillis(System.nanoTime() - requestTime);
949      if (connection.getConnectionOptions().abandonOnTimeout())
950      {
951        connection.abandon(messageID);
952      }
953
954      throw new LDAPException(ResultCode.TIMEOUT,
955           ERR_MODIFY_CLIENT_TIMEOUT.get(waitTime, messageID, dn,
956                connection.getHostPort()));
957    }
958
959    connection.getConnectionStatistics().incrementNumModifyResponses(
960         System.nanoTime() - requestTime);
961    if (response instanceof ConnectionClosedResponse)
962    {
963      // The connection was closed while waiting for the response.
964      if (allowRetry)
965      {
966        final LDAPResult retryResult = reconnectAndRetry(connection, depth,
967             ResultCode.SERVER_DOWN);
968        if (retryResult != null)
969        {
970          return retryResult;
971        }
972      }
973
974      final ConnectionClosedResponse ccr = (ConnectionClosedResponse) response;
975      final String message = ccr.getMessage();
976      if (message == null)
977      {
978        throw new LDAPException(ccr.getResultCode(),
979             ERR_CONN_CLOSED_WAITING_FOR_MODIFY_RESPONSE.get(
980                  connection.getHostPort(), toString()));
981      }
982      else
983      {
984        throw new LDAPException(ccr.getResultCode(),
985             ERR_CONN_CLOSED_WAITING_FOR_MODIFY_RESPONSE_WITH_MESSAGE.get(
986                  connection.getHostPort(), toString(), message));
987      }
988    }
989
990    final LDAPResult result = (LDAPResult) response;
991    if ((result.getResultCode().equals(ResultCode.REFERRAL)) &&
992        followReferrals(connection))
993    {
994      if (depth >= connection.getConnectionOptions().getReferralHopLimit())
995      {
996        return new LDAPResult(messageID, ResultCode.REFERRAL_LIMIT_EXCEEDED,
997                              ERR_TOO_MANY_REFERRALS.get(),
998                              result.getMatchedDN(), result.getReferralURLs(),
999                              result.getResponseControls());
1000      }
1001
1002      return followReferral(result, connection, depth);
1003    }
1004    else
1005    {
1006      if (allowRetry)
1007      {
1008        final LDAPResult retryResult = reconnectAndRetry(connection, depth,
1009             result.getResultCode());
1010        if (retryResult != null)
1011        {
1012          return retryResult;
1013        }
1014      }
1015
1016      return result;
1017    }
1018  }
1019
1020
1021
1022  /**
1023   * Attempts to re-establish the connection and retry processing this request
1024   * on it.
1025   *
1026   * @param  connection  The connection to be re-established.
1027   * @param  depth       The current referral depth for this request.  It should
1028   *                     always be one for the initial request, and should only
1029   *                     be incremented when following referrals.
1030   * @param  resultCode  The result code for the previous operation attempt.
1031   *
1032   * @return  The result from re-trying the add, or {@code null} if it could not
1033   *          be re-tried.
1034   */
1035  @Nullable()
1036  private LDAPResult reconnectAndRetry(@NotNull final LDAPConnection connection,
1037                                       final int depth,
1038                                       @NotNull final ResultCode resultCode)
1039  {
1040    try
1041    {
1042      // We will only want to retry for certain result codes that indicate a
1043      // connection problem.
1044      switch (resultCode.intValue())
1045      {
1046        case ResultCode.SERVER_DOWN_INT_VALUE:
1047        case ResultCode.DECODING_ERROR_INT_VALUE:
1048        case ResultCode.CONNECT_ERROR_INT_VALUE:
1049          connection.reconnect();
1050          return processSync(connection, depth, false);
1051      }
1052    }
1053    catch (final Exception e)
1054    {
1055      Debug.debugException(e);
1056    }
1057
1058    return null;
1059  }
1060
1061
1062
1063  /**
1064   * Attempts to follow a referral to perform a modify operation in the target
1065   * server.
1066   *
1067   * @param  referralResult  The LDAP result object containing information about
1068   *                         the referral to follow.
1069   * @param  connection      The connection on which the referral was received.
1070   * @param  depth           The number of referrals followed in the course of
1071   *                         processing this request.
1072   *
1073   * @return  The result of attempting to process the modify operation by
1074   *          following the referral.
1075   *
1076   * @throws  LDAPException  If a problem occurs while attempting to establish
1077   *                         the referral connection, sending the request, or
1078   *                         reading the result.
1079   */
1080  @NotNull()
1081  private LDAPResult followReferral(@NotNull final LDAPResult referralResult,
1082                                    @NotNull final LDAPConnection connection,
1083                                    final int depth)
1084          throws LDAPException
1085  {
1086    for (final String urlString : referralResult.getReferralURLs())
1087    {
1088      try
1089      {
1090        final LDAPURL referralURL = new LDAPURL(urlString);
1091        final String host = referralURL.getHost();
1092
1093        if (host == null)
1094        {
1095          // We can't handle a referral in which there is no host.
1096          continue;
1097        }
1098
1099        final ModifyRequest modifyRequest;
1100        if (referralURL.baseDNProvided())
1101        {
1102          modifyRequest = new ModifyRequest(referralURL.getBaseDN(),
1103                                            modifications, getControls());
1104        }
1105        else
1106        {
1107          modifyRequest = this;
1108        }
1109
1110        final LDAPConnection referralConn = getReferralConnector(connection).
1111             getReferralConnection(referralURL, connection);
1112        try
1113        {
1114          return modifyRequest.process(referralConn, depth+1);
1115        }
1116        finally
1117        {
1118          referralConn.setDisconnectInfo(DisconnectType.REFERRAL, null, null);
1119          referralConn.close();
1120        }
1121      }
1122      catch (final LDAPException le)
1123      {
1124        Debug.debugException(le);
1125      }
1126    }
1127
1128    // If we've gotten here, then we could not follow any of the referral URLs,
1129    // so we'll just return the original referral result.
1130    return referralResult;
1131  }
1132
1133
1134
1135  /**
1136   * {@inheritDoc}
1137   */
1138  @InternalUseOnly()
1139  @Override()
1140  public void responseReceived(@NotNull final LDAPResponse response)
1141         throws LDAPException
1142  {
1143    try
1144    {
1145      responseQueue.put(response);
1146    }
1147    catch (final Exception e)
1148    {
1149      Debug.debugException(e);
1150
1151      if (e instanceof InterruptedException)
1152      {
1153        Thread.currentThread().interrupt();
1154      }
1155
1156      throw new LDAPException(ResultCode.LOCAL_ERROR,
1157           ERR_EXCEPTION_HANDLING_RESPONSE.get(
1158                StaticUtils.getExceptionMessage(e)),
1159           e);
1160    }
1161  }
1162
1163
1164
1165  /**
1166   * {@inheritDoc}
1167   */
1168  @Override()
1169  public int getLastMessageID()
1170  {
1171    return messageID;
1172  }
1173
1174
1175
1176  /**
1177   * {@inheritDoc}
1178   */
1179  @Override()
1180  @NotNull()
1181  public OperationType getOperationType()
1182  {
1183    return OperationType.MODIFY;
1184  }
1185
1186
1187
1188  /**
1189   * {@inheritDoc}
1190   */
1191  @Override()
1192  @NotNull()
1193  public ModifyRequest duplicate()
1194  {
1195    return duplicate(getControls());
1196  }
1197
1198
1199
1200  /**
1201   * {@inheritDoc}
1202   */
1203  @Override()
1204  @NotNull()
1205  public ModifyRequest duplicate(@Nullable final Control[] controls)
1206  {
1207    final ModifyRequest r = new ModifyRequest(dn,
1208         new ArrayList<>(modifications), controls);
1209
1210    if (followReferralsInternal() != null)
1211    {
1212      r.setFollowReferrals(followReferralsInternal());
1213    }
1214
1215    if (getReferralConnectorInternal() != null)
1216    {
1217      r.setReferralConnector(getReferralConnectorInternal());
1218    }
1219
1220    r.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
1221
1222    return r;
1223  }
1224
1225
1226
1227  /**
1228   * {@inheritDoc}
1229   */
1230  @Override()
1231  @NotNull()
1232  public LDIFModifyChangeRecord toLDIFChangeRecord()
1233  {
1234    return new LDIFModifyChangeRecord(this);
1235  }
1236
1237
1238
1239  /**
1240   * {@inheritDoc}
1241   */
1242  @Override()
1243  @NotNull()
1244  public String[] toLDIF()
1245  {
1246    return toLDIFChangeRecord().toLDIF();
1247  }
1248
1249
1250
1251  /**
1252   * {@inheritDoc}
1253   */
1254  @Override()
1255  @NotNull()
1256  public String toLDIFString()
1257  {
1258    return toLDIFChangeRecord().toLDIFString();
1259  }
1260
1261
1262
1263  /**
1264   * {@inheritDoc}
1265   */
1266  @Override()
1267  public void toString(@NotNull final StringBuilder buffer)
1268  {
1269    buffer.append("ModifyRequest(dn='");
1270    buffer.append(dn);
1271    buffer.append("', mods={");
1272    for (int i=0; i < modifications.size(); i++)
1273    {
1274      final Modification m = modifications.get(i);
1275
1276      if (i > 0)
1277      {
1278        buffer.append(", ");
1279      }
1280
1281      switch (m.getModificationType().intValue())
1282      {
1283        case 0:
1284          buffer.append("ADD ");
1285          break;
1286
1287        case 1:
1288          buffer.append("DELETE ");
1289          break;
1290
1291        case 2:
1292          buffer.append("REPLACE ");
1293          break;
1294
1295        case 3:
1296          buffer.append("INCREMENT ");
1297          break;
1298      }
1299
1300      buffer.append(m.getAttributeName());
1301    }
1302    buffer.append('}');
1303
1304    final Control[] controls = getControls();
1305    if (controls.length > 0)
1306    {
1307      buffer.append(", controls={");
1308      for (int i=0; i < controls.length; i++)
1309      {
1310        if (i > 0)
1311        {
1312          buffer.append(", ");
1313        }
1314
1315        buffer.append(controls[i]);
1316      }
1317      buffer.append('}');
1318    }
1319
1320    buffer.append(')');
1321  }
1322
1323
1324
1325  /**
1326   * {@inheritDoc}
1327   */
1328  @Override()
1329  public void toCode(@NotNull final List<String> lineList,
1330                     @NotNull final String requestID,
1331                     final int indentSpaces, final boolean includeProcessing)
1332  {
1333    // Create the request variable.
1334    final ArrayList<ToCodeArgHelper> constructorArgs =
1335         new ArrayList<>(modifications.size() + 1);
1336    constructorArgs.add(ToCodeArgHelper.createString(dn, "Entry DN"));
1337
1338    boolean firstMod = true;
1339    for (final Modification m : modifications)
1340    {
1341      final String comment;
1342      if (firstMod)
1343      {
1344        firstMod = false;
1345        comment = "Modifications";
1346      }
1347      else
1348      {
1349        comment = null;
1350      }
1351
1352      constructorArgs.add(ToCodeArgHelper.createModification(m, comment));
1353    }
1354
1355    ToCodeHelper.generateMethodCall(lineList, indentSpaces, "ModifyRequest",
1356         requestID + "Request", "new ModifyRequest", constructorArgs);
1357
1358
1359    // If there are any controls, then add them to the request.
1360    for (final Control c : getControls())
1361    {
1362      ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null,
1363           requestID + "Request.addControl",
1364           ToCodeArgHelper.createControl(c, null));
1365    }
1366
1367
1368    // Add lines for processing the request and obtaining the result.
1369    if (includeProcessing)
1370    {
1371      // Generate a string with the appropriate indent.
1372      final StringBuilder buffer = new StringBuilder();
1373      for (int i=0; i < indentSpaces; i++)
1374      {
1375        buffer.append(' ');
1376      }
1377      final String indent = buffer.toString();
1378
1379      lineList.add("");
1380      lineList.add(indent + "try");
1381      lineList.add(indent + '{');
1382      lineList.add(indent + "  LDAPResult " + requestID +
1383           "Result = connection.modify(" + requestID + "Request);");
1384      lineList.add(indent + "  // The modify was processed successfully.");
1385      lineList.add(indent + '}');
1386      lineList.add(indent + "catch (LDAPException e)");
1387      lineList.add(indent + '{');
1388      lineList.add(indent + "  // The modify failed.  Maybe the following " +
1389           "will help explain why.");
1390      lineList.add(indent + "  ResultCode resultCode = e.getResultCode();");
1391      lineList.add(indent + "  String message = e.getMessage();");
1392      lineList.add(indent + "  String matchedDN = e.getMatchedDN();");
1393      lineList.add(indent + "  String[] referralURLs = e.getReferralURLs();");
1394      lineList.add(indent + "  Control[] responseControls = " +
1395           "e.getResponseControls();");
1396      lineList.add(indent + '}');
1397    }
1398  }
1399}