001/* 002 * Copyright 2007-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2007-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) 2007-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.ldif; 037 038 039 040import java.io.Closeable; 041import java.io.File; 042import java.io.IOException; 043import java.io.OutputStream; 044import java.io.FileOutputStream; 045import java.io.BufferedOutputStream; 046import java.util.List; 047import java.util.ArrayList; 048import java.util.Arrays; 049 050import com.unboundid.asn1.ASN1OctetString; 051import com.unboundid.ldap.sdk.Entry; 052import com.unboundid.util.Base64; 053import com.unboundid.util.ByteStringBuffer; 054import com.unboundid.util.Debug; 055import com.unboundid.util.LDAPSDKThreadFactory; 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; 062import com.unboundid.util.parallel.ParallelProcessor; 063import com.unboundid.util.parallel.Result; 064import com.unboundid.util.parallel.Processor; 065 066import static com.unboundid.ldif.LDIFMessages.*; 067 068 069 070/** 071 * This class provides an LDIF writer, which can be used to write entries and 072 * change records in the LDAP Data Interchange Format as per 073 * <A HREF="http://www.ietf.org/rfc/rfc2849.txt">RFC 2849</A>. 074 * <BR><BR> 075 * <H2>Example</H2> 076 * The following example performs a search to find all users in the "Sales" 077 * department and then writes their entries to an LDIF file: 078 * <PRE> 079 * // Perform a search to find all users who are members of the sales 080 * // department. 081 * SearchRequest searchRequest = new SearchRequest("dc=example,dc=com", 082 * SearchScope.SUB, Filter.createEqualityFilter("ou", "Sales")); 083 * SearchResult searchResult; 084 * try 085 * { 086 * searchResult = connection.search(searchRequest); 087 * } 088 * catch (LDAPSearchException lse) 089 * { 090 * searchResult = lse.getSearchResult(); 091 * } 092 * LDAPTestUtils.assertResultCodeEquals(searchResult, ResultCode.SUCCESS); 093 * 094 * // Write all of the matching entries to LDIF. 095 * int entriesWritten = 0; 096 * LDIFWriter ldifWriter = new LDIFWriter(pathToLDIF); 097 * for (SearchResultEntry entry : searchResult.getSearchEntries()) 098 * { 099 * ldifWriter.writeEntry(entry); 100 * entriesWritten++; 101 * } 102 * 103 * ldifWriter.close(); 104 * </PRE> 105 */ 106@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 107public final class LDIFWriter 108 implements Closeable 109{ 110 /** 111 * Indicates whether LDIF records should include a comment above each 112 * base64-encoded value that attempts to provide an unencoded representation 113 * of that value (with special characters escaped). 114 */ 115 private static volatile boolean commentAboutBase64EncodedValues = false; 116 117 118 119 /** 120 * The name of a system property that can be used to specify the default 121 * base64-encoding strategy that should be used for values. If this property 122 * is defined, the value should be one of "DEFAULT", "MINIMAL, or "MAXIMAL"; 123 */ 124 @NotNull private static final String PROPERTY_BASE64_ENCODING_STRATEGY = 125 "com.unboundid.ldif.base64EncodingStrategy"; 126 127 128 129 /** 130 * The strategy that should be used for determining whether values need to be 131 * base64-encoded. 132 */ 133 @NotNull private static volatile Base64EncodingStrategy 134 base64EncodingStrategy = Base64EncodingStrategy.DEFAULT; 135 static 136 { 137 final String propertyValue = 138 StaticUtils.getSystemProperty(PROPERTY_BASE64_ENCODING_STRATEGY); 139 if (propertyValue != null) 140 { 141 switch (StaticUtils.toUpperCase(propertyValue).replace('-', '_')) 142 { 143 case "MINIMAL": 144 case "MINIMAL_COMPLIANT": 145 base64EncodingStrategy = Base64EncodingStrategy.MINIMAL_COMPLIANT; 146 break; 147 case "USER_FRIENDLY": 148 case "USER_FRIENDLY_NON_COMPLIANT": 149 base64EncodingStrategy = 150 Base64EncodingStrategy.USER_FRIENDLY_NON_COMPLIANT; 151 break; 152 case "MAXIMAL": 153 base64EncodingStrategy = Base64EncodingStrategy.MAXIMAL; 154 break; 155 case "DEFAULT": 156 default: 157 base64EncodingStrategy = Base64EncodingStrategy.DEFAULT; 158 break; 159 } 160 } 161 } 162 163 164 165 /** 166 * The bytes that comprise the LDIF version header. 167 */ 168 @NotNull private static final byte[] VERSION_1_HEADER_BYTES = 169 StaticUtils.getBytes("version: 1" + StaticUtils.EOL); 170 171 172 173 /** 174 * The default buffer size (128KB) that will be used when writing LDIF data 175 * to the appropriate destination. 176 */ 177 private static final int DEFAULT_BUFFER_SIZE = 128 * 1024; 178 179 180 181 // The writer that will be used to actually write the data. 182 @NotNull private final BufferedOutputStream writer; 183 184 // The byte string buffer that will be used to convert LDIF records to LDIF. 185 // It will only be used when operating synchronously. 186 @NotNull private final ByteStringBuffer buffer; 187 188 // The translator to use for change records to be written, if any. 189 @Nullable private final LDIFWriterChangeRecordTranslator 190 changeRecordTranslator; 191 192 // The translator to use for entries to be written, if any. 193 @Nullable private final LDIFWriterEntryTranslator entryTranslator; 194 195 // The column at which to wrap long lines. 196 private int wrapColumn = 0; 197 198 // A pre-computed value that is two less than the wrap column. 199 private int wrapColumnMinusTwo = -2; 200 201 // non-null if this writer was configured to use multiple threads when 202 // writing batches of entries. 203 @Nullable private final ParallelProcessor<LDIFRecord,ByteStringBuffer> 204 toLdifBytesInvoker; 205 206 207 208 /** 209 * Creates a new LDIF writer that will write entries to the provided file. 210 * 211 * @param path The path to the LDIF file to be written. It must not be 212 * {@code null}. 213 * 214 * @throws IOException If a problem occurs while opening the provided file 215 * for writing. 216 */ 217 public LDIFWriter(@NotNull final String path) 218 throws IOException 219 { 220 this(new FileOutputStream(path)); 221 } 222 223 224 225 /** 226 * Creates a new LDIF writer that will write entries to the provided file. 227 * 228 * @param file The LDIF file to be written. It must not be {@code null}. 229 * 230 * @throws IOException If a problem occurs while opening the provided file 231 * for writing. 232 */ 233 public LDIFWriter(@NotNull final File file) 234 throws IOException 235 { 236 this(new FileOutputStream(file)); 237 } 238 239 240 241 /** 242 * Creates a new LDIF writer that will write entries to the provided output 243 * stream. 244 * 245 * @param outputStream The output stream to which the data is to be written. 246 * It must not be {@code null}. 247 */ 248 public LDIFWriter(@NotNull final OutputStream outputStream) 249 { 250 this(outputStream, 0); 251 } 252 253 254 255 /** 256 * Creates a new LDIF writer that will write entries to the provided output 257 * stream optionally using parallelThreads when writing batches of LDIF 258 * records. 259 * 260 * @param outputStream The output stream to which the data is to be 261 * written. It must not be {@code null}. 262 * @param parallelThreads If this value is greater than zero, then the 263 * specified number of threads will be used to 264 * encode entries before writing them to the output 265 * for the {@code writeLDIFRecords(List)} method. 266 * Note this is the only output method that will 267 * use multiple threads. 268 * This should only be set to greater than zero when 269 * performance analysis has demonstrated that writing 270 * the LDIF is a bottleneck. The default 271 * synchronous processing is normally fast enough. 272 * There is no benefit in passing in a value 273 * greater than the number of processors in the 274 * system. A value of zero implies the 275 * default behavior of reading and parsing LDIF 276 * records synchronously when one of the read 277 * methods is called. 278 */ 279 public LDIFWriter(@NotNull final OutputStream outputStream, 280 final int parallelThreads) 281 { 282 this(outputStream, parallelThreads, null); 283 } 284 285 286 287 /** 288 * Creates a new LDIF writer that will write entries to the provided output 289 * stream optionally using parallelThreads when writing batches of LDIF 290 * records. 291 * 292 * @param outputStream The output stream to which the data is to be 293 * written. It must not be {@code null}. 294 * @param parallelThreads If this value is greater than zero, then the 295 * specified number of threads will be used to 296 * encode entries before writing them to the output 297 * for the {@code writeLDIFRecords(List)} method. 298 * Note this is the only output method that will 299 * use multiple threads. 300 * This should only be set to greater than zero when 301 * performance analysis has demonstrated that writing 302 * the LDIF is a bottleneck. The default 303 * synchronous processing is normally fast enough. 304 * There is no benefit in passing in a value 305 * greater than the number of processors in the 306 * system. A value of zero implies the 307 * default behavior of reading and parsing LDIF 308 * records synchronously when one of the read 309 * methods is called. 310 * @param entryTranslator An optional translator that will be used to alter 311 * entries before they are actually written. This 312 * may be {@code null} if no translator is needed. 313 */ 314 public LDIFWriter(@NotNull final OutputStream outputStream, 315 final int parallelThreads, 316 @Nullable final LDIFWriterEntryTranslator entryTranslator) 317 { 318 this(outputStream, parallelThreads, entryTranslator, null); 319 } 320 321 322 323 /** 324 * Creates a new LDIF writer that will write entries to the provided output 325 * stream optionally using parallelThreads when writing batches of LDIF 326 * records. 327 * 328 * @param outputStream The output stream to which the data is to 329 * be written. It must not be {@code null}. 330 * @param parallelThreads If this value is greater than zero, then 331 * the specified number of threads will be 332 * used to encode entries before writing them 333 * to the output for the 334 * {@code writeLDIFRecords(List)} method. 335 * Note this is the only output method that 336 * will use multiple threads. This should 337 * only be set to greater than zero when 338 * performance analysis has demonstrated that 339 * writing the LDIF is a bottleneck. The 340 * default synchronous processing is normally 341 * fast enough. There is no benefit in 342 * passing in a value greater than the number 343 * of processors in the system. A value of 344 * zero implies the default behavior of 345 * reading and parsing LDIF records 346 * synchronously when one of the read methods 347 * is called. 348 * @param entryTranslator An optional translator that will be used to 349 * alter entries before they are actually 350 * written. This may be {@code null} if no 351 * translator is needed. 352 * @param changeRecordTranslator An optional translator that will be used to 353 * alter change records before they are 354 * actually written. This may be {@code null} 355 * if no translator is needed. 356 */ 357 public LDIFWriter(@NotNull final OutputStream outputStream, 358 final int parallelThreads, 359 @Nullable final LDIFWriterEntryTranslator entryTranslator, 360 @Nullable final LDIFWriterChangeRecordTranslator changeRecordTranslator) 361 { 362 Validator.ensureNotNull(outputStream); 363 Validator.ensureTrue(parallelThreads >= 0, 364 "LDIFWriter.parallelThreads must not be negative."); 365 366 this.entryTranslator = entryTranslator; 367 this.changeRecordTranslator = changeRecordTranslator; 368 buffer = new ByteStringBuffer(); 369 370 if (outputStream instanceof BufferedOutputStream) 371 { 372 writer = (BufferedOutputStream) outputStream; 373 } 374 else 375 { 376 writer = new BufferedOutputStream(outputStream, DEFAULT_BUFFER_SIZE); 377 } 378 379 if (parallelThreads == 0) 380 { 381 toLdifBytesInvoker = null; 382 } 383 else 384 { 385 final LDAPSDKThreadFactory threadFactory = 386 new LDAPSDKThreadFactory("LDIFWriter Worker", true, null); 387 toLdifBytesInvoker = new ParallelProcessor<>( 388 new Processor<LDIFRecord,ByteStringBuffer>() { 389 @Override() 390 @NotNull() 391 public ByteStringBuffer process(@NotNull final LDIFRecord input) 392 throws IOException 393 { 394 final LDIFRecord r; 395 if ((entryTranslator != null) && (input instanceof Entry)) 396 { 397 r = entryTranslator.translateEntryToWrite((Entry) input); 398 if (r == null) 399 { 400 return null; 401 } 402 } 403 else if ((changeRecordTranslator != null) && 404 (input instanceof LDIFChangeRecord)) 405 { 406 r = changeRecordTranslator.translateChangeRecordToWrite( 407 (LDIFChangeRecord) input); 408 if (r == null) 409 { 410 return null; 411 } 412 } 413 else 414 { 415 r = input; 416 } 417 418 final ByteStringBuffer b = new ByteStringBuffer(200); 419 r.toLDIF(b, wrapColumn); 420 return b; 421 } 422 }, threadFactory, parallelThreads, 5); 423 } 424 } 425 426 427 428 /** 429 * Flushes the output stream used by this LDIF writer to ensure any buffered 430 * data is written out. 431 * 432 * @throws IOException If a problem occurs while attempting to flush the 433 * output stream. 434 */ 435 public void flush() 436 throws IOException 437 { 438 writer.flush(); 439 } 440 441 442 443 /** 444 * Closes this LDIF writer and the underlying LDIF target. 445 * 446 * @throws IOException If a problem occurs while closing the underlying LDIF 447 * target. 448 */ 449 @Override() 450 public void close() 451 throws IOException 452 { 453 try 454 { 455 if (toLdifBytesInvoker != null) 456 { 457 try 458 { 459 toLdifBytesInvoker.shutdown(); 460 } 461 catch (final InterruptedException e) 462 { 463 Debug.debugException(e); 464 Thread.currentThread().interrupt(); 465 } 466 } 467 } 468 finally 469 { 470 writer.close(); 471 } 472 } 473 474 475 476 /** 477 * Retrieves the column at which to wrap long lines. 478 * 479 * @return The column at which to wrap long lines, or zero to indicate that 480 * long lines should not be wrapped. 481 */ 482 public int getWrapColumn() 483 { 484 return wrapColumn; 485 } 486 487 488 489 /** 490 * Specifies the column at which to wrap long lines. A value of zero 491 * indicates that long lines should not be wrapped. 492 * 493 * @param wrapColumn The column at which to wrap long lines. 494 */ 495 public void setWrapColumn(final int wrapColumn) 496 { 497 this.wrapColumn = wrapColumn; 498 499 wrapColumnMinusTwo = wrapColumn - 2; 500 } 501 502 503 504 /** 505 * Indicates whether the LDIF writer should generate comments that attempt to 506 * provide unencoded representations (with special characters escaped) of any 507 * base64-encoded values in entries and change records that are written by 508 * this writer. 509 * 510 * @return {@code true} if the LDIF writer should generate comments that 511 * attempt to provide unencoded representations of any base64-encoded 512 * values, or {@code false} if not. 513 */ 514 public static boolean commentAboutBase64EncodedValues() 515 { 516 return commentAboutBase64EncodedValues; 517 } 518 519 520 521 /** 522 * Specifies whether the LDIF writer should generate comments that attempt to 523 * provide unencoded representations (with special characters escaped) of any 524 * base64-encoded values in entries and change records that are written by 525 * this writer. 526 * 527 * @param commentAboutBase64EncodedValues Indicates whether the LDIF writer 528 * should generate comments that 529 * attempt to provide unencoded 530 * representations (with special 531 * characters escaped) of any 532 * base64-encoded values in entries 533 * and change records that are 534 * written by this writer. 535 */ 536 public static void setCommentAboutBase64EncodedValues( 537 final boolean commentAboutBase64EncodedValues) 538 { 539 LDIFWriter.commentAboutBase64EncodedValues = 540 commentAboutBase64EncodedValues; 541 } 542 543 544 545 /** 546 * Retrieves the strategy that the LDIF writer should use for determining 547 * whether values need to be base64-encoded. 548 * 549 * @return The strategy that the LDIF writer should use for determining 550 * whether values need to be base64-encoded. 551 */ 552 @NotNull() 553 public static Base64EncodingStrategy getBase64EncodingStrategy() 554 { 555 return base64EncodingStrategy; 556 } 557 558 559 560 /** 561 * Specifies the strategy that the LDIF writer should use for determining 562 * whether values need to be base64-encoded. 563 * 564 * @param base64EncodingStrategy The strategy that the LDIF writer should 565 * use for determining whether values need to 566 * be base64-encoded. 567 */ 568 public static void setBase64EncodingStrategy( 569 @NotNull final Base64EncodingStrategy base64EncodingStrategy) 570 { 571 Validator.ensureNotNull(base64EncodingStrategy); 572 LDIFWriter.base64EncodingStrategy = base64EncodingStrategy; 573 } 574 575 576 577 /** 578 * Writes the LDIF version header (i.e.,"version: 1"). If a version header 579 * is to be added to the LDIF content, it should be done before any entries or 580 * change records have been written. 581 * 582 * @throws IOException If a problem occurs while writing the version header. 583 */ 584 public void writeVersionHeader() 585 throws IOException 586 { 587 writer.write(VERSION_1_HEADER_BYTES); 588 } 589 590 591 592 /** 593 * Writes the provided entry in LDIF form. 594 * 595 * @param entry The entry to be written. It must not be {@code null}. 596 * 597 * @throws IOException If a problem occurs while writing the LDIF data. 598 */ 599 public void writeEntry(@NotNull final Entry entry) 600 throws IOException 601 { 602 writeEntry(entry, null); 603 } 604 605 606 607 /** 608 * Writes the provided entry in LDIF form, preceded by the provided comment. 609 * 610 * @param entry The entry to be written in LDIF form. It must not be 611 * {@code null}. 612 * @param comment The comment to be written before the entry. It may be 613 * {@code null} if no comment is to be written. 614 * 615 * @throws IOException If a problem occurs while writing the LDIF data. 616 */ 617 public void writeEntry(@NotNull final Entry entry, 618 @Nullable final String comment) 619 throws IOException 620 { 621 Validator.ensureNotNull(entry); 622 623 final Entry e; 624 if (entryTranslator == null) 625 { 626 e = entry; 627 } 628 else 629 { 630 e = entryTranslator.translateEntryToWrite(entry); 631 if (e == null) 632 { 633 return; 634 } 635 } 636 637 if (comment != null) 638 { 639 writeComment(comment, false, false); 640 } 641 642 Debug.debugLDIFWrite(e); 643 writeLDIF(e); 644 } 645 646 647 648 /** 649 * Writes the provided change record in LDIF form. 650 * 651 * @param changeRecord The change record to be written. It must not be 652 * {@code null}. 653 * 654 * @throws IOException If a problem occurs while writing the LDIF data. 655 */ 656 public void writeChangeRecord(@NotNull final LDIFChangeRecord changeRecord) 657 throws IOException 658 { 659 writeChangeRecord(changeRecord, null); 660 } 661 662 663 664 /** 665 * Writes the provided change record in LDIF form, preceded by the provided 666 * comment. 667 * 668 * @param changeRecord The change record to be written. It must not be 669 * {@code null}. 670 * @param comment The comment to be written before the entry. It may 671 * be {@code null} if no comment is to be written. 672 * 673 * @throws IOException If a problem occurs while writing the LDIF data. 674 */ 675 public void writeChangeRecord(@NotNull final LDIFChangeRecord changeRecord, 676 @Nullable final String comment) 677 throws IOException 678 { 679 Validator.ensureNotNull(changeRecord); 680 681 final LDIFChangeRecord r; 682 if (changeRecordTranslator == null) 683 { 684 r = changeRecord; 685 } 686 else 687 { 688 r = changeRecordTranslator.translateChangeRecordToWrite(changeRecord); 689 if (r == null) 690 { 691 return; 692 } 693 } 694 695 if (comment != null) 696 { 697 writeComment(comment, false, false); 698 } 699 700 Debug.debugLDIFWrite(r); 701 writeLDIF(r); 702 } 703 704 705 706 /** 707 * Writes the provided record in LDIF form. 708 * 709 * @param record The LDIF record to be written. It must not be 710 * {@code null}. 711 * 712 * @throws IOException If a problem occurs while writing the LDIF data. 713 */ 714 public void writeLDIFRecord(@NotNull final LDIFRecord record) 715 throws IOException 716 { 717 writeLDIFRecord(record, null); 718 } 719 720 721 722 /** 723 * Writes the provided record in LDIF form, preceded by the provided comment. 724 * 725 * @param record The LDIF record to be written. It must not be 726 * {@code null}. 727 * @param comment The comment to be written before the LDIF record. It may 728 * be {@code null} if no comment is to be written. 729 * 730 * @throws IOException If a problem occurs while writing the LDIF data. 731 */ 732 public void writeLDIFRecord(@NotNull final LDIFRecord record, 733 @Nullable final String comment) 734 throws IOException 735 { 736 737 Validator.ensureNotNull(record); 738 final LDIFRecord r; 739 if ((entryTranslator != null) && (record instanceof Entry)) 740 { 741 r = entryTranslator.translateEntryToWrite((Entry) record); 742 if (r == null) 743 { 744 return; 745 } 746 } 747 else if ((changeRecordTranslator != null) && 748 (record instanceof LDIFChangeRecord)) 749 { 750 r = changeRecordTranslator.translateChangeRecordToWrite( 751 (LDIFChangeRecord) record); 752 if (r == null) 753 { 754 return; 755 } 756 } 757 else 758 { 759 r = record; 760 } 761 762 Debug.debugLDIFWrite(r); 763 if (comment != null) 764 { 765 writeComment(comment, false, false); 766 } 767 768 writeLDIF(r); 769 } 770 771 772 773 /** 774 * Writes the provided list of LDIF records (most likely Entries) to the 775 * output. If this LDIFWriter was constructed without any parallel 776 * output threads, then this behaves identically to calling 777 * {@code writeLDIFRecord()} sequentially for each item in the list. 778 * If this LDIFWriter was constructed to write records in parallel, then 779 * the configured number of threads are used to convert the records to raw 780 * bytes, which are sequentially written to the input file. This can speed up 781 * the total time to write a large set of records. Either way, the output 782 * records are guaranteed to be written in the order they appear in the list. 783 * 784 * @param ldifRecords The LDIF records (most likely entries) to write to the 785 * output. 786 * 787 * @throws IOException If a problem occurs while writing the LDIF data. 788 * 789 * @throws InterruptedException If this thread is interrupted while waiting 790 * for the records to be written to the output. 791 */ 792 public void writeLDIFRecords( 793 @NotNull final List<? extends LDIFRecord> ldifRecords) 794 throws IOException, InterruptedException 795 { 796 if (toLdifBytesInvoker == null) 797 { 798 for (final LDIFRecord ldifRecord : ldifRecords) 799 { 800 writeLDIFRecord(ldifRecord); 801 } 802 } 803 else 804 { 805 final List<Result<LDIFRecord,ByteStringBuffer>> results = 806 toLdifBytesInvoker.processAll(ldifRecords); 807 for (final Result<LDIFRecord,ByteStringBuffer> result: results) 808 { 809 rethrow(result.getFailureCause()); 810 811 final ByteStringBuffer encodedBytes = result.getOutput(); 812 if (encodedBytes != null) 813 { 814 encodedBytes.write(writer); 815 writer.write(StaticUtils.EOL_BYTES); 816 } 817 } 818 } 819 } 820 821 822 823 /** 824 * Writes the provided comment to the LDIF target, wrapping long lines as 825 * necessary. 826 * 827 * @param comment The comment to be written to the LDIF target. It must 828 * not be {@code null}. 829 * @param spaceBefore Indicates whether to insert a blank line before the 830 * comment. 831 * @param spaceAfter Indicates whether to insert a blank line after the 832 * comment. 833 * 834 * @throws IOException If a problem occurs while writing the LDIF data. 835 */ 836 public void writeComment(@NotNull final String comment, 837 final boolean spaceBefore, final boolean spaceAfter) 838 throws IOException 839 { 840 Validator.ensureNotNull(comment); 841 if (spaceBefore) 842 { 843 writer.write(StaticUtils.EOL_BYTES); 844 } 845 846 // 847 // Check for a newline explicitly to avoid the overhead of the regex 848 // for the common case of a single-line comment. 849 // 850 851 if (comment.indexOf('\n') < 0) 852 { 853 writeSingleLineComment(comment); 854 } 855 else 856 { 857 // 858 // Split on blank lines and wrap each line individually. 859 // 860 861 final String[] lines = comment.split("\\r?\\n"); 862 for (final String line: lines) 863 { 864 writeSingleLineComment(line); 865 } 866 } 867 868 if (spaceAfter) 869 { 870 writer.write(StaticUtils.EOL_BYTES); 871 } 872 } 873 874 875 876 /** 877 * Writes the provided comment to the LDIF target, wrapping long lines as 878 * necessary. 879 * 880 * @param comment The comment to be written to the LDIF target. It must 881 * not be {@code null}, and it must not include any line 882 * breaks. 883 * 884 * @throws IOException If a problem occurs while writing the LDIF data. 885 */ 886 private void writeSingleLineComment(@NotNull final String comment) 887 throws IOException 888 { 889 // We will always wrap comments, even if we won't wrap LDIF entries. If 890 // there is a wrap column set, then use it. Otherwise use the terminal 891 // width and back off two characters for the "# " at the beginning. 892 final int commentWrapMinusTwo; 893 if (wrapColumn <= 0) 894 { 895 commentWrapMinusTwo = StaticUtils.TERMINAL_WIDTH_COLUMNS - 3; 896 } 897 else 898 { 899 commentWrapMinusTwo = wrapColumnMinusTwo; 900 } 901 902 buffer.clear(); 903 final int length = comment.length(); 904 if (length <= commentWrapMinusTwo) 905 { 906 buffer.append("# "); 907 buffer.append(comment); 908 buffer.append(StaticUtils.EOL_BYTES); 909 } 910 else 911 { 912 int minPos = 0; 913 while (minPos < length) 914 { 915 if ((length - minPos) <= commentWrapMinusTwo) 916 { 917 buffer.append("# "); 918 buffer.append(comment.substring(minPos)); 919 buffer.append(StaticUtils.EOL_BYTES); 920 break; 921 } 922 923 // First, adjust the position until we find a space. Go backwards if 924 // possible, but if we can't find one there then go forward. 925 boolean spaceFound = false; 926 final int pos = minPos + commentWrapMinusTwo; 927 int spacePos = pos; 928 while (spacePos > minPos) 929 { 930 if (comment.charAt(spacePos) == ' ') 931 { 932 spaceFound = true; 933 break; 934 } 935 936 spacePos--; 937 } 938 939 if (! spaceFound) 940 { 941 spacePos = pos + 1; 942 while (spacePos < length) 943 { 944 if (comment.charAt(spacePos) == ' ') 945 { 946 spaceFound = true; 947 break; 948 } 949 950 spacePos++; 951 } 952 953 if (! spaceFound) 954 { 955 // There are no spaces at all in the remainder of the comment, so 956 // we'll just write the remainder of it all at once. 957 buffer.append("# "); 958 buffer.append(comment.substring(minPos)); 959 buffer.append(StaticUtils.EOL_BYTES); 960 break; 961 } 962 } 963 964 // We have a space, so we'll write up to the space position and then 965 // start up after the next space. 966 buffer.append("# "); 967 buffer.append(comment.substring(minPos, spacePos)); 968 buffer.append(StaticUtils.EOL_BYTES); 969 970 minPos = spacePos + 1; 971 while ((minPos < length) && (comment.charAt(minPos) == ' ')) 972 { 973 minPos++; 974 } 975 } 976 } 977 978 buffer.write(writer); 979 } 980 981 982 983 /** 984 * Writes the provided record to the LDIF target, wrapping long lines as 985 * necessary. 986 * 987 * @param record The LDIF record to be written. 988 * 989 * @throws IOException If a problem occurs while writing the LDIF data. 990 */ 991 private void writeLDIF(@NotNull final LDIFRecord record) 992 throws IOException 993 { 994 buffer.clear(); 995 record.toLDIF(buffer, wrapColumn); 996 buffer.append(StaticUtils.EOL_BYTES); 997 buffer.write(writer); 998 } 999 1000 1001 1002 /** 1003 * Performs any appropriate wrapping for the provided set of LDIF lines. 1004 * 1005 * @param wrapColumn The column at which to wrap long lines. A value that 1006 * is less than or equal to two indicates that no 1007 * wrapping should be performed. 1008 * @param ldifLines The set of lines that make up the LDIF data to be 1009 * wrapped. 1010 * 1011 * @return A new list of lines that have been wrapped as appropriate. 1012 */ 1013 @NotNull() 1014 public static List<String> wrapLines(final int wrapColumn, 1015 @NotNull final String... ldifLines) 1016 { 1017 return wrapLines(wrapColumn, Arrays.asList(ldifLines)); 1018 } 1019 1020 1021 1022 /** 1023 * Performs any appropriate wrapping for the provided set of LDIF lines. 1024 * 1025 * @param wrapColumn The column at which to wrap long lines. A value that 1026 * is less than or equal to two indicates that no 1027 * wrapping should be performed. 1028 * @param ldifLines The set of lines that make up the LDIF data to be 1029 * wrapped. 1030 * 1031 * @return A new list of lines that have been wrapped as appropriate. 1032 */ 1033 @NotNull() 1034 public static List<String> wrapLines(final int wrapColumn, 1035 @NotNull final List<String> ldifLines) 1036 { 1037 if (wrapColumn <= 2) 1038 { 1039 return new ArrayList<>(ldifLines); 1040 } 1041 1042 final ArrayList<String> newLines = new ArrayList<>(ldifLines.size()); 1043 for (final String s : ldifLines) 1044 { 1045 final int length = s.length(); 1046 if (length <= wrapColumn) 1047 { 1048 newLines.add(s); 1049 continue; 1050 } 1051 1052 newLines.add(s.substring(0, wrapColumn)); 1053 1054 int pos = wrapColumn; 1055 while (pos < length) 1056 { 1057 if ((length - pos + 1) <= wrapColumn) 1058 { 1059 newLines.add(' ' + s.substring(pos)); 1060 break; 1061 } 1062 else 1063 { 1064 newLines.add(' ' + s.substring(pos, (pos+wrapColumn-1))); 1065 pos += wrapColumn - 1; 1066 } 1067 } 1068 } 1069 1070 return newLines; 1071 } 1072 1073 1074 1075 /** 1076 * Creates a string consisting of the provided attribute name followed by 1077 * either a single colon and the string representation of the provided value, 1078 * or two colons and the base64-encoded representation of the provided value. 1079 * 1080 * @param name The name for the attribute. 1081 * @param value The value for the attribute. 1082 * 1083 * @return A string consisting of the provided attribute name followed by 1084 * either a single colon and the string representation of the 1085 * provided value, or two colons and the base64-encoded 1086 * representation of the provided value. 1087 */ 1088 @NotNull() 1089 public static String encodeNameAndValue(@NotNull final String name, 1090 @NotNull final ASN1OctetString value) 1091 { 1092 final StringBuilder buffer = new StringBuilder(); 1093 encodeNameAndValue(name, value, buffer); 1094 return buffer.toString(); 1095 } 1096 1097 1098 1099 /** 1100 * Appends a string to the provided buffer consisting of the provided 1101 * attribute name followed by either a single colon and the string 1102 * representation of the provided value, or two colons and the base64-encoded 1103 * representation of the provided value. 1104 * 1105 * @param name The name for the attribute. 1106 * @param value The value for the attribute. 1107 * @param buffer The buffer to which the name and value are to be written. 1108 */ 1109 public static void encodeNameAndValue(@NotNull final String name, 1110 @NotNull final ASN1OctetString value, 1111 @NotNull final StringBuilder buffer) 1112 { 1113 encodeNameAndValue(name, value, buffer, 0); 1114 } 1115 1116 1117 1118 /** 1119 * Appends a string to the provided buffer consisting of the provided 1120 * attribute name followed by either a single colon and the string 1121 * representation of the provided value, or two colons and the base64-encoded 1122 * representation of the provided value. 1123 * 1124 * @param name The name for the attribute. 1125 * @param value The value for the attribute. 1126 * @param buffer The buffer to which the name and value are to be 1127 * written. 1128 * @param wrapColumn The column at which to wrap long lines. A value that 1129 * is less than or equal to two indicates that no 1130 * wrapping should be performed. 1131 */ 1132 public static void encodeNameAndValue(@NotNull final String name, 1133 @NotNull final ASN1OctetString value, 1134 @NotNull final StringBuilder buffer, 1135 final int wrapColumn) 1136 { 1137 final int bufferStartPos = buffer.length(); 1138 final byte[] valueBytes = value.getValue(); 1139 boolean base64Encoded = false; 1140 1141 try 1142 { 1143 buffer.append(name); 1144 buffer.append(':'); 1145 1146 if (base64EncodingStrategy.shouldBase64Encode(value)) 1147 { 1148 buffer.append(": "); 1149 Base64.encode(valueBytes, buffer); 1150 base64Encoded = true; 1151 } 1152 else 1153 { 1154 buffer.append(' '); 1155 buffer.append(value.stringValue()); 1156 } 1157 } 1158 finally 1159 { 1160 if (wrapColumn > 2) 1161 { 1162 final int length = buffer.length() - bufferStartPos; 1163 if (length > wrapColumn) 1164 { 1165 final String EOL_PLUS_SPACE = StaticUtils.EOL + ' '; 1166 1167 // Be careful not to wrap in the middle of a multi-byte character. 1168 // Select the position where we want to wrap, but if the character 1169 // in that position is a low surrogate character, then it needs to 1170 // stay with the previous character so we shouldn't wrap there. 1171 int pos = bufferStartPos + wrapColumn; 1172 while (pos < buffer.length()) 1173 { 1174 final char c = buffer.charAt(pos); 1175 if (Character.isLowSurrogate(c)) 1176 { 1177 pos++; 1178 continue; 1179 } 1180 1181 buffer.insert(pos, EOL_PLUS_SPACE); 1182 pos += (wrapColumn - 1 + EOL_PLUS_SPACE.length()); 1183 } 1184 } 1185 } 1186 1187 if (base64Encoded && commentAboutBase64EncodedValues) 1188 { 1189 writeBase64DecodedValueComment(valueBytes, buffer, wrapColumn); 1190 } 1191 } 1192 } 1193 1194 1195 1196 /** 1197 * Appends a comment to the provided buffer with an unencoded representation 1198 * of the provided value. This will only have any effect if 1199 * {@code commentAboutBase64EncodedValues} is {@code true}, and only if the 1200 * value contains no more than 1,000 bytes. 1201 * 1202 * @param valueBytes The bytes that comprise the value. 1203 * @param buffer The buffer to which the comment should be appended. 1204 * @param wrapColumn The column at which to wrap long lines. 1205 */ 1206 private static void writeBase64DecodedValueComment( 1207 @NotNull final byte[] valueBytes, 1208 @NotNull final StringBuilder buffer, 1209 final int wrapColumn) 1210 { 1211 if (commentAboutBase64EncodedValues && (valueBytes.length <= 1_000)) 1212 { 1213 final int wrapColumnMinusTwo; 1214 if (wrapColumn <= 5) 1215 { 1216 wrapColumnMinusTwo = StaticUtils.TERMINAL_WIDTH_COLUMNS - 3; 1217 } 1218 else 1219 { 1220 wrapColumnMinusTwo = wrapColumn - 2; 1221 } 1222 1223 final int wrapColumnMinusThree = wrapColumnMinusTwo - 1; 1224 1225 boolean first = true; 1226 final String comment = 1227 "Non-base64-encoded representation of the above value: " + 1228 getEscapedValue(valueBytes); 1229 for (final String s : 1230 StaticUtils.wrapLine(comment, wrapColumnMinusTwo, 1231 wrapColumnMinusThree)) 1232 { 1233 buffer.append(StaticUtils.EOL); 1234 buffer.append("# "); 1235 if (first) 1236 { 1237 first = false; 1238 } 1239 else 1240 { 1241 buffer.append(' '); 1242 } 1243 buffer.append(s); 1244 } 1245 } 1246 } 1247 1248 1249 1250 /** 1251 * Appends a string to the provided buffer consisting of the provided 1252 * attribute name followed by either a single colon and the string 1253 * representation of the provided value, or two colons and the base64-encoded 1254 * representation of the provided value. It may optionally be wrapped at the 1255 * specified column. 1256 * 1257 * @param name The name for the attribute. 1258 * @param value The value for the attribute. 1259 * @param buffer The buffer to which the name and value are to be 1260 * written. 1261 * @param wrapColumn The column at which to wrap long lines. A value that 1262 * is less than or equal to two indicates that no 1263 * wrapping should be performed. 1264 */ 1265 public static void encodeNameAndValue(@NotNull final String name, 1266 @NotNull final ASN1OctetString value, 1267 @NotNull final ByteStringBuffer buffer, 1268 final int wrapColumn) 1269 { 1270 final int bufferStartPos = buffer.length(); 1271 boolean base64Encoded = false; 1272 1273 try 1274 { 1275 buffer.append(name); 1276 base64Encoded = encodeValue(value, buffer); 1277 } 1278 finally 1279 { 1280 if (wrapColumn > 2) 1281 { 1282 final int length = buffer.length() - bufferStartPos; 1283 if (length > wrapColumn) 1284 { 1285 final byte[] EOL_BYTES_PLUS_SPACE = 1286 new byte[StaticUtils.EOL_BYTES.length + 1]; 1287 System.arraycopy(StaticUtils.EOL_BYTES, 0, EOL_BYTES_PLUS_SPACE, 0, 1288 StaticUtils.EOL_BYTES.length); 1289 EOL_BYTES_PLUS_SPACE[StaticUtils.EOL_BYTES.length] = ' '; 1290 1291 // Be careful not to wrap in the middle of a multi-byte character. 1292 // Select a byte where we expect to wrap and use the following logic 1293 // to determine if it's safe to wrap there: 1294 // 1295 // - If the most significant bit of a byte is set to zero, then it's 1296 // not a multi-byte character and it's safe to wrap before it. 1297 // 1298 // - If the most significant bit of a byte is set to one and the 1299 // second-most significant bit is also set to one, then it's the 1300 // first byte of a multi-byte UTF-8 character and it's safe to wrap 1301 // before it. 1302 // 1303 // - If the most significant bit of a byte is set to one and the 1304 // second-most significant bit is set to zero, then it's either in 1305 // the middle of a UTF-8 character or not part of a valid UTF-8 1306 // string. In either case, we'll read ahead until we find a byte 1307 // where it's safe to wrap. 1308 int pos = bufferStartPos + wrapColumn; 1309 while (pos < buffer.length()) 1310 { 1311 final byte byteAtPos = buffer.byteAt(pos); 1312 if ((byteAtPos & 0xC0) == 0x80) 1313 { 1314 pos++; 1315 continue; 1316 } 1317 1318 buffer.insert(pos, EOL_BYTES_PLUS_SPACE); 1319 pos += (wrapColumn - 1 + EOL_BYTES_PLUS_SPACE.length); 1320 } 1321 } 1322 } 1323 1324 if (base64Encoded && commentAboutBase64EncodedValues) 1325 { 1326 writeBase64DecodedValueComment(value.getValue(), buffer, wrapColumn); 1327 } 1328 } 1329 } 1330 1331 1332 1333 /** 1334 * Appends a string to the provided buffer consisting of the properly-encoded 1335 * representation of the provided value, including the necessary colon(s) and 1336 * space that precede it. Depending on the content of the value, it will 1337 * either be used as-is or base64-encoded. 1338 * 1339 * @param value The value for the attribute. 1340 * @param buffer The buffer to which the value is to be written. 1341 * 1342 * @return {@code true} if the value was base64-encoded, or {@code false} if 1343 * not. 1344 */ 1345 static boolean encodeValue(@NotNull final ASN1OctetString value, 1346 @NotNull final ByteStringBuffer buffer) 1347 { 1348 buffer.append(':'); 1349 1350 final byte[] valueBytes = value.getValue(); 1351 if (base64EncodingStrategy.shouldBase64Encode(valueBytes)) 1352 { 1353 buffer.append(": "); 1354 Base64.encode(valueBytes, buffer); 1355 return true; 1356 } 1357 else 1358 { 1359 buffer.append(' '); 1360 buffer.append(valueBytes); 1361 return false; 1362 } 1363 } 1364 1365 1366 1367 /** 1368 * Appends a comment to the provided buffer with an unencoded representation 1369 * of the provided value. This will only have any effect if 1370 * {@code commentAboutBase64EncodedValues} is {@code true}, if the value 1371 * contains no more than 1,000 bytes, and only if the value represents valid 1372 * UTF-8. 1373 * 1374 * @param valueBytes The bytes that comprise the value. 1375 * @param buffer The buffer to which the comment should be appended. 1376 * @param wrapColumn The column at which to wrap long lines. 1377 */ 1378 private static void writeBase64DecodedValueComment( 1379 @NotNull final byte[] valueBytes, 1380 @NotNull final ByteStringBuffer buffer, 1381 final int wrapColumn) 1382 { 1383 if (commentAboutBase64EncodedValues && (valueBytes.length <= 1_000) && 1384 StaticUtils.isValidUTF8(valueBytes)) 1385 { 1386 final int wrapColumnMinusTwo; 1387 if (wrapColumn <= 5) 1388 { 1389 wrapColumnMinusTwo = StaticUtils.TERMINAL_WIDTH_COLUMNS - 3; 1390 } 1391 else 1392 { 1393 wrapColumnMinusTwo = wrapColumn - 2; 1394 } 1395 1396 final int wrapColumnMinusThree = wrapColumnMinusTwo - 1; 1397 1398 boolean first = true; 1399 final String comment = 1400 "Non-base64-encoded representation of the above value: " + 1401 getEscapedValue(valueBytes); 1402 for (final String s : 1403 StaticUtils.wrapLine(comment, wrapColumnMinusTwo, 1404 wrapColumnMinusThree)) 1405 { 1406 buffer.append(StaticUtils.EOL); 1407 buffer.append("# "); 1408 if (first) 1409 { 1410 first = false; 1411 } 1412 else 1413 { 1414 buffer.append(' '); 1415 } 1416 buffer.append(s); 1417 } 1418 } 1419 } 1420 1421 1422 1423 /** 1424 * Retrieves a string representation of the provided value with all special 1425 * characters escaped with backslashes. 1426 * 1427 * @param valueBytes The byte array containing the value to encode. 1428 * 1429 * @return A string representation of the provided value with any special 1430 * characters. 1431 */ 1432 @NotNull() 1433 private static String getEscapedValue(@NotNull final byte[] valueBytes) 1434 { 1435 final String valueString = StaticUtils.toUTF8String(valueBytes); 1436 final StringBuilder buffer = new StringBuilder(valueString.length()); 1437 for (int i=0; i < valueString.length(); i++) 1438 { 1439 final char c = valueString.charAt(i); 1440 switch (c) 1441 { 1442 case '\t': 1443 buffer.append(INFO_LDIF_WRITER_CHAR_TAB.get()); 1444 break; 1445 case '\n': 1446 buffer.append(INFO_LDIF_WRITER_CHAR_CARRIAGE_RETURN.get()); 1447 break; 1448 case '\r': 1449 buffer.append(INFO_LDIF_WRITER_CHAR_LINE_FEED.get()); 1450 break; 1451 case ' ': 1452 if (i == 0) 1453 { 1454 buffer.append(INFO_LDIF_WRITER_CHAR_LEADING_SPACE.get()); 1455 } 1456 else if (i == (valueString.length() - 1)) 1457 { 1458 buffer.append(INFO_LDIF_WRITER_CHAR_TRAILING_SPACE.get()); 1459 } 1460 else 1461 { 1462 buffer.append(' '); 1463 } 1464 break; 1465 case ':': 1466 if (i == 0) 1467 { 1468 buffer.append(INFO_LDIF_WRITER_CHAR_LEADING_COLON.get()); 1469 } 1470 else 1471 { 1472 buffer.append(c); 1473 } 1474 break; 1475 case '<': 1476 if (i == 0) 1477 { 1478 buffer.append(INFO_LDIF_WRITER_CHAR_LEADING_LESS_THAN.get()); 1479 } 1480 else 1481 { 1482 buffer.append(c); 1483 } 1484 break; 1485 case '{': 1486 buffer.append(INFO_LDIF_WRITER_CHAR_OPENING_CURLY_BRACE.get()); 1487 break; 1488 case '}': 1489 buffer.append(INFO_LDIF_WRITER_CHAR_CLOSING_CURLY_BRACE.get()); 1490 break; 1491 default: 1492 if ((c >= '!') && (c <= '~')) 1493 { 1494 buffer.append(c); 1495 } 1496 else 1497 { 1498 // Try to figure out whether the character might be printable, even 1499 // if it's non-ASCII. If so, then print it. Otherwise, if we can 1500 // get the name for the Unicode code point, then print that name. 1501 // As a last resort, just print a hex representation of the bytes 1502 // that make up the 1503 final int codePoint = Character.codePointAt(valueString, i); 1504 if (StaticUtils.isLikelyDisplayableCharacter(codePoint)) 1505 { 1506 final int[] codePointArray = { codePoint }; 1507 buffer.append(new String(codePointArray, 0, 1)); 1508 } 1509 else 1510 { 1511 // See if we can get a name for the character. If so, then use 1512 // it. Otherwise, just print the escaped hex representation. 1513 String codePointName; 1514 try 1515 { 1516 codePointName = Character.getName(codePoint); 1517 } 1518 catch (final Exception e) 1519 { 1520 Debug.debugException(e); 1521 codePointName = null; 1522 } 1523 1524 if ((codePointName == null) || codePointName.isEmpty()) 1525 { 1526 final int[] codePointArray = { codePoint }; 1527 final byte[] codePointBytes = 1528 StaticUtils.getBytes(new String(codePointArray, 0, 1)); 1529 buffer.append(INFO_LDIF_WRITER_CHAR_HEX.get( 1530 StaticUtils.toHex(codePointBytes))); 1531 } 1532 else 1533 { 1534 buffer.append("{"); 1535 buffer.append(codePointName); 1536 buffer.append('}'); 1537 } 1538 } 1539 1540 final int numChars = Character.charCount(codePoint); 1541 i += (numChars - 1); 1542 } 1543 break; 1544 } 1545 } 1546 1547 return buffer.toString(); 1548 } 1549 1550 1551 1552 /** 1553 * If the provided exception is non-null, then it will be rethrown as an 1554 * unchecked exception or an IOException. 1555 * 1556 * @param t The exception to rethrow as an an unchecked exception or an 1557 * IOException or {@code null} if none. 1558 * 1559 * @throws IOException If t is a checked exception. 1560 */ 1561 static void rethrow(@Nullable final Throwable t) 1562 throws IOException 1563 { 1564 if (t == null) 1565 { 1566 return; 1567 } 1568 1569 if (t instanceof IOException) 1570 { 1571 throw (IOException) t; 1572 } 1573 else if (t instanceof RuntimeException) 1574 { 1575 throw (RuntimeException) t; 1576 } 1577 else if (t instanceof Error) 1578 { 1579 throw (Error) t; 1580 } 1581 else 1582 { 1583 throw new IOException(t); 1584 } 1585 } 1586}