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.extensions; 037 038 039 040import java.util.ArrayList; 041 042import com.unboundid.asn1.ASN1Element; 043import com.unboundid.asn1.ASN1OctetString; 044import com.unboundid.asn1.ASN1Sequence; 045import com.unboundid.ldap.sdk.Control; 046import com.unboundid.ldap.sdk.ExtendedRequest; 047import com.unboundid.ldap.sdk.ExtendedResult; 048import com.unboundid.ldap.sdk.LDAPConnection; 049import com.unboundid.ldap.sdk.LDAPException; 050import com.unboundid.ldap.sdk.ReferralHelper; 051import com.unboundid.ldap.sdk.ResultCode; 052import com.unboundid.util.Debug; 053import com.unboundid.util.NotMutable; 054import com.unboundid.util.NotNull; 055import com.unboundid.util.Nullable; 056import com.unboundid.util.StaticUtils; 057import com.unboundid.util.ThreadSafety; 058import com.unboundid.util.ThreadSafetyLevel; 059 060import static com.unboundid.ldap.sdk.extensions.ExtOpMessages.*; 061 062 063 064/** 065 * This class provides an implementation of the LDAP password modify extended 066 * request as defined in 067 * <A HREF="http://www.ietf.org/rfc/rfc3062.txt">RFC 3062</A>. It may be used 068 * to change the password for a user in the directory, and provides the ability 069 * to specify the current password for verification. It also offers the ability 070 * to request that the server generate a new password for the user. 071 * <BR><BR> 072 * The elements of a password modify extended request include: 073 * <UL> 074 * <LI>{@code userIdentity} -- This specifies the user for which to change the 075 * password. It should generally be the DN for the target user (although 076 * the specification does indicate that some servers may accept other 077 * values). If no value is provided, then the server will attempt to 078 * change the password for the currently-authenticated user.</LI> 079 * <LI>{@code oldPassword} -- This specifies the current password for the 080 * user. Some servers may require that the old password be provided when 081 * a user is changing his or her own password as an extra level of 082 * verification, but it is generally not necessary when an administrator 083 * is resetting the password for another user.</LI> 084 * <LI>{@code newPassword} -- This specifies the new password to use for the 085 * user. If it is not provided, then the server may attempt to generate a 086 * new password for the user, and in that case it will be included in the 087 * {@code generatedPassword} field of the corresponding 088 * {@link PasswordModifyExtendedResult}. Note that some servers may not 089 * support generating a new password, in which case the client will always 090 * be required to provide it.</LI> 091 * </UL> 092 * <H2>Example</H2> 093 * The following example demonstrates the use of the password modify extended 094 * operation to change the password for user 095 * "uid=test.user,ou=People,dc=example,dc=com". Neither the current password 096 * nor a new password will be provided, so the server will generate a new 097 * password for the user. 098 * <PRE> 099 * PasswordModifyExtendedRequest passwordModifyRequest = 100 * new PasswordModifyExtendedRequest( 101 * "uid=test.user,ou=People,dc=example,dc=com", // The user to update 102 * (String) null, // The current password for the user. 103 * (String) null); // The new password. null = server will generate 104 * 105 * PasswordModifyExtendedResult passwordModifyResult; 106 * try 107 * { 108 * passwordModifyResult = (PasswordModifyExtendedResult) 109 * connection.processExtendedOperation(passwordModifyRequest); 110 * // This doesn't necessarily mean that the operation was successful, since 111 * // some kinds of extended operations return non-success results under 112 * // normal conditions. 113 * } 114 * catch (LDAPException le) 115 * { 116 * // For an extended operation, this generally means that a problem was 117 * // encountered while trying to send the request or read the result. 118 * passwordModifyResult = new PasswordModifyExtendedResult( 119 * new ExtendedResult(le)); 120 * } 121 * 122 * LDAPTestUtils.assertResultCodeEquals(passwordModifyResult, 123 * ResultCode.SUCCESS); 124 * String serverGeneratedNewPassword = 125 * passwordModifyResult.getGeneratedPassword(); 126 * </PRE> 127 */ 128@NotMutable() 129@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 130public final class PasswordModifyExtendedRequest 131 extends ExtendedRequest 132{ 133 /** 134 * The OID (1.3.6.1.4.1.4203.1.11.1) for the password modify extended request. 135 */ 136 @NotNull public static final String PASSWORD_MODIFY_REQUEST_OID = 137 "1.3.6.1.4.1.4203.1.11.1"; 138 139 140 141 /** 142 * The BER type for the user identity element. 143 */ 144 private static final byte TYPE_USER_IDENTITY = (byte) 0x80; 145 146 147 148 /** 149 * The BER type for the old password element. 150 */ 151 private static final byte TYPE_OLD_PASSWORD = (byte) 0x81; 152 153 154 155 /** 156 * The BER type for the new password element. 157 */ 158 private static final byte TYPE_NEW_PASSWORD = (byte) 0x82; 159 160 161 162 /** 163 * The serial version UID for this serializable class. 164 */ 165 private static final long serialVersionUID = 4965048727456933570L; 166 167 168 169 // The old password for this request. 170 @Nullable private final ASN1OctetString oldPassword; 171 172 // The new password for this request. 173 @Nullable private final ASN1OctetString newPassword; 174 175 // The user identity string for this request. 176 @Nullable private final String userIdentity; 177 178 179 180 /** 181 * Creates a new password modify extended request that will attempt to change 182 * the password of the currently-authenticated user. 183 * 184 * @param newPassword The new password for the user. It may be {@code null} 185 * if the new password should be generated by the 186 * directory server. 187 */ 188 public PasswordModifyExtendedRequest(@Nullable final String newPassword) 189 { 190 this(null, null, newPassword, null); 191 } 192 193 194 195 /** 196 * Creates a new password modify extended request that will attempt to change 197 * the password of the currently-authenticated user. 198 * 199 * @param newPassword The new password for the user. It may be {@code null} 200 * if the new password should be generated by the 201 * directory server. 202 */ 203 public PasswordModifyExtendedRequest(@Nullable final byte[] newPassword) 204 { 205 this(null, null, newPassword, null); 206 } 207 208 209 210 /** 211 * Creates a new password modify extended request that will attempt to change 212 * the password of the currently-authenticated user. 213 * 214 * @param oldPassword The current password for the user. It may be 215 * {@code null} if the directory server does not require 216 * the user's current password for self changes. 217 * @param newPassword The new password for the user. It may be {@code null} 218 * if the new password should be generated by the 219 * directory server. 220 */ 221 public PasswordModifyExtendedRequest(@Nullable final String oldPassword, 222 @Nullable final String newPassword) 223 { 224 this(null, oldPassword, newPassword, null); 225 } 226 227 228 229 /** 230 * Creates a new password modify extended request that will attempt to change 231 * the password of the currently-authenticated user. 232 * 233 * @param oldPassword The current password for the user. It may be 234 * {@code null} if the directory server does not require 235 * the user's current password for self changes. 236 * @param newPassword The new password for the user. It may be {@code null} 237 * if the new password should be generated by the 238 * directory server. 239 */ 240 public PasswordModifyExtendedRequest(@Nullable final byte[] oldPassword, 241 @Nullable final byte[] newPassword) 242 { 243 this(null, oldPassword, newPassword, null); 244 } 245 246 247 248 /** 249 * Creates a new password modify extended request that will attempt to change 250 * the password for the specified user. 251 * 252 * @param userIdentity The string that identifies the user whose password 253 * should be changed. It may or may not be a DN, but if 254 * it is not a DN, then the directory server must be 255 * able to identify the appropriate user from the 256 * provided identifier. It may be {@code null} to 257 * indicate that the password change should be for the 258 * currently-authenticated user. 259 * @param oldPassword The current password for the user. It may be 260 * {@code null} if the directory server does not require 261 * the user's current password for self changes. 262 * @param newPassword The new password for the user. It may be 263 * {@code null} if the new password should be generated 264 * by the directory server. 265 */ 266 public PasswordModifyExtendedRequest(@Nullable final String userIdentity, 267 @Nullable final String oldPassword, 268 @Nullable final String newPassword) 269 { 270 this(userIdentity, oldPassword, newPassword, null); 271 } 272 273 274 275 /** 276 * Creates a new password modify extended request that will attempt to change 277 * the password for the specified user. 278 * 279 * @param userIdentity The string that identifies the user whose password 280 * should be changed. It may or may not be a DN, but if 281 * it is not a DN, then the directory server must be 282 * able to identify the appropriate user from the 283 * provided identifier. It may be {@code null} to 284 * indicate that the password change should be for the 285 * currently-authenticated user. 286 * @param oldPassword The current password for the user. It may be 287 * {@code null} if the directory server does not require 288 * the user's current password for self changes. 289 * @param newPassword The new password for the user. It may be 290 * {@code null} if the new password should be generated 291 * by the directory server. 292 */ 293 public PasswordModifyExtendedRequest(@Nullable final String userIdentity, 294 @Nullable final byte[] oldPassword, 295 @Nullable final byte[] newPassword) 296 { 297 this(userIdentity, oldPassword, newPassword, null); 298 } 299 300 301 302 /** 303 * Creates a new password modify extended request that will attempt to change 304 * the password for the specified user. 305 * 306 * @param userIdentity The string that identifies the user whose password 307 * should be changed. It may or may not be a DN, but if 308 * it is not a DN, then the directory server must be 309 * able to identify the appropriate user from the 310 * provided identifier. It may be {@code null} to 311 * indicate that the password change should be for the 312 * currently-authenticated user. 313 * @param oldPassword The current password for the user. It may be 314 * {@code null} if the directory server does not require 315 * the user's current password for self changes. 316 * @param newPassword The new password for the user. It may be 317 * {@code null} if the new password should be generated 318 * by the directory server. 319 * @param controls The set of controls to include in the request. 320 */ 321 public PasswordModifyExtendedRequest(@Nullable final String userIdentity, 322 @Nullable final String oldPassword, 323 @Nullable final String newPassword, 324 @Nullable final Control[] controls) 325 { 326 super(PASSWORD_MODIFY_REQUEST_OID, 327 encodeValue(userIdentity, oldPassword, newPassword), controls); 328 329 this.userIdentity = userIdentity; 330 331 if (oldPassword == null) 332 { 333 this.oldPassword = null; 334 } 335 else 336 { 337 this.oldPassword = new ASN1OctetString(TYPE_OLD_PASSWORD, oldPassword); 338 } 339 340 if (newPassword == null) 341 { 342 this.newPassword = null; 343 } 344 else 345 { 346 this.newPassword = new ASN1OctetString(TYPE_NEW_PASSWORD, newPassword); 347 } 348 } 349 350 351 352 /** 353 * Creates a new password modify extended request that will attempt to change 354 * the password for the specified user. 355 * 356 * @param userIdentity The string that identifies the user whose password 357 * should be changed. It may or may not be a DN, but if 358 * it is not a DN, then the directory server must be 359 * able to identify the appropriate user from the 360 * provided identifier. It may be {@code null} to 361 * indicate that the password change should be for the 362 * currently-authenticated user. 363 * @param oldPassword The current password for the user. It may be 364 * {@code null} if the directory server does not require 365 * the user's current password for self changes. 366 * @param newPassword The new password for the user. It may be 367 * {@code null} if the new password should be generated 368 * by the directory server. 369 * @param controls The set of controls to include in the request. 370 */ 371 public PasswordModifyExtendedRequest(@Nullable final String userIdentity, 372 @Nullable final byte[] oldPassword, 373 @Nullable final byte[] newPassword, 374 @Nullable final Control[] controls) 375 { 376 super(PASSWORD_MODIFY_REQUEST_OID, 377 encodeValue(userIdentity, oldPassword, newPassword), controls); 378 379 this.userIdentity = userIdentity; 380 381 if (oldPassword == null) 382 { 383 this.oldPassword = null; 384 } 385 else 386 { 387 this.oldPassword = new ASN1OctetString(TYPE_OLD_PASSWORD, oldPassword); 388 } 389 390 if (newPassword == null) 391 { 392 this.newPassword = null; 393 } 394 else 395 { 396 this.newPassword = new ASN1OctetString(TYPE_NEW_PASSWORD, newPassword); 397 } 398 } 399 400 401 402 /** 403 * Creates a new password modify extended request from the provided generic 404 * extended request. 405 * 406 * @param extendedRequest The generic extended request to use to create this 407 * password modify extended request. 408 * 409 * @throws LDAPException If a problem occurs while decoding the request. 410 */ 411 public PasswordModifyExtendedRequest( 412 @NotNull final ExtendedRequest extendedRequest) 413 throws LDAPException 414 { 415 super(extendedRequest); 416 417 final ASN1OctetString value = extendedRequest.getValue(); 418 if (value == null) 419 { 420 throw new LDAPException(ResultCode.DECODING_ERROR, 421 ERR_PW_MODIFY_REQUEST_NO_VALUE.get()); 422 } 423 424 try 425 { 426 ASN1OctetString oldPW = null; 427 ASN1OctetString newPW = null; 428 String userID = null; 429 430 final ASN1Element valueElement = ASN1Element.decode(value.getValue()); 431 final ASN1Element[] elements = 432 ASN1Sequence.decodeAsSequence(valueElement).elements(); 433 for (final ASN1Element e : elements) 434 { 435 switch (e.getType()) 436 { 437 case TYPE_USER_IDENTITY: 438 userID = ASN1OctetString.decodeAsOctetString(e).stringValue(); 439 break; 440 441 case TYPE_OLD_PASSWORD: 442 oldPW = ASN1OctetString.decodeAsOctetString(e); 443 break; 444 445 case TYPE_NEW_PASSWORD: 446 newPW = ASN1OctetString.decodeAsOctetString(e); 447 break; 448 449 default: 450 throw new LDAPException(ResultCode.DECODING_ERROR, 451 ERR_PW_MODIFY_REQUEST_INVALID_TYPE.get( 452 StaticUtils.toHex(e.getType()))); 453 } 454 } 455 456 userIdentity = userID; 457 oldPassword = oldPW; 458 newPassword = newPW; 459 } 460 catch (final LDAPException le) 461 { 462 Debug.debugException(le); 463 throw le; 464 } 465 catch (final Exception e) 466 { 467 Debug.debugException(e); 468 throw new LDAPException(ResultCode.DECODING_ERROR, 469 ERR_PW_MODIFY_REQUEST_CANNOT_DECODE.get(e), e); 470 } 471 } 472 473 474 475 /** 476 * Encodes the provided information into an ASN.1 octet string suitable for 477 * use as the value of this extended request. 478 * 479 * @param userIdentity The string that identifies the user whose password 480 * should be changed. It may or may not be a DN, but if 481 * it is not a DN, then the directory server must be 482 * able to identify the appropriate user from the 483 * provided identifier. It may be {@code null} to 484 * indicate that the password change should be for the 485 * currently-authenticated user. 486 * @param oldPassword The current password for the user. It may be 487 * {@code null} if the directory server does not require 488 * the user's current password for self changes. 489 * @param newPassword The new password for the user. It may be 490 * {@code null} if the new password should be generated 491 * by the directory server. 492 * 493 * @return The ASN.1 octet string containing the encoded value. 494 */ 495 @NotNull() 496 private static ASN1OctetString encodeValue( 497 @Nullable final String userIdentity, 498 @Nullable final String oldPassword, 499 @Nullable final String newPassword) 500 { 501 final ArrayList<ASN1Element> elements = new ArrayList<>(3); 502 503 if (userIdentity != null) 504 { 505 elements.add(new ASN1OctetString(TYPE_USER_IDENTITY, userIdentity)); 506 } 507 508 if (oldPassword != null) 509 { 510 elements.add(new ASN1OctetString(TYPE_OLD_PASSWORD, oldPassword)); 511 } 512 513 if (newPassword != null) 514 { 515 elements.add(new ASN1OctetString(TYPE_NEW_PASSWORD, newPassword)); 516 } 517 518 return new ASN1OctetString(new ASN1Sequence(elements).encode()); 519 } 520 521 522 523 /** 524 * Encodes the provided information into an ASN.1 octet string suitable for 525 * use as the value of this extended request. 526 * 527 * @param userIdentity The string that identifies the user whose password 528 * should be changed. It may or may not be a DN, but if 529 * it is not a DN, then the directory server must be 530 * able to identify the appropriate user from the 531 * provided identifier. It may be {@code null} to 532 * indicate that the password change should be for the 533 * currently-authenticated user. 534 * @param oldPassword The current password for the user. It may be 535 * {@code null} if the directory server does not require 536 * the user's current password for self changes. 537 * @param newPassword The new password for the user. It may be 538 * {@code null} if the new password should be generated 539 * by the directory server. 540 * 541 * @return The ASN.1 octet string containing the encoded value. 542 */ 543 @NotNull() 544 private static ASN1OctetString encodeValue( 545 @Nullable final String userIdentity, 546 @Nullable final byte[] oldPassword, 547 @Nullable final byte[] newPassword) 548 { 549 final ArrayList<ASN1Element> elements = new ArrayList<>(3); 550 551 if (userIdentity != null) 552 { 553 elements.add(new ASN1OctetString(TYPE_USER_IDENTITY, userIdentity)); 554 } 555 556 if (oldPassword != null) 557 { 558 elements.add(new ASN1OctetString(TYPE_OLD_PASSWORD, oldPassword)); 559 } 560 561 if (newPassword != null) 562 { 563 elements.add(new ASN1OctetString(TYPE_NEW_PASSWORD, newPassword)); 564 } 565 566 return new ASN1OctetString(new ASN1Sequence(elements).encode()); 567 } 568 569 570 571 /** 572 * Retrieves the user identity for this request, if available. 573 * 574 * @return The user identity for this request, or {@code null} if the 575 * password change should target the currently-authenticated user. 576 */ 577 @Nullable() 578 public String getUserIdentity() 579 { 580 return userIdentity; 581 } 582 583 584 585 /** 586 * Retrieves the string representation of the old password for this request, 587 * if available. 588 * 589 * @return The string representation of the old password for this request, or 590 * {@code null} if it was not provided. 591 */ 592 @Nullable() 593 public String getOldPassword() 594 { 595 if (oldPassword == null) 596 { 597 return null; 598 } 599 else 600 { 601 return oldPassword.stringValue(); 602 } 603 } 604 605 606 607 /** 608 * Retrieves the binary representation of the old password for this request, 609 * if available. 610 * 611 * @return The binary representation of the old password for this request, or 612 * {@code null} if it was not provided. 613 */ 614 @Nullable() 615 public byte[] getOldPasswordBytes() 616 { 617 if (oldPassword == null) 618 { 619 return null; 620 } 621 else 622 { 623 return oldPassword.getValue(); 624 } 625 } 626 627 628 629 /** 630 * Retrieves the raw old password for this request, if available. 631 * 632 * @return The raw old password for this request, or {@code null} if it was 633 * not provided. 634 */ 635 @Nullable() 636 public ASN1OctetString getRawOldPassword() 637 { 638 return oldPassword; 639 } 640 641 642 643 /** 644 * Retrieves the string representation of the new password for this request, 645 * if available. 646 * 647 * @return The string representation of the new password for this request, or 648 * {@code null} if it was not provided. 649 */ 650 @Nullable() 651 public String getNewPassword() 652 { 653 if (newPassword == null) 654 { 655 return null; 656 } 657 else 658 { 659 return newPassword.stringValue(); 660 } 661 } 662 663 664 665 /** 666 * Retrieves the binary representation of the new password for this request, 667 * if available. 668 * 669 * @return The binary representation of the new password for this request, or 670 * {@code null} if it was not provided. 671 */ 672 @Nullable() 673 public byte[] getNewPasswordBytes() 674 { 675 if (newPassword == null) 676 { 677 return null; 678 } 679 else 680 { 681 return newPassword.getValue(); 682 } 683 } 684 685 686 687 /** 688 * Retrieves the raw new password for this request, if available. 689 * 690 * @return The raw new password for this request, or {@code null} if it was 691 * not provided. 692 */ 693 @Nullable() 694 public ASN1OctetString getRawNewPassword() 695 { 696 return newPassword; 697 } 698 699 700 701 /** 702 * {@inheritDoc} 703 */ 704 @Override() 705 @NotNull() 706 public PasswordModifyExtendedResult process( 707 @NotNull final LDAPConnection connection, final int depth) 708 throws LDAPException 709 { 710 final ExtendedResult extendedResponse = super.process(connection, depth); 711 final PasswordModifyExtendedResult result = 712 new PasswordModifyExtendedResult(extendedResponse); 713 714 if ((result.getResultCode() == ResultCode.REFERRAL) && 715 followReferrals(connection)) 716 { 717 return ReferralHelper.handleReferral(this, result, connection); 718 } 719 else 720 { 721 return result; 722 } 723 } 724 725 726 727 /** 728 * {@inheritDoc} 729 */ 730 @Override() 731 @NotNull() 732 public PasswordModifyExtendedRequest duplicate() 733 { 734 return duplicate(getControls()); 735 } 736 737 738 739 /** 740 * {@inheritDoc} 741 */ 742 @Override() 743 @NotNull() 744 public PasswordModifyExtendedRequest duplicate( 745 @Nullable final Control[] controls) 746 { 747 final byte[] oldPWBytes = 748 (oldPassword == null) ? null : oldPassword.getValue(); 749 final byte[] newPWBytes = 750 (newPassword == null) ? null : newPassword.getValue(); 751 752 final PasswordModifyExtendedRequest r = 753 new PasswordModifyExtendedRequest(userIdentity, oldPWBytes, 754 newPWBytes, controls); 755 r.setResponseTimeoutMillis(getResponseTimeoutMillis(null)); 756 r.setIntermediateResponseListener(getIntermediateResponseListener()); 757 r.setReferralDepth(getReferralDepth()); 758 r.setReferralConnector(getReferralConnectorInternal()); 759 return r; 760 } 761 762 763 764 /** 765 * {@inheritDoc} 766 */ 767 @Override() 768 @NotNull() 769 public String getExtendedRequestName() 770 { 771 return INFO_EXTENDED_REQUEST_NAME_PASSWORD_MODIFY.get(); 772 } 773 774 775 776 /** 777 * {@inheritDoc} 778 */ 779 @Override() 780 public void toString(@NotNull final StringBuilder buffer) 781 { 782 buffer.append("PasswordModifyExtendedRequest("); 783 784 boolean dataAdded = false; 785 786 if (userIdentity != null) 787 { 788 buffer.append("userIdentity='"); 789 buffer.append(userIdentity); 790 buffer.append('\''); 791 dataAdded = true; 792 } 793 794 if (oldPassword != null) 795 { 796 if (dataAdded) 797 { 798 buffer.append(", "); 799 } 800 801 buffer.append("oldPassword='"); 802 buffer.append(oldPassword.stringValue()); 803 buffer.append('\''); 804 dataAdded = true; 805 } 806 807 if (newPassword != null) 808 { 809 if (dataAdded) 810 { 811 buffer.append(", "); 812 } 813 814 buffer.append("newPassword='"); 815 buffer.append(newPassword.stringValue()); 816 buffer.append('\''); 817 dataAdded = true; 818 } 819 820 final Control[] controls = getControls(); 821 if (controls.length > 0) 822 { 823 if (dataAdded) 824 { 825 buffer.append(", "); 826 } 827 828 buffer.append("controls={"); 829 for (int i=0; i < controls.length; i++) 830 { 831 if (i > 0) 832 { 833 buffer.append(", "); 834 } 835 836 buffer.append(controls[i]); 837 } 838 buffer.append('}'); 839 } 840 841 buffer.append(')'); 842 } 843}