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}