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