001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2013-2015 ForgeRock AS.
025 */
026
027package org.forgerock.opendj.ldap;
028
029import static org.forgerock.opendj.ldap.Attributes.renameAttribute;
030
031import java.util.Arrays;
032import java.util.Collection;
033import java.util.Collections;
034import java.util.HashMap;
035import java.util.Iterator;
036import java.util.Map;
037import java.util.NoSuchElementException;
038
039import org.forgerock.opendj.ldap.schema.AttributeType;
040import org.forgerock.opendj.ldap.schema.ObjectClass;
041import org.forgerock.opendj.ldap.schema.Schema;
042
043import com.forgerock.opendj.util.Iterables;
044
045/**
046 * A configurable factory for filtering the attributes exposed by an entry. An
047 * {@code AttributeFilter} is useful for performing fine-grained access control,
048 * selecting attributes based on search request criteria, and selecting
049 * attributes based on post- and pre- read request control criteria.
050 * <p>
051 * In cases where methods accept a string based list of attribute descriptions,
052 * the following special attribute descriptions are permitted:
053 * <ul>
054 * <li><b>*</b> - include all user attributes
055 * <li><b>+</b> - include all operational attributes
056 * <li><b>1.1</b> - exclude all attributes
057 * <li><b>@<i>objectclass</i></b> - include all attributes identified by the
058 * named object class.
059 * </ul>
060 */
061public final class AttributeFilter {
062    // TODO: exclude specific attributes, matched values, custom predicates, etc.
063    private boolean includeAllOperationalAttributes;
064    /** Depends on constructor. */
065    private boolean includeAllUserAttributes;
066    private boolean typesOnly;
067
068    /**
069     * Use a map so that we can perform membership checks as well as recover the
070     * user requested attribute description.
071     */
072    private Map<AttributeDescription, AttributeDescription> requestedAttributes = Collections
073            .emptyMap();
074
075    /**
076     * Creates a new attribute filter which will include all user attributes but
077     * no operational attributes.
078     */
079    public AttributeFilter() {
080        includeAllUserAttributes = true;
081    }
082
083    /**
084     * Creates a new attribute filter which will include the attributes
085     * identified by the provided search request attribute list. Attributes will
086     * be decoded using the default schema. See the class description for
087     * details regarding the types of supported attribute description.
088     *
089     * @param attributeDescriptions
090     *            The names of the attributes to be included with each entry.
091     */
092    public AttributeFilter(final Collection<String> attributeDescriptions) {
093        this(attributeDescriptions, Schema.getDefaultSchema());
094    }
095
096    /**
097     * Creates a new attribute filter which will include the attributes
098     * identified by the provided search request attribute list. Attributes will
099     * be decoded using the provided schema. See the class description for
100     * details regarding the types of supported attribute description.
101     *
102     * @param attributeDescriptions
103     *            The names of the attributes to be included with each entry.
104     * @param schema
105     *            The schema The schema to use when parsing attribute
106     *            descriptions and object class names.
107     */
108    public AttributeFilter(final Collection<String> attributeDescriptions, final Schema schema) {
109        if (attributeDescriptions == null || attributeDescriptions.isEmpty()) {
110            // Fast-path for common case.
111            includeAllUserAttributes = true;
112        } else {
113            for (final String attribute : attributeDescriptions) {
114                includeAttribute(attribute, schema);
115            }
116        }
117    }
118
119    /**
120     * Creates a new attribute filter which will include the attributes
121     * identified by the provided search request attribute list. Attributes will
122     * be decoded using the default schema. See the class description for
123     * details regarding the types of supported attribute description.
124     *
125     * @param attributeDescriptions
126     *            The names of the attributes to be included with each entry.
127     */
128    public AttributeFilter(final String... attributeDescriptions) {
129        this(Arrays.asList(attributeDescriptions));
130    }
131
132    /**
133     * Returns a modifiable filtered copy of the provided entry.
134     *
135     * @param entry
136     *            The entry to be filtered and copied.
137     * @return The modifiable filtered copy of the provided entry.
138     */
139    public Entry filteredCopyOf(final Entry entry) {
140        return new LinkedHashMapEntry(filteredViewOf(entry));
141    }
142
143    /**
144     * Returns an unmodifiable filtered view of the provided entry. The returned
145     * entry supports all operations except those which modify the contents of
146     * the entry.
147     *
148     * @param entry
149     *            The entry to be filtered.
150     * @return The unmodifiable filtered view of the provided entry.
151     */
152    public Entry filteredViewOf(final Entry entry) {
153        return new AbstractEntry() {
154
155            @Override
156            public boolean addAttribute(final Attribute attribute,
157                    final Collection<? super ByteString> duplicateValues) {
158                throw new UnsupportedOperationException();
159            }
160
161            @Override
162            public Entry clearAttributes() {
163                throw new UnsupportedOperationException();
164            }
165
166            @Override
167            public Iterable<Attribute> getAllAttributes() {
168                /*
169                 * Unfortunately we cannot efficiently re-use the iterators in
170                 * {@code Iterators} because we need to transform and filter in
171                 * a single step. Transformation is required in order to ensure
172                 * that we return an attribute whose name is the same as the one
173                 * requested by the user.
174                 */
175                return new Iterable<Attribute>() {
176                    private boolean hasNextMustIterate = true;
177                    private final Iterator<Attribute> iterator = entry.getAllAttributes().iterator();
178                    private Attribute next = null;
179
180                    @Override
181                    public Iterator<Attribute> iterator() {
182                        return new Iterator<Attribute>() {
183                            @Override
184                            public boolean hasNext() {
185                                if (hasNextMustIterate) {
186                                    hasNextMustIterate = false;
187                                    while (iterator.hasNext()) {
188                                        final Attribute attribute = iterator.next();
189                                        final AttributeDescription ad = attribute.getAttributeDescription();
190                                        final AttributeType at = ad.getAttributeType();
191                                        final AttributeDescription requestedAd = requestedAttributes.get(ad);
192                                        if (requestedAd != null) {
193                                            next = renameAttribute(attribute, requestedAd);
194                                            return true;
195                                        } else if ((at.isOperational() && includeAllOperationalAttributes)
196                                                || (!at.isOperational() && includeAllUserAttributes)) {
197                                            next = attribute;
198                                            return true;
199                                        }
200                                    }
201                                    next = null;
202                                    return false;
203                                } else {
204                                    return next != null;
205                                }
206                            }
207
208                            @Override
209                            public Attribute next() {
210                                if (!hasNext()) {
211                                    throw new NoSuchElementException();
212                                }
213                                hasNextMustIterate = true;
214                                return filterAttribute(next);
215                            }
216
217                            @Override
218                            public void remove() {
219                                throw new UnsupportedOperationException();
220                            }
221                        };
222                    }
223
224                    @Override
225                    public String toString() {
226                        return Iterables.toString(this);
227                    }
228                };
229            }
230
231            @Override
232            public Attribute getAttribute(final AttributeDescription attributeDescription) {
233                /*
234                 * It is tempting to filter based on the passed in attribute
235                 * description, but we may get inaccurate results due to
236                 * placeholder attribute names.
237                 */
238                final Attribute attribute = entry.getAttribute(attributeDescription);
239                if (attribute != null) {
240                    final AttributeDescription ad = attribute.getAttributeDescription();
241                    final AttributeType at = ad.getAttributeType();
242                    final AttributeDescription requestedAd = requestedAttributes.get(ad);
243                    if (requestedAd != null) {
244                        return filterAttribute(renameAttribute(attribute, requestedAd));
245                    } else if ((at.isOperational() && includeAllOperationalAttributes)
246                            || (!at.isOperational() && includeAllUserAttributes)) {
247                        return filterAttribute(attribute);
248                    }
249                }
250                return null;
251            }
252
253            @Override
254            public int getAttributeCount() {
255                return Iterables.size(getAllAttributes());
256            }
257
258            @Override
259            public DN getName() {
260                return entry.getName();
261            }
262
263            @Override
264            public Entry setName(final DN dn) {
265                throw new UnsupportedOperationException();
266            }
267        };
268    }
269
270    /**
271     * Specifies whether or not all operational attributes should be included in
272     * filtered entries. By default operational attributes are not included.
273     *
274     * @param include
275     *            {@code true} if operational attributes should be included in
276     *            filtered entries.
277     * @return A reference to this attribute filter.
278     */
279    public AttributeFilter includeAllOperationalAttributes(final boolean include) {
280        this.includeAllOperationalAttributes = include;
281        return this;
282    }
283
284    /**
285     * Specifies whether or not all user attributes should be included in
286     * filtered entries. By default user attributes are included.
287     *
288     * @param include
289     *            {@code true} if user attributes should be included in filtered
290     *            entries.
291     * @return A reference to this attribute filter.
292     */
293    public AttributeFilter includeAllUserAttributes(final boolean include) {
294        this.includeAllUserAttributes = include;
295        return this;
296    }
297
298    /**
299     * Specifies that the named attribute should be included in filtered
300     * entries.
301     *
302     * @param attributeDescription
303     *            The name of the attribute to be included in filtered entries.
304     * @return A reference to this attribute filter.
305     */
306    public AttributeFilter includeAttribute(final AttributeDescription attributeDescription) {
307        allocatedRequestedAttributes();
308        requestedAttributes.put(attributeDescription, attributeDescription);
309        return this;
310    }
311
312    /**
313     * Specifies that the named attribute should be included in filtered
314     * entries. The attribute will be decoded using the default schema. See the
315     * class description for details regarding the types of supported attribute
316     * description.
317     *
318     * @param attributeDescription
319     *            The name of the attribute to be included in filtered entries.
320     * @return A reference to this attribute filter.
321     */
322    public AttributeFilter includeAttribute(final String attributeDescription) {
323        return includeAttribute(attributeDescription, Schema.getDefaultSchema());
324    }
325
326    /**
327     * Specifies that the named attribute should be included in filtered
328     * entries. The attribute will be decoded using the provided schema. See the
329     * class description for details regarding the types of supported attribute
330     * description.
331     *
332     * @param attributeDescription
333     *            The name of the attribute to be included in filtered entries.
334     * @param schema
335     *            The schema The schema to use when parsing attribute
336     *            descriptions and object class names.
337     * @return A reference to this attribute filter.
338     */
339    public AttributeFilter includeAttribute(final String attributeDescription, final Schema schema) {
340        if (attributeDescription.equals("*")) {
341            includeAllUserAttributes = true;
342        } else if (attributeDescription.equals("+")) {
343            includeAllOperationalAttributes = true;
344        } else if (attributeDescription.equals("1.1")) {
345            // Ignore - by default no attributes are included.
346        } else if (attributeDescription.startsWith("@") && attributeDescription.length() > 1) {
347            final String objectClassName = attributeDescription.substring(1);
348            final ObjectClass objectClass = schema.getObjectClass(objectClassName);
349            if (objectClass != null) {
350                allocatedRequestedAttributes();
351                for (final AttributeType at : objectClass.getRequiredAttributes()) {
352                    final AttributeDescription ad = AttributeDescription.create(at);
353                    requestedAttributes.put(ad, ad);
354                }
355                for (final AttributeType at : objectClass.getOptionalAttributes()) {
356                    final AttributeDescription ad = AttributeDescription.create(at);
357                    requestedAttributes.put(ad, ad);
358                }
359            }
360        } else {
361            allocatedRequestedAttributes();
362            final AttributeDescription ad =
363                    AttributeDescription.valueOf(attributeDescription, schema);
364            requestedAttributes.put(ad, ad);
365        }
366        return this;
367    }
368
369    @Override
370    public String toString() {
371        if (!includeAllOperationalAttributes
372                && !includeAllUserAttributes
373                && requestedAttributes.isEmpty()) {
374            return "1.1";
375        }
376
377        final StringBuilder builder = new StringBuilder();
378        if (includeAllUserAttributes) {
379            builder.append('*');
380        }
381        if (includeAllOperationalAttributes) {
382            if (builder.length() > 0) {
383                builder.append(", ");
384            }
385            builder.append('+');
386        }
387        for (final AttributeDescription requestedAttribute : requestedAttributes.keySet()) {
388            if (builder.length() > 0) {
389                builder.append(", ");
390            }
391            builder.append(requestedAttribute);
392        }
393        return builder.toString();
394    }
395
396    /**
397     * Specifies whether or not filtered attributes are to contain both
398     * attribute descriptions and values, or just attribute descriptions.
399     *
400     * @param typesOnly
401     *            {@code true} if only attribute descriptions (and not values)
402     *            are to be included, or {@code false} (the default) if both
403     *            attribute descriptions and values are to be included.
404     * @return A reference to this attribute filter.
405     */
406    public AttributeFilter typesOnly(final boolean typesOnly) {
407        this.typesOnly = typesOnly;
408        return this;
409    }
410
411    private void allocatedRequestedAttributes() {
412        if (requestedAttributes.isEmpty()) {
413            requestedAttributes = new HashMap<>();
414        }
415    }
416
417    private Attribute filterAttribute(final Attribute attribute) {
418        return typesOnly
419            ? Attributes.emptyAttribute(attribute.getAttributeDescription())
420            : attribute;
421    }
422}