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 2009 Sun Microsystems, Inc.
025 *      Portions copyright 2012-2015 ForgeRock AS.
026 */
027package org.forgerock.opendj.ldap.controls;
028
029import static com.forgerock.opendj.ldap.CoreMessages.*;
030import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
031
032import java.io.IOException;
033import java.util.ArrayList;
034import java.util.Collection;
035import java.util.Collections;
036import java.util.LinkedList;
037import java.util.List;
038
039import org.forgerock.i18n.LocalizableMessage;
040import org.forgerock.i18n.LocalizedIllegalArgumentException;
041import org.forgerock.i18n.slf4j.LocalizedLogger;
042import org.forgerock.opendj.io.ASN1;
043import org.forgerock.opendj.io.ASN1Reader;
044import org.forgerock.opendj.io.ASN1Writer;
045import org.forgerock.opendj.io.LDAP;
046import org.forgerock.opendj.ldap.AbstractFilterVisitor;
047import org.forgerock.opendj.ldap.ByteString;
048import org.forgerock.opendj.ldap.ByteStringBuilder;
049import org.forgerock.opendj.ldap.DecodeException;
050import org.forgerock.opendj.ldap.DecodeOptions;
051import org.forgerock.opendj.ldap.Filter;
052import org.forgerock.util.Reject;
053
054/**
055 * The matched values request control as defined in RFC 3876. The matched values
056 * control may be included in a search request to indicate that only attribute
057 * values matching one or more filters contained in the matched values control
058 * should be returned to the client.
059 * <p>
060 * The matched values request control supports a subset of the LDAP filter type
061 * defined in RFC 4511, and is defined as follows:
062 *
063 * <pre>
064 * ValuesReturnFilter ::= SEQUENCE OF SimpleFilterItem
065 *
066 * SimpleFilterItem ::= CHOICE {
067 *        equalityMatch   [3] AttributeValueAssertion,
068 *        substrings      [4] SubstringFilter,
069 *        greaterOrEqual  [5] AttributeValueAssertion,
070 *        lessOrEqual     [6] AttributeValueAssertion,
071 *        present         [7] AttributeDescription,
072 *        approxMatch     [8] AttributeValueAssertion,
073 *        extensibleMatch [9] SimpleMatchingAssertion }
074 *
075 * SimpleMatchingAssertion ::= SEQUENCE {
076 *        matchingRule    [1] MatchingRuleId OPTIONAL,
077 *        type            [2] AttributeDescription OPTIONAL,
078 * --- at least one of the above must be present
079 *        matchValue      [3] AssertionValue}
080 * </pre>
081 *
082 * For example Barbara Jensen's entry contains two common name values, Barbara
083 * Jensen and Babs Jensen. The following code retrieves only the latter.
084 *
085 * <pre>
086 * String DN = &quot;uid=bjensen,ou=People,dc=example,dc=com&quot;;
087 * SearchRequest request = Requests.newSearchRequest(DN,
088 *          SearchScope.BASE_OBJECT, &quot;(objectclass=*)&quot;, &quot;cn&quot;)
089 *          .addControl(MatchedValuesRequestControl
090 *                  .newControl(true, &quot;(cn=Babs Jensen)&quot;));
091 *
092 * // Get the entry, retrieving cn: Babs Jensen, not cn: Barbara Jensen
093 * SearchResultEntry entry = connection.searchSingleEntry(request);
094 * </pre>
095 *
096 * @see <a href="http://tools.ietf.org/html/rfc3876">RFC 3876 - Returning
097 *      Matched Values with the Lightweight Directory Access Protocol version 3
098 *      (LDAPv3) </a>
099 */
100public final class MatchedValuesRequestControl implements Control {
101    /**
102     * Visitor for validating matched values filters.
103     */
104    private static final class FilterValidator extends
105            AbstractFilterVisitor<LocalizedIllegalArgumentException, Filter> {
106
107        @Override
108        public LocalizedIllegalArgumentException visitAndFilter(final Filter p,
109                final List<Filter> subFilters) {
110            final LocalizableMessage message = ERR_MVFILTER_BAD_FILTER_AND.get(p.toString());
111            return new LocalizedIllegalArgumentException(message);
112        }
113
114        @Override
115        public LocalizedIllegalArgumentException visitExtensibleMatchFilter(final Filter p,
116                final String matchingRule, final String attributeDescription,
117                final ByteString assertionValue, final boolean dnAttributes) {
118            if (dnAttributes) {
119                final LocalizableMessage message = ERR_MVFILTER_BAD_FILTER_EXT.get(p.toString());
120                return new LocalizedIllegalArgumentException(message);
121            } else {
122                return null;
123            }
124        }
125
126        @Override
127        public LocalizedIllegalArgumentException visitNotFilter(final Filter p,
128                final Filter subFilter) {
129            final LocalizableMessage message = ERR_MVFILTER_BAD_FILTER_NOT.get(p.toString());
130            return new LocalizedIllegalArgumentException(message);
131        }
132
133        @Override
134        public LocalizedIllegalArgumentException visitOrFilter(final Filter p,
135                final List<Filter> subFilters) {
136            final LocalizableMessage message = ERR_MVFILTER_BAD_FILTER_OR.get(p.toString());
137            return new LocalizedIllegalArgumentException(message);
138        }
139
140        @Override
141        public LocalizedIllegalArgumentException visitUnrecognizedFilter(final Filter p,
142                final byte filterTag, final ByteString filterBytes) {
143            final LocalizableMessage message =
144                    ERR_MVFILTER_BAD_FILTER_UNRECOGNIZED.get(p.toString(), filterTag);
145            return new LocalizedIllegalArgumentException(message);
146        }
147    }
148
149    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
150
151    /**
152     * The OID for the matched values request control used to specify which
153     * particular attribute values should be returned in a search result entry.
154     */
155    public static final String OID = "1.2.826.0.1.3344810.2.3";
156
157    /**
158     * A decoder which can be used for decoding the matched values request
159     * control.
160     */
161    public static final ControlDecoder<MatchedValuesRequestControl> DECODER =
162            new ControlDecoder<MatchedValuesRequestControl>() {
163
164                public MatchedValuesRequestControl decodeControl(final Control control,
165                        final DecodeOptions options) throws DecodeException {
166                    Reject.ifNull(control);
167
168                    if (control instanceof MatchedValuesRequestControl) {
169                        return (MatchedValuesRequestControl) control;
170                    }
171
172                    if (!control.getOID().equals(OID)) {
173                        final LocalizableMessage message =
174                                ERR_MATCHEDVALUES_CONTROL_BAD_OID.get(control.getOID(), OID);
175                        throw DecodeException.error(message);
176                    }
177
178                    if (!control.hasValue()) {
179                        // The response control must always have a value.
180                        final LocalizableMessage message = ERR_MATCHEDVALUES_NO_CONTROL_VALUE.get();
181                        throw DecodeException.error(message);
182                    }
183
184                    final ASN1Reader reader = ASN1.getReader(control.getValue());
185                    try {
186                        reader.readStartSequence();
187                        if (!reader.hasNextElement()) {
188                            final LocalizableMessage message = ERR_MATCHEDVALUES_NO_FILTERS.get();
189                            throw DecodeException.error(message);
190                        }
191
192                        final LinkedList<Filter> filters = new LinkedList<>();
193                        do {
194                            final Filter filter = LDAP.readFilter(reader);
195                            try {
196                                validateFilter(filter);
197                            } catch (final LocalizedIllegalArgumentException e) {
198                                throw DecodeException.error(e.getMessageObject());
199                            }
200                            filters.add(filter);
201                        } while (reader.hasNextElement());
202
203                        reader.readEndSequence();
204
205                        return new MatchedValuesRequestControl(control.isCritical(), Collections
206                                .unmodifiableList(filters));
207                    } catch (final IOException e) {
208                        logger.debug(LocalizableMessage.raw("%s", e));
209
210                        final LocalizableMessage message =
211                                ERR_MATCHEDVALUES_CANNOT_DECODE_VALUE_AS_SEQUENCE
212                                        .get(getExceptionMessage(e));
213                        throw DecodeException.error(message);
214                    }
215                }
216
217                public String getOID() {
218                    return OID;
219                }
220            };
221
222    private static final FilterValidator FILTER_VALIDATOR = new FilterValidator();
223
224    /**
225     * Creates a new matched values request control with the provided
226     * criticality and list of filters.
227     *
228     * @param isCritical
229     *            {@code true} if it is unacceptable to perform the operation
230     *            without applying the semantics of this control, or
231     *            {@code false} if it can be ignored.
232     * @param filters
233     *            The list of filters of which at least one must match an
234     *            attribute value in order for the attribute value to be
235     *            returned to the client. The list must not be empty.
236     * @return The new control.
237     * @throws LocalizedIllegalArgumentException
238     *             If one or more filters failed to conform to the filter
239     *             constraints defined in RFC 3876.
240     * @throws IllegalArgumentException
241     *             If {@code filters} was empty.
242     * @throws NullPointerException
243     *             If {@code filters} was {@code null}.
244     */
245    public static MatchedValuesRequestControl newControl(final boolean isCritical,
246            final Collection<Filter> filters) {
247        Reject.ifNull(filters);
248        Reject.ifFalse(filters.size() > 0, "filters is empty");
249
250        List<Filter> copyOfFilters;
251        if (filters.size() == 1) {
252            copyOfFilters = Collections.singletonList(validateFilter(filters.iterator().next()));
253        } else {
254            copyOfFilters = new ArrayList<>(filters.size());
255            for (final Filter filter : filters) {
256                copyOfFilters.add(validateFilter(filter));
257            }
258            copyOfFilters = Collections.unmodifiableList(copyOfFilters);
259        }
260
261        return new MatchedValuesRequestControl(isCritical, copyOfFilters);
262    }
263
264    /**
265     * Creates a new matched values request control with the provided
266     * criticality and list of filters.
267     *
268     * @param isCritical
269     *            {@code true} if it is unacceptable to perform the operation
270     *            without applying the semantics of this control, or
271     *            {@code false} if it can be ignored.
272     * @param filters
273     *            The list of filters of which at least one must match an
274     *            attribute value in order for the attribute value to be
275     *            returned to the client. The list must not be empty.
276     * @return The new control.
277     * @throws LocalizedIllegalArgumentException
278     *             If one or more filters could not be parsed, or if one or more
279     *             filters failed to conform to the filter constraints defined
280     *             in RFC 3876.
281     * @throws NullPointerException
282     *             If {@code filters} was {@code null}.
283     */
284    public static MatchedValuesRequestControl newControl(final boolean isCritical,
285            final String... filters) {
286        Reject.ifFalse(filters.length > 0, "filters is empty");
287
288        final List<Filter> parsedFilters = new ArrayList<>(filters.length);
289        for (final String filter : filters) {
290            parsedFilters.add(validateFilter(Filter.valueOf(filter)));
291        }
292        return new MatchedValuesRequestControl(isCritical, Collections
293                .unmodifiableList(parsedFilters));
294    }
295
296    private static Filter validateFilter(final Filter filter) {
297        final LocalizedIllegalArgumentException e = filter.accept(FILTER_VALIDATOR, filter);
298        if (e != null) {
299            throw e;
300        }
301        return filter;
302    }
303
304    private final Collection<Filter> filters;
305
306    private final boolean isCritical;
307
308    private MatchedValuesRequestControl(final boolean isCritical, final Collection<Filter> filters) {
309        this.isCritical = isCritical;
310        this.filters = filters;
311    }
312
313    /**
314     * Returns an unmodifiable collection containing the list of filters
315     * associated with this matched values control.
316     *
317     * @return An unmodifiable collection containing the list of filters
318     *         associated with this matched values control.
319     */
320    public Collection<Filter> getFilters() {
321        return filters;
322    }
323
324    /** {@inheritDoc} */
325    public String getOID() {
326        return OID;
327    }
328
329    /** {@inheritDoc} */
330    @Override
331    public ByteString getValue() {
332        final ByteStringBuilder buffer = new ByteStringBuilder();
333        final ASN1Writer writer = ASN1.getWriter(buffer);
334        try {
335            writer.writeStartSequence();
336            for (final Filter f : filters) {
337                LDAP.writeFilter(writer, f);
338            }
339            writer.writeEndSequence();
340            return buffer.toByteString();
341        } catch (final IOException ioe) {
342            // This should never happen unless there is a bug somewhere.
343            throw new RuntimeException(ioe);
344        }
345    }
346
347    /** {@inheritDoc} */
348    public boolean hasValue() {
349        return true;
350    }
351
352    /** {@inheritDoc} */
353    public boolean isCritical() {
354        return isCritical;
355    }
356
357    /** {@inheritDoc} */
358    @Override
359    public String toString() {
360        final StringBuilder builder = new StringBuilder();
361        builder.append("MatchedValuesRequestControl(oid=");
362        builder.append(getOID());
363        builder.append(", criticality=");
364        builder.append(isCritical());
365        builder.append(")");
366        return builder.toString();
367    }
368}