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}