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