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