001/* 002 * Copyright 2020 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2020 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) 2020 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; 037 038 039 040import java.util.ArrayList; 041import java.util.List; 042 043import com.unboundid.asn1.ASN1OctetString; 044import com.unboundid.util.ByteStringBuffer; 045import com.unboundid.util.Debug; 046import com.unboundid.util.NotMutable; 047import com.unboundid.util.NotNull; 048import com.unboundid.util.Nullable; 049import com.unboundid.util.StaticUtils; 050import com.unboundid.util.ThreadSafety; 051import com.unboundid.util.ThreadSafetyLevel; 052import com.unboundid.util.Validator; 053 054 055 056/** 057 * This class provides an implementation of a SASL bind request that uses the 058 * OAUTHBEARER SASL mechanism described in 059 * <A HREF="http://www.ietf.org/rfc/rfc7628.txt">RFC 7628</A> to allow a user 060 * to authenticate with an OAuth 2.0 bearer token. 061 * 062 * @see OAUTHBEARERBindRequestProperties 063 * @see OAUTHBEARERBindResult 064 */ 065@NotMutable() 066@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 067public final class OAUTHBEARERBindRequest 068 extends SASLBindRequest 069{ 070 /** 071 * The name for the OAUTHBEARER SASL mechanism. 072 */ 073 @NotNull public static final String OAUTHBEARER_MECHANISM_NAME = 074 "OAUTHBEARER"; 075 076 077 078 /** 079 * The delimiter that appears between elements of the GS2 header. 080 */ 081 private static final byte GS2_HEADER_DELIMITER = ','; 082 083 084 085 /** 086 * The delimiter that appears after each element of the encoded credentials. 087 */ 088 private static final byte OAUTHBEARER_DELIMITER = (byte) 0x01; 089 090 091 092 /** 093 * The component of the GS2 header that indicates that channel binding is not 094 * supported. 095 */ 096 @NotNull private static final byte[] GS2_HEADER_ELEMENT_NO_CHANNEL_BINDING = 097 StaticUtils.getBytes("n"); 098 099 100 101 /** 102 * The component of the GS2 header that precedes the authorization ID. 103 */ 104 @NotNull private static final byte[] GS2_HEADER_ELEMENT_AUTHZ_ID_PREFIX = 105 StaticUtils.getBytes("a="); 106 107 108 109 /** 110 * The component of the OAUTHBEARER bind request credentials that precedes the 111 * access token. 112 */ 113 @NotNull private static final byte[] 114 OAUTHBEARER_CRED_ELEMENT_ACCESS_TOKEN_PREFIX = 115 StaticUtils.getBytes("auth=Bearer "); 116 117 118 119 /** 120 * The component of the OAUTHBEARER bind request credentials that precedes the 121 * server address. 122 */ 123 @NotNull private static final byte[] 124 OAUTHBEARER_CRED_ELEMENT_SERVER_ADDRESS_PREFIX = 125 StaticUtils.getBytes("host="); 126 127 128 129 /** 130 * The component of the OAUTHBEARER bind request credentials that precedes the 131 * server port. 132 */ 133 @NotNull private static final byte[] 134 OAUTHBEARER_CRED_ELEMENT_SERVER_PORT_PREFIX = 135 StaticUtils.getBytes("port="); 136 137 138 139 /** 140 * The component of the OAUTHBEARER bind request credentials that precedes the 141 * request method. 142 */ 143 @NotNull private static final byte[] 144 OAUTHBEARER_CRED_ELEMENT_REQUEST_METHOD_PREFIX = 145 StaticUtils.getBytes("mthd="); 146 147 148 149 /** 150 * The component of the OAUTHBEARER bind request credentials that precedes the 151 * request path. 152 */ 153 @NotNull private static final byte[] 154 OAUTHBEARER_CRED_ELEMENT_REQUEST_PATH_PREFIX = 155 StaticUtils.getBytes("path="); 156 157 158 159 /** 160 * The component of the OAUTHBEARER bind request credentials that precedes the 161 * request post data. 162 */ 163 @NotNull private static final byte[] 164 OAUTHBEARER_CRED_ELEMENT_REQUEST_POST_DATA_PREFIX = 165 StaticUtils.getBytes("post="); 166 167 168 169 /** 170 * The component of the OAUTHBEARER bind request credentials that precedes the 171 * request query string. 172 */ 173 @NotNull private static final byte[] 174 OAUTHBEARER_CRED_ELEMENT_REQUEST_QUERY_STRING_PREFIX = 175 StaticUtils.getBytes("qs="); 176 177 178 179 /** 180 * The SASL credentials that should be included in the dummy bind request that 181 * is used to complete a failed authentication attempt. 182 */ 183 @NotNull private static final ASN1OctetString DUMMY_REQUEST_CREDENTIALS = 184 new ASN1OctetString(new byte[] { OAUTHBEARER_DELIMITER }); 185 186 187 188 /** 189 * The serial version UID for this serializable class. 190 */ 191 private static final long serialVersionUID = -1216152242833705618L; 192 193 194 195 // The message ID from the last LDAP message sent from this request. 196 private volatile int messageID; 197 198 // The port of the server to which the request will be sent. 199 @Nullable private final Integer serverPort; 200 201 // The access token to include in the bind request. 202 @NotNull private final String accessToken; 203 204 // The authorization identity to include in the GS2 header for the bind 205 // request. 206 @Nullable private final String authorizationID; 207 208 // The method to use for HTTP-based requests. 209 @Nullable private final String requestMethod; 210 211 // The path to use for HTTP-based requests. 212 @Nullable private final String requestPath; 213 214 // The post data for HTTP-based requests. 215 @Nullable private final String requestPostData; 216 217 // The query string for HTTP-based requests. 218 @Nullable private final String requestQueryString; 219 220 // The address of the server to which the request will be sent. 221 @Nullable private final String serverAddress; 222 223 224 225 /** 226 * Creates a new OAUTHBEARER bind request with the provided access token. 227 * All other properties will be unset. 228 * 229 * @param accessToken The access token to use for this bind request. It 230 * must not be {@code null} or empty. 231 * @param controls The set of controls to include in the bind request. 232 * It may be {@code null} or empty if no controls are 233 * needed. 234 */ 235 public OAUTHBEARERBindRequest(@NotNull final String accessToken, 236 @Nullable final Control... controls) 237 { 238 super(controls); 239 240 Validator.ensureNotNullOrEmpty(accessToken, 241 "OAUTHBEARERBindRequest.accessToken must not be null or empty."); 242 243 this.accessToken = accessToken; 244 245 authorizationID = null; 246 serverAddress = null; 247 serverPort = null; 248 requestMethod = null; 249 requestPath = null; 250 requestPostData = null; 251 requestQueryString = null; 252 253 messageID = -1; 254 } 255 256 257 258 /** 259 * Creates a new OAUTHBEARER bind request with the provided set of properties. 260 * 261 * @param properties The set of properties to use to create this bind 262 * request. It must not be {@code null}. 263 * @param controls The set of controls to include in the bind request. It 264 * may be {@code null} or empty if no controls are needed. 265 */ 266 public OAUTHBEARERBindRequest( 267 @NotNull final OAUTHBEARERBindRequestProperties properties, 268 @Nullable final Control... controls) 269 { 270 super(controls); 271 272 accessToken = properties.getAccessToken(); 273 authorizationID = properties.getAuthorizationID(); 274 serverAddress = properties.getServerAddress(); 275 serverPort = properties.getServerPort(); 276 requestMethod = properties.getRequestMethod(); 277 requestPath = properties.getRequestPath(); 278 requestPostData = properties.getRequestPostData(); 279 requestQueryString = properties.getRequestQueryString(); 280 281 messageID = -1; 282 } 283 284 285 286 /** 287 * {@inheritDoc} 288 */ 289 @Override() 290 @NotNull() 291 public String getSASLMechanismName() 292 { 293 return OAUTHBEARER_MECHANISM_NAME; 294 } 295 296 297 298 /** 299 * Retrieves the access token to include in the bind request. 300 * 301 * @return The access token to include in the bind request. 302 */ 303 @NotNull() 304 public String getAccessToken() 305 { 306 return accessToken; 307 } 308 309 310 311 /** 312 * Retrieves the authorization ID to include in the GS2 header for the bind 313 * request, if any. 314 * 315 * @return The authorization ID to include in the GS2 header for the bind 316 * request, or {@code null} if no authorization ID should be 317 * included. 318 */ 319 @Nullable() 320 public String getAuthorizationID() 321 { 322 return authorizationID; 323 } 324 325 326 327 /** 328 * Retrieves the server address to include in the bind request, if any. 329 * 330 * @return The server address to include in the bind request, or {@code null} 331 * if it should be omitted. 332 */ 333 @Nullable() 334 public String getServerAddress() 335 { 336 return serverAddress; 337 } 338 339 340 341 /** 342 * Retrieves the server port to include in the bind request, if any. 343 * 344 * @return The server port to include in the bind request, or {@code null} 345 * if it should be omitted. 346 */ 347 @Nullable() 348 public Integer getServerPort() 349 { 350 return serverPort; 351 } 352 353 354 355 /** 356 * Retrieves the method to use for HTTP-based requests, if any. 357 * 358 * @return The method to use for HTTP-based requests, or {@code null} if it 359 * should be omitted from the bind request. 360 */ 361 @Nullable() 362 public String getRequestMethod() 363 { 364 return requestMethod; 365 } 366 367 368 369 /** 370 * Retrieves the path to use for HTTP-based requests, if any. 371 * 372 * @return The path to use for HTTP-based requests, or {@code null} if it 373 * should be omitted from the bind request. 374 */ 375 @Nullable() 376 public String getRequestPath() 377 { 378 return requestPath; 379 } 380 381 382 383 /** 384 * Retrieves the data to submit when posting an HTTP-based request, if any. 385 * 386 * @return The post data for HTTP-based requests, or {@code null} if it 387 * should be omitted from the bind request. 388 */ 389 @Nullable() 390 public String getRequestPostData() 391 { 392 return requestPostData; 393 } 394 395 396 397 /** 398 * Retrieves the query string to use for HTTP-based requests, if any. 399 * 400 * @return The query string to use for HTTP-based requests, or {@code null} 401 * if it should be omitted from the bind request. 402 */ 403 @Nullable() 404 public String getRequestQueryString() 405 { 406 return requestQueryString; 407 } 408 409 410 411 /** 412 * {@inheritDoc} 413 */ 414 @Override() 415 @NotNull() 416 protected OAUTHBEARERBindResult process( 417 @NotNull final LDAPConnection connection, final int depth) 418 throws LDAPException 419 { 420 // Send the initial request. If the response has a result code that is 421 // anything other than SASL_BIND_IN_PROGRESS, then we can just return it 422 // directly without needing to do anything else. 423 messageID = InternalSDKHelper.nextMessageID(connection); 424 final BindResult initialBindResult = sendBindRequest(connection, "", 425 encodeCredentials(), getControls(), 426 getResponseTimeoutMillis(connection)); 427 if (initialBindResult.getResultCode() != ResultCode.SASL_BIND_IN_PROGRESS) 428 { 429 return new OAUTHBEARERBindResult(initialBindResult); 430 } 431 432 433 // If we've gotten here, then it indicates that the attempt failed. We need 434 // to send a second, dummy request to complete the bind process and get the 435 // ultimate failure result. 436 BindResult finalBindResult; 437 try 438 { 439 messageID = InternalSDKHelper.nextMessageID(connection); 440 finalBindResult = sendBindRequest(connection, "", 441 DUMMY_REQUEST_CREDENTIALS, getControls(), 442 getResponseTimeoutMillis(connection)); 443 } 444 catch (final LDAPException e) 445 { 446 Debug.debugException(e); 447 finalBindResult = new BindResult(e); 448 } 449 450 return new OAUTHBEARERBindResult(initialBindResult, finalBindResult); 451 } 452 453 454 455 /** 456 * Encodes the credentials as appropriate for this bind request. 457 * 458 * @return An ASN.1 octet string containing the encoded credentials. 459 */ 460 @NotNull() 461 ASN1OctetString encodeCredentials() 462 { 463 final ByteStringBuffer buffer = new ByteStringBuffer(); 464 465 // Construct the GS2 header and follow it with the necessary delimiter. 466 buffer.append(GS2_HEADER_ELEMENT_NO_CHANNEL_BINDING); 467 buffer.append(GS2_HEADER_DELIMITER); 468 469 if (authorizationID != null) 470 { 471 buffer.append(GS2_HEADER_ELEMENT_AUTHZ_ID_PREFIX); 472 escapeAuthorizationID(authorizationID, buffer); 473 } 474 475 buffer.append(GS2_HEADER_DELIMITER); 476 buffer.append(OAUTHBEARER_DELIMITER); 477 478 479 // Append the access token. 480 buffer.append(OAUTHBEARER_CRED_ELEMENT_ACCESS_TOKEN_PREFIX); 481 buffer.append(accessToken); 482 buffer.append(OAUTHBEARER_DELIMITER); 483 484 485 // Append the server address, if appropriate. 486 if (serverAddress != null) 487 { 488 buffer.append(OAUTHBEARER_CRED_ELEMENT_SERVER_ADDRESS_PREFIX); 489 buffer.append(serverAddress); 490 buffer.append(OAUTHBEARER_DELIMITER); 491 } 492 493 494 // Append the server port, if appropriate. 495 if (serverPort != null) 496 { 497 buffer.append(OAUTHBEARER_CRED_ELEMENT_SERVER_PORT_PREFIX); 498 buffer.append(serverPort.toString()); 499 buffer.append(OAUTHBEARER_DELIMITER); 500 } 501 502 503 // Append the request method, if appropriate. 504 if (requestMethod != null) 505 { 506 buffer.append(OAUTHBEARER_CRED_ELEMENT_REQUEST_METHOD_PREFIX); 507 buffer.append(requestMethod); 508 buffer.append(OAUTHBEARER_DELIMITER); 509 } 510 511 512 // Append the request path, if appropriate. 513 if (requestPath != null) 514 { 515 buffer.append(OAUTHBEARER_CRED_ELEMENT_REQUEST_PATH_PREFIX); 516 buffer.append(requestPath); 517 buffer.append(OAUTHBEARER_DELIMITER); 518 } 519 520 521 // Append the request post data, if appropriate. 522 if (requestPostData != null) 523 { 524 buffer.append(OAUTHBEARER_CRED_ELEMENT_REQUEST_POST_DATA_PREFIX); 525 buffer.append(requestPostData); 526 buffer.append(OAUTHBEARER_DELIMITER); 527 } 528 529 530 // Append the request query string, if appropriate. 531 if (requestQueryString != null) 532 { 533 buffer.append(OAUTHBEARER_CRED_ELEMENT_REQUEST_QUERY_STRING_PREFIX); 534 buffer.append(requestQueryString); 535 buffer.append(OAUTHBEARER_DELIMITER); 536 } 537 538 return new ASN1OctetString(buffer.toByteArray()); 539 } 540 541 542 543 /** 544 * Appends an escaped version of the provided authorization ID to the given 545 * buffer. Any equal signs will be replaced with "=3D" and any commas will be 546 * replaced with "=2C". 547 * 548 * @param authorizationID The authorization ID to be escaped. 549 * @param buffer The buffer to which the escaped authorization ID 550 * should be appended. 551 */ 552 private static void escapeAuthorizationID( 553 @NotNull final String authorizationID, 554 @NotNull final ByteStringBuffer buffer) 555 { 556 final int length = authorizationID.length(); 557 for (int i=0; i < length; i++) 558 { 559 final char c = authorizationID.charAt(i); 560 switch (c) 561 { 562 case ',': 563 buffer.append("=2C"); 564 break; 565 case '=': 566 buffer.append("=3D"); 567 break; 568 default: 569 buffer.append(c); 570 break; 571 } 572 } 573 } 574 575 576 577 /** 578 * {@inheritDoc} 579 */ 580 @Override() 581 @NotNull() 582 public OAUTHBEARERBindRequest duplicate() 583 { 584 return duplicate(getControls()); 585 } 586 587 588 589 /** 590 * {@inheritDoc} 591 */ 592 @Override() 593 @NotNull() 594 public OAUTHBEARERBindRequest duplicate(@Nullable final Control[] controls) 595 { 596 final OAUTHBEARERBindRequestProperties properties = 597 new OAUTHBEARERBindRequestProperties(this); 598 final OAUTHBEARERBindRequest bindRequest = 599 new OAUTHBEARERBindRequest(properties, controls); 600 bindRequest.setResponseTimeoutMillis(getResponseTimeoutMillis(null)); 601 return bindRequest; 602 } 603 604 605 606 /** 607 * {@inheritDoc} 608 */ 609 @Override() 610 public int getLastMessageID() 611 { 612 return messageID; 613 } 614 615 616 617 /** 618 * Retrieves a string representation of the OAUTHBEARER bind request. 619 * 620 * @return A string representation of the OAUTHBEARER bind request. 621 */ 622 @Override() 623 @NotNull() 624 public String toString() 625 { 626 final StringBuilder buffer = new StringBuilder(); 627 toString(buffer); 628 return buffer.toString(); 629 } 630 631 632 633 /** 634 * Appends a string representation of the OAUTHBEARER bind request to the 635 * provided buffer. 636 * 637 * @param buffer The buffer to which the information should be appended. It 638 * must not be {@code null}. 639 */ 640 @Override() 641 public void toString(@NotNull final StringBuilder buffer) 642 { 643 buffer.append("OAUTHBEARERBindRequest(accessToken='{redacted}'"); 644 645 if (authorizationID != null) 646 { 647 buffer.append(", authorizationID='"); 648 buffer.append(authorizationID); 649 buffer.append('\''); 650 } 651 652 if (serverAddress != null) 653 { 654 buffer.append(", serverAddress='"); 655 buffer.append(serverAddress); 656 buffer.append('\''); 657 } 658 659 if (serverPort != null) 660 { 661 buffer.append(", serverPort="); 662 buffer.append(serverPort); 663 } 664 665 if (requestMethod != null) 666 { 667 buffer.append(", requestMethod='"); 668 buffer.append(requestMethod); 669 buffer.append('\''); 670 } 671 672 if (requestPath != null) 673 { 674 buffer.append(", requestPath='"); 675 buffer.append(requestPath); 676 buffer.append('\''); 677 } 678 679 if (requestPostData != null) 680 { 681 buffer.append(", requestPostData='{redacted}'"); 682 } 683 684 if (requestQueryString != null) 685 { 686 buffer.append(", requestQueryString='"); 687 buffer.append(requestQueryString); 688 buffer.append('\''); 689 } 690 691 buffer.append(')'); 692 } 693 694 695 696 /** 697 * {@inheritDoc} 698 */ 699 @Override() 700 public void toCode(@NotNull final List<String> lineList, 701 @NotNull final String requestID, 702 final int indentSpaces, final boolean includeProcessing) 703 { 704 // Create and update the request properties object. 705 ToCodeHelper.generateMethodCall(lineList, indentSpaces, 706 "OAUTHBEARERBindRequestProperties", requestID + "RequestProperties", 707 "new OAUTHBEARERBindRequestProperties", 708 ToCodeArgHelper.createString(accessToken, "Access Token")); 709 710 if (authorizationID != null) 711 { 712 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 713 requestID + "RequestProperties.setAuthorizationID", 714 ToCodeArgHelper.createString(authorizationID, null)); 715 } 716 717 if (serverAddress != null) 718 { 719 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 720 requestID + "RequestProperties.setServerAddress", 721 ToCodeArgHelper.createString(serverAddress, null)); 722 } 723 724 if (serverPort != null) 725 { 726 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 727 requestID + "RequestProperties.setServerPort", 728 ToCodeArgHelper.createInteger(serverPort, null)); 729 } 730 731 if (requestMethod != null) 732 { 733 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 734 requestID + "RequestProperties.setRequestMethod", 735 ToCodeArgHelper.createString(requestMethod, null)); 736 } 737 738 if (requestPath != null) 739 { 740 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 741 requestID + "RequestProperties.setRequestPath", 742 ToCodeArgHelper.createString(requestPath, null)); 743 } 744 745 if (requestPostData != null) 746 { 747 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 748 requestID + "RequestProperties.setRequestPostData", 749 ToCodeArgHelper.createString(requestPostData, null)); 750 } 751 752 if (requestQueryString != null) 753 { 754 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 755 requestID + "RequestProperties.setRequestQueryString", 756 ToCodeArgHelper.createString(requestQueryString, null)); 757 } 758 759 760 // Create the request variable. 761 final ArrayList<ToCodeArgHelper> constructorArgs = new ArrayList<>(2); 762 constructorArgs.add( 763 ToCodeArgHelper.createRaw(requestID + "RequestProperties", null)); 764 765 final Control[] controls = getControls(); 766 if (controls.length > 0) 767 { 768 constructorArgs.add(ToCodeArgHelper.createControlArray(controls, 769 "Bind Controls")); 770 } 771 772 ToCodeHelper.generateMethodCall(lineList, indentSpaces, 773 "OAUTHBEARERBindRequest", requestID + "Request", 774 "new OAUTHBEARERBindRequest", constructorArgs); 775 776 777 // Add lines for processing the request and obtaining the result. 778 if (includeProcessing) 779 { 780 // Generate a string with the appropriate indent. 781 final StringBuilder buffer = new StringBuilder(); 782 for (int i=0; i < indentSpaces; i++) 783 { 784 buffer.append(' '); 785 } 786 final String indent = buffer.toString(); 787 788 lineList.add(""); 789 lineList.add(indent + "try"); 790 lineList.add(indent + '{'); 791 lineList.add(indent + " BindResult " + requestID + 792 "Result = connection.bind(" + requestID + "Request);"); 793 lineList.add(indent + " // The bind was processed successfully."); 794 lineList.add(indent + '}'); 795 lineList.add(indent + "catch (LDAPException e)"); 796 lineList.add(indent + '{'); 797 lineList.add(indent + " // The bind failed. Maybe the following will " + 798 "help explain why."); 799 lineList.add(indent + " // Note that the connection is now likely in " + 800 "an unauthenticated state."); 801 lineList.add(indent + " ResultCode resultCode = e.getResultCode();"); 802 lineList.add(indent + " String message = e.getMessage();"); 803 lineList.add(indent + " String matchedDN = e.getMatchedDN();"); 804 lineList.add(indent + " String[] referralURLs = e.getReferralURLs();"); 805 lineList.add(indent + " Control[] responseControls = " + 806 "e.getResponseControls();"); 807 808 lineList.add(""); 809 lineList.add("OAUTHBEARERBindResult bindResult = " + 810 "new OAUTHBEARERBindResult(new BindResult(e));"); 811 lineList.add("String authorizationErrorCode = " + 812 "bindResult.getAuthorizationErrorCode();"); 813 lineList.add("Set<String> scopes = bindResult.getScopes();"); 814 lineList.add("String openIDConfigurationURL = " + 815 "bindResult.getOpenIDConfigurationURL();"); 816 817 lineList.add(indent + '}'); 818 } 819 } 820}