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 2010 Sun Microsystems, Inc.
025 *      Portions copyright 2012-2015 ForgeRock AS.
026 */
027package org.forgerock.opendj.ldap.controls;
028
029import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
030import static com.forgerock.opendj.ldap.CoreMessages.*;
031
032import java.io.IOException;
033import java.util.ArrayList;
034import java.util.Arrays;
035import java.util.Collection;
036import java.util.Collections;
037import java.util.LinkedList;
038import java.util.List;
039import java.util.StringTokenizer;
040
041import org.forgerock.i18n.LocalizableMessage;
042import org.forgerock.i18n.LocalizedIllegalArgumentException;
043import org.forgerock.opendj.io.ASN1;
044import org.forgerock.opendj.io.ASN1Reader;
045import org.forgerock.opendj.io.ASN1Writer;
046import org.forgerock.opendj.ldap.ByteString;
047import org.forgerock.opendj.ldap.ByteStringBuilder;
048import org.forgerock.opendj.ldap.DecodeException;
049import org.forgerock.opendj.ldap.DecodeOptions;
050import org.forgerock.opendj.ldap.SortKey;
051import org.forgerock.util.Reject;
052
053/**
054 * The server-side sort request control as defined in RFC 2891. This control may
055 * be included in a search request to indicate that search result entries should
056 * be sorted by the server before being returned. The sort order is specified
057 * using one or more sort keys, the first being the primary key, and so on.
058 * <p>
059 * This controls may be useful when the client has limited functionality or for
060 * some other reason cannot sort the results but still needs them sorted. In
061 * cases where the client can sort the results client-side sorting is
062 * recommended in order to reduce load on the server. See {@link SortKey} for an
063 * example of client-side sorting.
064 * <p>
065 * The following example demonstrates how to work with a server-side sort.
066 *
067 * <pre>
068 * Connection connection = ...;
069 *
070 * SearchRequest request = Requests.newSearchRequest(
071 *         "ou=People,dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(sn=Jensen)", "cn")
072 *         .addControl(ServerSideSortRequestControl.newControl(true, new SortKey("cn")));
073 *
074 * SearchResultHandler resultHandler = new MySearchResultHandler();
075 * Result result = connection.search(request, resultHandler);
076 *
077 * ServerSideSortResponseControl control = result.getControl(
078 *         ServerSideSortResponseControl.DECODER, new DecodeOptions());
079 * if (control != null && control.getResult() == ResultCode.SUCCESS) {
080 *     // Entries are sorted.
081 * } else {
082 *     // Entries not sorted.
083 * }
084 * </pre>
085 *
086 * @see ServerSideSortResponseControl
087 * @see SortKey
088 * @see <a href="http://tools.ietf.org/html/rfc2891">RFC 2891 - LDAP Control
089 *      Extension for Server Side Sorting of Search Results </a>
090 */
091public final class ServerSideSortRequestControl implements Control {
092    /**
093     * The OID for the server-side sort request control.
094     */
095    public static final String OID = "1.2.840.113556.1.4.473";
096
097    /**
098     * The BER type to use when encoding the orderingRule element.
099     */
100    private static final byte TYPE_ORDERING_RULE_ID = (byte) 0x80;
101
102    /**
103     * The BER type to use when encoding the reverseOrder element.
104     */
105    private static final byte TYPE_REVERSE_ORDER = (byte) 0x81;
106
107    /**
108     * A decoder which can be used for decoding the server side sort request
109     * control.
110     */
111    public static final ControlDecoder<ServerSideSortRequestControl> DECODER =
112            new ControlDecoder<ServerSideSortRequestControl>() {
113
114                public ServerSideSortRequestControl decodeControl(final Control control,
115                        final DecodeOptions options) throws DecodeException {
116                    Reject.ifNull(control);
117
118                    if (control instanceof ServerSideSortRequestControl) {
119                        return (ServerSideSortRequestControl) control;
120                    }
121
122                    if (!control.getOID().equals(OID)) {
123                        final LocalizableMessage message =
124                                ERR_SORTREQ_CONTROL_BAD_OID.get(control.getOID(), OID);
125                        throw DecodeException.error(message);
126                    }
127
128                    if (!control.hasValue()) {
129                        // The request control must always have a value.
130                        final LocalizableMessage message = INFO_SORTREQ_CONTROL_NO_VALUE.get();
131                        throw DecodeException.error(message);
132                    }
133
134                    final ASN1Reader reader = ASN1.getReader(control.getValue());
135                    try {
136                        reader.readStartSequence();
137                        if (!reader.hasNextElement()) {
138                            final LocalizableMessage message =
139                                    INFO_SORTREQ_CONTROL_NO_SORT_KEYS.get();
140                            throw DecodeException.error(message);
141                        }
142
143                        final List<SortKey> keys = new LinkedList<>();
144                        while (reader.hasNextElement()) {
145                            reader.readStartSequence();
146                            final String attrName = reader.readOctetStringAsString();
147
148                            String orderingRule = null;
149                            boolean reverseOrder = false;
150                            if (reader.hasNextElement()
151                                    && (reader.peekType() == TYPE_ORDERING_RULE_ID)) {
152                                orderingRule = reader.readOctetStringAsString();
153                            }
154                            if (reader.hasNextElement()
155                                    && (reader.peekType() == TYPE_REVERSE_ORDER)) {
156                                reverseOrder = reader.readBoolean();
157                            }
158                            reader.readEndSequence();
159
160                            keys.add(new SortKey(attrName, reverseOrder, orderingRule));
161                        }
162                        reader.readEndSequence();
163
164                        return new ServerSideSortRequestControl(control.isCritical(), Collections
165                                .unmodifiableList(keys));
166                    } catch (final IOException e) {
167                        final LocalizableMessage message =
168                                INFO_SORTREQ_CONTROL_CANNOT_DECODE_VALUE
169                                        .get(getExceptionMessage(e));
170                        throw DecodeException.error(message, e);
171                    }
172                }
173
174                public String getOID() {
175                    return OID;
176                }
177            };
178
179    /**
180     * Creates a new server side sort request control with the provided
181     * criticality and list of sort keys.
182     *
183     * @param isCritical
184     *            {@code true} if it is unacceptable to perform the operation
185     *            without applying the semantics of this control, or
186     *            {@code false} if it can be ignored.
187     * @param keys
188     *            The list of sort keys.
189     * @return The new control.
190     * @throws IllegalArgumentException
191     *             If {@code keys} was empty.
192     * @throws NullPointerException
193     *             If {@code keys} was {@code null}.
194     */
195    public static ServerSideSortRequestControl newControl(final boolean isCritical,
196            final Collection<SortKey> keys) {
197        Reject.ifNull(keys);
198        Reject.ifFalse(!keys.isEmpty(), "keys must not be empty");
199
200        return new ServerSideSortRequestControl(isCritical, Collections
201                .unmodifiableList(new ArrayList<SortKey>(keys)));
202    }
203
204    /**
205     * Creates a new server side sort request control with the provided
206     * criticality and list of sort keys.
207     *
208     * @param isCritical
209     *            {@code true} if it is unacceptable to perform the operation
210     *            without applying the semantics of this control, or
211     *            {@code false} if it can be ignored.
212     * @param keys
213     *            The list of sort keys.
214     * @return The new control.
215     * @throws IllegalArgumentException
216     *             If {@code keys} was empty.
217     * @throws NullPointerException
218     *             If {@code keys} was {@code null}.
219     */
220    public static ServerSideSortRequestControl newControl(final boolean isCritical,
221            final SortKey... keys) {
222        return newControl(isCritical, Arrays.asList(keys));
223    }
224
225    /**
226     * Creates a new server side sort request control with the provided
227     * criticality and string representation of a list of sort keys. The string
228     * representation is comprised of a comma separate list of sort keys as
229     * defined in {@link SortKey#valueOf(String)}. There must be at least one
230     * sort key present in the string representation.
231     *
232     * @param isCritical
233     *            {@code true} if it is unacceptable to perform the operation
234     *            without applying the semantics of this control, or
235     *            {@code false} if it can be ignored.
236     * @param sortKeys
237     *            The list of sort keys.
238     * @return The new control.
239     * @throws LocalizedIllegalArgumentException
240     *             If {@code sortKeys} is not a valid string representation of a
241     *             list of sort keys.
242     * @throws NullPointerException
243     *             If {@code sortKeys} was {@code null}.
244     */
245    public static ServerSideSortRequestControl newControl(final boolean isCritical,
246            final String sortKeys) {
247        Reject.ifNull(sortKeys);
248
249        final List<SortKey> keys = new LinkedList<>();
250        final StringTokenizer tokenizer = new StringTokenizer(sortKeys, ",");
251        while (tokenizer.hasMoreTokens()) {
252            final String token = tokenizer.nextToken().trim();
253            keys.add(SortKey.valueOf(token));
254        }
255        if (keys.isEmpty()) {
256            final LocalizableMessage message = ERR_SORT_KEY_NO_SORT_KEYS.get(sortKeys);
257            throw new LocalizedIllegalArgumentException(message);
258        }
259        return new ServerSideSortRequestControl(isCritical, Collections.unmodifiableList(keys));
260    }
261
262    private final List<SortKey> sortKeys;
263
264    private final boolean isCritical;
265
266    private ServerSideSortRequestControl(final boolean isCritical, final List<SortKey> keys) {
267        this.isCritical = isCritical;
268        this.sortKeys = keys;
269    }
270
271    /** {@inheritDoc} */
272    public String getOID() {
273        return OID;
274    }
275
276    /**
277     * Returns an unmodifiable list containing the sort keys associated with
278     * this server side sort request control. The list will contain at least one
279     * sort key.
280     *
281     * @return An unmodifiable list containing the sort keys associated with
282     *         this server side sort request control.
283     */
284    public List<SortKey> getSortKeys() {
285        return sortKeys;
286    }
287
288    /** {@inheritDoc} */
289    public ByteString getValue() {
290        final ByteStringBuilder buffer = new ByteStringBuilder();
291        final ASN1Writer writer = ASN1.getWriter(buffer);
292        try {
293            writer.writeStartSequence();
294            for (final SortKey sortKey : sortKeys) {
295                writer.writeStartSequence();
296                writer.writeOctetString(sortKey.getAttributeDescription());
297
298                if (sortKey.getOrderingMatchingRule() != null) {
299                    writer.writeOctetString(TYPE_ORDERING_RULE_ID, sortKey
300                            .getOrderingMatchingRule());
301                }
302
303                if (sortKey.isReverseOrder()) {
304                    writer.writeBoolean(TYPE_REVERSE_ORDER, true);
305                }
306
307                writer.writeEndSequence();
308            }
309            writer.writeEndSequence();
310            return buffer.toByteString();
311        } catch (final IOException ioe) {
312            // This should never happen unless there is a bug somewhere.
313            throw new RuntimeException(ioe);
314        }
315    }
316
317    /** {@inheritDoc} */
318    public boolean hasValue() {
319        return true;
320    }
321
322    /** {@inheritDoc} */
323    public boolean isCritical() {
324        return isCritical;
325    }
326
327    /** {@inheritDoc} */
328    @Override
329    public String toString() {
330        final StringBuilder buffer = new StringBuilder();
331        buffer.append("ServerSideSortRequestControl(oid=");
332        buffer.append(getOID());
333        buffer.append(", criticality=");
334        buffer.append(isCritical());
335        buffer.append(", sortKeys=");
336        buffer.append(sortKeys);
337        buffer.append(")");
338        return buffer.toString();
339    }
340
341}