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