001/*
002 * Copyright 2012-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2012-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) 2012-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.extensions;
037
038
039
040import java.util.ArrayList;
041import java.util.Collection;
042import java.util.Collections;
043import java.util.Iterator;
044import java.util.List;
045
046import com.unboundid.asn1.ASN1Element;
047import com.unboundid.asn1.ASN1Enumerated;
048import com.unboundid.asn1.ASN1OctetString;
049import com.unboundid.asn1.ASN1Sequence;
050import com.unboundid.ldap.sdk.Control;
051import com.unboundid.ldap.sdk.ExtendedRequest;
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;
062
063import static com.unboundid.ldap.sdk.unboundidds.extensions.ExtOpMessages.*;
064
065
066
067/**
068 * This class provides an implementation of an extended request that may be used
069 * to set the accessibility of one or more subtrees in the Ping Identity,
070 * UnboundID, or Nokia/Alcatel-Lucent 8661 Directory Server.  It may be used to
071 * indicate that a specified set of entries and all their subordinates should be
072 * invisible or read-only, or to restore it to full accessibility.
073 * <BR>
074 * <BLOCKQUOTE>
075 *   <B>NOTE:</B>  This class, and other classes within the
076 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
077 *   supported for use against Ping Identity, UnboundID, and
078 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
079 *   for proprietary functionality or for external specifications that are not
080 *   considered stable or mature enough to be guaranteed to work in an
081 *   interoperable way with other types of LDAP servers.
082 * </BLOCKQUOTE>
083 * <BR>
084 * The OID for this request is 1.3.6.1.4.1.30221.2.6.19, and the
085 * value must have the encoding specified below.  Note that the initial
086 * specification for this extended request only allowed for the specification of
087 * a single subtree, whereas it is now possible to affect the accessibility of
088 * multiple subtrees in a single request.  In order to preserve compatibility
089 * with the original encoding, if there is more than one target subtree, then
090 * the first subtree must be specified as the first element in the value
091 * sequence and the remaining subtrees must be specified in the
092 * additionalSubtreeBaseDNs element.
093 * <BR><BR>
094 * <PRE>
095 *   SetSubtreeAccessibilityRequestValue ::= SEQUENCE {
096 *        subtreeBaseDN                LDAPDN,
097 *        subtreeAccessibility         ENUMERATED {
098 *             accessible                 (0),
099 *             read-only-bind-allowed     (1),
100 *             read-only-bind-denied      (2),
101 *             hidden                     (3),
102 *             ... },
103 *        bypassUserDN                 [0] LDAPDN OPTIONAL,
104 *        additionalSubtreeBaseDNs     [1] SEQUENCE OF LDAPDN OPTIONAL,
105 *        ... }
106 * </PRE>
107 */
108@NotMutable()
109@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
110public final class SetSubtreeAccessibilityExtendedRequest
111       extends ExtendedRequest
112{
113  /**
114   * The OID (1.3.6.1.4.1.30221.2.6.19) for the set subtree accessibility
115   * extended request.
116   */
117  @NotNull public static final String SET_SUBTREE_ACCESSIBILITY_REQUEST_OID =
118       "1.3.6.1.4.1.30221.2.6.19";
119
120
121
122  /**
123   * The BER type for the bypass user DN element of the request.
124   */
125  private static final byte TYPE_BYPASS_USER_DN = (byte) 0x80;
126
127
128
129  /**
130   * The BER type for the set of additional subtree base DNs.
131   */
132  private static final byte TYPE_ADDITIONAL_SUBTREE_BASE_DNS = (byte) 0xA1;
133
134
135
136  /**
137   * The serial version UID for this serializable class.
138   */
139  private static final long serialVersionUID = -3003738735546060245L;
140
141
142
143  // The set of subtree base DNs included in the request.
144  @NotNull private final List<String> subtreeBaseDNs;
145
146  // The DN of a user who will be exempted from the restrictions.  This is not
147  // applicable for a subtree accessibility of ACCESSIBLE.
148  @Nullable private final String bypassUserDN;
149
150  // The accessibility state to use for the target subtrees.
151  @NotNull private final SubtreeAccessibilityState accessibilityState;
152
153
154
155  /**
156   * Creates a new set subtree accessibility extended request with the provided
157   * information.
158   *
159   * @param  subtreeBaseDNs      The set of base DNs for the target subtree.
160   *                             It must not be {@code null} or empty.
161   * @param  accessibilityState  The accessibility state to use for the target
162   *                             subtrees.
163   * @param  bypassUserDN        The DN of a user that will be allowed to bypass
164   *                             restrictions on the target subtrees.
165   * @param  controls            The set of controls to include in the request.
166   */
167  private SetSubtreeAccessibilityExtendedRequest(
168               @NotNull final Collection<String> subtreeBaseDNs,
169               @NotNull final SubtreeAccessibilityState accessibilityState,
170               @Nullable final String bypassUserDN,
171               @Nullable final Control... controls)
172  {
173    super(SET_SUBTREE_ACCESSIBILITY_REQUEST_OID,
174         encodeValue(subtreeBaseDNs, accessibilityState, bypassUserDN),
175         controls);
176
177    this.subtreeBaseDNs = Collections.unmodifiableList(
178         new ArrayList<>(subtreeBaseDNs));
179    this.accessibilityState = accessibilityState;
180    this.bypassUserDN = bypassUserDN;
181  }
182
183
184
185  /**
186   * Encodes the provided information for use as the extended request value.
187   *
188   * @param  subtreeBaseDNs      The set of base DNs for the target subtrees.
189   *                             It must not be {@code null} or empty.
190   * @param  accessibilityState  The accessibility state to use for the target
191   *                             subtrees.
192   * @param  bypassUserDN        The DN of a user that will be allowed to bypass
193   *                             restrictions on the target subtrees.
194   *
195   * @return  An ASN.1 octet string containing the encoded value.
196   */
197  @NotNull()
198  private static ASN1OctetString encodeValue(
199               @NotNull final Collection<String> subtreeBaseDNs,
200               @NotNull final SubtreeAccessibilityState accessibilityState,
201               @Nullable final String bypassUserDN)
202  {
203    final Iterator<String> dnIterator = subtreeBaseDNs.iterator();
204    final String subtreeBaseDN = dnIterator.next();
205    Validator.ensureNotNull(subtreeBaseDN);
206
207    final ArrayList<ASN1Element> elements = new ArrayList<>(4);
208    elements.add(new ASN1OctetString(subtreeBaseDN));
209    elements.add(new ASN1Enumerated(accessibilityState.intValue()));
210
211    if (bypassUserDN != null)
212    {
213      elements.add(new ASN1OctetString(TYPE_BYPASS_USER_DN, bypassUserDN));
214    }
215
216    if (dnIterator.hasNext())
217    {
218      final ArrayList<ASN1Element> additionalDNElements =
219           new ArrayList<>(subtreeBaseDNs.size()-1);
220      while (dnIterator.hasNext())
221      {
222        final String additionalDN = dnIterator.next();
223        Validator.ensureNotNull(additionalDN);
224        additionalDNElements.add(new ASN1OctetString(additionalDN));
225      }
226      elements.add(new ASN1Sequence(TYPE_ADDITIONAL_SUBTREE_BASE_DNS,
227           additionalDNElements));
228    }
229
230    return new ASN1OctetString(new ASN1Sequence(elements).encode());
231  }
232
233
234
235  /**
236   * Creates a new set subtree accessibility extended request from the provided
237   * generic extended request.
238   *
239   * @param  extendedRequest  The generic extended request to use to create this
240   *                          set subtree accessibility extended request.
241   *
242   * @throws  LDAPException  If a problem occurs while decoding the request.
243   */
244  public SetSubtreeAccessibilityExtendedRequest(
245              @NotNull final ExtendedRequest extendedRequest)
246         throws LDAPException
247  {
248    super(extendedRequest);
249
250    final ASN1OctetString value = extendedRequest.getValue();
251    if (value == null)
252    {
253      throw new LDAPException(ResultCode.DECODING_ERROR,
254           ERR_SET_SUBTREE_ACCESSIBILITY_NO_VALUE.get());
255    }
256
257    try
258    {
259      final ASN1Element[] elements =
260           ASN1Sequence.decodeAsSequence(value.getValue()).elements();
261
262      final List<String> baseDNs = new ArrayList<>(10);
263      baseDNs.add(ASN1OctetString.decodeAsOctetString(
264           elements[0]).stringValue());
265
266      final int accessibilityStateValue =
267           ASN1Enumerated.decodeAsEnumerated(elements[1]).intValue();
268      accessibilityState =
269           SubtreeAccessibilityState.valueOf(accessibilityStateValue);
270      if (accessibilityState == null)
271      {
272        throw new LDAPException(ResultCode.DECODING_ERROR,
273             ERR_SET_SUBTREE_ACCESSIBILITY_INVALID_ACCESSIBILITY_STATE.get(
274                  accessibilityStateValue));
275      }
276
277      String bypassDN = null;
278      for (int i=2; i < elements.length; i++)
279      {
280        switch (elements[i].getType())
281        {
282          case TYPE_BYPASS_USER_DN:
283            bypassDN =
284                 ASN1OctetString.decodeAsOctetString(elements[i]).stringValue();
285            break;
286
287          case TYPE_ADDITIONAL_SUBTREE_BASE_DNS:
288            for (final ASN1Element e :
289                 ASN1Sequence.decodeAsSequence(elements[i]).elements())
290            {
291              baseDNs.add(ASN1OctetString.decodeAsOctetString(e).stringValue());
292            }
293            break;
294
295          default:
296            throw new LDAPException(ResultCode.DECODING_ERROR,
297                 ERR_SET_SUBTREE_ACCESSIBILITY_INVALID_ELEMENT_TYPE.get(
298                      StaticUtils.toHex(elements[i].getType())));
299        }
300      }
301      bypassUserDN = bypassDN;
302      subtreeBaseDNs = Collections.unmodifiableList(baseDNs);
303    }
304    catch (final LDAPException le)
305    {
306      Debug.debugException(le);
307      throw le;
308    }
309    catch (final Exception e)
310    {
311      Debug.debugException(e);
312      throw new LDAPException(ResultCode.DECODING_ERROR,
313           ERR_SET_SUBTREE_ACCESSIBILITY_CANNOT_DECODE.get(
314                StaticUtils.getExceptionMessage(e)),
315           e);
316    }
317
318
319    if ((accessibilityState == SubtreeAccessibilityState.ACCESSIBLE) &&
320        (bypassUserDN != null))
321    {
322      throw new LDAPException(ResultCode.DECODING_ERROR,
323           ERR_SET_SUBTREE_ACCESSIBILITY_UNEXPECTED_BYPASS_DN.get(
324                accessibilityState.getStateName()));
325    }
326  }
327
328
329
330  /**
331   * Creates a new set subtree accessibility extended request that will make the
332   * specified subtree accessible.
333   *
334   * @param  subtreeBaseDN  The base DN for the subtree to make accessible.  It
335   *                        must not be {@code null}.
336   * @param  controls       The set of controls to include in the request.  It
337   *                        may be {@code null} or empty if no controls are
338   *                        needed.
339   *
340   * @return  The set subtree accessibility extended request that was created.
341   */
342  @NotNull()
343  public static SetSubtreeAccessibilityExtendedRequest
344              createSetAccessibleRequest(@NotNull final String subtreeBaseDN,
345                                         @Nullable final Control... controls)
346  {
347    Validator.ensureNotNull(subtreeBaseDN);
348
349    return new SetSubtreeAccessibilityExtendedRequest(
350         Collections.singletonList(subtreeBaseDN),
351         SubtreeAccessibilityState.ACCESSIBLE, null, controls);
352  }
353
354
355
356  /**
357   * Creates a new set subtree accessibility extended request that will make the
358   * specified subtrees accessible.
359   *
360   * @param  subtreeBaseDNs  The base DNs for the subtrees to make accessible.
361   *                         It must not be {@code null} or empty.  If multiple
362   *                         base DNs are specified, then all must reside below
363   *                         the same backend base DN.
364   * @param  controls        The set of controls to include in the request.  It
365   *                         may be {@code null} or empty if no controls are
366   *                         needed.
367   *
368   * @return  The set subtree accessibility extended request that was created.
369   */
370  @NotNull()
371  public static SetSubtreeAccessibilityExtendedRequest
372                     createSetAccessibleRequest(
373                          @NotNull final Collection<String> subtreeBaseDNs,
374                          @Nullable final Control... controls)
375  {
376    Validator.ensureNotNull(subtreeBaseDNs);
377    Validator.ensureFalse(subtreeBaseDNs.isEmpty());
378
379    return new SetSubtreeAccessibilityExtendedRequest(subtreeBaseDNs,
380         SubtreeAccessibilityState.ACCESSIBLE, null, controls);
381  }
382
383
384
385  /**
386   * Creates a new set subtree accessibility extended request that will make the
387   * specified subtree read-only.
388   *
389   * @param  subtreeBaseDN  The base DN for the subtree to make read-only.  It
390   *                        must not be {@code null}.
391   * @param  allowBind      Indicates whether users within the specified subtree
392   *                        will be allowed to bind.
393   * @param  bypassUserDN   The DN of a user that will be allowed to perform
394   *                        write (add, delete, modify, and modify DN)
395   *                        operations in the specified subtree.  It may be
396   *                        {@code null} if no bypass user is needed.
397   * @param  controls       The set of controls to include in the request.  It
398   *                        may be {@code null} or empty if no controls are
399   *                        needed.
400   *
401   * @return  The set subtree accessibility extended request that was created.
402   */
403  @NotNull()
404  public static SetSubtreeAccessibilityExtendedRequest
405              createSetReadOnlyRequest(@NotNull final String subtreeBaseDN,
406                                       final boolean allowBind,
407                                       @Nullable final String bypassUserDN,
408                                       @Nullable final Control... controls)
409  {
410    Validator.ensureNotNull(subtreeBaseDN);
411
412    if (allowBind)
413    {
414      return new SetSubtreeAccessibilityExtendedRequest(
415           Collections.singletonList(subtreeBaseDN),
416           SubtreeAccessibilityState.READ_ONLY_BIND_ALLOWED, bypassUserDN,
417           controls);
418    }
419    else
420    {
421      return new SetSubtreeAccessibilityExtendedRequest(
422           Collections.singletonList(subtreeBaseDN),
423           SubtreeAccessibilityState.READ_ONLY_BIND_DENIED, bypassUserDN,
424           controls);
425    }
426  }
427
428
429
430  /**
431   * Creates a new set subtree accessibility extended request that will make the
432   * specified subtrees read-only.
433   *
434   * @param  subtreeBaseDNs  The base DNs for the subtrees to make read-only.
435   *                         It must not be {@code null} or empty.  If multiple
436   *                         base DNs are specified, then all must reside below
437   *                         the same backend base DN.
438   * @param  allowBind       Indicates whether users within the specified
439   *                         subtrees will be allowed to bind.
440   * @param  bypassUserDN    The DN of a user that will be allowed to perform
441   *                         write (add, delete, modify, and modify DN)
442   *                         operations in the specified subtrees.  It may be
443   *                         {@code null} if no bypass user is needed.
444   * @param  controls        The set of controls to include in the request.  It
445   *                         may be {@code null} or empty if no controls are
446   *                         needed.
447   *
448   * @return  The set subtree accessibility extended request that was created.
449   */
450  @NotNull()
451  public static SetSubtreeAccessibilityExtendedRequest
452              createSetReadOnlyRequest(
453                   @NotNull final Collection<String> subtreeBaseDNs,
454                   final boolean allowBind,
455                   @Nullable final String bypassUserDN,
456                   @Nullable final Control... controls)
457  {
458    Validator.ensureNotNull(subtreeBaseDNs);
459    Validator.ensureFalse(subtreeBaseDNs.isEmpty());
460
461    if (allowBind)
462    {
463      return new SetSubtreeAccessibilityExtendedRequest(subtreeBaseDNs,
464           SubtreeAccessibilityState.READ_ONLY_BIND_ALLOWED, bypassUserDN,
465           controls);
466    }
467    else
468    {
469      return new SetSubtreeAccessibilityExtendedRequest(subtreeBaseDNs,
470           SubtreeAccessibilityState.READ_ONLY_BIND_DENIED, bypassUserDN,
471           controls);
472    }
473  }
474
475
476
477  /**
478   * Creates a new set subtree accessibility extended request that will make the
479   * specified subtree hidden.
480   *
481   * @param  subtreeBaseDN  The base DN for the subtree to make hidden.  It must
482   *                        not be {@code null}.
483   * @param  bypassUserDN   The DN of a user that will be allowed to perform
484   *                        write (add, delete, modify, and modify DN)
485   *                        operations in the specified subtree.  It may be
486   *                        {@code null} if no bypass user is needed.
487   * @param  controls       The set of controls to include in the request.  It
488   *                        may be {@code null} or empty if no controls are
489   *                        needed.
490   *
491   * @return  The set subtree accessibility extended request that was created.
492   */
493  @NotNull()
494  public static SetSubtreeAccessibilityExtendedRequest
495              createSetHiddenRequest(@NotNull final String subtreeBaseDN,
496                                     @Nullable final String bypassUserDN,
497                                     @Nullable final Control... controls)
498  {
499    Validator.ensureNotNull(subtreeBaseDN);
500
501    return new SetSubtreeAccessibilityExtendedRequest(
502         Collections.singletonList(subtreeBaseDN),
503         SubtreeAccessibilityState.HIDDEN, bypassUserDN, controls);
504  }
505
506
507
508  /**
509   * Creates a new set subtree accessibility extended request that will make the
510   * specified subtrees hidden.
511   *
512   * @param  subtreeBaseDNs  The base DNs for the subtrees to make hidden.  It
513   *                         must not be {@code null} or empty.  If multiple
514   *                         base DNs are specified, then all must reside below
515   *                         the same backend base DN.
516   * @param  bypassUserDN    The DN of a user that will be allowed to perform
517   *                         write (add, delete, modify, and modify DN)
518   *                         operations in the specified subtrees.  It may be
519   *                         {@code null} if no bypass user is needed.
520   * @param  controls        The set of controls to include in the request.  It
521   *                         may be {@code null} or empty if no controls are
522   *                         needed.
523   *
524   * @return  The set subtree accessibility extended request that was created.
525   */
526  @NotNull()
527  public static SetSubtreeAccessibilityExtendedRequest
528              createSetHiddenRequest(
529                   @NotNull final Collection<String> subtreeBaseDNs,
530                   @Nullable final String bypassUserDN,
531                   @Nullable final Control... controls)
532  {
533    Validator.ensureNotNull(subtreeBaseDNs);
534    Validator.ensureFalse(subtreeBaseDNs.isEmpty());
535
536    return new SetSubtreeAccessibilityExtendedRequest(subtreeBaseDNs,
537         SubtreeAccessibilityState.HIDDEN, bypassUserDN, controls);
538  }
539
540
541
542  /**
543   * Retrieves the base DN for the target subtree.  Note that if multiple
544   * base DNs are defined, this will only retrieve the first.  The
545   * {@link #getSubtreeBaseDNs()} method should be used to get the complete set
546   * of target subtree base DNs.
547   *
548   * @return  The base DN for the target subtree.
549   */
550  @NotNull()
551  public String getSubtreeBaseDN()
552  {
553    return subtreeBaseDNs.get(0);
554  }
555
556
557
558  /**
559   * Retrieves the base DNs for all target subtrees.
560   *
561   * @return  The base DNs for all target subtrees.
562   */
563  @NotNull()
564  public List<String> getSubtreeBaseDNs()
565  {
566    return subtreeBaseDNs;
567  }
568
569
570
571  /**
572   * Retrieves the accessibility state to apply to the target subtrees.
573   *
574   * @return  The accessibility state to apply to the target subtrees.
575   */
576  @NotNull()
577  public SubtreeAccessibilityState getAccessibilityState()
578  {
579    return accessibilityState;
580  }
581
582
583
584  /**
585   * Retrieves the DN of the user that will be allowed to bypass the
586   * restrictions imposed on the target subtrees for all other users.
587   *
588   * @return  The DN of the user that will be allowed to bypass the restrictions
589   *          imposed on the target subtrees for all other users, or
590   *          {@code null} if there are no restrictions to be imposed on the
591   *          target subtrees or if no bypass user is defined for those
592   *          subtrees.
593   */
594  @Nullable()
595  public String getBypassUserDN()
596  {
597    return bypassUserDN;
598  }
599
600
601
602  /**
603   * {@inheritDoc}
604   */
605  @Override()
606  @NotNull()
607  public SetSubtreeAccessibilityExtendedRequest duplicate()
608  {
609    return duplicate(getControls());
610  }
611
612
613
614  /**
615   * {@inheritDoc}
616   */
617  @Override()
618  @NotNull()
619  public SetSubtreeAccessibilityExtendedRequest duplicate(
620              @Nullable final Control[] controls)
621  {
622    final SetSubtreeAccessibilityExtendedRequest r =
623         new SetSubtreeAccessibilityExtendedRequest(subtreeBaseDNs,
624              accessibilityState, bypassUserDN, controls);
625    r.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
626    r.setIntermediateResponseListener(getIntermediateResponseListener());
627    r.setReferralDepth(getReferralDepth());
628    r.setReferralConnector(getReferralConnectorInternal());
629    return r;
630  }
631
632
633
634  /**
635   * {@inheritDoc}
636   */
637  @Override()
638  @NotNull()
639  public String getExtendedRequestName()
640  {
641    return INFO_EXTENDED_REQUEST_NAME_SET_SUBTREE_ACCESSIBILITY.get();
642  }
643
644
645
646  /**
647   * {@inheritDoc}
648   */
649  @Override()
650  public void toString(@NotNull final StringBuilder buffer)
651  {
652    buffer.append("SetSubtreeAccessibilityExtendedRequest(baseDNs={");
653
654    final Iterator<String> dnIterator = subtreeBaseDNs.iterator();
655    while (dnIterator.hasNext())
656    {
657      buffer.append('"');
658      buffer.append(dnIterator.next());
659      buffer.append('"');
660
661      if (dnIterator.hasNext())
662      {
663        buffer.append(", ");
664      }
665    }
666
667    buffer.append("}, accessibilityType=\"");
668    buffer.append(accessibilityState.getStateName());
669    buffer.append('"');
670
671    if (bypassUserDN != null)
672    {
673      buffer.append(", bypassUserDN=\"");
674      buffer.append(bypassUserDN);
675      buffer.append('"');
676    }
677
678    final Control[] controls = getControls();
679    if (controls.length > 0)
680    {
681      buffer.append(", controls={");
682      for (int i=0; i < controls.length; i++)
683      {
684        if (i > 0)
685        {
686          buffer.append(", ");
687        }
688
689        buffer.append(controls[i]);
690      }
691      buffer.append('}');
692    }
693
694    buffer.append(')');
695  }
696}