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