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