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