001/*
002 * Copyright 2007-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2018 Ping Identity Corporation
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk;
022
023
024
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collections;
028import java.util.List;
029import java.util.Timer;
030import java.util.concurrent.LinkedBlockingQueue;
031import java.util.concurrent.TimeUnit;
032import java.util.logging.Level;
033
034import com.unboundid.asn1.ASN1Buffer;
035import com.unboundid.asn1.ASN1BufferSequence;
036import com.unboundid.asn1.ASN1Element;
037import com.unboundid.asn1.ASN1OctetString;
038import com.unboundid.asn1.ASN1Sequence;
039import com.unboundid.ldap.protocol.LDAPMessage;
040import com.unboundid.ldap.protocol.LDAPResponse;
041import com.unboundid.ldap.protocol.ProtocolOp;
042import com.unboundid.ldif.LDIFChangeRecord;
043import com.unboundid.ldif.LDIFException;
044import com.unboundid.ldif.LDIFModifyChangeRecord;
045import com.unboundid.ldif.LDIFReader;
046import com.unboundid.util.InternalUseOnly;
047import com.unboundid.util.Mutable;
048import com.unboundid.util.ThreadSafety;
049import com.unboundid.util.ThreadSafetyLevel;
050
051import static com.unboundid.ldap.sdk.LDAPMessages.*;
052import static com.unboundid.util.Debug.*;
053import static com.unboundid.util.StaticUtils.*;
054import static com.unboundid.util.Validator.*;
055
056
057
058/**
059 * This class implements the processing necessary to perform an LDAPv3 modify
060 * operation, which can be used to update an entry in the directory server.  A
061 * modify request contains the DN of the entry to modify, as well as one or more
062 * changes to apply to that entry.  See the {@link Modification} class for more
063 * information about the types of modifications that may be processed.
064 * <BR><BR>
065 * A modify request can be created with a DN and set of modifications, but it
066 * can also be as a list of the lines that comprise the LDIF representation of
067 * the modification as described in
068 * <A HREF="http://www.ietf.org/rfc/rfc2849.txt">RFC 2849</A>.  For example, the
069 * following code demonstrates creating a modify request from the LDIF
070 * representation of the modification:
071 * <PRE>
072 *   ModifyRequest modifyRequest = new ModifyRequest(
073 *     "dn: dc=example,dc=com",
074 *     "changetype: modify",
075 *     "replace: description",
076 *     "description: This is the new description.");
077 * </PRE>
078 * <BR><BR>
079 * {@code ModifyRequest} objects are mutable and therefore can be altered and
080 * re-used for multiple requests.  Note, however, that {@code ModifyRequest}
081 * objects are not threadsafe and therefore a single {@code ModifyRequest}
082 * object instance should not be used to process multiple requests at the same
083 * time.
084 */
085@Mutable()
086@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
087public final class ModifyRequest
088       extends UpdatableLDAPRequest
089       implements ReadOnlyModifyRequest, ResponseAcceptor, ProtocolOp
090{
091  /**
092   * The serial version UID for this serializable class.
093   */
094  private static final long serialVersionUID = -4747622844001634758L;
095
096
097
098  // The queue that will be used to receive response messages from the server.
099  private final LinkedBlockingQueue<LDAPResponse> responseQueue =
100       new LinkedBlockingQueue<LDAPResponse>();
101
102  // The set of modifications to perform.
103  private final ArrayList<Modification> modifications;
104
105  // The message ID from the last LDAP message sent from this request.
106  private int messageID = -1;
107
108  // The DN of the entry to modify.
109  private String dn;
110
111
112
113  /**
114   * Creates a new modify request with the provided information.
115   *
116   * @param  dn   The DN of the entry to modify.  It must not be {@code null}.
117   * @param  mod  The modification to apply to the entry.  It must not be
118   *              {@code null}.
119   */
120  public ModifyRequest(final String dn, final Modification mod)
121  {
122    super(null);
123
124    ensureNotNull(dn, mod);
125
126    this.dn = dn;
127
128    modifications = new ArrayList<Modification>(1);
129    modifications.add(mod);
130  }
131
132
133
134  /**
135   * Creates a new modify request with the provided information.
136   *
137   * @param  dn    The DN of the entry to modify.  It must not be {@code null}.
138   * @param  mods  The set of modifications to apply to the entry.  It must not
139   *               be {@code null} or empty.
140   */
141  public ModifyRequest(final String dn, final Modification... mods)
142  {
143    super(null);
144
145    ensureNotNull(dn, mods);
146    ensureFalse(mods.length == 0,
147         "ModifyRequest.mods must not be empty.");
148
149    this.dn = dn;
150
151    modifications = new ArrayList<Modification>(mods.length);
152    modifications.addAll(Arrays.asList(mods));
153  }
154
155
156
157  /**
158   * Creates a new modify request with the provided information.
159   *
160   * @param  dn    The DN of the entry to modify.  It must not be {@code null}.
161   * @param  mods  The set of modifications to apply to the entry.  It must not
162   *               be {@code null} or empty.
163   */
164  public ModifyRequest(final String dn, final List<Modification> mods)
165  {
166    super(null);
167
168    ensureNotNull(dn, mods);
169    ensureFalse(mods.isEmpty(),
170                "ModifyRequest.mods must not be empty.");
171
172    this.dn = dn;
173
174    modifications = new ArrayList<Modification>(mods);
175  }
176
177
178
179  /**
180   * Creates a new modify request with the provided information.
181   *
182   * @param  dn   The DN of the entry to modify.  It must not be {@code null}.
183   * @param  mod  The modification to apply to the entry.  It must not be
184   *              {@code null}.
185   */
186  public ModifyRequest(final DN dn, final Modification mod)
187  {
188    super(null);
189
190    ensureNotNull(dn, mod);
191
192    this.dn = dn.toString();
193
194    modifications = new ArrayList<Modification>(1);
195    modifications.add(mod);
196  }
197
198
199
200  /**
201   * Creates a new modify request with the provided information.
202   *
203   * @param  dn    The DN of the entry to modify.  It must not be {@code null}.
204   * @param  mods  The set of modifications to apply to the entry.  It must not
205   *               be {@code null} or empty.
206   */
207  public ModifyRequest(final DN dn, final Modification... mods)
208  {
209    super(null);
210
211    ensureNotNull(dn, mods);
212    ensureFalse(mods.length == 0,
213         "ModifyRequest.mods must not be empty.");
214
215    this.dn = dn.toString();
216
217    modifications = new ArrayList<Modification>(mods.length);
218    modifications.addAll(Arrays.asList(mods));
219  }
220
221
222
223  /**
224   * Creates a new modify request with the provided information.
225   *
226   * @param  dn    The DN of the entry to modify.  It must not be {@code null}.
227   * @param  mods  The set of modifications to apply to the entry.  It must not
228   *               be {@code null} or empty.
229   */
230  public ModifyRequest(final DN dn, final List<Modification> mods)
231  {
232    super(null);
233
234    ensureNotNull(dn, mods);
235    ensureFalse(mods.isEmpty(),
236                "ModifyRequest.mods must not be empty.");
237
238    this.dn = dn.toString();
239
240    modifications = new ArrayList<Modification>(mods);
241  }
242
243
244
245  /**
246   * Creates a new modify request with the provided information.
247   *
248   * @param  dn        The DN of the entry to modify.  It must not be
249   *                   {@code null}.
250   * @param  mod       The modification to apply to the entry.  It must not be
251   *                   {@code null}.
252   * @param  controls  The set of controls to include in the request.
253   */
254  public ModifyRequest(final String dn, final Modification mod,
255                       final Control[] controls)
256  {
257    super(controls);
258
259    ensureNotNull(dn, mod);
260
261    this.dn = dn;
262
263    modifications = new ArrayList<Modification>(1);
264    modifications.add(mod);
265  }
266
267
268
269  /**
270   * Creates a new modify request with the provided information.
271   *
272   * @param  dn        The DN of the entry to modify.  It must not be
273   *                   {@code null}.
274   * @param  mods      The set of modifications to apply to the entry.  It must
275   *                   not be {@code null} or empty.
276   * @param  controls  The set of controls to include in the request.
277   */
278  public ModifyRequest(final String dn, final Modification[] mods,
279                       final Control[] controls)
280  {
281    super(controls);
282
283    ensureNotNull(dn, mods);
284    ensureFalse(mods.length == 0,
285         "ModifyRequest.mods must not be empty.");
286
287    this.dn = dn;
288
289    modifications = new ArrayList<Modification>(mods.length);
290    modifications.addAll(Arrays.asList(mods));
291  }
292
293
294
295  /**
296   * Creates a new modify request with the provided information.
297   *
298   * @param  dn        The DN of the entry to modify.  It must not be
299   *                   {@code null}.
300   * @param  mods      The set of modifications to apply to the entry.  It must
301   *                   not be {@code null} or empty.
302   * @param  controls  The set of controls to include in the request.
303   */
304  public ModifyRequest(final String dn, final List<Modification> mods,
305                       final Control[] controls)
306  {
307    super(controls);
308
309    ensureNotNull(dn, mods);
310    ensureFalse(mods.isEmpty(),
311                "ModifyRequest.mods must not be empty.");
312
313    this.dn = dn;
314
315    modifications = new ArrayList<Modification>(mods);
316  }
317
318
319
320  /**
321   * Creates a new modify request with the provided information.
322   *
323   * @param  dn        The DN of the entry to modify.  It must not be
324   *                   {@code null}.
325   * @param  mod       The modification to apply to the entry.  It must not be
326   *                   {@code null}.
327   * @param  controls  The set of controls to include in the request.
328   */
329  public ModifyRequest(final DN dn, final Modification mod,
330                       final Control[] controls)
331  {
332    super(controls);
333
334    ensureNotNull(dn, mod);
335
336    this.dn = dn.toString();
337
338    modifications = new ArrayList<Modification>(1);
339    modifications.add(mod);
340  }
341
342
343
344  /**
345   * Creates a new modify request with the provided information.
346   *
347   * @param  dn        The DN of the entry to modify.  It must not be
348   *                   {@code null}.
349   * @param  mods      The set of modifications to apply to the entry.  It must
350   *                   not be {@code null} or empty.
351   * @param  controls  The set of controls to include in the request.
352   */
353  public ModifyRequest(final DN dn, final Modification[] mods,
354                       final Control[] controls)
355  {
356    super(controls);
357
358    ensureNotNull(dn, mods);
359    ensureFalse(mods.length == 0,
360         "ModifyRequest.mods must not be empty.");
361
362    this.dn = dn.toString();
363
364    modifications = new ArrayList<Modification>(mods.length);
365    modifications.addAll(Arrays.asList(mods));
366  }
367
368
369
370  /**
371   * Creates a new modify request with the provided information.
372   *
373   * @param  dn        The DN of the entry to modify.  It must not be
374   *                   {@code null}.
375   * @param  mods      The set of modifications to apply to the entry.  It must
376   *                   not be {@code null} or empty.
377   * @param  controls  The set of controls to include in the request.
378   */
379  public ModifyRequest(final DN dn, final List<Modification> mods,
380                       final Control[] controls)
381  {
382    super(controls);
383
384    ensureNotNull(dn, mods);
385    ensureFalse(mods.isEmpty(),
386                "ModifyRequest.mods must not be empty.");
387
388    this.dn = dn.toString();
389
390    modifications = new ArrayList<Modification>(mods);
391  }
392
393
394
395  /**
396   * Creates a new modify request from the provided LDIF representation of the
397   * changes.
398   *
399   * @param  ldifModificationLines  The lines that comprise an LDIF
400   *                                representation of a modify change record.
401   *                                It must not be {@code null} or empty.
402   *
403   * @throws  LDIFException  If the provided set of lines cannot be parsed as an
404   *                         LDIF modify change record.
405   */
406  public ModifyRequest(final String... ldifModificationLines)
407         throws LDIFException
408  {
409    super(null);
410
411    final LDIFChangeRecord changeRecord =
412         LDIFReader.decodeChangeRecord(ldifModificationLines);
413    if (! (changeRecord instanceof LDIFModifyChangeRecord))
414    {
415      throw new LDIFException(ERR_MODIFY_INVALID_LDIF.get(), 0, false,
416                              ldifModificationLines, null);
417    }
418
419    final LDIFModifyChangeRecord modifyRecord =
420         (LDIFModifyChangeRecord) changeRecord;
421    final ModifyRequest r = modifyRecord.toModifyRequest();
422
423    dn            = r.dn;
424    modifications = r.modifications;
425  }
426
427
428
429  /**
430   * {@inheritDoc}
431   */
432  @Override()
433  public String getDN()
434  {
435    return dn;
436  }
437
438
439
440  /**
441   * Specifies the DN of the entry to modify.
442   *
443   * @param  dn  The DN of the entry to modify.  It must not be {@code null}.
444   */
445  public void setDN(final String dn)
446  {
447    ensureNotNull(dn);
448
449    this.dn = dn;
450  }
451
452
453
454  /**
455   * Specifies the DN of the entry to modify.
456   *
457   * @param  dn  The DN of the entry to modify.  It must not be {@code null}.
458   */
459  public void setDN(final DN dn)
460  {
461    ensureNotNull(dn);
462
463    this.dn = dn.toString();
464  }
465
466
467
468  /**
469   * {@inheritDoc}
470   */
471  @Override()
472  public List<Modification> getModifications()
473  {
474    return Collections.unmodifiableList(modifications);
475  }
476
477
478
479  /**
480   * Adds the provided modification to the set of modifications for this modify
481   * request.
482   *
483   * @param  mod  The modification to be added.  It must not be {@code null}.
484   */
485  public void addModification(final Modification mod)
486  {
487    ensureNotNull(mod);
488
489    modifications.add(mod);
490  }
491
492
493
494  /**
495   * Removes the provided modification from the set of modifications for this
496   * modify request.
497   *
498   * @param  mod  The modification to be removed.  It must not be {@code null}.
499   *
500   * @return  {@code true} if the specified modification was found and removed,
501   *          or {@code false} if not.
502   */
503  public boolean removeModification(final Modification mod)
504  {
505    ensureNotNull(mod);
506
507    return modifications.remove(mod);
508  }
509
510
511
512  /**
513   * Replaces the existing set of modifications for this modify request with the
514   * provided modification.
515   *
516   * @param  mod  The modification to use for this modify request.  It must not
517   *              be {@code null}.
518   */
519  public void setModifications(final Modification mod)
520  {
521    ensureNotNull(mod);
522
523    modifications.clear();
524    modifications.add(mod);
525  }
526
527
528
529  /**
530   * Replaces the existing set of modifications for this modify request with the
531   * provided modifications.
532   *
533   * @param  mods  The set of modification to use for this modify request.  It
534   *               must not be {@code null} or empty.
535   */
536  public void setModifications(final Modification[] mods)
537  {
538    ensureNotNull(mods);
539    ensureFalse(mods.length == 0,
540         "ModifyRequest.setModifications.mods must not be empty.");
541
542    modifications.clear();
543    modifications.addAll(Arrays.asList(mods));
544  }
545
546
547
548  /**
549   * Replaces the existing set of modifications for this modify request with the
550   * provided modifications.
551   *
552   * @param  mods  The set of modification to use for this modify request.  It
553   *               must not be {@code null} or empty.
554   */
555  public void setModifications(final List<Modification> mods)
556  {
557    ensureNotNull(mods);
558    ensureFalse(mods.isEmpty(),
559                "ModifyRequest.setModifications.mods must not be empty.");
560
561    modifications.clear();
562    modifications.addAll(mods);
563  }
564
565
566
567  /**
568   * {@inheritDoc}
569   */
570  @Override()
571  public byte getProtocolOpType()
572  {
573    return LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_REQUEST;
574  }
575
576
577
578  /**
579   * {@inheritDoc}
580   */
581  @Override()
582  public void writeTo(final ASN1Buffer writer)
583  {
584    final ASN1BufferSequence requestSequence =
585         writer.beginSequence(LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_REQUEST);
586    writer.addOctetString(dn);
587
588    final ASN1BufferSequence modSequence = writer.beginSequence();
589    for (final Modification m : modifications)
590    {
591      m.writeTo(writer);
592    }
593    modSequence.end();
594    requestSequence.end();
595  }
596
597
598
599  /**
600   * Encodes the modify request protocol op to an ASN.1 element.
601   *
602   * @return  The ASN.1 element with the encoded modify request protocol op.
603   */
604  @Override()
605  public ASN1Element encodeProtocolOp()
606  {
607    final ASN1Element[] modElements = new ASN1Element[modifications.size()];
608    for (int i=0; i < modElements.length; i++)
609    {
610      modElements[i] = modifications.get(i).encode();
611    }
612
613    final ASN1Element[] protocolOpElements =
614    {
615      new ASN1OctetString(dn),
616      new ASN1Sequence(modElements)
617    };
618
619
620
621    return new ASN1Sequence(LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_REQUEST,
622                            protocolOpElements);
623  }
624
625
626
627  /**
628   * Sends this modify request to the directory server over the provided
629   * connection and returns the associated response.
630   *
631   * @param  connection  The connection to use to communicate with the directory
632   *                     server.
633   * @param  depth       The current referral depth for this request.  It should
634   *                     always be one for the initial request, and should only
635   *                     be incremented when following referrals.
636   *
637   * @return  An LDAP result object that provides information about the result
638   *          of the modify processing.
639   *
640   * @throws  LDAPException  If a problem occurs while sending the request or
641   *                         reading the response.
642   */
643  @Override()
644  protected LDAPResult process(final LDAPConnection connection, final int depth)
645            throws LDAPException
646  {
647    if (connection.synchronousMode())
648    {
649      @SuppressWarnings("deprecation")
650      final boolean autoReconnect =
651           connection.getConnectionOptions().autoReconnect();
652      return processSync(connection, depth, autoReconnect);
653    }
654
655    final long requestTime = System.nanoTime();
656    processAsync(connection, null);
657
658    try
659    {
660      // Wait for and process the response.
661      final LDAPResponse response;
662      try
663      {
664        final long responseTimeout = getResponseTimeoutMillis(connection);
665        if (responseTimeout > 0)
666        {
667          response = responseQueue.poll(responseTimeout, TimeUnit.MILLISECONDS);
668        }
669        else
670        {
671          response = responseQueue.take();
672        }
673      }
674      catch (final InterruptedException ie)
675      {
676        debugException(ie);
677        Thread.currentThread().interrupt();
678        throw new LDAPException(ResultCode.LOCAL_ERROR,
679             ERR_MODIFY_INTERRUPTED.get(connection.getHostPort()), ie);
680      }
681
682      return handleResponse(connection, response, requestTime, depth, false);
683    }
684    finally
685    {
686      connection.deregisterResponseAcceptor(messageID);
687    }
688  }
689
690
691
692  /**
693   * Sends this modify request to the directory server over the provided
694   * connection and returns the message ID for the request.
695   *
696   * @param  connection      The connection to use to communicate with the
697   *                         directory server.
698   * @param  resultListener  The async result listener that is to be notified
699   *                         when the response is received.  It may be
700   *                         {@code null} only if the result is to be processed
701   *                         by this class.
702   *
703   * @return  The async request ID created for the operation, or {@code null} if
704   *          the provided {@code resultListener} is {@code null} and the
705   *          operation will not actually be processed asynchronously.
706   *
707   * @throws  LDAPException  If a problem occurs while sending the request.
708   */
709  AsyncRequestID processAsync(final LDAPConnection connection,
710                              final AsyncResultListener resultListener)
711                 throws LDAPException
712  {
713    // Create the LDAP message.
714    messageID = connection.nextMessageID();
715    final LDAPMessage message = new LDAPMessage(messageID, this, getControls());
716
717
718    // If the provided async result listener is {@code null}, then we'll use
719    // this class as the message acceptor.  Otherwise, create an async helper
720    // and use it as the message acceptor.
721    final AsyncRequestID asyncRequestID;
722    final long timeout = getResponseTimeoutMillis(connection);
723    if (resultListener == null)
724    {
725      asyncRequestID = null;
726      connection.registerResponseAcceptor(messageID, this);
727    }
728    else
729    {
730      final AsyncHelper helper = new AsyncHelper(connection,
731           OperationType.MODIFY, messageID, resultListener,
732           getIntermediateResponseListener());
733      connection.registerResponseAcceptor(messageID, helper);
734      asyncRequestID = helper.getAsyncRequestID();
735
736      if (timeout > 0L)
737      {
738        final Timer timer = connection.getTimer();
739        final AsyncTimeoutTimerTask timerTask =
740             new AsyncTimeoutTimerTask(helper);
741        timer.schedule(timerTask, timeout);
742        asyncRequestID.setTimerTask(timerTask);
743      }
744    }
745
746
747    // Send the request to the server.
748    try
749    {
750      debugLDAPRequest(Level.INFO, this, messageID, connection);
751      connection.getConnectionStatistics().incrementNumModifyRequests();
752      connection.sendMessage(message, timeout);
753      return asyncRequestID;
754    }
755    catch (final LDAPException le)
756    {
757      debugException(le);
758
759      connection.deregisterResponseAcceptor(messageID);
760      throw le;
761    }
762  }
763
764
765
766  /**
767   * Processes this modify operation in synchronous mode, in which the same
768   * thread will send the request and read the response.
769   *
770   * @param  connection  The connection to use to communicate with the directory
771   *                     server.
772   * @param  depth       The current referral depth for this request.  It should
773   *                     always be one for the initial request, and should only
774   *                     be incremented when following referrals.
775   * @param  allowRetry  Indicates whether the request may be re-tried on a
776   *                     re-established connection if the initial attempt fails
777   *                     in a way that indicates the connection is no longer
778   *                     valid and autoReconnect is true.
779   *
780   * @return  An LDAP result object that provides information about the result
781   *          of the modify processing.
782   *
783   * @throws  LDAPException  If a problem occurs while sending the request or
784   *                         reading the response.
785   */
786  private LDAPResult processSync(final LDAPConnection connection,
787                                 final int depth, final boolean allowRetry)
788          throws LDAPException
789  {
790    // Create the LDAP message.
791    messageID = connection.nextMessageID();
792    final LDAPMessage message =
793         new LDAPMessage(messageID,  this, getControls());
794
795
796    // Send the request to the server.
797    final long requestTime = System.nanoTime();
798    debugLDAPRequest(Level.INFO, this, messageID, connection);
799    connection.getConnectionStatistics().incrementNumModifyRequests();
800    try
801    {
802      connection.sendMessage(message, getResponseTimeoutMillis(connection));
803    }
804    catch (final LDAPException le)
805    {
806      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 = getReferralConnector(connection).
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 (final 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  @Override()
1087  public void responseReceived(final LDAPResponse response)
1088         throws LDAPException
1089  {
1090    try
1091    {
1092      responseQueue.put(response);
1093    }
1094    catch (final Exception e)
1095    {
1096      debugException(e);
1097
1098      if (e instanceof InterruptedException)
1099      {
1100        Thread.currentThread().interrupt();
1101      }
1102
1103      throw new LDAPException(ResultCode.LOCAL_ERROR,
1104           ERR_EXCEPTION_HANDLING_RESPONSE.get(getExceptionMessage(e)), e);
1105    }
1106  }
1107
1108
1109
1110  /**
1111   * {@inheritDoc}
1112   */
1113  @Override()
1114  public int getLastMessageID()
1115  {
1116    return messageID;
1117  }
1118
1119
1120
1121  /**
1122   * {@inheritDoc}
1123   */
1124  @Override()
1125  public OperationType getOperationType()
1126  {
1127    return OperationType.MODIFY;
1128  }
1129
1130
1131
1132  /**
1133   * {@inheritDoc}
1134   */
1135  @Override()
1136  public ModifyRequest duplicate()
1137  {
1138    return duplicate(getControls());
1139  }
1140
1141
1142
1143  /**
1144   * {@inheritDoc}
1145   */
1146  @Override()
1147  public ModifyRequest duplicate(final Control[] controls)
1148  {
1149    final ModifyRequest r = new ModifyRequest(dn,
1150         new ArrayList<Modification>(modifications), controls);
1151
1152    if (followReferralsInternal() != null)
1153    {
1154      r.setFollowReferrals(followReferralsInternal());
1155    }
1156
1157    if (getReferralConnectorInternal() != null)
1158    {
1159      r.setReferralConnector(getReferralConnectorInternal());
1160    }
1161
1162    r.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
1163
1164    return r;
1165  }
1166
1167
1168
1169  /**
1170   * {@inheritDoc}
1171   */
1172  @Override()
1173  public LDIFModifyChangeRecord toLDIFChangeRecord()
1174  {
1175    return new LDIFModifyChangeRecord(this);
1176  }
1177
1178
1179
1180  /**
1181   * {@inheritDoc}
1182   */
1183  @Override()
1184  public String[] toLDIF()
1185  {
1186    return toLDIFChangeRecord().toLDIF();
1187  }
1188
1189
1190
1191  /**
1192   * {@inheritDoc}
1193   */
1194  @Override()
1195  public String toLDIFString()
1196  {
1197    return toLDIFChangeRecord().toLDIFString();
1198  }
1199
1200
1201
1202  /**
1203   * {@inheritDoc}
1204   */
1205  @Override()
1206  public void toString(final StringBuilder buffer)
1207  {
1208    buffer.append("ModifyRequest(dn='");
1209    buffer.append(dn);
1210    buffer.append("', mods={");
1211    for (int i=0; i < modifications.size(); i++)
1212    {
1213      final Modification m = modifications.get(i);
1214
1215      if (i > 0)
1216      {
1217        buffer.append(", ");
1218      }
1219
1220      switch (m.getModificationType().intValue())
1221      {
1222        case 0:
1223          buffer.append("ADD ");
1224          break;
1225
1226        case 1:
1227          buffer.append("DELETE ");
1228          break;
1229
1230        case 2:
1231          buffer.append("REPLACE ");
1232          break;
1233
1234        case 3:
1235          buffer.append("INCREMENT ");
1236          break;
1237      }
1238
1239      buffer.append(m.getAttributeName());
1240    }
1241    buffer.append('}');
1242
1243    final Control[] controls = getControls();
1244    if (controls.length > 0)
1245    {
1246      buffer.append(", controls={");
1247      for (int i=0; i < controls.length; i++)
1248      {
1249        if (i > 0)
1250        {
1251          buffer.append(", ");
1252        }
1253
1254        buffer.append(controls[i]);
1255      }
1256      buffer.append('}');
1257    }
1258
1259    buffer.append(')');
1260  }
1261
1262
1263
1264  /**
1265   * {@inheritDoc}
1266   */
1267  @Override()
1268  public void toCode(final List<String> lineList, final String requestID,
1269                     final int indentSpaces, final boolean includeProcessing)
1270  {
1271    // Create the request variable.
1272    final ArrayList<ToCodeArgHelper> constructorArgs =
1273         new ArrayList<ToCodeArgHelper>(modifications.size() + 1);
1274    constructorArgs.add(ToCodeArgHelper.createString(dn, "Entry DN"));
1275
1276    boolean firstMod = true;
1277    for (final Modification m : modifications)
1278    {
1279      final String comment;
1280      if (firstMod)
1281      {
1282        firstMod = false;
1283        comment = "Modifications";
1284      }
1285      else
1286      {
1287        comment = null;
1288      }
1289
1290      constructorArgs.add(ToCodeArgHelper.createModification(m, comment));
1291    }
1292
1293    ToCodeHelper.generateMethodCall(lineList, indentSpaces, "ModifyRequest",
1294         requestID + "Request", "new ModifyRequest", constructorArgs);
1295
1296
1297    // If there are any controls, then add them to the request.
1298    for (final Control c : getControls())
1299    {
1300      ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null,
1301           requestID + "Request.addControl",
1302           ToCodeArgHelper.createControl(c, null));
1303    }
1304
1305
1306    // Add lines for processing the request and obtaining the result.
1307    if (includeProcessing)
1308    {
1309      // Generate a string with the appropriate indent.
1310      final StringBuilder buffer = new StringBuilder();
1311      for (int i=0; i < indentSpaces; i++)
1312      {
1313        buffer.append(' ');
1314      }
1315      final String indent = buffer.toString();
1316
1317      lineList.add("");
1318      lineList.add(indent + "try");
1319      lineList.add(indent + '{');
1320      lineList.add(indent + "  LDAPResult " + requestID +
1321           "Result = connection.modify(" + requestID + "Request);");
1322      lineList.add(indent + "  // The modify was processed successfully.");
1323      lineList.add(indent + '}');
1324      lineList.add(indent + "catch (LDAPException e)");
1325      lineList.add(indent + '{');
1326      lineList.add(indent + "  // The modify failed.  Maybe the following " +
1327           "will help explain why.");
1328      lineList.add(indent + "  ResultCode resultCode = e.getResultCode();");
1329      lineList.add(indent + "  String message = e.getMessage();");
1330      lineList.add(indent + "  String matchedDN = e.getMatchedDN();");
1331      lineList.add(indent + "  String[] referralURLs = e.getReferralURLs();");
1332      lineList.add(indent + "  Control[] responseControls = " +
1333           "e.getResponseControls();");
1334      lineList.add(indent + '}');
1335    }
1336  }
1337}