001    /*
002     * Copyright 2007-2017 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2017 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     */
021    package com.unboundid.ldap.sdk;
022    
023    
024    
025    import java.util.ArrayList;
026    import java.util.Arrays;
027    import java.util.Collections;
028    import java.util.List;
029    import java.util.Timer;
030    import java.util.concurrent.LinkedBlockingQueue;
031    import java.util.concurrent.TimeUnit;
032    
033    import com.unboundid.asn1.ASN1Buffer;
034    import com.unboundid.asn1.ASN1BufferSequence;
035    import com.unboundid.asn1.ASN1Element;
036    import com.unboundid.asn1.ASN1OctetString;
037    import com.unboundid.asn1.ASN1Sequence;
038    import com.unboundid.ldap.protocol.LDAPMessage;
039    import com.unboundid.ldap.protocol.LDAPResponse;
040    import com.unboundid.ldap.protocol.ProtocolOp;
041    import com.unboundid.ldif.LDIFChangeRecord;
042    import com.unboundid.ldif.LDIFException;
043    import com.unboundid.ldif.LDIFModifyChangeRecord;
044    import com.unboundid.ldif.LDIFReader;
045    import com.unboundid.util.InternalUseOnly;
046    import com.unboundid.util.Mutable;
047    import com.unboundid.util.ThreadSafety;
048    import com.unboundid.util.ThreadSafetyLevel;
049    
050    import static com.unboundid.ldap.sdk.LDAPMessages.*;
051    import static com.unboundid.util.Debug.*;
052    import static com.unboundid.util.StaticUtils.*;
053    import 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)
086    public 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          @SuppressWarnings("deprecation")
644          final boolean autoReconnect =
645               connection.getConnectionOptions().autoReconnect();
646          return processSync(connection, depth, autoReconnect);
647        }
648    
649        final long requestTime = System.nanoTime();
650        processAsync(connection, null);
651    
652        try
653        {
654          // Wait for and process the response.
655          final LDAPResponse response;
656          try
657          {
658            final long responseTimeout = getResponseTimeoutMillis(connection);
659            if (responseTimeout > 0)
660            {
661              response = responseQueue.poll(responseTimeout, TimeUnit.MILLISECONDS);
662            }
663            else
664            {
665              response = responseQueue.take();
666            }
667          }
668          catch (InterruptedException ie)
669          {
670            debugException(ie);
671            Thread.currentThread().interrupt();
672            throw new LDAPException(ResultCode.LOCAL_ERROR,
673                 ERR_MODIFY_INTERRUPTED.get(connection.getHostPort()), ie);
674          }
675    
676          return handleResponse(connection, response, requestTime, depth, false);
677        }
678        finally
679        {
680          connection.deregisterResponseAcceptor(messageID);
681        }
682      }
683    
684    
685    
686      /**
687       * Sends this modify request to the directory server over the provided
688       * connection and returns the message ID for the request.
689       *
690       * @param  connection      The connection to use to communicate with the
691       *                         directory server.
692       * @param  resultListener  The async result listener that is to be notified
693       *                         when the response is received.  It may be
694       *                         {@code null} only if the result is to be processed
695       *                         by this class.
696       *
697       * @return  The async request ID created for the operation, or {@code null} if
698       *          the provided {@code resultListener} is {@code null} and the
699       *          operation will not actually be processed asynchronously.
700       *
701       * @throws  LDAPException  If a problem occurs while sending the request.
702       */
703      AsyncRequestID processAsync(final LDAPConnection connection,
704                                  final AsyncResultListener resultListener)
705                     throws LDAPException
706      {
707        // Create the LDAP message.
708        messageID = connection.nextMessageID();
709        final LDAPMessage message = new LDAPMessage(messageID, this, getControls());
710    
711    
712        // If the provided async result listener is {@code null}, then we'll use
713        // this class as the message acceptor.  Otherwise, create an async helper
714        // and use it as the message acceptor.
715        final AsyncRequestID asyncRequestID;
716        if (resultListener == null)
717        {
718          asyncRequestID = null;
719          connection.registerResponseAcceptor(messageID, this);
720        }
721        else
722        {
723          final AsyncHelper helper = new AsyncHelper(connection,
724               OperationType.MODIFY, messageID, resultListener,
725               getIntermediateResponseListener());
726          connection.registerResponseAcceptor(messageID, helper);
727          asyncRequestID = helper.getAsyncRequestID();
728    
729          final long timeout = getResponseTimeoutMillis(connection);
730          if (timeout > 0L)
731          {
732            final Timer timer = connection.getTimer();
733            final AsyncTimeoutTimerTask timerTask =
734                 new AsyncTimeoutTimerTask(helper);
735            timer.schedule(timerTask, timeout);
736            asyncRequestID.setTimerTask(timerTask);
737          }
738        }
739    
740    
741        // Send the request to the server.
742        try
743        {
744          debugLDAPRequest(this);
745          connection.getConnectionStatistics().incrementNumModifyRequests();
746          connection.sendMessage(message);
747          return asyncRequestID;
748        }
749        catch (LDAPException le)
750        {
751          debugException(le);
752    
753          connection.deregisterResponseAcceptor(messageID);
754          throw le;
755        }
756      }
757    
758    
759    
760      /**
761       * Processes this modify operation in synchronous mode, in which the same
762       * thread will send the request and read the response.
763       *
764       * @param  connection  The connection to use to communicate with the directory
765       *                     server.
766       * @param  depth       The current referral depth for this request.  It should
767       *                     always be one for the initial request, and should only
768       *                     be incremented when following referrals.
769       * @param  allowRetry  Indicates whether the request may be re-tried on a
770       *                     re-established connection if the initial attempt fails
771       *                     in a way that indicates the connection is no longer
772       *                     valid and autoReconnect is true.
773       *
774       * @return  An LDAP result object that provides information about the result
775       *          of the modify processing.
776       *
777       * @throws  LDAPException  If a problem occurs while sending the request or
778       *                         reading the response.
779       */
780      private LDAPResult processSync(final LDAPConnection connection,
781                                     final int depth, final boolean allowRetry)
782              throws LDAPException
783      {
784        // Create the LDAP message.
785        messageID = connection.nextMessageID();
786        final LDAPMessage message =
787             new LDAPMessage(messageID,  this, getControls());
788    
789    
790        // Set the appropriate timeout on the socket.
791        try
792        {
793          connection.getConnectionInternals(true).getSocket().setSoTimeout(
794               (int) getResponseTimeoutMillis(connection));
795        }
796        catch (Exception e)
797        {
798          debugException(e);
799        }
800    
801    
802        // Send the request to the server.
803        final long requestTime = System.nanoTime();
804        debugLDAPRequest(this);
805        connection.getConnectionStatistics().incrementNumModifyRequests();
806        try
807        {
808          connection.sendMessage(message);
809        }
810        catch (final LDAPException le)
811        {
812          debugException(le);
813    
814          if (allowRetry)
815          {
816            final LDAPResult retryResult = reconnectAndRetry(connection, depth,
817                 le.getResultCode());
818            if (retryResult != null)
819            {
820              return retryResult;
821            }
822          }
823    
824          throw le;
825        }
826    
827        while (true)
828        {
829          final LDAPResponse response;
830          try
831          {
832            response = connection.readResponse(messageID);
833          }
834          catch (final LDAPException le)
835          {
836            debugException(le);
837    
838            if ((le.getResultCode() == ResultCode.TIMEOUT) &&
839                connection.getConnectionOptions().abandonOnTimeout())
840            {
841              connection.abandon(messageID);
842            }
843    
844            if (allowRetry)
845            {
846              final LDAPResult retryResult = reconnectAndRetry(connection, depth,
847                   le.getResultCode());
848              if (retryResult != null)
849              {
850                return retryResult;
851              }
852            }
853    
854            throw le;
855          }
856    
857          if (response instanceof IntermediateResponse)
858          {
859            final IntermediateResponseListener listener =
860                 getIntermediateResponseListener();
861            if (listener != null)
862            {
863              listener.intermediateResponseReturned(
864                   (IntermediateResponse) response);
865            }
866          }
867          else
868          {
869            return handleResponse(connection, response, requestTime, depth,
870                 allowRetry);
871          }
872        }
873      }
874    
875    
876    
877      /**
878       * Performs the necessary processing for handling a response.
879       *
880       * @param  connection   The connection used to read the response.
881       * @param  response     The response to be processed.
882       * @param  requestTime  The time the request was sent to the server.
883       * @param  depth        The current referral depth for this request.  It
884       *                      should always be one for the initial request, and
885       *                      should only be incremented when following referrals.
886       * @param  allowRetry   Indicates whether the request may be re-tried on a
887       *                      re-established connection if the initial attempt fails
888       *                      in a way that indicates the connection is no longer
889       *                      valid and autoReconnect is true.
890       *
891       * @return  The modify result.
892       *
893       * @throws  LDAPException  If a problem occurs.
894       */
895      private LDAPResult handleResponse(final LDAPConnection connection,
896                                        final LDAPResponse response,
897                                        final long requestTime, final int depth,
898                                        final boolean allowRetry)
899              throws LDAPException
900      {
901        if (response == null)
902        {
903          final long waitTime = nanosToMillis(System.nanoTime() - requestTime);
904          if (connection.getConnectionOptions().abandonOnTimeout())
905          {
906            connection.abandon(messageID);
907          }
908    
909          throw new LDAPException(ResultCode.TIMEOUT,
910               ERR_MODIFY_CLIENT_TIMEOUT.get(waitTime, messageID, dn,
911                    connection.getHostPort()));
912        }
913    
914        connection.getConnectionStatistics().incrementNumModifyResponses(
915             System.nanoTime() - requestTime);
916        if (response instanceof ConnectionClosedResponse)
917        {
918          // The connection was closed while waiting for the response.
919          if (allowRetry)
920          {
921            final LDAPResult retryResult = reconnectAndRetry(connection, depth,
922                 ResultCode.SERVER_DOWN);
923            if (retryResult != null)
924            {
925              return retryResult;
926            }
927          }
928    
929          final ConnectionClosedResponse ccr = (ConnectionClosedResponse) response;
930          final String message = ccr.getMessage();
931          if (message == null)
932          {
933            throw new LDAPException(ccr.getResultCode(),
934                 ERR_CONN_CLOSED_WAITING_FOR_MODIFY_RESPONSE.get(
935                      connection.getHostPort(), toString()));
936          }
937          else
938          {
939            throw new LDAPException(ccr.getResultCode(),
940                 ERR_CONN_CLOSED_WAITING_FOR_MODIFY_RESPONSE_WITH_MESSAGE.get(
941                      connection.getHostPort(), toString(), message));
942          }
943        }
944    
945        final LDAPResult result = (LDAPResult) response;
946        if ((result.getResultCode().equals(ResultCode.REFERRAL)) &&
947            followReferrals(connection))
948        {
949          if (depth >= connection.getConnectionOptions().getReferralHopLimit())
950          {
951            return new LDAPResult(messageID, ResultCode.REFERRAL_LIMIT_EXCEEDED,
952                                  ERR_TOO_MANY_REFERRALS.get(),
953                                  result.getMatchedDN(), result.getReferralURLs(),
954                                  result.getResponseControls());
955          }
956    
957          return followReferral(result, connection, depth);
958        }
959        else
960        {
961          if (allowRetry)
962          {
963            final LDAPResult retryResult = reconnectAndRetry(connection, depth,
964                 result.getResultCode());
965            if (retryResult != null)
966            {
967              return retryResult;
968            }
969          }
970    
971          return result;
972        }
973      }
974    
975    
976    
977      /**
978       * Attempts to re-establish the connection and retry processing this request
979       * on it.
980       *
981       * @param  connection  The connection to be re-established.
982       * @param  depth       The current referral depth for this request.  It should
983       *                     always be one for the initial request, and should only
984       *                     be incremented when following referrals.
985       * @param  resultCode  The result code for the previous operation attempt.
986       *
987       * @return  The result from re-trying the add, or {@code null} if it could not
988       *          be re-tried.
989       */
990      private LDAPResult reconnectAndRetry(final LDAPConnection connection,
991                                           final int depth,
992                                           final ResultCode resultCode)
993      {
994        try
995        {
996          // We will only want to retry for certain result codes that indicate a
997          // connection problem.
998          switch (resultCode.intValue())
999          {
1000            case ResultCode.SERVER_DOWN_INT_VALUE:
1001            case ResultCode.DECODING_ERROR_INT_VALUE:
1002            case ResultCode.CONNECT_ERROR_INT_VALUE:
1003              connection.reconnect();
1004              return processSync(connection, depth, false);
1005          }
1006        }
1007        catch (final Exception e)
1008        {
1009          debugException(e);
1010        }
1011    
1012        return null;
1013      }
1014    
1015    
1016    
1017      /**
1018       * Attempts to follow a referral to perform a modify operation in the target
1019       * server.
1020       *
1021       * @param  referralResult  The LDAP result object containing information about
1022       *                         the referral to follow.
1023       * @param  connection      The connection on which the referral was received.
1024       * @param  depth           The number of referrals followed in the course of
1025       *                         processing this request.
1026       *
1027       * @return  The result of attempting to process the modify operation by
1028       *          following the referral.
1029       *
1030       * @throws  LDAPException  If a problem occurs while attempting to establish
1031       *                         the referral connection, sending the request, or
1032       *                         reading the result.
1033       */
1034      private LDAPResult followReferral(final LDAPResult referralResult,
1035                                        final LDAPConnection connection,
1036                                        final int depth)
1037              throws LDAPException
1038      {
1039        for (final String urlString : referralResult.getReferralURLs())
1040        {
1041          try
1042          {
1043            final LDAPURL referralURL = new LDAPURL(urlString);
1044            final String host = referralURL.getHost();
1045    
1046            if (host == null)
1047            {
1048              // We can't handle a referral in which there is no host.
1049              continue;
1050            }
1051    
1052            final ModifyRequest modifyRequest;
1053            if (referralURL.baseDNProvided())
1054            {
1055              modifyRequest = new ModifyRequest(referralURL.getBaseDN(),
1056                                                modifications, getControls());
1057            }
1058            else
1059            {
1060              modifyRequest = this;
1061            }
1062    
1063            final LDAPConnection referralConn = connection.getReferralConnector().
1064                 getReferralConnection(referralURL, connection);
1065            try
1066            {
1067              return modifyRequest.process(referralConn, depth+1);
1068            }
1069            finally
1070            {
1071              referralConn.setDisconnectInfo(DisconnectType.REFERRAL, null, null);
1072              referralConn.close();
1073            }
1074          }
1075          catch (LDAPException le)
1076          {
1077            debugException(le);
1078          }
1079        }
1080    
1081        // If we've gotten here, then we could not follow any of the referral URLs,
1082        // so we'll just return the original referral result.
1083        return referralResult;
1084      }
1085    
1086    
1087    
1088      /**
1089       * {@inheritDoc}
1090       */
1091      @InternalUseOnly()
1092      public void responseReceived(final LDAPResponse response)
1093             throws LDAPException
1094      {
1095        try
1096        {
1097          responseQueue.put(response);
1098        }
1099        catch (Exception e)
1100        {
1101          debugException(e);
1102    
1103          if (e instanceof InterruptedException)
1104          {
1105            Thread.currentThread().interrupt();
1106          }
1107    
1108          throw new LDAPException(ResultCode.LOCAL_ERROR,
1109               ERR_EXCEPTION_HANDLING_RESPONSE.get(getExceptionMessage(e)), e);
1110        }
1111      }
1112    
1113    
1114    
1115      /**
1116       * {@inheritDoc}
1117       */
1118      @Override()
1119      public int getLastMessageID()
1120      {
1121        return messageID;
1122      }
1123    
1124    
1125    
1126      /**
1127       * {@inheritDoc}
1128       */
1129      @Override()
1130      public OperationType getOperationType()
1131      {
1132        return OperationType.MODIFY;
1133      }
1134    
1135    
1136    
1137      /**
1138       * {@inheritDoc}
1139       */
1140      public ModifyRequest duplicate()
1141      {
1142        return duplicate(getControls());
1143      }
1144    
1145    
1146    
1147      /**
1148       * {@inheritDoc}
1149       */
1150      public ModifyRequest duplicate(final Control[] controls)
1151      {
1152        final ModifyRequest r = new ModifyRequest(dn,
1153             new ArrayList<Modification>(modifications), controls);
1154    
1155        if (followReferralsInternal() != null)
1156        {
1157          r.setFollowReferrals(followReferralsInternal());
1158        }
1159    
1160        r.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
1161    
1162        return r;
1163      }
1164    
1165    
1166    
1167      /**
1168       * {@inheritDoc}
1169       */
1170      public LDIFModifyChangeRecord toLDIFChangeRecord()
1171      {
1172        return new LDIFModifyChangeRecord(this);
1173      }
1174    
1175    
1176    
1177      /**
1178       * {@inheritDoc}
1179       */
1180      public String[] toLDIF()
1181      {
1182        return toLDIFChangeRecord().toLDIF();
1183      }
1184    
1185    
1186    
1187      /**
1188       * {@inheritDoc}
1189       */
1190      public String toLDIFString()
1191      {
1192        return toLDIFChangeRecord().toLDIFString();
1193      }
1194    
1195    
1196    
1197      /**
1198       * {@inheritDoc}
1199       */
1200      @Override()
1201      public void toString(final StringBuilder buffer)
1202      {
1203        buffer.append("ModifyRequest(dn='");
1204        buffer.append(dn);
1205        buffer.append("', mods={");
1206        for (int i=0; i < modifications.size(); i++)
1207        {
1208          final Modification m = modifications.get(i);
1209    
1210          if (i > 0)
1211          {
1212            buffer.append(", ");
1213          }
1214    
1215          switch (m.getModificationType().intValue())
1216          {
1217            case 0:
1218              buffer.append("ADD ");
1219              break;
1220    
1221            case 1:
1222              buffer.append("DELETE ");
1223              break;
1224    
1225            case 2:
1226              buffer.append("REPLACE ");
1227              break;
1228    
1229            case 3:
1230              buffer.append("INCREMENT ");
1231              break;
1232          }
1233    
1234          buffer.append(m.getAttributeName());
1235        }
1236        buffer.append('}');
1237    
1238        final Control[] controls = getControls();
1239        if (controls.length > 0)
1240        {
1241          buffer.append(", controls={");
1242          for (int i=0; i < controls.length; i++)
1243          {
1244            if (i > 0)
1245            {
1246              buffer.append(", ");
1247            }
1248    
1249            buffer.append(controls[i]);
1250          }
1251          buffer.append('}');
1252        }
1253    
1254        buffer.append(')');
1255      }
1256    
1257    
1258    
1259      /**
1260       * {@inheritDoc}
1261       */
1262      public void toCode(final List<String> lineList, final String requestID,
1263                         final int indentSpaces, final boolean includeProcessing)
1264      {
1265        // Create the request variable.
1266        final ArrayList<ToCodeArgHelper> constructorArgs =
1267             new ArrayList<ToCodeArgHelper>(modifications.size() + 1);
1268        constructorArgs.add(ToCodeArgHelper.createString(dn, "Entry DN"));
1269    
1270        boolean firstMod = true;
1271        for (final Modification m : modifications)
1272        {
1273          final String comment;
1274          if (firstMod)
1275          {
1276            firstMod = false;
1277            comment = "Modifications";
1278          }
1279          else
1280          {
1281            comment = null;
1282          }
1283    
1284          constructorArgs.add(ToCodeArgHelper.createModification(m, comment));
1285        }
1286    
1287        ToCodeHelper.generateMethodCall(lineList, indentSpaces, "ModifyRequest",
1288             requestID + "Request", "new ModifyRequest", constructorArgs);
1289    
1290    
1291        // If there are any controls, then add them to the request.
1292        for (final Control c : getControls())
1293        {
1294          ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null,
1295               requestID + "Request.addControl",
1296               ToCodeArgHelper.createControl(c, null));
1297        }
1298    
1299    
1300        // Add lines for processing the request and obtaining the result.
1301        if (includeProcessing)
1302        {
1303          // Generate a string with the appropriate indent.
1304          final StringBuilder buffer = new StringBuilder();
1305          for (int i=0; i < indentSpaces; i++)
1306          {
1307            buffer.append(' ');
1308          }
1309          final String indent = buffer.toString();
1310    
1311          lineList.add("");
1312          lineList.add(indent + "try");
1313          lineList.add(indent + '{');
1314          lineList.add(indent + "  LDAPResult " + requestID +
1315               "Result = connection.modify(" + requestID + "Request);");
1316          lineList.add(indent + "  // The modify was processed successfully.");
1317          lineList.add(indent + '}');
1318          lineList.add(indent + "catch (LDAPException e)");
1319          lineList.add(indent + '{');
1320          lineList.add(indent + "  // The modify failed.  Maybe the following " +
1321               "will help explain why.");
1322          lineList.add(indent + "  ResultCode resultCode = e.getResultCode();");
1323          lineList.add(indent + "  String message = e.getMessage();");
1324          lineList.add(indent + "  String matchedDN = e.getMatchedDN();");
1325          lineList.add(indent + "  String[] referralURLs = e.getReferralURLs();");
1326          lineList.add(indent + "  Control[] responseControls = " +
1327               "e.getResponseControls();");
1328          lineList.add(indent + '}');
1329        }
1330      }
1331    }