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