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