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