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