001/* 002 * Copyright 2008-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2008-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) 2008-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.unboundidds.controls; 037 038 039 040import java.util.ArrayList; 041import java.util.Arrays; 042import java.util.Collections; 043import java.util.Iterator; 044import java.util.LinkedHashMap; 045import java.util.List; 046import java.util.Map; 047 048import com.unboundid.asn1.ASN1Boolean; 049import com.unboundid.asn1.ASN1Element; 050import com.unboundid.asn1.ASN1OctetString; 051import com.unboundid.asn1.ASN1Sequence; 052import com.unboundid.ldap.sdk.Control; 053import com.unboundid.ldap.sdk.JSONControlDecodeHelper; 054import com.unboundid.ldap.sdk.LDAPException; 055import com.unboundid.ldap.sdk.ResultCode; 056import com.unboundid.util.Debug; 057import com.unboundid.util.NotMutable; 058import com.unboundid.util.NotNull; 059import com.unboundid.util.Nullable; 060import com.unboundid.util.StaticUtils; 061import com.unboundid.util.ThreadSafety; 062import com.unboundid.util.ThreadSafetyLevel; 063import com.unboundid.util.json.JSONArray; 064import com.unboundid.util.json.JSONBoolean; 065import com.unboundid.util.json.JSONField; 066import com.unboundid.util.json.JSONObject; 067import com.unboundid.util.json.JSONString; 068import com.unboundid.util.json.JSONValue; 069 070import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*; 071 072 073 074/** 075 * This class provides an implementation of an LDAP control that can be included 076 * in a bind request to request that the Directory Server return the 077 * authentication and authorization entries for the user that authenticated. 078 * <BR> 079 * <BLOCKQUOTE> 080 * <B>NOTE:</B> This class, and other classes within the 081 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 082 * supported for use against Ping Identity, UnboundID, and 083 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 084 * for proprietary functionality or for external specifications that are not 085 * considered stable or mature enough to be guaranteed to work in an 086 * interoperable way with other types of LDAP servers. 087 * </BLOCKQUOTE> 088 * <BR> 089 * The value of this control may be absent, but if it is present then will be 090 * encoded as follows: 091 * <PRE> 092 * GetAuthorizationEntryRequest ::= SEQUENCE { 093 * includeAuthNEntry [0] BOOLEAN DEFAULT TRUE, 094 * includeAuthZEntry [1] BOOLEAN DEFAULT TRUE, 095 * attributes [2] AttributeSelection OPTIONAL } 096 * </PRE> 097 * <BR><BR> 098 * <H2>Example</H2> 099 * The following example demonstrates the process for processing a bind 100 * operation using the get authorization entry request control to return all 101 * user attributes in both the authentication and authorization entries: 102 * <PRE> 103 * ReadOnlyEntry authNEntry = null; 104 * ReadOnlyEntry authZEntry = null; 105 * 106 * BindRequest bindRequest = new SimpleBindRequest( 107 * "uid=john.doe,ou=People,dc=example,dc=com", "password", 108 * new GetAuthorizationEntryRequestControl()); 109 * 110 * BindResult bindResult = connection.bind(bindRequest); 111 * GetAuthorizationEntryResponseControl c = 112 * GetAuthorizationEntryResponseControl.get(bindResult); 113 * if (c != null) 114 * { 115 * authNEntry = c.getAuthNEntry(); 116 * authZEntry = c.getAuthZEntry(); 117 * } 118 * </PRE> 119 */ 120@NotMutable() 121@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 122public final class GetAuthorizationEntryRequestControl 123 extends Control 124{ 125 /** 126 * The OID (1.3.6.1.4.1.30221.2.5.6) for the get authorization entry request 127 * control. 128 */ 129 @NotNull public static final String GET_AUTHORIZATION_ENTRY_REQUEST_OID = 130 "1.3.6.1.4.1.30221.2.5.6"; 131 132 133 134 /** 135 * The BER type for the {@code includeAuthNEntry} element. 136 */ 137 private static final byte TYPE_INCLUDE_AUTHN_ENTRY = (byte) 0x80; 138 139 140 141 /** 142 * The BER type for the {@code includeAuthZEntry} element. 143 */ 144 private static final byte TYPE_INCLUDE_AUTHZ_ENTRY = (byte) 0x81; 145 146 147 148 /** 149 * The BER type for the {@code attributes} element. 150 */ 151 private static final byte TYPE_ATTRIBUTES = (byte) 0xA2; 152 153 154 155 /** 156 * The name of the field used to hold the names of the requested attributes in 157 * the JSON representation of this control. 158 */ 159 @NotNull private static final String JSON_FIELD_ATTRIBUTES = "attributes"; 160 161 162 163 /** 164 * The name of the field used to indicate whether to include the 165 * authentication entry in the JSON representation of this control. 166 */ 167 @NotNull private static final String JSON_FIELD_INCLUDE_AUTHENTICATION_ENTRY = 168 "include-authentication-entry"; 169 170 171 172 /** 173 * The name of the field used to indicate whether to include the 174 * authorization entry in the JSON representation of this control. 175 */ 176 @NotNull private static final String JSON_FIELD_INCLUDE_AUTHORIZATION_ENTRY = 177 "include-authorization-entry"; 178 179 180 181 /** 182 * The serial version UID for this serializable class. 183 */ 184 private static final long serialVersionUID = -5540345171260624216L; 185 186 187 188 // Indicates whether to include the authentication entry in the response. 189 private final boolean includeAuthNEntry; 190 191 // Indicates whether to include the authorization entry in the response. 192 private final boolean includeAuthZEntry; 193 194 // The list of attributes to include in entries that are returned. 195 @NotNull private final List<String> attributes; 196 197 198 199 /** 200 * Creates a new get authorization entry request control that will request all 201 * user attributes in both the authentication and authorization entries. It 202 * will not be marked critical. 203 */ 204 public GetAuthorizationEntryRequestControl() 205 { 206 this(false, true, true, (List<String>) null); 207 } 208 209 210 211 /** 212 * Creates a new get authorization entry request control with the provided 213 * information. 214 * 215 * @param includeAuthNEntry Indicates whether to include the authentication 216 * entry in the response. 217 * @param includeAuthZEntry Indicates whether to include the authorization 218 * entry in the response. 219 * @param attributes The attributes to include in the entries in the 220 * response. It may be empty or {@code null} to 221 * request all user attributes. 222 */ 223 public GetAuthorizationEntryRequestControl(final boolean includeAuthNEntry, 224 final boolean includeAuthZEntry, 225 @Nullable final String... attributes) 226 { 227 this(false, includeAuthNEntry, includeAuthZEntry, 228 (attributes == null) ? null : Arrays.asList(attributes)); 229 } 230 231 232 233 /** 234 * Creates a new get authorization entry request control with the provided 235 * information. 236 * 237 * @param includeAuthNEntry Indicates whether to include the authentication 238 * entry in the response. 239 * @param includeAuthZEntry Indicates whether to include the authorization 240 * entry in the response. 241 * @param attributes The attributes to include in the entries in the 242 * response. It may be empty or {@code null} to 243 * request all user attributes. 244 */ 245 public GetAuthorizationEntryRequestControl(final boolean includeAuthNEntry, 246 final boolean includeAuthZEntry, 247 @Nullable final List<String> attributes) 248 { 249 this(false, includeAuthNEntry, includeAuthZEntry, attributes); 250 } 251 252 253 254 /** 255 * Creates a new get authorization entry request control with the provided 256 * information. 257 * 258 * @param isCritical Indicates whether the control should be marked 259 * critical. 260 * @param includeAuthNEntry Indicates whether to include the authentication 261 * entry in the response. 262 * @param includeAuthZEntry Indicates whether to include the authorization 263 * entry in the response. 264 * @param attributes The attributes to include in the entries in the 265 * response. It may be empty or {@code null} to 266 * request all user attributes. 267 */ 268 public GetAuthorizationEntryRequestControl(final boolean isCritical, 269 final boolean includeAuthNEntry, 270 final boolean includeAuthZEntry, 271 @Nullable final String... attributes) 272 { 273 this(isCritical, includeAuthNEntry, includeAuthZEntry, 274 (attributes == null) ? null : Arrays.asList(attributes)); 275 } 276 277 278 279 /** 280 * Creates a new get authorization entry request control with the provided 281 * information. 282 * 283 * @param isCritical Indicates whether the control should be marked 284 * critical. 285 * @param includeAuthNEntry Indicates whether to include the authentication 286 * entry in the response. 287 * @param includeAuthZEntry Indicates whether to include the authorization 288 * entry in the response. 289 * @param attributes The attributes to include in the entries in the 290 * response. It may be empty or {@code null} to 291 * request all user attributes. 292 */ 293 public GetAuthorizationEntryRequestControl(final boolean isCritical, 294 final boolean includeAuthNEntry, 295 final boolean includeAuthZEntry, 296 @Nullable final List<String> attributes) 297 { 298 super(GET_AUTHORIZATION_ENTRY_REQUEST_OID, isCritical, 299 encodeValue(includeAuthNEntry, includeAuthZEntry, attributes)); 300 301 this.includeAuthNEntry = includeAuthNEntry; 302 this.includeAuthZEntry = includeAuthZEntry; 303 304 if ((attributes == null) || attributes.isEmpty()) 305 { 306 this.attributes = Collections.emptyList(); 307 } 308 else 309 { 310 this.attributes = 311 Collections.unmodifiableList(new ArrayList<>(attributes)); 312 } 313 } 314 315 316 317 /** 318 * Creates a new get authorization entry request control which is decoded from 319 * the provided generic control. 320 * 321 * @param control The generic control to decode as a get authorization entry 322 * request control. 323 * 324 * @throws LDAPException If the provided control cannot be decoded as a get 325 * authorization entry request control. 326 */ 327 public GetAuthorizationEntryRequestControl(@NotNull final Control control) 328 throws LDAPException 329 { 330 super(control); 331 332 final ASN1OctetString value = control.getValue(); 333 if (value == null) 334 { 335 includeAuthNEntry = true; 336 includeAuthZEntry = true; 337 attributes = Collections.emptyList(); 338 return; 339 } 340 341 try 342 { 343 final ArrayList<String> attrs = new ArrayList<>(20); 344 boolean includeAuthN = true; 345 boolean includeAuthZ = true; 346 347 final ASN1Element element = ASN1Element.decode(value.getValue()); 348 for (final ASN1Element e : 349 ASN1Sequence.decodeAsSequence(element).elements()) 350 { 351 switch (e.getType()) 352 { 353 case TYPE_INCLUDE_AUTHN_ENTRY: 354 includeAuthN = ASN1Boolean.decodeAsBoolean(e).booleanValue(); 355 break; 356 case TYPE_INCLUDE_AUTHZ_ENTRY: 357 includeAuthZ = ASN1Boolean.decodeAsBoolean(e).booleanValue(); 358 break; 359 case TYPE_ATTRIBUTES: 360 for (final ASN1Element ae : 361 ASN1Sequence.decodeAsSequence(e).elements()) 362 { 363 attrs.add(ASN1OctetString.decodeAsOctetString(ae).stringValue()); 364 } 365 break; 366 default: 367 throw new LDAPException(ResultCode.DECODING_ERROR, 368 ERR_GET_AUTHORIZATION_ENTRY_REQUEST_INVALID_SEQUENCE_ELEMENT. 369 get(StaticUtils.toHex(e.getType()))); 370 } 371 } 372 373 includeAuthNEntry = includeAuthN; 374 includeAuthZEntry = includeAuthZ; 375 attributes = attrs; 376 } 377 catch (final LDAPException le) 378 { 379 throw le; 380 } 381 catch (final Exception e) 382 { 383 Debug.debugException(e); 384 throw new LDAPException(ResultCode.DECODING_ERROR, 385 ERR_GET_AUTHORIZATION_ENTRY_REQUEST_CANNOT_DECODE_VALUE.get( 386 StaticUtils.getExceptionMessage(e)), 387 e); 388 } 389 } 390 391 392 393 /** 394 * Encodes the provided information as appropriate for use as the value of 395 * this control. 396 * 397 * @param includeAuthNEntry Indicates whether to include the authentication 398 * entry in the response. 399 * @param includeAuthZEntry Indicates whether to include the authorization 400 * entry in the response. 401 * @param attributes The attributes to include in the entries in the 402 * response. It may be empty or {@code null} to 403 * request all user attributes. 404 * 405 * @return An ASN.1 octet string appropriately encoded for use as the control 406 * value, or {@code null} if no value is needed. 407 */ 408 @Nullable() 409 private static ASN1OctetString encodeValue(final boolean includeAuthNEntry, 410 final boolean includeAuthZEntry, 411 @Nullable final List<String> attributes) 412 { 413 if (includeAuthNEntry && includeAuthZEntry && 414 ((attributes == null) || attributes.isEmpty())) 415 { 416 return null; 417 } 418 419 final ArrayList<ASN1Element> elements = new ArrayList<>(3); 420 421 if (! includeAuthNEntry) 422 { 423 elements.add(new ASN1Boolean(TYPE_INCLUDE_AUTHN_ENTRY, false)); 424 } 425 426 if (! includeAuthZEntry) 427 { 428 elements.add(new ASN1Boolean(TYPE_INCLUDE_AUTHZ_ENTRY, false)); 429 } 430 431 if ((attributes != null) && (! attributes.isEmpty())) 432 { 433 final ArrayList<ASN1Element> attrElements = 434 new ArrayList<>(attributes.size()); 435 for (final String s : attributes) 436 { 437 attrElements.add(new ASN1OctetString(s)); 438 } 439 440 elements.add(new ASN1Sequence(TYPE_ATTRIBUTES, attrElements)); 441 } 442 443 return new ASN1OctetString(new ASN1Sequence(elements).encode()); 444 } 445 446 447 448 /** 449 * Indicates whether the entry for the authenticated user should be included 450 * in the response control. 451 * 452 * @return {@code true} if the entry for the authenticated user should be 453 * included in the response control, or {@code false} if not. 454 */ 455 public boolean includeAuthNEntry() 456 { 457 return includeAuthNEntry; 458 } 459 460 461 462 /** 463 * Indicates whether the entry for the authorized user should be included 464 * in the response control. 465 * 466 * @return {@code true} if the entry for the authorized user should be 467 * included in the response control, or {@code false} if not. 468 */ 469 public boolean includeAuthZEntry() 470 { 471 return includeAuthZEntry; 472 } 473 474 475 476 /** 477 * Retrieves the attributes that will be requested for the authentication 478 * and/or authorization entries. 479 * 480 * @return The attributes that will be requested for the authentication 481 * and/or authorization entries, or an empty list if all user 482 * attributes should be included. 483 */ 484 @NotNull() 485 public List<String> getAttributes() 486 { 487 return attributes; 488 } 489 490 491 492 /** 493 * {@inheritDoc} 494 */ 495 @Override() 496 @NotNull() 497 public String getControlName() 498 { 499 return INFO_CONTROL_NAME_GET_AUTHORIZATION_ENTRY_REQUEST.get(); 500 } 501 502 503 504 /** 505 * Retrieves a representation of this get authorization entry request control 506 * as a JSON object. The JSON object uses the following fields: 507 * <UL> 508 * <LI> 509 * {@code oid} -- A mandatory string field whose value is the object 510 * identifier for this control. For the get authorization entry request 511 * control, the OID is "1.3.6.1.4.1.30221.2.5.6". 512 * </LI> 513 * <LI> 514 * {@code control-name} -- An optional string field whose value is a 515 * human-readable name for this control. This field is only intended for 516 * descriptive purposes, and when decoding a control, the {@code oid} 517 * field should be used to identify the type of control. 518 * </LI> 519 * <LI> 520 * {@code criticality} -- A mandatory Boolean field used to indicate 521 * whether this control is considered critical. 522 * </LI> 523 * <LI> 524 * {@code value-base64} -- An optional string field whose value is a 525 * base64-encoded representation of the raw value for this get 526 * authorization entry request control. Exactly one of the 527 * {@code value-base64} and {@code value-json} fields must be present. 528 * </LI> 529 * <LI> 530 * {@code value-json} -- An optional JSON object field whose value is a 531 * user-friendly representation of the value for this get authorization 532 * entry request control. Exactly one of the {@code value-base64} and 533 * {@code value-json} fields must be present, and if the 534 * {@code value-json} field is used, then it will use the following 535 * fields: 536 * <UL> 537 * <LI> 538 * {@code include-authentication-entry} -- A mandatory Boolean field 539 * that indicates whether to include the entry for the authentication 540 * identity in the response control. 541 * </LI> 542 * <LI> 543 * {@code include-authorization-entry} -- A mandatory Boolean field 544 * that indicates whether to include the entry for the authorization 545 * identity in the response control. 546 * </LI> 547 * <LI> 548 * {@code attributes} -- An optional array field whose values are 549 * strings that represent the names of the attributes that should be 550 * included in the entries returned in the repsonse control. 551 * </LI> 552 * </UL> 553 * </LI> 554 * </UL> 555 * 556 * @return A JSON object that contains a representation of this control. 557 */ 558 @Override() 559 @NotNull() 560 public JSONObject toJSONControl() 561 { 562 final Map<String,JSONValue> valueFields = new LinkedHashMap<>(); 563 valueFields.put(JSON_FIELD_INCLUDE_AUTHENTICATION_ENTRY, 564 new JSONBoolean(includeAuthNEntry)); 565 valueFields.put(JSON_FIELD_INCLUDE_AUTHORIZATION_ENTRY, 566 new JSONBoolean(includeAuthZEntry)); 567 568 if (! attributes.isEmpty()) 569 { 570 final List<JSONValue> attributeValues = 571 new ArrayList<>(attributes.size()); 572 for (final String attribute : attributes) 573 { 574 attributeValues.add(new JSONString(attribute)); 575 } 576 577 valueFields.put(JSON_FIELD_ATTRIBUTES, new JSONArray(attributeValues)); 578 } 579 580 581 return new JSONObject( 582 new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID, 583 GET_AUTHORIZATION_ENTRY_REQUEST_OID), 584 new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME, 585 INFO_CONTROL_NAME_GET_AUTHORIZATION_ENTRY_REQUEST.get()), 586 new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY, 587 isCritical()), 588 new JSONField(JSONControlDecodeHelper.JSON_FIELD_VALUE_JSON, 589 new JSONObject(valueFields))); 590 } 591 592 593 594 /** 595 * Attempts to decode the provided object as a JSON representation of a get 596 * authorization entry request control. 597 * 598 * @param controlObject The JSON object to be decoded. It must not be 599 * {@code null}. 600 * @param strict Indicates whether to use strict mode when decoding 601 * the provided JSON object. If this is {@code true}, 602 * then this method will throw an exception if the 603 * provided JSON object contains any unrecognized 604 * fields. If this is {@code false}, then unrecognized 605 * fields will be ignored. 606 * 607 * @return The get authorization entry request control that was decoded from 608 * the provided JSON object. 609 * 610 * @throws LDAPException If the provided JSON object cannot be parsed as a 611 * valid get authorization entry request control. 612 */ 613 @NotNull() 614 public static GetAuthorizationEntryRequestControl decodeJSONControl( 615 @NotNull final JSONObject controlObject, 616 final boolean strict) 617 throws LDAPException 618 { 619 final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper( 620 controlObject, strict, true, true); 621 622 final ASN1OctetString rawValue = jsonControl.getRawValue(); 623 if (rawValue != null) 624 { 625 return new GetAuthorizationEntryRequestControl(new Control( 626 jsonControl.getOID(), jsonControl.getCriticality(), rawValue)); 627 } 628 629 630 final JSONObject valueObject = jsonControl.getValueObject(); 631 632 final Boolean includeAuthNEntry = 633 valueObject.getFieldAsBoolean(JSON_FIELD_INCLUDE_AUTHENTICATION_ENTRY); 634 if (includeAuthNEntry == null) 635 { 636 throw new LDAPException(ResultCode.DECODING_ERROR, 637 ERR_GET_AUTHORIZATION_ENTRY_REQUEST_JSON_MISSING_FIELD.get( 638 controlObject.toSingleLineString(), 639 JSON_FIELD_INCLUDE_AUTHENTICATION_ENTRY)); 640 } 641 642 final Boolean includeAuthZEntry = 643 valueObject.getFieldAsBoolean(JSON_FIELD_INCLUDE_AUTHORIZATION_ENTRY); 644 if (includeAuthZEntry == null) 645 { 646 throw new LDAPException(ResultCode.DECODING_ERROR, 647 ERR_GET_AUTHORIZATION_ENTRY_REQUEST_JSON_MISSING_FIELD.get( 648 controlObject.toSingleLineString(), 649 JSON_FIELD_INCLUDE_AUTHORIZATION_ENTRY)); 650 } 651 652 653 final List<String> attributes; 654 final List<JSONValue> attributeValues = 655 valueObject.getFieldAsArray(JSON_FIELD_ATTRIBUTES); 656 if (attributeValues == null) 657 { 658 attributes = null; 659 } 660 else 661 { 662 attributes = new ArrayList<>(attributeValues.size()); 663 for (final JSONValue v : attributeValues) 664 { 665 if (v instanceof JSONString) 666 { 667 attributes.add(((JSONString) v).stringValue()); 668 } 669 else 670 { 671 throw new LDAPException(ResultCode.DECODING_ERROR, 672 ERR_GET_AUTHORIZATION_ENTRY_REQUEST_JSON_ATTR_NOT_STRING.get( 673 controlObject.toSingleLineString(), 674 JSON_FIELD_ATTRIBUTES)); 675 } 676 } 677 } 678 679 680 if (strict) 681 { 682 final List<String> unrecognizedFields = 683 JSONControlDecodeHelper.getControlObjectUnexpectedFields( 684 valueObject, JSON_FIELD_INCLUDE_AUTHENTICATION_ENTRY, 685 JSON_FIELD_INCLUDE_AUTHORIZATION_ENTRY, JSON_FIELD_ATTRIBUTES); 686 if (! unrecognizedFields.isEmpty()) 687 { 688 throw new LDAPException(ResultCode.DECODING_ERROR, 689 ERR_GET_AUTHORIZATION_ENTRY_REQUEST_JSON_CONTROL_UNRECOGNIZED_FIELD 690 .get(controlObject.toSingleLineString(), 691 unrecognizedFields.get(0))); 692 } 693 } 694 695 696 return new GetAuthorizationEntryRequestControl(jsonControl.getCriticality(), 697 includeAuthNEntry, includeAuthZEntry, attributes); 698 } 699 700 701 702 /** 703 * {@inheritDoc} 704 */ 705 @Override() 706 public void toString(@NotNull final StringBuilder buffer) 707 { 708 buffer.append("GetAuthorizationEntryRequestControl(isCritical="); 709 buffer.append(isCritical()); 710 buffer.append(", includeAuthNEntry="); 711 buffer.append(includeAuthNEntry); 712 buffer.append(", includeAuthZEntry="); 713 buffer.append(includeAuthZEntry); 714 buffer.append(", attributes={"); 715 716 final Iterator<String> iterator = attributes.iterator(); 717 while (iterator.hasNext()) 718 { 719 buffer.append(iterator.next()); 720 if (iterator.hasNext()) 721 { 722 buffer.append(", "); 723 } 724 } 725 726 buffer.append("})"); 727 } 728}