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