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