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