001/*
002 * Copyright 2007-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2007-2024 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) 2007-2024 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.unboundidds.extensions;
037
038
039import java.util.ArrayList;
040import java.util.Map;
041import java.util.TreeMap;
042
043import com.unboundid.asn1.ASN1Constants;
044import com.unboundid.asn1.ASN1Element;
045import com.unboundid.asn1.ASN1Exception;
046import com.unboundid.asn1.ASN1Integer;
047import com.unboundid.asn1.ASN1OctetString;
048import com.unboundid.asn1.ASN1Sequence;
049import com.unboundid.ldap.sdk.Control;
050import com.unboundid.ldap.sdk.ExtendedResult;
051import com.unboundid.ldap.sdk.LDAPException;
052import com.unboundid.ldap.sdk.ResultCode;
053import com.unboundid.util.Debug;
054import com.unboundid.util.NotMutable;
055import com.unboundid.util.NotNull;
056import com.unboundid.util.Nullable;
057import com.unboundid.util.StaticUtils;
058import com.unboundid.util.ThreadSafety;
059import com.unboundid.util.ThreadSafetyLevel;
060
061import static com.unboundid.ldap.sdk.unboundidds.extensions.ExtOpMessages.*;
062
063
064
065/**
066 * This class provides an implementation of the end batched transaction extended
067 * result.  It is able to decode a generic extended result to extract the
068 * appropriate response information.
069 * <BR>
070 * <BLOCKQUOTE>
071 *   <B>NOTE:</B>  This class, and other classes within the
072 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
073 *   supported for use against Ping Identity, UnboundID, and
074 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
075 *   for proprietary functionality or for external specifications that are not
076 *   considered stable or mature enough to be guaranteed to work in an
077 *   interoperable way with other types of LDAP servers.
078 * </BLOCKQUOTE>
079 * <BR>
080 * The end batched transaction result may include two elements:
081 * <UL>
082 *   <LI>{@code failedOpMessageID} -- The message ID associated with the LDAP
083 *       request that caused the transaction to fail.  It will be "{@code -1}"
084 *       if the transaction was committed successfully.</LI>
085 *   <LI>{@code opResponseControls} -- A map containing the response controls
086 *       associated with each of the operations processed as part of the
087 *       transaction, mapped from the message ID of the associated request to
088 *       the array of response controls for that operation.  If there are no
089 *       response controls for a given request, then it will not be included in
090 *       the map.</LI>
091 * </UL>
092 * Note that both of these elements reference the LDAP message ID for the
093 * associated request.  Normally, this is not something that developers using
094 * the UnboundID LDAP SDK for Java need to access since it is handled behind the
095 * scenes, but the LDAP message ID for an operation is available through the
096 * {@link com.unboundid.ldap.sdk.LDAPResult#getMessageID} method in the response
097 * for that operation.  When processing operations that are part of a batched,
098 * transaction it may be desirable to keep references to the associated requests
099 * mapped by message ID so that they can be available if necessary for the
100 * {@code failedOpMessageID} and/or {@code opResponseControls} elements.
101 * <BR><BR>
102 * See the documentation for the {@link StartBatchedTransactionExtendedRequest}
103 * for an example of performing a batched transaction.
104 */
105@NotMutable()
106@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
107public final class EndBatchedTransactionExtendedResult
108       extends ExtendedResult
109{
110  /**
111   * The serial version UID for this serializable class.
112   */
113  private static final long serialVersionUID = 1514265185948328221L;
114
115
116
117  // The message ID for the operation that failed, if applicable.
118  private final int failedOpMessageID;
119
120  // A mapping of the response controls for the operations performed as part of
121  // the transaction.
122  @NotNull private final TreeMap<Integer,Control[]> opResponseControls;
123
124
125
126  /**
127   * Creates a new end batched transaction extended result from the provided
128   * extended result.
129   *
130   * @param  extendedResult  The extended result to be decoded as an end batched
131   *                         transaction extended result.  It must not be
132   *                         {@code null}.
133   *
134   * @throws  LDAPException  If a problem occurs while attempting to decode the
135   *                         provided extended result as an end batched
136   *                         transaction extended result.
137   */
138  public EndBatchedTransactionExtendedResult(
139              @NotNull final ExtendedResult extendedResult)
140         throws LDAPException
141  {
142    super(extendedResult);
143
144    opResponseControls = new TreeMap<>();
145
146    final ASN1OctetString value = extendedResult.getValue();
147    if (value == null)
148    {
149      failedOpMessageID = -1;
150      return;
151    }
152
153    final ASN1Sequence valueSequence;
154    try
155    {
156      final ASN1Element valueElement = ASN1Element.decode(value.getValue());
157      valueSequence = ASN1Sequence.decodeAsSequence(valueElement);
158    }
159    catch (final ASN1Exception ae)
160    {
161      Debug.debugException(ae);
162      throw new LDAPException(ResultCode.DECODING_ERROR,
163           ERR_END_TXN_RESPONSE_VALUE_NOT_SEQUENCE.get(ae.getMessage()), ae);
164    }
165
166    final ASN1Element[] valueElements = valueSequence.elements();
167    if (valueElements.length == 0)
168    {
169      failedOpMessageID = -1;
170      return;
171    }
172    else if (valueElements.length > 2)
173    {
174      throw new LDAPException(ResultCode.DECODING_ERROR,
175           ERR_END_TXN_RESPONSE_INVALID_ELEMENT_COUNT.get(
176                valueElements.length));
177    }
178
179    int msgID = -1;
180    for (final ASN1Element e : valueElements)
181    {
182      if (e.getType() == ASN1Constants.UNIVERSAL_INTEGER_TYPE)
183      {
184        try
185        {
186          msgID = ASN1Integer.decodeAsInteger(e).intValue();
187        }
188        catch (final ASN1Exception ae)
189        {
190          Debug.debugException(ae);
191          throw new LDAPException(ResultCode.DECODING_ERROR,
192               ERR_END_TXN_RESPONSE_CANNOT_DECODE_MSGID.get(ae), ae);
193        }
194      }
195      else if (e.getType() == ASN1Constants.UNIVERSAL_SEQUENCE_TYPE)
196      {
197        decodeOpControls(e, opResponseControls);
198      }
199      else
200      {
201        throw new LDAPException(ResultCode.DECODING_ERROR,
202             ERR_END_TXN_RESPONSE_INVALID_TYPE.get(
203                  StaticUtils.toHex(e.getType())));
204      }
205    }
206
207    failedOpMessageID = msgID;
208  }
209
210
211
212  /**
213   * Creates a new end batched transaction extended result with the provided
214   * information.
215   *
216   * @param  messageID           The message ID for the LDAP message that is
217   *                             associated with this LDAP result.
218   * @param  resultCode          The result code from the response.
219   * @param  diagnosticMessage   The diagnostic message from the response, if
220   *                             available.
221   * @param  matchedDN           The matched DN from the response, if available.
222   * @param  referralURLs        The set of referral URLs from the response, if
223   *                             available.
224   * @param  failedOpMessageID   The message ID for the operation that failed,
225   *                             or {@code null} if there was no failure.
226   * @param  opResponseControls  A map containing the response controls for each
227   *                             operation, indexed by message ID.  It may be
228   *                             {@code null} if there were no response
229   *                             controls.
230   * @param  responseControls    The set of controls from the response, if
231   *                             available.
232   */
233  public EndBatchedTransactionExtendedResult(final int messageID,
234              @NotNull final ResultCode resultCode,
235              @Nullable final String diagnosticMessage,
236              @Nullable final String matchedDN,
237              @Nullable final String[] referralURLs,
238              @Nullable final Integer failedOpMessageID,
239              @Nullable final Map<Integer,Control[]> opResponseControls,
240              @Nullable final Control[] responseControls)
241  {
242    super(messageID, resultCode, diagnosticMessage, matchedDN, referralURLs,
243          null, encodeValue(failedOpMessageID, opResponseControls),
244                            responseControls);
245
246    if ((failedOpMessageID == null) || (failedOpMessageID <= 0))
247    {
248      this.failedOpMessageID = -1;
249    }
250    else
251    {
252      this.failedOpMessageID = failedOpMessageID;
253    }
254
255    if (opResponseControls == null)
256    {
257      this.opResponseControls = new TreeMap<>();
258    }
259    else
260    {
261      this.opResponseControls = new TreeMap<>(opResponseControls);
262    }
263  }
264
265
266
267  /**
268   * Decodes the provided ASN.1 element as an update controls sequence.  Each
269   * element of the sequence should itself be a sequence containing the message
270   * ID associated with the operation in which the control was returned and a
271   * sequence of the controls included in the response for that operation.
272   *
273   * @param  element     The ASN.1 element to be decoded.
274   * @param  controlMap  The map into which to place the decoded controls.
275   *
276   * @throws  LDAPException  If a problem occurs while attempting to decode the
277   *                         contents of the provided ASN.1 element.
278   */
279  private static void decodeOpControls(@NotNull final ASN1Element element,
280                           @NotNull final Map<Integer,Control[]> controlMap)
281          throws LDAPException
282  {
283    final ASN1Sequence ctlsSequence;
284    try
285    {
286      ctlsSequence = ASN1Sequence.decodeAsSequence(element);
287    }
288    catch (final ASN1Exception ae)
289    {
290      Debug.debugException(ae);
291      throw new LDAPException(ResultCode.DECODING_ERROR,
292           ERR_END_TXN_RESPONSE_CONTROLS_NOT_SEQUENCE.get(ae), ae);
293    }
294
295    for (final ASN1Element e : ctlsSequence.elements())
296    {
297      final ASN1Sequence ctlSequence;
298      try
299      {
300        ctlSequence = ASN1Sequence.decodeAsSequence(e);
301      }
302      catch (final ASN1Exception ae)
303      {
304        Debug.debugException(ae);
305        throw new LDAPException(ResultCode.DECODING_ERROR,
306             ERR_END_TXN_RESPONSE_CONTROL_NOT_SEQUENCE.get(ae), ae);
307      }
308
309      final ASN1Element[] ctlSequenceElements = ctlSequence.elements();
310      if (ctlSequenceElements.length != 2)
311      {
312        throw new LDAPException(ResultCode.DECODING_ERROR,
313             ERR_END_TXN_RESPONSE_CONTROL_INVALID_ELEMENT_COUNT.get(
314                  ctlSequenceElements.length));
315      }
316
317      final int msgID;
318      try
319      {
320        msgID = ASN1Integer.decodeAsInteger(ctlSequenceElements[0]).intValue();
321      }
322      catch (final ASN1Exception ae)
323      {
324        Debug.debugException(ae);
325        throw new LDAPException(ResultCode.DECODING_ERROR,
326             ERR_END_TXN_RESPONSE_CONTROL_MSGID_NOT_INT.get(ae), ae);
327      }
328
329      final ASN1Sequence controlsSequence;
330      try
331      {
332        controlsSequence =
333             ASN1Sequence.decodeAsSequence(ctlSequenceElements[1]);
334      }
335      catch (final ASN1Exception ae)
336      {
337        Debug.debugException(ae);
338        throw new LDAPException(ResultCode.DECODING_ERROR,
339             ERR_END_TXN_RESPONSE_CONTROLS_ELEMENT_NOT_SEQUENCE.get(ae), ae);
340      }
341
342      final Control[] controls = Control.decodeControls(controlsSequence);
343      if (controls.length == 0)
344      {
345        continue;
346      }
347
348      controlMap.put(msgID, controls);
349    }
350  }
351
352
353
354  /**
355   * Encodes the provided information into an appropriate value for this
356   * control.
357   *
358   * @param  failedOpMessageID   The message ID for the operation that failed,
359   *                             or {@code null} if there was no failure.
360   * @param  opResponseControls  A map containing the response controls for each
361   *                             operation, indexed by message ID.  It may be
362   *                             {@code null} if there were no response
363   *                             controls.
364   *
365   * @return  An ASN.1 octet string containing the encoded value for this
366   *          control, or {@code null} if there should not be a value.
367   */
368  @Nullable()
369  private static ASN1OctetString encodeValue(
370               @Nullable final Integer failedOpMessageID,
371               @Nullable final Map<Integer,Control[]> opResponseControls)
372  {
373    if ((failedOpMessageID == null) && (opResponseControls == null))
374    {
375      return null;
376    }
377
378    final ArrayList<ASN1Element> elements = new ArrayList<>(2);
379    if (failedOpMessageID != null)
380    {
381      elements.add(new ASN1Integer(failedOpMessageID));
382    }
383
384    if ((opResponseControls != null) && (! opResponseControls.isEmpty()))
385    {
386      final ArrayList<ASN1Element> controlElements = new ArrayList<>(10);
387      for (final Map.Entry<Integer,Control[]> e : opResponseControls.entrySet())
388      {
389        final ASN1Element[] ctlElements =
390        {
391          new ASN1Integer(e.getKey()),
392          Control.encodeControls(e.getValue())
393        };
394        controlElements.add(new ASN1Sequence(ctlElements));
395      }
396
397      elements.add(new ASN1Sequence(controlElements));
398    }
399
400    return new ASN1OctetString(new ASN1Sequence(elements).encode());
401  }
402
403
404
405  /**
406   * Retrieves the message ID of the operation that caused the transaction
407   * processing to fail, if applicable.
408   *
409   * @return  The message ID of the operation that caused the transaction
410   *          processing to fail, or -1 if no message ID was included in the
411   *          end transaction response.
412   */
413  public int getFailedOpMessageID()
414  {
415    return failedOpMessageID;
416  }
417
418
419
420  /**
421   * Retrieves the set of response controls returned by the operations
422   * processed as part of the transaction.  The value returned will contain a
423   * mapping between the message ID of the associated request message and a list
424   * of the response controls for that operation.
425   *
426   * @return  The set of response controls returned by the operations processed
427   *          as part of the transaction.  It may be an empty map if none of the
428   *          operations had any response controls.
429   */
430  @NotNull()
431  public Map<Integer,Control[]> getOperationResponseControls()
432  {
433    return opResponseControls;
434  }
435
436
437
438  /**
439   * Retrieves the set of response controls returned by the specified operation
440   * processed as part of the transaction.
441   *
442   * @param  messageID  The message ID of the operation for which to retrieve
443   *                    the response controls.
444   *
445   * @return  The response controls for the specified operation, or
446   *          {@code null} if there were no controls returned for the specified
447   *          operation.
448   */
449  @Nullable()
450  public Control[] getOperationResponseControls(final int messageID)
451  {
452    return opResponseControls.get(messageID);
453  }
454
455
456
457  /**
458   * {@inheritDoc}
459   */
460  @Override()
461  @NotNull()
462  public String getExtendedResultName()
463  {
464    return INFO_EXTENDED_RESULT_NAME_END_BATCHED_TXN.get();
465  }
466
467
468
469  /**
470   * Appends a string representation of this extended result to the provided
471   * buffer.
472   *
473   * @param  buffer  The buffer to which a string representation of this
474   *                 extended result will be appended.
475   */
476  @Override()
477  public void toString(@NotNull final StringBuilder buffer)
478  {
479    buffer.append("EndBatchedTransactionExtendedResult(resultCode=");
480    buffer.append(getResultCode());
481
482    final int messageID = getMessageID();
483    if (messageID >= 0)
484    {
485      buffer.append(", messageID=");
486      buffer.append(messageID);
487    }
488
489    if (failedOpMessageID > 0)
490    {
491      buffer.append(", failedOpMessageID=");
492      buffer.append(failedOpMessageID);
493    }
494
495    if (! opResponseControls.isEmpty())
496    {
497      buffer.append(", opResponseControls={");
498
499      for (final int msgID : opResponseControls.keySet())
500      {
501        buffer.append("opMsgID=");
502        buffer.append(msgID);
503        buffer.append(", opControls={");
504
505        boolean first = true;
506        for (final Control c : opResponseControls.get(msgID))
507        {
508          if (first)
509          {
510            first = false;
511          }
512          else
513          {
514            buffer.append(", ");
515          }
516
517          buffer.append(c);
518        }
519        buffer.append('}');
520      }
521
522      buffer.append('}');
523    }
524
525    final String diagnosticMessage = getDiagnosticMessage();
526    if (diagnosticMessage != null)
527    {
528      buffer.append(", diagnosticMessage='");
529      buffer.append(diagnosticMessage);
530      buffer.append('\'');
531    }
532
533    final String matchedDN = getMatchedDN();
534    if (matchedDN != null)
535    {
536      buffer.append(", matchedDN='");
537      buffer.append(matchedDN);
538      buffer.append('\'');
539    }
540
541    final String[] referralURLs = getReferralURLs();
542    if (referralURLs.length > 0)
543    {
544      buffer.append(", referralURLs={");
545      for (int i=0; i < referralURLs.length; i++)
546      {
547        if (i > 0)
548        {
549          buffer.append(", ");
550        }
551
552        buffer.append('\'');
553        buffer.append(referralURLs[i]);
554        buffer.append('\'');
555      }
556      buffer.append('}');
557    }
558
559    final Control[] responseControls = getResponseControls();
560    if (responseControls.length > 0)
561    {
562      buffer.append(", responseControls={");
563      for (int i=0; i < responseControls.length; i++)
564      {
565        if (i > 0)
566        {
567          buffer.append(", ");
568        }
569
570        buffer.append(responseControls[i]);
571      }
572      buffer.append('}');
573    }
574
575    buffer.append(')');
576  }
577}