001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2011-2016 ForgeRock AS. 015 */ 016package org.forgerock.opendj.ldif; 017 018import static com.forgerock.opendj.ldap.CoreMessages.*; 019 020import java.io.IOException; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.Comparator; 025import java.util.Iterator; 026import java.util.List; 027import java.util.Map; 028import java.util.NoSuchElementException; 029import java.util.SortedMap; 030import java.util.TreeMap; 031 032import org.forgerock.i18n.LocalizedIllegalArgumentException; 033import org.forgerock.opendj.io.ASN1; 034import org.forgerock.opendj.io.LDAP; 035import org.forgerock.opendj.ldap.AVA; 036import org.forgerock.opendj.ldap.Attribute; 037import org.forgerock.opendj.ldap.AttributeDescription; 038import org.forgerock.opendj.ldap.Attributes; 039import org.forgerock.opendj.ldap.ByteString; 040import org.forgerock.opendj.ldap.ByteStringBuilder; 041import org.forgerock.opendj.ldap.DN; 042import org.forgerock.opendj.ldap.DecodeException; 043import org.forgerock.opendj.ldap.DecodeOptions; 044import org.forgerock.opendj.ldap.Entry; 045import org.forgerock.opendj.ldap.LinkedHashMapEntry; 046import org.forgerock.opendj.ldap.Matcher; 047import org.forgerock.opendj.ldap.Modification; 048import org.forgerock.opendj.ldap.ModificationType; 049import org.forgerock.opendj.ldap.RDN; 050import org.forgerock.opendj.ldap.SearchScope; 051import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl; 052import org.forgerock.opendj.ldap.requests.AddRequest; 053import org.forgerock.opendj.ldap.requests.DeleteRequest; 054import org.forgerock.opendj.ldap.requests.ModifyDNRequest; 055import org.forgerock.opendj.ldap.requests.ModifyRequest; 056import org.forgerock.opendj.ldap.requests.Requests; 057import org.forgerock.opendj.ldap.requests.SearchRequest; 058import org.forgerock.opendj.ldap.schema.AttributeUsage; 059import org.forgerock.opendj.ldap.schema.Schema; 060 061/** 062 * This class contains common utility methods for creating and manipulating 063 * readers and writers. 064 */ 065public final class LDIF { 066 // @formatter:off 067 private static final class EntryIteratorReader implements EntryReader { 068 private final Iterator<Entry> iterator; 069 private EntryIteratorReader(final Iterator<Entry> iterator) { this.iterator = iterator; } 070 @Override 071 public void close() { } 072 @Override 073 public boolean hasNext() { return iterator.hasNext(); } 074 @Override 075 public Entry readEntry() { return iterator.next(); } 076 } 077 // @formatter:on 078 079 /** 080 * Comparator ordering the DN ASC. 081 */ 082 private static final Comparator<byte[][]> DN_ORDER2 = new Comparator<byte[][]>() { 083 @Override 084 public int compare(byte[][] b1, byte[][] b2) { 085 return DN_ORDER.compare(b1[0], b2[0]); 086 } 087 }; 088 089 /** 090 * Comparator ordering the DN ASC. 091 */ 092 private static final Comparator<byte[]> DN_ORDER = new Comparator<byte[]>() { 093 @Override 094 public int compare(byte[] b1, byte[] b2) { 095 final ByteString bs = ByteString.valueOfBytes(b1); 096 final ByteString bs2 = ByteString.valueOfBytes(b2); 097 return bs.compareTo(bs2); 098 } 099 }; 100 101 /** 102 * Copies the content of {@code input} to {@code output}. This method does 103 * not close {@code input} or {@code output}. 104 * 105 * @param input 106 * The input change record reader. 107 * @param output 108 * The output change record reader. 109 * @return The output change record reader. 110 * @throws IOException 111 * If an unexpected IO error occurred. 112 */ 113 public static ChangeRecordWriter copyTo(final ChangeRecordReader input, 114 final ChangeRecordWriter output) throws IOException { 115 while (input.hasNext()) { 116 output.writeChangeRecord(input.readChangeRecord()); 117 } 118 return output; 119 } 120 121 /** 122 * Copies the content of {@code input} to {@code output}. This method does 123 * not close {@code input} or {@code output}. 124 * 125 * @param input 126 * The input entry reader. 127 * @param output 128 * The output entry reader. 129 * @return The output entry reader. 130 * @throws IOException 131 * If an unexpected IO error occurred. 132 */ 133 public static EntryWriter copyTo(final EntryReader input, final EntryWriter output) 134 throws IOException { 135 while (input.hasNext()) { 136 output.writeEntry(input.readEntry()); 137 } 138 return output; 139 } 140 141 /** 142 * Compares the content of {@code source} to the content of {@code target} 143 * and returns the differences in a change record reader. Closing the 144 * returned reader will cause {@code source} and {@code target} to be closed 145 * as well. 146 * <p> 147 * <b>NOTE:</b> this method reads the content of {@code source} and 148 * {@code target} into memory before calculating the differences, and is 149 * therefore not suited for use in cases where a very large number of 150 * entries are to be compared. 151 * 152 * @param source 153 * The entry reader containing the source entries to be compared. 154 * @param target 155 * The entry reader containing the target entries to be compared. 156 * @return A change record reader containing the differences. 157 * @throws IOException 158 * If an unexpected IO error occurred. 159 */ 160 public static ChangeRecordReader diff(final EntryReader source, final EntryReader target) 161 throws IOException { 162 163 final List<byte[][]> source2 = readEntriesAsList(source); 164 final List<byte[][]> target2 = readEntriesAsList(target); 165 final Iterator<byte[][]> sourceIterator = source2.iterator(); 166 final Iterator<byte[][]> targetIterator = target2.iterator(); 167 168 return new ChangeRecordReader() { 169 private Entry sourceEntry = nextEntry(sourceIterator); 170 private Entry targetEntry = nextEntry(targetIterator); 171 172 @Override 173 public void close() throws IOException { 174 try { 175 source.close(); 176 } finally { 177 target.close(); 178 } 179 } 180 181 @Override 182 public boolean hasNext() { 183 return sourceEntry != null || targetEntry != null; 184 } 185 186 @Override 187 public ChangeRecord readChangeRecord() throws IOException { 188 if (sourceEntry != null && targetEntry != null) { 189 final DN sourceDN = sourceEntry.getName(); 190 final DN targetDN = targetEntry.getName(); 191 final int cmp = sourceDN.compareTo(targetDN); 192 193 if (cmp == 0) { 194 // Modify record: entry in both source and target. 195 final ModifyRequest request = 196 Requests.newModifyRequest(sourceEntry, targetEntry); 197 sourceEntry = nextEntry(sourceIterator); 198 targetEntry = nextEntry(targetIterator); 199 return request; 200 } else if (cmp < 0) { 201 // Delete record: entry in source but not in target. 202 final DeleteRequest request = 203 Requests.newDeleteRequest(sourceEntry.getName()); 204 sourceEntry = nextEntry(sourceIterator); 205 return request; 206 } else { 207 // Add record: entry in target but not in source. 208 final AddRequest request = Requests.newAddRequest(targetEntry); 209 targetEntry = nextEntry(targetIterator); 210 return request; 211 } 212 } else if (sourceEntry != null) { 213 // Delete remaining source records. 214 final DeleteRequest request = Requests.newDeleteRequest(sourceEntry.getName()); 215 sourceEntry = nextEntry(sourceIterator); 216 return request; 217 } else if (targetEntry != null) { 218 // Add remaining target records. 219 final AddRequest request = Requests.newAddRequest(targetEntry); 220 targetEntry = nextEntry(targetIterator); 221 return request; 222 } else { 223 throw new NoSuchElementException(); 224 } 225 } 226 227 private Entry nextEntry(final Iterator<byte[][]> i) { 228 if (i.hasNext()) { 229 return decodeEntry(i.next()[1]); 230 } 231 return null; 232 } 233 }; 234 } 235 236 /** 237 * Builds an entry from the provided lines of LDIF. 238 * <p> 239 * Sample usage: 240 * <pre> 241 * Entry john = makeEntry( 242 * "dn: cn=John Smith,dc=example,dc=com", 243 * "objectclass: inetorgperson", 244 * "cn: John Smith", 245 * "sn: Smith", 246 * "givenname: John"); 247 * </pre> 248 * 249 * @param ldifLines 250 * LDIF lines that contains entry definition. 251 * @return an entry 252 * @throws LocalizedIllegalArgumentException 253 * If {@code ldifLines} did not contain an LDIF entry, or 254 * contained multiple entries, or contained malformed LDIF, or 255 * if the entry could not be decoded using the default schema. 256 * @throws NullPointerException 257 * If {@code ldifLines} was {@code null}. 258 */ 259 public static Entry makeEntry(String... ldifLines) { 260 // returns a non-empty list 261 List<Entry> entries = makeEntries(ldifLines); 262 if (entries.size() > 1) { 263 throw new LocalizedIllegalArgumentException( 264 WARN_READ_LDIF_ENTRY_MULTIPLE_ENTRIES_FOUND.get(entries.size())); 265 } 266 return entries.get(0); 267 } 268 269 /** 270 * Builds an entry from the provided lines of LDIF. 271 * 272 * @param ldifLines 273 * LDIF lines that contains entry definition. 274 * @return an entry 275 * @throws LocalizedIllegalArgumentException 276 * If {@code ldifLines} did not contain an LDIF entry, or 277 * contained multiple entries, or contained malformed LDIF, or 278 * if the entry could not be decoded using the default schema. 279 * @throws NullPointerException 280 * If {@code ldifLines} was {@code null}. 281 * @see LDIF#makeEntry(String...) 282 */ 283 public static Entry makeEntry(List<String> ldifLines) { 284 return makeEntry(ldifLines.toArray(new String[ldifLines.size()])); 285 } 286 287 /** 288 * Builds a list of entries from the provided lines of LDIF. 289 * <p> 290 * Sample usage: 291 * <pre> 292 * List<Entry> smiths = TestCaseUtils.makeEntries( 293 * "dn: cn=John Smith,dc=example,dc=com", 294 * "objectclass: inetorgperson", 295 * "cn: John Smith", 296 * "sn: Smith", 297 * "givenname: John", 298 * "", 299 * "dn: cn=Jane Smith,dc=example,dc=com", 300 * "objectclass: inetorgperson", 301 * "cn: Jane Smith", 302 * "sn: Smith", 303 * "givenname: Jane"); 304 * </pre> 305 * @param ldifLines 306 * LDIF lines that contains entries definition. 307 * Entries are separated by an empty string: {@code ""}. 308 * @return a non empty list of entries 309 * @throws LocalizedIllegalArgumentException 310 * If {@code ldifLines} did not contain LDIF entries, 311 * or contained malformed LDIF, or if the entries 312 * could not be decoded using the default schema. 313 * @throws NullPointerException 314 * If {@code ldifLines} was {@code null}. 315 */ 316 public static List<Entry> makeEntries(String... ldifLines) { 317 List<Entry> entries = new ArrayList<>(); 318 try (LDIFEntryReader reader = new LDIFEntryReader(ldifLines)) { 319 while (reader.hasNext()) { 320 entries.add(reader.readEntry()); 321 } 322 } catch (final DecodeException e) { 323 // Badly formed LDIF. 324 throw new LocalizedIllegalArgumentException(e.getMessageObject()); 325 } catch (final IOException e) { 326 // This should never happen for a String based reader. 327 throw new LocalizedIllegalArgumentException(WARN_READ_LDIF_RECORD_UNEXPECTED_IO_ERROR.get(e.getMessage())); 328 } 329 if (entries.isEmpty()) { 330 throw new LocalizedIllegalArgumentException(WARN_READ_LDIF_ENTRY_NO_ENTRY_FOUND.get()); 331 } 332 return entries; 333 } 334 335 /** 336 * Builds a list of entries from the provided lines of LDIF. 337 * 338 * @param ldifLines 339 * LDIF lines that contains entries definition. Entries are 340 * separated by an empty string: {@code ""}. 341 * @return a non empty list of entries 342 * @throws LocalizedIllegalArgumentException 343 * If {@code ldifLines} did not contain LDIF entries, or 344 * contained malformed LDIF, or if the entries could not be 345 * decoded using the default schema. 346 * @throws NullPointerException 347 * If {@code ldifLines} was {@code null}. 348 * @see LDIF#makeEntries(String...) 349 */ 350 public static List<Entry> makeEntries(List<String> ldifLines) { 351 return makeEntries(ldifLines.toArray(new String[ldifLines.size()])); 352 } 353 354 /** 355 * Returns an entry reader over the provided entry collection. 356 * 357 * @param entries 358 * The entry collection. 359 * @return An entry reader over the provided entry collection. 360 */ 361 public static EntryReader newEntryCollectionReader(final Collection<Entry> entries) { 362 return new EntryIteratorReader(entries.iterator()); 363 } 364 365 /** 366 * Returns an entry reader over the provided entry iterator. 367 * 368 * @param entries 369 * The entry iterator. 370 * @return An entry reader over the provided entry iterator. 371 */ 372 public static EntryReader newEntryIteratorReader(final Iterator<Entry> entries) { 373 return new EntryIteratorReader(entries); 374 } 375 376 /** 377 * Applies the set of changes contained in {@code patch} to the content of 378 * {@code input} and returns the result in an entry reader. This method 379 * ignores missing entries, and overwrites existing entries. Closing the 380 * returned reader will cause {@code input} and {@code patch} to be closed 381 * as well. 382 * <p> 383 * <b>NOTE:</b> this method reads the content of {@code input} into memory 384 * before applying the changes, and is therefore not suited for use in cases 385 * where a very large number of entries are to be patched. 386 * <p> 387 * <b>NOTE:</b> this method will not perform modifications required in order 388 * to maintain referential integrity. In particular, if an entry references 389 * another entry using a DN valued attribute and the referenced entry is 390 * deleted, then the DN reference will not be removed. The same applies to 391 * renamed entries and their references. 392 * 393 * @param input 394 * The entry reader containing the set of entries to be patched. 395 * @param patch 396 * The change record reader containing the set of changes to be 397 * applied. 398 * @return An entry reader containing the patched entries. 399 * @throws IOException 400 * If an unexpected IO error occurred. 401 */ 402 public static EntryReader patch(final EntryReader input, final ChangeRecordReader patch) 403 throws IOException { 404 return patch(input, patch, RejectedChangeRecordListener.OVERWRITE); 405 } 406 407 /** 408 * Applies the set of changes contained in {@code patch} to the content of 409 * {@code input} and returns the result in an entry reader. Closing the 410 * returned reader will cause {@code input} and {@code patch} to be closed 411 * as well. 412 * <p> 413 * <b>NOTE:</b> this method reads the content of {@code input} into memory 414 * before applying the changes, and is therefore not suited for use in cases 415 * where a very large number of entries are to be patched. 416 * <p> 417 * <b>NOTE:</b> this method will not perform modifications required in order 418 * to maintain referential integrity. In particular, if an entry references 419 * another entry using a DN valued attribute and the referenced entry is 420 * deleted, then the DN reference will not be removed. The same applies to 421 * renamed entries and their references. 422 * 423 * @param input 424 * The entry reader containing the set of entries to be patched. 425 * @param patch 426 * The change record reader containing the set of changes to be 427 * applied. 428 * @param listener 429 * The rejected change listener. 430 * @return An entry reader containing the patched entries. 431 * @throws IOException 432 * If an unexpected IO error occurred. 433 */ 434 public static EntryReader patch(final EntryReader input, final ChangeRecordReader patch, 435 final RejectedChangeRecordListener listener) throws IOException { 436 final SortedMap<byte[], byte[]> entries = readEntriesAsMap(input); 437 438 while (patch.hasNext()) { 439 final ChangeRecord change = patch.readChangeRecord(); 440 final DN changeDN = change.getName(); 441 final byte[] changeNormDN = toNormalizedByteArray(change.getName()); 442 443 final DecodeException de = 444 change.accept(new ChangeRecordVisitor<DecodeException, Void>() { 445 446 @Override 447 public DecodeException visitChangeRecord(final Void p, 448 final AddRequest change) { 449 450 if (entries.get(changeNormDN) != null) { 451 final Entry existingEntry = decodeEntry(entries.get(changeNormDN)); 452 try { 453 final Entry entry = 454 listener.handleDuplicateEntry(change, existingEntry); 455 entries.put(toNormalizedByteArray(entry.getName()), encodeEntry(entry)[1]); 456 } catch (final DecodeException e) { 457 return e; 458 } 459 } else { 460 entries.put(changeNormDN, encodeEntry(change)[1]); 461 } 462 return null; 463 } 464 465 @Override 466 public DecodeException visitChangeRecord(final Void p, 467 final DeleteRequest change) { 468 if (entries.get(changeNormDN) == null) { 469 try { 470 listener.handleRejectedChangeRecord(change, 471 REJECTED_CHANGE_FAIL_DELETE.get(change.getName() 472 .toString())); 473 } catch (final DecodeException e) { 474 return e; 475 } 476 } else { 477 try { 478 if (change.getControl(SubtreeDeleteRequestControl.DECODER, 479 new DecodeOptions()) != null) { 480 entries.subMap( 481 toNormalizedByteArray(change.getName()), 482 toNormalizedByteArray(change.getName().child(RDN.maxValue()))).clear(); 483 } else { 484 entries.remove(changeNormDN); 485 } 486 } catch (final DecodeException e) { 487 return e; 488 } 489 490 } 491 return null; 492 } 493 494 @Override 495 public DecodeException visitChangeRecord(final Void p, 496 final ModifyDNRequest change) { 497 if (entries.get(changeNormDN) == null) { 498 try { 499 listener.handleRejectedChangeRecord(change, 500 REJECTED_CHANGE_FAIL_MODIFYDN.get(change.getName() 501 .toString())); 502 } catch (final DecodeException e) { 503 return e; 504 } 505 } else { 506 // Calculate the old and new DN. 507 final DN oldDN = changeDN; 508 509 DN newSuperior = change.getNewSuperior(); 510 if (newSuperior == null) { 511 newSuperior = change.getName().parent(); 512 if (newSuperior == null) { 513 newSuperior = DN.rootDN(); 514 } 515 } 516 final DN newDN = newSuperior.child(change.getNewRDN()); 517 518 // Move the renamed entries into a separate map 519 // in order to avoid cases where the renamed subtree overlaps. 520 final SortedMap<byte[], byte[]> renamedEntries = new TreeMap<>(DN_ORDER); 521 522 // @formatter:off 523 final Iterator<Map.Entry<byte[], byte[]>> i = 524 entries.subMap(changeNormDN, 525 toNormalizedByteArray(changeDN.child(RDN.maxValue()))).entrySet().iterator(); 526 // @formatter:on 527 528 while (i.hasNext()) { 529 final Map.Entry<byte[], byte[]> e = i.next(); 530 final Entry entry = decodeEntry(e.getValue()); 531 final DN renamedDN = entry.getName().rename(oldDN, newDN); 532 entry.setName(renamedDN); 533 renamedEntries.put(toNormalizedByteArray(renamedDN), encodeEntry(entry)[1]); 534 i.remove(); 535 } 536 537 // Modify target entry 538 final Entry targetEntry = 539 decodeEntry(renamedEntries.values().iterator().next()); 540 541 if (change.isDeleteOldRDN()) { 542 for (final AVA ava : oldDN.rdn()) { 543 targetEntry.removeAttribute(ava.toAttribute(), null); 544 } 545 } 546 for (final AVA ava : newDN.rdn()) { 547 targetEntry.addAttribute(ava.toAttribute()); 548 } 549 550 renamedEntries.remove(toNormalizedByteArray(targetEntry.getName())); 551 renamedEntries.put(toNormalizedByteArray(targetEntry.getName()), 552 encodeEntry(targetEntry)[1]); 553 554 // Add the renamed entries. 555 final Iterator<byte[]> j = renamedEntries.values().iterator(); 556 while (j.hasNext()) { 557 final Entry renamedEntry = decodeEntry(j.next()); 558 final byte[] existingEntryDn = 559 entries.get(toNormalizedByteArray(renamedEntry.getName())); 560 561 if (existingEntryDn != null) { 562 final Entry existingEntry = decodeEntry(existingEntryDn); 563 try { 564 final Entry tmp = 565 listener.handleDuplicateEntry(change, 566 existingEntry, renamedEntry); 567 entries.put(toNormalizedByteArray(tmp.getName()), encodeEntry(tmp)[1]); 568 } catch (final DecodeException e) { 569 return e; 570 } 571 } else { 572 entries.put(toNormalizedByteArray(renamedEntry.getName()), 573 encodeEntry(renamedEntry)[1]); 574 } 575 } 576 renamedEntries.clear(); 577 } 578 return null; 579 } 580 581 @Override 582 public DecodeException visitChangeRecord(final Void p, 583 final ModifyRequest change) { 584 if (entries.get(changeNormDN) == null) { 585 try { 586 listener.handleRejectedChangeRecord(change, 587 REJECTED_CHANGE_FAIL_MODIFY.get(change.getName() 588 .toString())); 589 } catch (final DecodeException e) { 590 return e; 591 } 592 } else { 593 final Entry entry = decodeEntry(entries.get(changeNormDN)); 594 for (final Modification modification : change.getModifications()) { 595 final ModificationType modType = 596 modification.getModificationType(); 597 if (modType.equals(ModificationType.ADD)) { 598 entry.addAttribute(modification.getAttribute(), null); 599 } else if (modType.equals(ModificationType.DELETE)) { 600 entry.removeAttribute(modification.getAttribute(), null); 601 } else if (modType.equals(ModificationType.REPLACE)) { 602 entry.replaceAttribute(modification.getAttribute()); 603 } else { 604 System.err.println("Unable to apply \"" + modType 605 + "\" modification to entry \"" + change.getName() 606 + "\": modification type not supported"); 607 } 608 } 609 entries.put(changeNormDN, encodeEntry(entry)[1]); 610 } 611 return null; 612 } 613 614 }, null); 615 616 if (de != null) { 617 throw de; 618 } 619 } 620 621 return new EntryReader() { 622 private final Iterator<byte[]> iterator = entries.values().iterator(); 623 624 @Override 625 public void close() throws IOException { 626 try { 627 input.close(); 628 } finally { 629 patch.close(); 630 } 631 } 632 633 @Override 634 public boolean hasNext() throws IOException { 635 return iterator.hasNext(); 636 } 637 638 @Override 639 public Entry readEntry() throws IOException { 640 return decodeEntry(iterator.next()); 641 } 642 }; 643 } 644 645 /** 646 * Returns a filtered view of {@code input} containing only those entries 647 * which match the search base DN, scope, and filtered defined in 648 * {@code search}. In addition, returned entries will be filtered according 649 * to any attribute filtering criteria defined in the search request. 650 * <p> 651 * The filter and attribute descriptions will be decoded using the default 652 * schema. 653 * 654 * @param input 655 * The entry reader containing the set of entries to be filtered. 656 * @param search 657 * The search request defining the filtering criteria. 658 * @return A filtered view of {@code input} containing only those entries 659 * which match the provided search request. 660 */ 661 public static EntryReader search(final EntryReader input, final SearchRequest search) { 662 return search(input, search, Schema.getDefaultSchema()); 663 } 664 665 /** 666 * Returns a filtered view of {@code input} containing only those entries 667 * which match the search base DN, scope, and filtered defined in 668 * {@code search}. In addition, returned entries will be filtered according 669 * to any attribute filtering criteria defined in the search request. 670 * <p> 671 * The filter and attribute descriptions will be decoded using the provided 672 * schema. 673 * 674 * @param input 675 * The entry reader containing the set of entries to be filtered. 676 * @param search 677 * The search request defining the filtering criteria. 678 * @param schema 679 * The schema which should be used to decode the search filter 680 * and attribute descriptions. 681 * @return A filtered view of {@code input} containing only those entries 682 * which match the provided search request. 683 */ 684 public static EntryReader search(final EntryReader input, final SearchRequest search, 685 final Schema schema) { 686 final Matcher matcher = search.getFilter().matcher(schema); 687 688 return new EntryReader() { 689 private Entry nextEntry = null; 690 private int entryCount = 0; 691 692 @Override 693 public void close() throws IOException { 694 input.close(); 695 } 696 697 @Override 698 public boolean hasNext() throws IOException { 699 if (nextEntry == null) { 700 final int sizeLimit = search.getSizeLimit(); 701 if (sizeLimit == 0 || entryCount < sizeLimit) { 702 final DN baseDN = search.getName(); 703 final SearchScope scope = search.getScope(); 704 while (input.hasNext()) { 705 final Entry entry = input.readEntry(); 706 if (entry.getName().isInScopeOf(baseDN, scope) 707 && matcher.matches(entry).toBoolean()) { 708 nextEntry = filterEntry(entry); 709 break; 710 } 711 } 712 } 713 } 714 return nextEntry != null; 715 } 716 717 @Override 718 public Entry readEntry() throws IOException { 719 if (hasNext()) { 720 final Entry entry = nextEntry; 721 nextEntry = null; 722 entryCount++; 723 return entry; 724 } else { 725 throw new NoSuchElementException(); 726 } 727 } 728 729 private Entry filterEntry(final Entry entry) { 730 // TODO: rename attributes; move functionality to Entries. 731 if (search.getAttributes().isEmpty()) { 732 if (search.isTypesOnly()) { 733 final Entry filteredEntry = new LinkedHashMapEntry(entry.getName()); 734 for (final Attribute attribute : entry.getAllAttributes()) { 735 filteredEntry.addAttribute(Attributes.emptyAttribute(attribute 736 .getAttributeDescription())); 737 } 738 return filteredEntry; 739 } else { 740 return entry; 741 } 742 } else { 743 final Entry filteredEntry = new LinkedHashMapEntry(entry.getName()); 744 for (final String atd : search.getAttributes()) { 745 if ("*".equals(atd)) { 746 for (final Attribute attribute : entry.getAllAttributes()) { 747 if (attribute.getAttributeDescription().getAttributeType() 748 .getUsage() == AttributeUsage.USER_APPLICATIONS) { 749 if (search.isTypesOnly()) { 750 filteredEntry 751 .addAttribute(Attributes.emptyAttribute(attribute 752 .getAttributeDescription())); 753 } else { 754 filteredEntry.addAttribute(attribute); 755 } 756 } 757 } 758 } else if ("+".equals(atd)) { 759 for (final Attribute attribute : entry.getAllAttributes()) { 760 if (attribute.getAttributeDescription().getAttributeType() 761 .getUsage() != AttributeUsage.USER_APPLICATIONS) { 762 if (search.isTypesOnly()) { 763 filteredEntry 764 .addAttribute(Attributes.emptyAttribute(attribute 765 .getAttributeDescription())); 766 } else { 767 filteredEntry.addAttribute(attribute); 768 } 769 } 770 } 771 } else { 772 final AttributeDescription ad = 773 AttributeDescription.valueOf(atd, schema); 774 for (final Attribute attribute : entry.getAllAttributes(ad)) { 775 if (search.isTypesOnly()) { 776 filteredEntry.addAttribute(Attributes.emptyAttribute(attribute 777 .getAttributeDescription())); 778 } else { 779 filteredEntry.addAttribute(attribute); 780 } 781 } 782 } 783 } 784 return filteredEntry; 785 } 786 } 787 788 }; 789 } 790 791 private static List<byte[][]> readEntriesAsList(final EntryReader reader) throws IOException { 792 final List<byte[][]> entries = new ArrayList<>(); 793 794 while (reader.hasNext()) { 795 final Entry entry = reader.readEntry(); 796 entries.add(encodeEntry(entry)); 797 } 798 // Sorting the list by DN 799 Collections.sort(entries, DN_ORDER2); 800 801 return entries; 802 } 803 804 private static TreeMap<byte[], byte[]> readEntriesAsMap(final EntryReader reader) 805 throws IOException { 806 final TreeMap<byte[], byte[]> entries = new TreeMap<>(DN_ORDER); 807 808 while (reader.hasNext()) { 809 final Entry entry = reader.readEntry(); 810 final byte[][] bEntry = encodeEntry(entry); 811 entries.put(bEntry[0], bEntry[1]); 812 } 813 814 return entries; 815 } 816 817 private static Entry decodeEntry(final byte[] asn1EntryFormat) { 818 try { 819 return LDAP.readEntry(ASN1.getReader(asn1EntryFormat), new DecodeOptions()); 820 } catch (IOException ex) { 821 throw new IllegalStateException(ex); 822 } 823 } 824 825 private static byte[] toNormalizedByteArray(DN dn) { 826 return dn.toNormalizedByteString().toByteArray(); 827 } 828 829 private static byte[][] encodeEntry(final Entry entry) { 830 final byte[][] bEntry = new byte[2][]; 831 // Store normalized DN 832 bEntry[0] = toNormalizedByteArray(entry.getName()); 833 try { 834 // Store ASN1 representation of the entry. 835 final ByteStringBuilder bsb = new ByteStringBuilder(); 836 LDAP.writeEntry(ASN1.getWriter(bsb), entry); 837 bEntry[1] = bsb.toByteArray(); 838 return bEntry; 839 } catch (final IOException ioe) { 840 throw new IllegalStateException(ioe); 841 } 842 } 843 844 /** Prevent instantiation. */ 845 private LDIF() { 846 // Do nothing. 847 } 848}