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