001/* 002 * Copyright 2007-2017 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2017 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.ldap.sdk; 022 023 024 025import java.util.List; 026import java.util.Timer; 027import java.util.concurrent.LinkedBlockingQueue; 028import java.util.concurrent.TimeUnit; 029 030import com.unboundid.asn1.ASN1Buffer; 031import com.unboundid.asn1.ASN1Element; 032import com.unboundid.asn1.ASN1OctetString; 033import com.unboundid.ldap.protocol.LDAPMessage; 034import com.unboundid.ldap.protocol.LDAPResponse; 035import com.unboundid.ldap.protocol.ProtocolOp; 036import com.unboundid.ldif.LDIFDeleteChangeRecord; 037import com.unboundid.util.InternalUseOnly; 038import com.unboundid.util.Mutable; 039import com.unboundid.util.ThreadSafety; 040import com.unboundid.util.ThreadSafetyLevel; 041 042import static com.unboundid.ldap.sdk.LDAPMessages.*; 043import static com.unboundid.util.Debug.*; 044import static com.unboundid.util.StaticUtils.*; 045import static com.unboundid.util.Validator.*; 046 047 048 049/** 050 * This class implements the processing necessary to perform an LDAPv3 delete 051 * operation, which removes an entry from the directory. A delete request 052 * contains the DN of the entry to remove. It may also include a set of 053 * controls to send to the server. 054 * {@code DeleteRequest} objects are mutable and therefore can be altered and 055 * re-used for multiple requests. Note, however, that {@code DeleteRequest} 056 * objects are not threadsafe and therefore a single {@code DeleteRequest} 057 * object instance should not be used to process multiple requests at the same 058 * time. 059 * <BR><BR> 060 * <H2>Example</H2> 061 * The following example demonstrates the process for performing a delete 062 * operation: 063 * <PRE> 064 * DeleteRequest deleteRequest = 065 * new DeleteRequest("cn=entry to delete,dc=example,dc=com"); 066 * LDAPResult deleteResult; 067 * try 068 * { 069 * deleteResult = connection.delete(deleteRequest); 070 * // If we get here, the delete was successful. 071 * } 072 * catch (LDAPException le) 073 * { 074 * // The delete operation failed. 075 * deleteResult = le.toLDAPResult(); 076 * ResultCode resultCode = le.getResultCode(); 077 * String errorMessageFromServer = le.getDiagnosticMessage(); 078 * } 079 * </PRE> 080 */ 081@Mutable() 082@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 083public final class DeleteRequest 084 extends UpdatableLDAPRequest 085 implements ReadOnlyDeleteRequest, ResponseAcceptor, ProtocolOp 086{ 087 /** 088 * The serial version UID for this serializable class. 089 */ 090 private static final long serialVersionUID = -6126029442850884239L; 091 092 093 094 // The message ID from the last LDAP message sent from this request. 095 private int messageID = -1; 096 097 // The queue that will be used to receive response messages from the server. 098 private final LinkedBlockingQueue<LDAPResponse> responseQueue = 099 new LinkedBlockingQueue<LDAPResponse>(); 100 101 // The DN of the entry to delete. 102 private String dn; 103 104 105 106 /** 107 * Creates a new delete request with the provided DN. 108 * 109 * @param dn The DN of the entry to delete. It must not be {@code null}. 110 */ 111 public DeleteRequest(final String dn) 112 { 113 super(null); 114 115 ensureNotNull(dn); 116 117 this.dn = dn; 118 } 119 120 121 122 /** 123 * Creates a new delete request with the provided DN. 124 * 125 * @param dn The DN of the entry to delete. It must not be 126 * {@code null}. 127 * @param controls The set of controls to include in the request. 128 */ 129 public DeleteRequest(final String dn, final Control[] controls) 130 { 131 super(controls); 132 133 ensureNotNull(dn); 134 135 this.dn = dn; 136 } 137 138 139 140 /** 141 * Creates a new delete request with the provided DN. 142 * 143 * @param dn The DN of the entry to delete. It must not be {@code null}. 144 */ 145 public DeleteRequest(final DN dn) 146 { 147 super(null); 148 149 ensureNotNull(dn); 150 151 this.dn = dn.toString(); 152 } 153 154 155 156 /** 157 * Creates a new delete request with the provided DN. 158 * 159 * @param dn The DN of the entry to delete. It must not be 160 * {@code null}. 161 * @param controls The set of controls to include in the request. 162 */ 163 public DeleteRequest(final DN dn, final Control[] controls) 164 { 165 super(controls); 166 167 ensureNotNull(dn); 168 169 this.dn = dn.toString(); 170 } 171 172 173 174 /** 175 * {@inheritDoc} 176 */ 177 public String getDN() 178 { 179 return dn; 180 } 181 182 183 184 /** 185 * Specifies the DN of the entry to delete. 186 * 187 * @param dn The DN of the entry to delete. It must not be {@code null}. 188 */ 189 public void setDN(final String dn) 190 { 191 ensureNotNull(dn); 192 193 this.dn = dn; 194 } 195 196 197 198 /** 199 * Specifies the DN of the entry to delete. 200 * 201 * @param dn The DN of the entry to delete. It must not be {@code null}. 202 */ 203 public void setDN(final DN dn) 204 { 205 ensureNotNull(dn); 206 207 this.dn = dn.toString(); 208 } 209 210 211 212 /** 213 * {@inheritDoc} 214 */ 215 public byte getProtocolOpType() 216 { 217 return LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST; 218 } 219 220 221 222 /** 223 * {@inheritDoc} 224 */ 225 public void writeTo(final ASN1Buffer buffer) 226 { 227 buffer.addOctetString(LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST, dn); 228 } 229 230 231 232 /** 233 * Encodes the delete request protocol op to an ASN.1 element. 234 * 235 * @return The ASN.1 element with the encoded delete request protocol op. 236 */ 237 public ASN1Element encodeProtocolOp() 238 { 239 return new ASN1OctetString(LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST, dn); 240 } 241 242 243 244 /** 245 * Sends this delete request to the directory server over the provided 246 * connection and returns the associated response. 247 * 248 * @param connection The connection to use to communicate with the directory 249 * server. 250 * @param depth The current referral depth for this request. It should 251 * always be one for the initial request, and should only 252 * be incremented when following referrals. 253 * 254 * @return An LDAP result object that provides information about the result 255 * of the delete processing. 256 * 257 * @throws LDAPException If a problem occurs while sending the request or 258 * reading the response. 259 */ 260 @Override() 261 protected LDAPResult process(final LDAPConnection connection, final int depth) 262 throws LDAPException 263 { 264 if (connection.synchronousMode()) 265 { 266 @SuppressWarnings("deprecation") 267 final boolean autoReconnect = 268 connection.getConnectionOptions().autoReconnect(); 269 return processSync(connection, depth, autoReconnect); 270 } 271 272 final long requestTime = System.nanoTime(); 273 processAsync(connection, null); 274 275 try 276 { 277 // Wait for and process the response. 278 final LDAPResponse response; 279 try 280 { 281 final long responseTimeout = getResponseTimeoutMillis(connection); 282 if (responseTimeout > 0) 283 { 284 response = responseQueue.poll(responseTimeout, TimeUnit.MILLISECONDS); 285 } 286 else 287 { 288 response = responseQueue.take(); 289 } 290 } 291 catch (InterruptedException ie) 292 { 293 debugException(ie); 294 Thread.currentThread().interrupt(); 295 throw new LDAPException(ResultCode.LOCAL_ERROR, 296 ERR_DELETE_INTERRUPTED.get(connection.getHostPort()), ie); 297 } 298 299 return handleResponse(connection, response, requestTime, depth, false); 300 } 301 finally 302 { 303 connection.deregisterResponseAcceptor(messageID); 304 } 305 } 306 307 308 309 /** 310 * Sends this delete request to the directory server over the provided 311 * connection and returns the message ID for the request. 312 * 313 * @param connection The connection to use to communicate with the 314 * directory server. 315 * @param resultListener The async result listener that is to be notified 316 * when the response is received. It may be 317 * {@code null} only if the result is to be processed 318 * by this class. 319 * 320 * @return The async request ID created for the operation, or {@code null} if 321 * the provided {@code resultListener} is {@code null} and the 322 * operation will not actually be processed asynchronously. 323 * 324 * @throws LDAPException If a problem occurs while sending the request. 325 */ 326 AsyncRequestID processAsync(final LDAPConnection connection, 327 final AsyncResultListener resultListener) 328 throws LDAPException 329 { 330 // Create the LDAP message. 331 messageID = connection.nextMessageID(); 332 final LDAPMessage message = new LDAPMessage(messageID, this, getControls()); 333 334 335 // If the provided async result listener is {@code null}, then we'll use 336 // this class as the message acceptor. Otherwise, create an async helper 337 // and use it as the message acceptor. 338 final AsyncRequestID asyncRequestID; 339 if (resultListener == null) 340 { 341 asyncRequestID = null; 342 connection.registerResponseAcceptor(messageID, this); 343 } 344 else 345 { 346 final AsyncHelper helper = new AsyncHelper(connection, 347 OperationType.DELETE, messageID, resultListener, 348 getIntermediateResponseListener()); 349 connection.registerResponseAcceptor(messageID, helper); 350 asyncRequestID = helper.getAsyncRequestID(); 351 352 final long timeout = getResponseTimeoutMillis(connection); 353 if (timeout > 0L) 354 { 355 final Timer timer = connection.getTimer(); 356 final AsyncTimeoutTimerTask timerTask = 357 new AsyncTimeoutTimerTask(helper); 358 timer.schedule(timerTask, timeout); 359 asyncRequestID.setTimerTask(timerTask); 360 } 361 } 362 363 364 // Send the request to the server. 365 try 366 { 367 debugLDAPRequest(this); 368 connection.getConnectionStatistics().incrementNumDeleteRequests(); 369 connection.sendMessage(message); 370 return asyncRequestID; 371 } 372 catch (LDAPException le) 373 { 374 debugException(le); 375 376 connection.deregisterResponseAcceptor(messageID); 377 throw le; 378 } 379 } 380 381 382 383 /** 384 * Processes this delete operation in synchronous mode, in which the same 385 * thread will send the request and read the response. 386 * 387 * @param connection The connection to use to communicate with the directory 388 * server. 389 * @param depth The current referral depth for this request. It should 390 * always be one for the initial request, and should only 391 * be incremented when following referrals. 392 * @param allowRetry Indicates whether the request may be re-tried on a 393 * re-established connection if the initial attempt fails 394 * in a way that indicates the connection is no longer 395 * valid and autoReconnect is true. 396 * 397 * @return An LDAP result object that provides information about the result 398 * of the delete processing. 399 * 400 * @throws LDAPException If a problem occurs while sending the request or 401 * reading the response. 402 */ 403 private LDAPResult processSync(final LDAPConnection connection, 404 final int depth, final boolean allowRetry) 405 throws LDAPException 406 { 407 // Create the LDAP message. 408 messageID = connection.nextMessageID(); 409 final LDAPMessage message = 410 new LDAPMessage(messageID, this, getControls()); 411 412 413 // Set the appropriate timeout on the socket. 414 try 415 { 416 connection.getConnectionInternals(true).getSocket().setSoTimeout( 417 (int) getResponseTimeoutMillis(connection)); 418 } 419 catch (Exception e) 420 { 421 debugException(e); 422 } 423 424 425 // Send the request to the server. 426 final long requestTime = System.nanoTime(); 427 debugLDAPRequest(this); 428 connection.getConnectionStatistics().incrementNumDeleteRequests(); 429 try 430 { 431 connection.sendMessage(message); 432 } 433 catch (final LDAPException le) 434 { 435 debugException(le); 436 437 if (allowRetry) 438 { 439 final LDAPResult retryResult = reconnectAndRetry(connection, depth, 440 le.getResultCode()); 441 if (retryResult != null) 442 { 443 return retryResult; 444 } 445 } 446 447 throw le; 448 } 449 450 while (true) 451 { 452 final LDAPResponse response; 453 try 454 { 455 response = connection.readResponse(messageID); 456 } 457 catch (final LDAPException le) 458 { 459 debugException(le); 460 461 if ((le.getResultCode() == ResultCode.TIMEOUT) && 462 connection.getConnectionOptions().abandonOnTimeout()) 463 { 464 connection.abandon(messageID); 465 } 466 467 if (allowRetry) 468 { 469 final LDAPResult retryResult = reconnectAndRetry(connection, depth, 470 le.getResultCode()); 471 if (retryResult != null) 472 { 473 return retryResult; 474 } 475 } 476 477 throw le; 478 } 479 480 if (response instanceof IntermediateResponse) 481 { 482 final IntermediateResponseListener listener = 483 getIntermediateResponseListener(); 484 if (listener != null) 485 { 486 listener.intermediateResponseReturned( 487 (IntermediateResponse) response); 488 } 489 } 490 else 491 { 492 return handleResponse(connection, response, requestTime, depth, 493 allowRetry); 494 } 495 } 496 } 497 498 499 500 /** 501 * Performs the necessary processing for handling a response. 502 * 503 * @param connection The connection used to read the response. 504 * @param response The response to be processed. 505 * @param requestTime The time the request was sent to the server. 506 * @param depth The current referral depth for this request. It 507 * should always be one for the initial request, and 508 * should only be incremented when following referrals. 509 * @param allowRetry Indicates whether the request may be re-tried on a 510 * re-established connection if the initial attempt fails 511 * in a way that indicates the connection is no longer 512 * valid and autoReconnect is true. 513 * 514 * @return The delete result. 515 * 516 * @throws LDAPException If a problem occurs. 517 */ 518 private LDAPResult handleResponse(final LDAPConnection connection, 519 final LDAPResponse response, 520 final long requestTime, final int depth, 521 final boolean allowRetry) 522 throws LDAPException 523 { 524 if (response == null) 525 { 526 final long waitTime = nanosToMillis(System.nanoTime() - requestTime); 527 if (connection.getConnectionOptions().abandonOnTimeout()) 528 { 529 connection.abandon(messageID); 530 } 531 532 throw new LDAPException(ResultCode.TIMEOUT, 533 ERR_DELETE_CLIENT_TIMEOUT.get(waitTime, messageID, dn, 534 connection.getHostPort())); 535 } 536 537 connection.getConnectionStatistics().incrementNumDeleteResponses( 538 System.nanoTime() - requestTime); 539 if (response instanceof ConnectionClosedResponse) 540 { 541 // The connection was closed while waiting for the response. 542 if (allowRetry) 543 { 544 final LDAPResult retryResult = reconnectAndRetry(connection, depth, 545 ResultCode.SERVER_DOWN); 546 if (retryResult != null) 547 { 548 return retryResult; 549 } 550 } 551 552 final ConnectionClosedResponse ccr = (ConnectionClosedResponse) response; 553 final String message = ccr.getMessage(); 554 if (message == null) 555 { 556 throw new LDAPException(ccr.getResultCode(), 557 ERR_CONN_CLOSED_WAITING_FOR_DELETE_RESPONSE.get( 558 connection.getHostPort(), toString())); 559 } 560 else 561 { 562 throw new LDAPException(ccr.getResultCode(), 563 ERR_CONN_CLOSED_WAITING_FOR_DELETE_RESPONSE_WITH_MESSAGE.get( 564 connection.getHostPort(), toString(), message)); 565 } 566 } 567 568 final LDAPResult result = (LDAPResult) response; 569 if ((result.getResultCode().equals(ResultCode.REFERRAL)) && 570 followReferrals(connection)) 571 { 572 if (depth >= connection.getConnectionOptions().getReferralHopLimit()) 573 { 574 return new LDAPResult(messageID, ResultCode.REFERRAL_LIMIT_EXCEEDED, 575 ERR_TOO_MANY_REFERRALS.get(), 576 result.getMatchedDN(), result.getReferralURLs(), 577 result.getResponseControls()); 578 } 579 580 return followReferral(result, connection, depth); 581 } 582 else 583 { 584 if (allowRetry) 585 { 586 final LDAPResult retryResult = reconnectAndRetry(connection, depth, 587 result.getResultCode()); 588 if (retryResult != null) 589 { 590 return retryResult; 591 } 592 } 593 594 return result; 595 } 596 } 597 598 599 600 /** 601 * Attempts to re-establish the connection and retry processing this request 602 * on it. 603 * 604 * @param connection The connection to be re-established. 605 * @param depth The current referral depth for this request. It should 606 * always be one for the initial request, and should only 607 * be incremented when following referrals. 608 * @param resultCode The result code for the previous operation attempt. 609 * 610 * @return The result from re-trying the add, or {@code null} if it could not 611 * be re-tried. 612 */ 613 private LDAPResult reconnectAndRetry(final LDAPConnection connection, 614 final int depth, 615 final ResultCode resultCode) 616 { 617 try 618 { 619 // We will only want to retry for certain result codes that indicate a 620 // connection problem. 621 switch (resultCode.intValue()) 622 { 623 case ResultCode.SERVER_DOWN_INT_VALUE: 624 case ResultCode.DECODING_ERROR_INT_VALUE: 625 case ResultCode.CONNECT_ERROR_INT_VALUE: 626 connection.reconnect(); 627 return processSync(connection, depth, false); 628 } 629 } 630 catch (final Exception e) 631 { 632 debugException(e); 633 } 634 635 return null; 636 } 637 638 639 640 /** 641 * Attempts to follow a referral to perform a delete operation in the target 642 * server. 643 * 644 * @param referralResult The LDAP result object containing information about 645 * the referral to follow. 646 * @param connection The connection on which the referral was received. 647 * @param depth The number of referrals followed in the course of 648 * processing this request. 649 * 650 * @return The result of attempting to process the delete operation by 651 * following the referral. 652 * 653 * @throws LDAPException If a problem occurs while attempting to establish 654 * the referral connection, sending the request, or 655 * reading the result. 656 */ 657 private LDAPResult followReferral(final LDAPResult referralResult, 658 final LDAPConnection connection, 659 final int depth) 660 throws LDAPException 661 { 662 for (final String urlString : referralResult.getReferralURLs()) 663 { 664 try 665 { 666 final LDAPURL referralURL = new LDAPURL(urlString); 667 final String host = referralURL.getHost(); 668 669 if (host == null) 670 { 671 // We can't handle a referral in which there is no host. 672 continue; 673 } 674 675 final DeleteRequest deleteRequest; 676 if (referralURL.baseDNProvided()) 677 { 678 deleteRequest = new DeleteRequest(referralURL.getBaseDN(), 679 getControls()); 680 } 681 else 682 { 683 deleteRequest = this; 684 } 685 686 final LDAPConnection referralConn = connection.getReferralConnector(). 687 getReferralConnection(referralURL, connection); 688 try 689 { 690 return deleteRequest.process(referralConn, depth+1); 691 } 692 finally 693 { 694 referralConn.setDisconnectInfo(DisconnectType.REFERRAL, null, null); 695 referralConn.close(); 696 } 697 } 698 catch (LDAPException le) 699 { 700 debugException(le); 701 } 702 } 703 704 // If we've gotten here, then we could not follow any of the referral URLs, 705 // so we'll just return the original referral result. 706 return referralResult; 707 } 708 709 710 711 /** 712 * {@inheritDoc} 713 */ 714 @InternalUseOnly() 715 public void responseReceived(final LDAPResponse response) 716 throws LDAPException 717 { 718 try 719 { 720 responseQueue.put(response); 721 } 722 catch (Exception e) 723 { 724 debugException(e); 725 726 if (e instanceof InterruptedException) 727 { 728 Thread.currentThread().interrupt(); 729 } 730 731 throw new LDAPException(ResultCode.LOCAL_ERROR, 732 ERR_EXCEPTION_HANDLING_RESPONSE.get(getExceptionMessage(e)), e); 733 } 734 } 735 736 737 738 /** 739 * {@inheritDoc} 740 */ 741 @Override() 742 public int getLastMessageID() 743 { 744 return messageID; 745 } 746 747 748 749 /** 750 * {@inheritDoc} 751 */ 752 @Override() 753 public OperationType getOperationType() 754 { 755 return OperationType.DELETE; 756 } 757 758 759 760 /** 761 * {@inheritDoc} 762 */ 763 public DeleteRequest duplicate() 764 { 765 return duplicate(getControls()); 766 } 767 768 769 770 /** 771 * {@inheritDoc} 772 */ 773 public DeleteRequest duplicate(final Control[] controls) 774 { 775 final DeleteRequest r = new DeleteRequest(dn, controls); 776 777 if (followReferralsInternal() != null) 778 { 779 r.setFollowReferrals(followReferralsInternal()); 780 } 781 782 r.setResponseTimeoutMillis(getResponseTimeoutMillis(null)); 783 784 return r; 785 } 786 787 788 789 /** 790 * {@inheritDoc} 791 */ 792 public LDIFDeleteChangeRecord toLDIFChangeRecord() 793 { 794 return new LDIFDeleteChangeRecord(this); 795 } 796 797 798 799 /** 800 * {@inheritDoc} 801 */ 802 public String[] toLDIF() 803 { 804 return toLDIFChangeRecord().toLDIF(); 805 } 806 807 808 809 /** 810 * {@inheritDoc} 811 */ 812 public String toLDIFString() 813 { 814 return toLDIFChangeRecord().toLDIFString(); 815 } 816 817 818 819 /** 820 * {@inheritDoc} 821 */ 822 @Override() 823 public void toString(final StringBuilder buffer) 824 { 825 buffer.append("DeleteRequest(dn='"); 826 buffer.append(dn); 827 buffer.append('\''); 828 829 final Control[] controls = getControls(); 830 if (controls.length > 0) 831 { 832 buffer.append(", controls={"); 833 for (int i=0; i < controls.length; i++) 834 { 835 if (i > 0) 836 { 837 buffer.append(", "); 838 } 839 840 buffer.append(controls[i]); 841 } 842 buffer.append('}'); 843 } 844 845 buffer.append(')'); 846 } 847 848 849 850 /** 851 * {@inheritDoc} 852 */ 853 public void toCode(final List<String> lineList, final String requestID, 854 final int indentSpaces, final boolean includeProcessing) 855 { 856 // Create the request variable. 857 ToCodeHelper.generateMethodCall(lineList, indentSpaces, "DeleteRequest", 858 requestID + "Request", "new DeleteRequest", 859 ToCodeArgHelper.createString(dn, "Entry DN")); 860 861 // If there are any controls, then add them to the request. 862 for (final Control c : getControls()) 863 { 864 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 865 requestID + "Request.addControl", 866 ToCodeArgHelper.createControl(c, null)); 867 } 868 869 870 // Add lines for processing the request and obtaining the result. 871 if (includeProcessing) 872 { 873 // Generate a string with the appropriate indent. 874 final StringBuilder buffer = new StringBuilder(); 875 for (int i=0; i < indentSpaces; i++) 876 { 877 buffer.append(' '); 878 } 879 final String indent = buffer.toString(); 880 881 lineList.add(""); 882 lineList.add(indent + "try"); 883 lineList.add(indent + '{'); 884 lineList.add(indent + " LDAPResult " + requestID + 885 "Result = connection.delete(" + requestID + "Request);"); 886 lineList.add(indent + " // The delete was processed successfully."); 887 lineList.add(indent + '}'); 888 lineList.add(indent + "catch (LDAPException e)"); 889 lineList.add(indent + '{'); 890 lineList.add(indent + " // The delete failed. Maybe the following " + 891 "will help explain why."); 892 lineList.add(indent + " ResultCode resultCode = e.getResultCode();"); 893 lineList.add(indent + " String message = e.getMessage();"); 894 lineList.add(indent + " String matchedDN = e.getMatchedDN();"); 895 lineList.add(indent + " String[] referralURLs = e.getReferralURLs();"); 896 lineList.add(indent + " Control[] responseControls = " + 897 "e.getResponseControls();"); 898 lineList.add(indent + '}'); 899 } 900 } 901}