001/*
002 * Copyright 2011-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2015-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.unboundidds.controls;
022
023
024
025import java.util.ArrayList;
026
027import com.unboundid.asn1.ASN1Element;
028import com.unboundid.asn1.ASN1OctetString;
029import com.unboundid.asn1.ASN1Sequence;
030import com.unboundid.ldap.sdk.Control;
031import com.unboundid.ldap.sdk.LDAPException;
032import com.unboundid.ldap.sdk.ResultCode;
033import com.unboundid.util.Debug;
034import com.unboundid.util.NotMutable;
035import com.unboundid.util.StaticUtils;
036import com.unboundid.util.ThreadSafety;
037import com.unboundid.util.ThreadSafetyLevel;
038import com.unboundid.util.Validator;
039
040import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*;
041
042
043
044/**
045 * This class provides a request control that can be used by the client to
046 * identify the purpose of the associated operation.  It can be used in
047 * conjunction with any kind of operation, and may be used to provide
048 * information about the reason for that operation, as well as about the client
049 * application used to generate the request.  This may be very useful for
050 * debugging and auditing purposes.
051 * <BR>
052 * <BLOCKQUOTE>
053 *   <B>NOTE:</B>  This class, and other classes within the
054 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
055 *   supported for use against Ping Identity, UnboundID, and Alcatel-Lucent 8661
056 *   server products.  These classes provide support for proprietary
057 *   functionality or for external specifications that are not considered stable
058 *   or mature enough to be guaranteed to work in an interoperable way with
059 *   other types of LDAP servers.
060 * </BLOCKQUOTE>
061 * <BR>
062 * The criticality for this control may be either {@code true} or {@code false}.
063 * It must have a value with the following encoding:
064 * <PRE>
065 *   OperationPurposeRequest ::= SEQUENCE {
066 *        applicationName     [0] OCTET STRING OPTIONAL,
067 *        applicationVersion  [1] OCTET STRING OPTIONAL,
068 *        codeLocation        [2] OCTET STRING OPTIONAL,
069 *        requestPurpose      [3] OCTET STRING OPTIONAL
070 *        ... }
071 * </PRE>
072 * At least one of the elements in the value sequence must be present.
073 * <BR><BR>
074 * <H2>Example</H2>
075 * The following example demonstrates a sample authentication consisting of a
076 * search to find a user followed by a bind to verify that user's password.
077 * Both the search and bind requests will include operation purpose controls
078 * with information about the reason for the request.  Note that for the sake
079 * of brevity and clarity, error handling has been omitted from this example.
080 * <PRE>
081 * SearchRequest searchRequest = new SearchRequest("dc=example,dc=com",
082 *      SearchScope.SUB, Filter.createEqualityFilter("uid", uidValue),
083 *      "1.1");
084 * searchRequest.addControl(new OperationPurposeRequestControl(appName,
085 *      appVersion, 0,  "Retrieve the entry for a user with a given uid"));
086 * Entry userEntry = connection.searchForEntry(searchRequest);
087 *
088 * SimpleBindRequest bindRequest = new SimpleBindRequest(userEntry.getDN(),
089 *      password, new OperationPurposeRequestControl(appName, appVersion, 0,
090 *      "Bind as a user to verify the provided credentials."));
091 * BindResult bindResult = connection.bind(bindRequest);
092 * </PRE>
093 */
094@NotMutable()
095@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
096public final class OperationPurposeRequestControl
097       extends Control
098{
099  /**
100   * The OID (1.3.6.1.4.1.30221.2.5.19) for the operation purpose request
101   * control.
102   */
103  public static final String OPERATION_PURPOSE_REQUEST_OID =
104       "1.3.6.1.4.1.30221.2.5.19";
105
106
107
108  /**
109   * The BER type for the element that specifies the application name.
110   */
111  private static final byte TYPE_APP_NAME = (byte) 0x80;
112
113
114
115  /**
116   * The BER type for the element that specifies the application version.
117   */
118  private static final byte TYPE_APP_VERSION = (byte) 0x81;
119
120
121
122  /**
123   * The BER type for the element that specifies the code location.
124   */
125  private static final byte TYPE_CODE_LOCATION = (byte) 0x82;
126
127
128
129  /**
130   * The BER type for the element that specifies the request purpose.
131   */
132  private static final byte TYPE_REQUEST_PURPOSE = (byte) 0x83;
133
134
135
136  /**
137   * The serial version UID for this serializable class.
138   */
139  private static final long serialVersionUID = -5552051862785419833L;
140
141
142
143  // The application name for this control, if any.
144  private final String applicationName;
145
146  // The application version for this control, if any.
147  private final String applicationVersion;
148
149  // The code location for this control, if any.
150  private final String codeLocation;
151
152  // The request purpose for this control, if any.
153  private final String requestPurpose;
154
155
156
157  /**
158   * Creates a new operation purpose request control with the provided
159   * information.  It will not be critical.  If the generateCodeLocation
160   * argument has a value of {@code false}, then at least one of the
161   * applicationName, applicationVersion, and requestPurpose arguments must
162   * be non-{@code null}.
163   *
164   * @param  applicationName     The name of the application generating the
165   *                             associated request.  It may be {@code null} if
166   *                             this should not be included in the control.
167   * @param  applicationVersion  Information about the version of the
168   *                             application generating the associated request.
169   *                             It may be {@code null} if this should not be
170   *                             included in the control.
171   * @param  codeLocationFrames  Indicates that the code location should be
172   *                             automatically generated with a condensed stack
173   *                             trace for the current thread, using the
174   *                             specified number of stack frames.  A value that
175   *                             is less than or equal to zero indicates an
176   *                             unlimited number of stack frames should be
177   *                             included.
178   * @param  requestPurpose      A string identifying the purpose of the
179   *                             associated request.  It may be {@code null} if
180   *                             this should not be included in the control.
181   */
182  public OperationPurposeRequestControl(final String applicationName,
183                                        final String applicationVersion,
184                                        final int codeLocationFrames,
185                                        final String requestPurpose)
186  {
187    this(false, applicationName, applicationVersion,
188         generateStackTrace(codeLocationFrames), requestPurpose);
189  }
190
191
192
193  /**
194   * Creates a new operation purpose request control with the provided
195   * information.  At least one of the applicationName, applicationVersion,
196   * codeLocation, and requestPurpose arguments must be non-{@code null}.
197   *
198   * @param  isCritical          Indicates whether the control should be
199   *                             considered critical.
200   * @param  applicationName     The name of the application generating the
201   *                             associated request.  It may be {@code null} if
202   *                             this should not be included in the control.
203   * @param  applicationVersion  Information about the version of the
204   *                             application generating the associated request.
205   *                             It may be {@code null} if this should not be
206   *                             included in the control.
207   * @param  codeLocation        Information about the location in the
208   *                             application code in which the associated
209   *                             request is generated (e.g., the class and/or
210   *                             method name, or any other useful identifier).
211   *                             It may be {@code null} if this should not be
212   *                             included in the control.
213   * @param  requestPurpose      A string identifying the purpose of the
214   *                             associated request.  It may be {@code null} if
215   *                             this should not be included in the control.
216   */
217  public OperationPurposeRequestControl(final boolean isCritical,
218                                        final String applicationName,
219                                        final String applicationVersion,
220                                        final String codeLocation,
221                                        final String requestPurpose)
222  {
223    super(OPERATION_PURPOSE_REQUEST_OID, isCritical,
224         encodeValue(applicationName, applicationVersion, codeLocation,
225              requestPurpose));
226
227    this.applicationName    = applicationName;
228    this.applicationVersion = applicationVersion;
229    this.codeLocation       = codeLocation;
230    this.requestPurpose     = requestPurpose;
231  }
232
233
234
235  /**
236   * Creates a new operation purpose request control which is decoded from the
237   * provided generic control.
238   *
239   * @param  control  The generic control to be decoded as an operation purpose
240   *                  request control.
241   *
242   * @throws  LDAPException  If the provided control cannot be decoded as an
243   *                         operation purpose request control.
244   */
245  public OperationPurposeRequestControl(final Control control)
246         throws LDAPException
247  {
248    super(control);
249
250    final ASN1OctetString value = control.getValue();
251    if (value == null)
252    {
253      throw new LDAPException(ResultCode.DECODING_ERROR,
254           ERR_OP_PURPOSE_NO_VALUE.get());
255    }
256
257    final ASN1Element[] valueElements;
258    try
259    {
260      valueElements =
261           ASN1Sequence.decodeAsSequence(value.getValue()).elements();
262    }
263    catch (final Exception e)
264    {
265      Debug.debugException(e);
266      throw new LDAPException(ResultCode.DECODING_ERROR,
267           ERR_OP_PURPOSE_VALUE_NOT_SEQUENCE.get(
268                StaticUtils.getExceptionMessage(e)),
269           e);
270    }
271
272    if (valueElements.length == 0)
273    {
274      throw new LDAPException(ResultCode.DECODING_ERROR,
275           ERR_OP_PURPOSE_VALUE_SEQUENCE_EMPTY.get());
276    }
277
278
279    String appName    = null;
280    String appVersion = null;
281    String codeLoc    = null;
282    String reqPurpose = null;
283    for (final ASN1Element e : valueElements)
284    {
285      switch (e.getType())
286      {
287        case TYPE_APP_NAME:
288          appName = ASN1OctetString.decodeAsOctetString(e).stringValue();
289          break;
290
291        case TYPE_APP_VERSION:
292          appVersion = ASN1OctetString.decodeAsOctetString(e).stringValue();
293          break;
294
295        case TYPE_CODE_LOCATION:
296          codeLoc = ASN1OctetString.decodeAsOctetString(e).stringValue();
297          break;
298
299        case TYPE_REQUEST_PURPOSE:
300          reqPurpose = ASN1OctetString.decodeAsOctetString(e).stringValue();
301          break;
302
303        default:
304          throw new LDAPException(ResultCode.DECODING_ERROR,
305               ERR_OP_PURPOSE_VALUE_UNSUPPORTED_ELEMENT.get(
306                    StaticUtils.toHex(e.getType())));
307      }
308    }
309
310    applicationName    = appName;
311    applicationVersion = appVersion;
312    codeLocation       = codeLoc;
313    requestPurpose     = reqPurpose;
314  }
315
316
317
318  /**
319   * Generates a compact stack trace for the current thread,  The stack trace
320   * elements will start with the last frame to call into this class (so that
321   * frames referencing this class, and anything called by this class in the
322   * process of getting the stack trace will be omitted).  Elements will be
323   * space-delimited and will contain the unqualified class name, a period,
324   * the method name, a colon, and the source line number.
325   *
326   * @param  numFrames  The maximum number of frames to capture in the stack
327   *                    trace.
328   *
329   * @return  The generated stack trace for the current thread.
330   */
331  private static String generateStackTrace(final int numFrames)
332  {
333    final StringBuilder buffer = new StringBuilder();
334    final int n = (numFrames > 0) ? numFrames : Integer.MAX_VALUE;
335
336    int c = 0;
337    boolean skip = true;
338    for (final StackTraceElement e : Thread.currentThread().getStackTrace())
339    {
340      final String className = e.getClassName();
341      if (className.equals(OperationPurposeRequestControl.class.getName()))
342      {
343        skip = false;
344        continue;
345      }
346      else if (skip)
347      {
348        continue;
349      }
350
351      if (buffer.length() > 0)
352      {
353        buffer.append(' ');
354      }
355
356      final int lastPeriodPos = className.lastIndexOf('.');
357      if (lastPeriodPos > 0)
358      {
359        buffer.append(className.substring(lastPeriodPos+1));
360      }
361      else
362      {
363        buffer.append(className);
364      }
365
366      buffer.append('.');
367      buffer.append(e.getMethodName());
368      buffer.append(':');
369      buffer.append(e.getLineNumber());
370
371      c++;
372      if (c >= n)
373      {
374        break;
375      }
376    }
377
378    return buffer.toString();
379  }
380
381
382
383  /**
384   * Encodes the provided information into a form suitable for use as the value
385   * of this control.
386   *
387   * @param  applicationName     The name of the application generating the
388   *                             associated request.  It may be {@code null} if
389   *                             this should not be included in the control.
390   * @param  applicationVersion  Information about the version of the
391   *                             application generating the associated request.
392   *                             It may be {@code null} if this should not be
393   *                             included in the control.
394   * @param  codeLocation        Information about the location in the
395   *                             application code in which the associated
396   *                             request is generated (e.g., the class and/or
397   *                             method name, or any other useful identifier).
398   *                             It may be {@code null} if this should not be
399   *                             included in the control.
400   * @param  requestPurpose      A string identifying the purpose of the
401   *                             associated request.  It may be {@code null} if
402   *                             this should not be included in the control.
403   *
404   * @return  The encoded value for this control.
405   */
406  private static ASN1OctetString encodeValue(final String applicationName,
407                                             final String applicationVersion,
408                                             final String codeLocation,
409                                             final String requestPurpose)
410  {
411    Validator.ensureFalse((applicationName == null) &&
412         (applicationVersion == null) && (codeLocation == null) &&
413         (requestPurpose == null));
414
415    final ArrayList<ASN1Element> elements = new ArrayList<ASN1Element>(4);
416
417    if (applicationName != null)
418    {
419      elements.add(new ASN1OctetString(TYPE_APP_NAME, applicationName));
420    }
421
422    if (applicationVersion != null)
423    {
424      elements.add(new ASN1OctetString(TYPE_APP_VERSION, applicationVersion));
425    }
426
427    if (codeLocation != null)
428    {
429      elements.add(new ASN1OctetString(TYPE_CODE_LOCATION, codeLocation));
430    }
431
432    if (requestPurpose != null)
433    {
434      elements.add(new ASN1OctetString(TYPE_REQUEST_PURPOSE, requestPurpose));
435    }
436
437    return new ASN1OctetString(new ASN1Sequence(elements).encode());
438  }
439
440
441
442  /**
443   * Retrieves the name of the application that generated the associated
444   * request, if available.
445   *
446   * @return  The name of the application that generated the associated request,
447   *          or {@code null} if that is not available.
448   */
449  public String getApplicationName()
450  {
451    return applicationName;
452  }
453
454
455
456  /**
457   * Retrieves information about the version of the application that generated
458   * the associated request, if available.
459   *
460   * @return  Information about the version of the application that generated
461   *          the associated request, or {@code null} if that is not available.
462   */
463  public String getApplicationVersion()
464  {
465    return applicationVersion;
466  }
467
468
469
470  /**
471   * Retrieves information about the location in the application code in which
472   * the associated request was created, if available.
473   *
474   * @return  Information about the location in the application code in which
475   *          the associated request was created, or {@code null} if that is not
476   *          available.
477   */
478  public String getCodeLocation()
479  {
480    return codeLocation;
481  }
482
483
484
485  /**
486   * Retrieves a message with information about the purpose of the associated
487   * request, if available.
488   *
489   * @return  A message with information about the purpose of the associated
490   *          request, or {@code null} if that is not available.
491   */
492  public String getRequestPurpose()
493  {
494    return requestPurpose;
495  }
496
497
498
499  /**
500   * {@inheritDoc}
501   */
502  @Override()
503  public String getControlName()
504  {
505    return INFO_CONTROL_NAME_OP_PURPOSE.get();
506  }
507
508
509
510  /**
511   * {@inheritDoc}
512   */
513  @Override()
514  public void toString(final StringBuilder buffer)
515  {
516    buffer.append("OperationPurposeRequestControl(isCritical=");
517    buffer.append(isCritical());
518
519    if (applicationName != null)
520    {
521      buffer.append(", appName='");
522      buffer.append(applicationName);
523      buffer.append('\'');
524    }
525
526
527    if (applicationVersion != null)
528    {
529      buffer.append(", appVersion='");
530      buffer.append(applicationVersion);
531      buffer.append('\'');
532    }
533
534
535    if (codeLocation != null)
536    {
537      buffer.append(", codeLocation='");
538      buffer.append(codeLocation);
539      buffer.append('\'');
540    }
541
542
543    if (requestPurpose != null)
544    {
545      buffer.append(", purpose='");
546      buffer.append(requestPurpose);
547      buffer.append('\'');
548    }
549
550    buffer.append(')');
551  }
552}