001/*
002 * Copyright 2007-2017 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2017 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;
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  @Override()
432  public String getDN()
433  {
434    return dn;
435  }
436
437
438
439  /**
440   * Specifies the DN of the entry to modify.
441   *
442   * @param  dn  The DN of the entry to modify.  It must not be {@code null}.
443   */
444  public void setDN(final String dn)
445  {
446    ensureNotNull(dn);
447
448    this.dn = dn;
449  }
450
451
452
453  /**
454   * Specifies the DN of the entry to modify.
455   *
456   * @param  dn  The DN of the entry to modify.  It must not be {@code null}.
457   */
458  public void setDN(final DN dn)
459  {
460    ensureNotNull(dn);
461
462    this.dn = dn.toString();
463  }
464
465
466
467  /**
468   * {@inheritDoc}
469   */
470  @Override()
471  public List<Modification> getModifications()
472  {
473    return Collections.unmodifiableList(modifications);
474  }
475
476
477
478  /**
479   * Adds the provided modification to the set of modifications for this modify
480   * request.
481   *
482   * @param  mod  The modification to be added.  It must not be {@code null}.
483   */
484  public void addModification(final Modification mod)
485  {
486    ensureNotNull(mod);
487
488    modifications.add(mod);
489  }
490
491
492
493  /**
494   * Removes the provided modification from the set of modifications for this
495   * modify request.
496   *
497   * @param  mod  The modification to be removed.  It must not be {@code null}.
498   *
499   * @return  {@code true} if the specified modification was found and removed,
500   *          or {@code false} if not.
501   */
502  public boolean removeModification(final Modification mod)
503  {
504    ensureNotNull(mod);
505
506    return modifications.remove(mod);
507  }
508
509
510
511  /**
512   * Replaces the existing set of modifications for this modify request with the
513   * provided modification.
514   *
515   * @param  mod  The modification to use for this modify request.  It must not
516   *              be {@code null}.
517   */
518  public void setModifications(final Modification mod)
519  {
520    ensureNotNull(mod);
521
522    modifications.clear();
523    modifications.add(mod);
524  }
525
526
527
528  /**
529   * Replaces the existing set of modifications for this modify request with the
530   * provided modifications.
531   *
532   * @param  mods  The set of modification to use for this modify request.  It
533   *               must not be {@code null} or empty.
534   */
535  public void setModifications(final Modification[] mods)
536  {
537    ensureNotNull(mods);
538    ensureFalse(mods.length == 0,
539         "ModifyRequest.setModifications.mods must not be empty.");
540
541    modifications.clear();
542    modifications.addAll(Arrays.asList(mods));
543  }
544
545
546
547  /**
548   * Replaces the existing set of modifications for this modify request with the
549   * provided modifications.
550   *
551   * @param  mods  The set of modification to use for this modify request.  It
552   *               must not be {@code null} or empty.
553   */
554  public void setModifications(final List<Modification> mods)
555  {
556    ensureNotNull(mods);
557    ensureFalse(mods.isEmpty(),
558                "ModifyRequest.setModifications.mods must not be empty.");
559
560    modifications.clear();
561    modifications.addAll(mods);
562  }
563
564
565
566  /**
567   * {@inheritDoc}
568   */
569  @Override()
570  public byte getProtocolOpType()
571  {
572    return LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_REQUEST;
573  }
574
575
576
577  /**
578   * {@inheritDoc}
579   */
580  @Override()
581  public void writeTo(final ASN1Buffer writer)
582  {
583    final ASN1BufferSequence requestSequence =
584         writer.beginSequence(LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_REQUEST);
585    writer.addOctetString(dn);
586
587    final ASN1BufferSequence modSequence = writer.beginSequence();
588    for (final Modification m : modifications)
589    {
590      m.writeTo(writer);
591    }
592    modSequence.end();
593    requestSequence.end();
594  }
595
596
597
598  /**
599   * Encodes the modify request protocol op to an ASN.1 element.
600   *
601   * @return  The ASN.1 element with the encoded modify request protocol op.
602   */
603  @Override()
604  public ASN1Element encodeProtocolOp()
605  {
606    final ASN1Element[] modElements = new ASN1Element[modifications.size()];
607    for (int i=0; i < modElements.length; i++)
608    {
609      modElements[i] = modifications.get(i).encode();
610    }
611
612    final ASN1Element[] protocolOpElements =
613    {
614      new ASN1OctetString(dn),
615      new ASN1Sequence(modElements)
616    };
617
618
619
620    return new ASN1Sequence(LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_REQUEST,
621                            protocolOpElements);
622  }
623
624
625
626  /**
627   * Sends this modify request to the directory server over the provided
628   * connection and returns the associated response.
629   *
630   * @param  connection  The connection to use to communicate with the directory
631   *                     server.
632   * @param  depth       The current referral depth for this request.  It should
633   *                     always be one for the initial request, and should only
634   *                     be incremented when following referrals.
635   *
636   * @return  An LDAP result object that provides information about the result
637   *          of the modify processing.
638   *
639   * @throws  LDAPException  If a problem occurs while sending the request or
640   *                         reading the response.
641   */
642  @Override()
643  protected LDAPResult process(final LDAPConnection connection, final int depth)
644            throws LDAPException
645  {
646    if (connection.synchronousMode())
647    {
648      @SuppressWarnings("deprecation")
649      final boolean autoReconnect =
650           connection.getConnectionOptions().autoReconnect();
651      return processSync(connection, depth, autoReconnect);
652    }
653
654    final long requestTime = System.nanoTime();
655    processAsync(connection, null);
656
657    try
658    {
659      // Wait for and process the response.
660      final LDAPResponse response;
661      try
662      {
663        final long responseTimeout = getResponseTimeoutMillis(connection);
664        if (responseTimeout > 0)
665        {
666          response = responseQueue.poll(responseTimeout, TimeUnit.MILLISECONDS);
667        }
668        else
669        {
670          response = responseQueue.take();
671        }
672      }
673      catch (final InterruptedException ie)
674      {
675        debugException(ie);
676        Thread.currentThread().interrupt();
677        throw new LDAPException(ResultCode.LOCAL_ERROR,
678             ERR_MODIFY_INTERRUPTED.get(connection.getHostPort()), ie);
679      }
680
681      return handleResponse(connection, response, requestTime, depth, false);
682    }
683    finally
684    {
685      connection.deregisterResponseAcceptor(messageID);
686    }
687  }
688
689
690
691  /**
692   * Sends this modify request to the directory server over the provided
693   * connection and returns the message ID for the request.
694   *
695   * @param  connection      The connection to use to communicate with the
696   *                         directory server.
697   * @param  resultListener  The async result listener that is to be notified
698   *                         when the response is received.  It may be
699   *                         {@code null} only if the result is to be processed
700   *                         by this class.
701   *
702   * @return  The async request ID created for the operation, or {@code null} if
703   *          the provided {@code resultListener} is {@code null} and the
704   *          operation will not actually be processed asynchronously.
705   *
706   * @throws  LDAPException  If a problem occurs while sending the request.
707   */
708  AsyncRequestID processAsync(final LDAPConnection connection,
709                              final AsyncResultListener resultListener)
710                 throws LDAPException
711  {
712    // Create the LDAP message.
713    messageID = connection.nextMessageID();
714    final LDAPMessage message = new LDAPMessage(messageID, this, getControls());
715
716
717    // If the provided async result listener is {@code null}, then we'll use
718    // this class as the message acceptor.  Otherwise, create an async helper
719    // and use it as the message acceptor.
720    final AsyncRequestID asyncRequestID;
721    if (resultListener == null)
722    {
723      asyncRequestID = null;
724      connection.registerResponseAcceptor(messageID, this);
725    }
726    else
727    {
728      final AsyncHelper helper = new AsyncHelper(connection,
729           OperationType.MODIFY, messageID, resultListener,
730           getIntermediateResponseListener());
731      connection.registerResponseAcceptor(messageID, helper);
732      asyncRequestID = helper.getAsyncRequestID();
733
734      final long timeout = getResponseTimeoutMillis(connection);
735      if (timeout > 0L)
736      {
737        final Timer timer = connection.getTimer();
738        final AsyncTimeoutTimerTask timerTask =
739             new AsyncTimeoutTimerTask(helper);
740        timer.schedule(timerTask, timeout);
741        asyncRequestID.setTimerTask(timerTask);
742      }
743    }
744
745
746    // Send the request to the server.
747    try
748    {
749      debugLDAPRequest(this);
750      connection.getConnectionStatistics().incrementNumModifyRequests();
751      connection.sendMessage(message);
752      return asyncRequestID;
753    }
754    catch (final LDAPException le)
755    {
756      debugException(le);
757
758      connection.deregisterResponseAcceptor(messageID);
759      throw le;
760    }
761  }
762
763
764
765  /**
766   * Processes this modify operation in synchronous mode, in which the same
767   * thread will send the request and read the response.
768   *
769   * @param  connection  The connection to use to communicate with the directory
770   *                     server.
771   * @param  depth       The current referral depth for this request.  It should
772   *                     always be one for the initial request, and should only
773   *                     be incremented when following referrals.
774   * @param  allowRetry  Indicates whether the request may be re-tried on a
775   *                     re-established connection if the initial attempt fails
776   *                     in a way that indicates the connection is no longer
777   *                     valid and autoReconnect is true.
778   *
779   * @return  An LDAP result object that provides information about the result
780   *          of the modify processing.
781   *
782   * @throws  LDAPException  If a problem occurs while sending the request or
783   *                         reading the response.
784   */
785  private LDAPResult processSync(final LDAPConnection connection,
786                                 final int depth, final boolean allowRetry)
787          throws LDAPException
788  {
789    // Create the LDAP message.
790    messageID = connection.nextMessageID();
791    final LDAPMessage message =
792         new LDAPMessage(messageID,  this, getControls());
793
794
795    // Set the appropriate timeout on the socket.
796    try
797    {
798      connection.getConnectionInternals(true).getSocket().setSoTimeout(
799           (int) getResponseTimeoutMillis(connection));
800    }
801    catch (final Exception e)
802    {
803      debugException(e);
804    }
805
806
807    // Send the request to the server.
808    final long requestTime = System.nanoTime();
809    debugLDAPRequest(this);
810    connection.getConnectionStatistics().incrementNumModifyRequests();
811    try
812    {
813      connection.sendMessage(message);
814    }
815    catch (final LDAPException le)
816    {
817      debugException(le);
818
819      if (allowRetry)
820      {
821        final LDAPResult retryResult = reconnectAndRetry(connection, depth,
822             le.getResultCode());
823        if (retryResult != null)
824        {
825          return retryResult;
826        }
827      }
828
829      throw le;
830    }
831
832    while (true)
833    {
834      final LDAPResponse response;
835      try
836      {
837        response = connection.readResponse(messageID);
838      }
839      catch (final LDAPException le)
840      {
841        debugException(le);
842
843        if ((le.getResultCode() == ResultCode.TIMEOUT) &&
844            connection.getConnectionOptions().abandonOnTimeout())
845        {
846          connection.abandon(messageID);
847        }
848
849        if (allowRetry)
850        {
851          final LDAPResult retryResult = reconnectAndRetry(connection, depth,
852               le.getResultCode());
853          if (retryResult != null)
854          {
855            return retryResult;
856          }
857        }
858
859        throw le;
860      }
861
862      if (response instanceof IntermediateResponse)
863      {
864        final IntermediateResponseListener listener =
865             getIntermediateResponseListener();
866        if (listener != null)
867        {
868          listener.intermediateResponseReturned(
869               (IntermediateResponse) response);
870        }
871      }
872      else
873      {
874        return handleResponse(connection, response, requestTime, depth,
875             allowRetry);
876      }
877    }
878  }
879
880
881
882  /**
883   * Performs the necessary processing for handling a response.
884   *
885   * @param  connection   The connection used to read the response.
886   * @param  response     The response to be processed.
887   * @param  requestTime  The time the request was sent to the server.
888   * @param  depth        The current referral depth for this request.  It
889   *                      should always be one for the initial request, and
890   *                      should only be incremented when following referrals.
891   * @param  allowRetry   Indicates whether the request may be re-tried on a
892   *                      re-established connection if the initial attempt fails
893   *                      in a way that indicates the connection is no longer
894   *                      valid and autoReconnect is true.
895   *
896   * @return  The modify result.
897   *
898   * @throws  LDAPException  If a problem occurs.
899   */
900  private LDAPResult handleResponse(final LDAPConnection connection,
901                                    final LDAPResponse response,
902                                    final long requestTime, final int depth,
903                                    final boolean allowRetry)
904          throws LDAPException
905  {
906    if (response == null)
907    {
908      final long waitTime = nanosToMillis(System.nanoTime() - requestTime);
909      if (connection.getConnectionOptions().abandonOnTimeout())
910      {
911        connection.abandon(messageID);
912      }
913
914      throw new LDAPException(ResultCode.TIMEOUT,
915           ERR_MODIFY_CLIENT_TIMEOUT.get(waitTime, messageID, dn,
916                connection.getHostPort()));
917    }
918
919    connection.getConnectionStatistics().incrementNumModifyResponses(
920         System.nanoTime() - requestTime);
921    if (response instanceof ConnectionClosedResponse)
922    {
923      // The connection was closed while waiting for the response.
924      if (allowRetry)
925      {
926        final LDAPResult retryResult = reconnectAndRetry(connection, depth,
927             ResultCode.SERVER_DOWN);
928        if (retryResult != null)
929        {
930          return retryResult;
931        }
932      }
933
934      final ConnectionClosedResponse ccr = (ConnectionClosedResponse) response;
935      final String message = ccr.getMessage();
936      if (message == null)
937      {
938        throw new LDAPException(ccr.getResultCode(),
939             ERR_CONN_CLOSED_WAITING_FOR_MODIFY_RESPONSE.get(
940                  connection.getHostPort(), toString()));
941      }
942      else
943      {
944        throw new LDAPException(ccr.getResultCode(),
945             ERR_CONN_CLOSED_WAITING_FOR_MODIFY_RESPONSE_WITH_MESSAGE.get(
946                  connection.getHostPort(), toString(), message));
947      }
948    }
949
950    final LDAPResult result = (LDAPResult) response;
951    if ((result.getResultCode().equals(ResultCode.REFERRAL)) &&
952        followReferrals(connection))
953    {
954      if (depth >= connection.getConnectionOptions().getReferralHopLimit())
955      {
956        return new LDAPResult(messageID, ResultCode.REFERRAL_LIMIT_EXCEEDED,
957                              ERR_TOO_MANY_REFERRALS.get(),
958                              result.getMatchedDN(), result.getReferralURLs(),
959                              result.getResponseControls());
960      }
961
962      return followReferral(result, connection, depth);
963    }
964    else
965    {
966      if (allowRetry)
967      {
968        final LDAPResult retryResult = reconnectAndRetry(connection, depth,
969             result.getResultCode());
970        if (retryResult != null)
971        {
972          return retryResult;
973        }
974      }
975
976      return result;
977    }
978  }
979
980
981
982  /**
983   * Attempts to re-establish the connection and retry processing this request
984   * on it.
985   *
986   * @param  connection  The connection to be re-established.
987   * @param  depth       The current referral depth for this request.  It should
988   *                     always be one for the initial request, and should only
989   *                     be incremented when following referrals.
990   * @param  resultCode  The result code for the previous operation attempt.
991   *
992   * @return  The result from re-trying the add, or {@code null} if it could not
993   *          be re-tried.
994   */
995  private LDAPResult reconnectAndRetry(final LDAPConnection connection,
996                                       final int depth,
997                                       final ResultCode resultCode)
998  {
999    try
1000    {
1001      // We will only want to retry for certain result codes that indicate a
1002      // connection problem.
1003      switch (resultCode.intValue())
1004      {
1005        case ResultCode.SERVER_DOWN_INT_VALUE:
1006        case ResultCode.DECODING_ERROR_INT_VALUE:
1007        case ResultCode.CONNECT_ERROR_INT_VALUE:
1008          connection.reconnect();
1009          return processSync(connection, depth, false);
1010      }
1011    }
1012    catch (final Exception e)
1013    {
1014      debugException(e);
1015    }
1016
1017    return null;
1018  }
1019
1020
1021
1022  /**
1023   * Attempts to follow a referral to perform a modify operation in the target
1024   * server.
1025   *
1026   * @param  referralResult  The LDAP result object containing information about
1027   *                         the referral to follow.
1028   * @param  connection      The connection on which the referral was received.
1029   * @param  depth           The number of referrals followed in the course of
1030   *                         processing this request.
1031   *
1032   * @return  The result of attempting to process the modify operation by
1033   *          following the referral.
1034   *
1035   * @throws  LDAPException  If a problem occurs while attempting to establish
1036   *                         the referral connection, sending the request, or
1037   *                         reading the result.
1038   */
1039  private LDAPResult followReferral(final LDAPResult referralResult,
1040                                    final LDAPConnection connection,
1041                                    final int depth)
1042          throws LDAPException
1043  {
1044    for (final String urlString : referralResult.getReferralURLs())
1045    {
1046      try
1047      {
1048        final LDAPURL referralURL = new LDAPURL(urlString);
1049        final String host = referralURL.getHost();
1050
1051        if (host == null)
1052        {
1053          // We can't handle a referral in which there is no host.
1054          continue;
1055        }
1056
1057        final ModifyRequest modifyRequest;
1058        if (referralURL.baseDNProvided())
1059        {
1060          modifyRequest = new ModifyRequest(referralURL.getBaseDN(),
1061                                            modifications, getControls());
1062        }
1063        else
1064        {
1065          modifyRequest = this;
1066        }
1067
1068        final LDAPConnection referralConn = connection.getReferralConnector().
1069             getReferralConnection(referralURL, connection);
1070        try
1071        {
1072          return modifyRequest.process(referralConn, depth+1);
1073        }
1074        finally
1075        {
1076          referralConn.setDisconnectInfo(DisconnectType.REFERRAL, null, null);
1077          referralConn.close();
1078        }
1079      }
1080      catch (final LDAPException le)
1081      {
1082        debugException(le);
1083      }
1084    }
1085
1086    // If we've gotten here, then we could not follow any of the referral URLs,
1087    // so we'll just return the original referral result.
1088    return referralResult;
1089  }
1090
1091
1092
1093  /**
1094   * {@inheritDoc}
1095   */
1096  @InternalUseOnly()
1097  @Override()
1098  public void responseReceived(final LDAPResponse response)
1099         throws LDAPException
1100  {
1101    try
1102    {
1103      responseQueue.put(response);
1104    }
1105    catch (final Exception e)
1106    {
1107      debugException(e);
1108
1109      if (e instanceof InterruptedException)
1110      {
1111        Thread.currentThread().interrupt();
1112      }
1113
1114      throw new LDAPException(ResultCode.LOCAL_ERROR,
1115           ERR_EXCEPTION_HANDLING_RESPONSE.get(getExceptionMessage(e)), e);
1116    }
1117  }
1118
1119
1120
1121  /**
1122   * {@inheritDoc}
1123   */
1124  @Override()
1125  public int getLastMessageID()
1126  {
1127    return messageID;
1128  }
1129
1130
1131
1132  /**
1133   * {@inheritDoc}
1134   */
1135  @Override()
1136  public OperationType getOperationType()
1137  {
1138    return OperationType.MODIFY;
1139  }
1140
1141
1142
1143  /**
1144   * {@inheritDoc}
1145   */
1146  @Override()
1147  public ModifyRequest duplicate()
1148  {
1149    return duplicate(getControls());
1150  }
1151
1152
1153
1154  /**
1155   * {@inheritDoc}
1156   */
1157  @Override()
1158  public ModifyRequest duplicate(final Control[] controls)
1159  {
1160    final ModifyRequest r = new ModifyRequest(dn,
1161         new ArrayList<Modification>(modifications), controls);
1162
1163    if (followReferralsInternal() != null)
1164    {
1165      r.setFollowReferrals(followReferralsInternal());
1166    }
1167
1168    r.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
1169
1170    return r;
1171  }
1172
1173
1174
1175  /**
1176   * {@inheritDoc}
1177   */
1178  @Override()
1179  public LDIFModifyChangeRecord toLDIFChangeRecord()
1180  {
1181    return new LDIFModifyChangeRecord(this);
1182  }
1183
1184
1185
1186  /**
1187   * {@inheritDoc}
1188   */
1189  @Override()
1190  public String[] toLDIF()
1191  {
1192    return toLDIFChangeRecord().toLDIF();
1193  }
1194
1195
1196
1197  /**
1198   * {@inheritDoc}
1199   */
1200  @Override()
1201  public String toLDIFString()
1202  {
1203    return toLDIFChangeRecord().toLDIFString();
1204  }
1205
1206
1207
1208  /**
1209   * {@inheritDoc}
1210   */
1211  @Override()
1212  public void toString(final StringBuilder buffer)
1213  {
1214    buffer.append("ModifyRequest(dn='");
1215    buffer.append(dn);
1216    buffer.append("', mods={");
1217    for (int i=0; i < modifications.size(); i++)
1218    {
1219      final Modification m = modifications.get(i);
1220
1221      if (i > 0)
1222      {
1223        buffer.append(", ");
1224      }
1225
1226      switch (m.getModificationType().intValue())
1227      {
1228        case 0:
1229          buffer.append("ADD ");
1230          break;
1231
1232        case 1:
1233          buffer.append("DELETE ");
1234          break;
1235
1236        case 2:
1237          buffer.append("REPLACE ");
1238          break;
1239
1240        case 3:
1241          buffer.append("INCREMENT ");
1242          break;
1243      }
1244
1245      buffer.append(m.getAttributeName());
1246    }
1247    buffer.append('}');
1248
1249    final Control[] controls = getControls();
1250    if (controls.length > 0)
1251    {
1252      buffer.append(", controls={");
1253      for (int i=0; i < controls.length; i++)
1254      {
1255        if (i > 0)
1256        {
1257          buffer.append(", ");
1258        }
1259
1260        buffer.append(controls[i]);
1261      }
1262      buffer.append('}');
1263    }
1264
1265    buffer.append(')');
1266  }
1267
1268
1269
1270  /**
1271   * {@inheritDoc}
1272   */
1273  @Override()
1274  public void toCode(final List<String> lineList, final String requestID,
1275                     final int indentSpaces, final boolean includeProcessing)
1276  {
1277    // Create the request variable.
1278    final ArrayList<ToCodeArgHelper> constructorArgs =
1279         new ArrayList<ToCodeArgHelper>(modifications.size() + 1);
1280    constructorArgs.add(ToCodeArgHelper.createString(dn, "Entry DN"));
1281
1282    boolean firstMod = true;
1283    for (final Modification m : modifications)
1284    {
1285      final String comment;
1286      if (firstMod)
1287      {
1288        firstMod = false;
1289        comment = "Modifications";
1290      }
1291      else
1292      {
1293        comment = null;
1294      }
1295
1296      constructorArgs.add(ToCodeArgHelper.createModification(m, comment));
1297    }
1298
1299    ToCodeHelper.generateMethodCall(lineList, indentSpaces, "ModifyRequest",
1300         requestID + "Request", "new ModifyRequest", constructorArgs);
1301
1302
1303    // If there are any controls, then add them to the request.
1304    for (final Control c : getControls())
1305    {
1306      ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null,
1307           requestID + "Request.addControl",
1308           ToCodeArgHelper.createControl(c, null));
1309    }
1310
1311
1312    // Add lines for processing the request and obtaining the result.
1313    if (includeProcessing)
1314    {
1315      // Generate a string with the appropriate indent.
1316      final StringBuilder buffer = new StringBuilder();
1317      for (int i=0; i < indentSpaces; i++)
1318      {
1319        buffer.append(' ');
1320      }
1321      final String indent = buffer.toString();
1322
1323      lineList.add("");
1324      lineList.add(indent + "try");
1325      lineList.add(indent + '{');
1326      lineList.add(indent + "  LDAPResult " + requestID +
1327           "Result = connection.modify(" + requestID + "Request);");
1328      lineList.add(indent + "  // The modify was processed successfully.");
1329      lineList.add(indent + '}');
1330      lineList.add(indent + "catch (LDAPException e)");
1331      lineList.add(indent + '{');
1332      lineList.add(indent + "  // The modify failed.  Maybe the following " +
1333           "will help explain why.");
1334      lineList.add(indent + "  ResultCode resultCode = e.getResultCode();");
1335      lineList.add(indent + "  String message = e.getMessage();");
1336      lineList.add(indent + "  String matchedDN = e.getMatchedDN();");
1337      lineList.add(indent + "  String[] referralURLs = e.getReferralURLs();");
1338      lineList.add(indent + "  Control[] responseControls = " +
1339           "e.getResponseControls();");
1340      lineList.add(indent + '}');
1341    }
1342  }
1343}