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}