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