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.ERR_SORTRES_CONTROL_BAD_OID;
031import static com.forgerock.opendj.ldap.CoreMessages.INFO_SORTRES_CONTROL_CANNOT_DECODE_VALUE;
032import static com.forgerock.opendj.ldap.CoreMessages.INFO_SORTRES_CONTROL_NO_VALUE;
033
034import java.io.IOException;
035
036import org.forgerock.i18n.LocalizableMessage;
037import org.forgerock.i18n.LocalizedIllegalArgumentException;
038import org.forgerock.opendj.io.ASN1;
039import org.forgerock.opendj.io.ASN1Reader;
040import org.forgerock.opendj.io.ASN1Writer;
041import org.forgerock.opendj.ldap.AttributeDescription;
042import org.forgerock.opendj.ldap.ByteString;
043import org.forgerock.opendj.ldap.ByteStringBuilder;
044import org.forgerock.opendj.ldap.DecodeException;
045import org.forgerock.opendj.ldap.DecodeOptions;
046import org.forgerock.opendj.ldap.ResultCode;
047import org.forgerock.opendj.ldap.schema.Schema;
048import org.forgerock.util.Reject;
049
050/**
051 * The server-side sort response control as defined in RFC 2891. This control is
052 * included with a search result in response to a server-side sort request
053 * included with a search request. The client application is assured that the
054 * search results are sorted in the specified key order if and only if the
055 * result code in this control is success. If the server omits this control from
056 * the search result, the client SHOULD assume that the sort control was ignored
057 * by the server.
058 * <p>
059 * The following example demonstrates how to work with a server-side sort.
060 *
061 * <pre>
062 * Connection connection = ...;
063 *
064 * SearchRequest request = Requests.newSearchRequest(
065 *         "ou=People,dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(sn=Jensen)", "cn")
066 *         .addControl(ServerSideSortRequestControl.newControl(true, new SortKey("cn")));
067 *
068 * SearchResultHandler resultHandler = new MySearchResultHandler();
069 * Result result = connection.search(request, resultHandler);
070 *
071 * ServerSideSortResponseControl control = result.getControl(
072 *         ServerSideSortResponseControl.DECODER, new DecodeOptions());
073 * if (control != null && control.getResult() == ResultCode.SUCCESS) {
074 *     // Entries are sorted.
075 * } else {
076 *     // Entries not sorted.
077 * }
078 * </pre>
079 *
080 * @see ServerSideSortRequestControl
081 * @see <a href="http://tools.ietf.org/html/rfc2891">RFC 2891 - LDAP Control
082 *      Extension for Server Side Sorting of Search Results </a>
083 */
084public final class ServerSideSortResponseControl implements Control {
085    /**
086     * The OID for the server-side sort response control.
087     */
088    public static final String OID = "1.2.840.113556.1.4.474";
089
090    /**
091     * A decoder which can be used for decoding the server side sort response
092     * control.
093     */
094    public static final ControlDecoder<ServerSideSortResponseControl> DECODER =
095            new ControlDecoder<ServerSideSortResponseControl>() {
096
097                public ServerSideSortResponseControl decodeControl(final Control control,
098                        final DecodeOptions options) throws DecodeException {
099                    Reject.ifNull(control, options);
100
101                    if (control instanceof ServerSideSortResponseControl) {
102                        return (ServerSideSortResponseControl) control;
103                    }
104
105                    if (!control.getOID().equals(OID)) {
106                        final LocalizableMessage message =
107                                ERR_SORTRES_CONTROL_BAD_OID.get(control.getOID(), OID);
108                        throw DecodeException.error(message);
109                    }
110
111                    if (!control.hasValue()) {
112                        // The request control must always have a value.
113                        final LocalizableMessage message = INFO_SORTRES_CONTROL_NO_VALUE.get();
114                        throw DecodeException.error(message);
115                    }
116
117                    final ASN1Reader reader = ASN1.getReader(control.getValue());
118                    try {
119                        reader.readStartSequence();
120
121                        // FIXME: should really check that result code is one of
122                        // the expected
123                        // values listed in the RFC.
124                        final ResultCode result = ResultCode.valueOf(reader.readEnumerated());
125
126                        AttributeDescription attributeDescription = null;
127                        if (reader.hasNextElement()) {
128                            // FIXME: which schema should we use?
129                            final Schema schema = options.getSchemaResolver().resolveSchema("");
130                            final String ads = reader.readOctetStringAsString();
131                            attributeDescription = AttributeDescription.valueOf(ads, schema);
132                        }
133
134                        return new ServerSideSortResponseControl(control.isCritical(), result,
135                                attributeDescription);
136                    } catch (final IOException | LocalizedIllegalArgumentException e) {
137                        final LocalizableMessage message =
138                                INFO_SORTRES_CONTROL_CANNOT_DECODE_VALUE.get(getExceptionMessage(e));
139                        throw DecodeException.error(message, e);
140                    }
141                }
142
143                public String getOID() {
144                    return OID;
145                }
146            };
147
148    /**
149     * The BER type to use when encoding the attribute type element.
150     */
151    private static final byte TYPE_ATTRIBUTE_TYPE = (byte) 0x80;
152
153    /**
154     * Creates a new server-side response control with the provided sort result
155     * and no attribute description.
156     *
157     * @param result
158     *            The result code indicating the outcome of the server-side sort
159     *            request. {@link ResultCode#SUCCESS} if the search results were
160     *            sorted in accordance with the keys specified in the
161     *            server-side sort request control, or an error code indicating
162     *            why the results could not be sorted (such as
163     *            {@link ResultCode#NO_SUCH_ATTRIBUTE} or
164     *            {@link ResultCode#INAPPROPRIATE_MATCHING}).
165     * @return The new control.
166     * @throws NullPointerException
167     *             If {@code result} was {@code null}.
168     */
169    public static ServerSideSortResponseControl newControl(final ResultCode result) {
170        Reject.ifNull(result);
171
172        return new ServerSideSortResponseControl(false, result, null);
173    }
174
175    /**
176     * Creates a new server-side response control with the provided sort result
177     * and attribute description.
178     *
179     * @param result
180     *            The result code indicating the outcome of the server-side sort
181     *            request. {@link ResultCode#SUCCESS} if the search results were
182     *            sorted in accordance with the keys specified in the
183     *            server-side sort request control, or an error code indicating
184     *            why the results could not be sorted (such as
185     *            {@link ResultCode#NO_SUCH_ATTRIBUTE} or
186     *            {@link ResultCode#INAPPROPRIATE_MATCHING}).
187     * @param attributeDescription
188     *            The first attribute description specified in the list of sort
189     *            keys that was in error, may be {@code null}.
190     * @return The new control.
191     * @throws NullPointerException
192     *             If {@code result} was {@code null}.
193     */
194    public static ServerSideSortResponseControl newControl(final ResultCode result,
195            final AttributeDescription attributeDescription) {
196        Reject.ifNull(result);
197
198        return new ServerSideSortResponseControl(false, result, attributeDescription);
199    }
200
201    /**
202     * Creates a new server-side response control with the provided sort result
203     * and attribute description. The attribute description will be decoded
204     * using the default schema.
205     *
206     * @param result
207     *            The result code indicating the outcome of the server-side sort
208     *            request. {@link ResultCode#SUCCESS} if the search results were
209     *            sorted in accordance with the keys specified in the
210     *            server-side sort request control, or an error code indicating
211     *            why the results could not be sorted (such as
212     *            {@link ResultCode#NO_SUCH_ATTRIBUTE} or
213     *            {@link ResultCode#INAPPROPRIATE_MATCHING}).
214     * @param attributeDescription
215     *            The first attribute description specified in the list of sort
216     *            keys that was in error, may be {@code null}.
217     * @return The new control.
218     * @throws LocalizedIllegalArgumentException
219     *             If {@code attributeDescription} could not be parsed using the
220     *             default schema.
221     * @throws NullPointerException
222     *             If {@code result} was {@code null}.
223     */
224    public static ServerSideSortResponseControl newControl(final ResultCode result,
225            final String attributeDescription) {
226        Reject.ifNull(result);
227
228        if (attributeDescription != null) {
229            return new ServerSideSortResponseControl(false, result, AttributeDescription
230                    .valueOf(attributeDescription));
231        } else {
232            return new ServerSideSortResponseControl(false, result, null);
233        }
234    }
235
236    private final ResultCode result;
237
238    private final AttributeDescription attributeDescription;
239
240    private final boolean isCritical;
241
242    private ServerSideSortResponseControl(final boolean isCritical, final ResultCode result,
243            final AttributeDescription attributeDescription) {
244        this.isCritical = isCritical;
245        this.result = result;
246        this.attributeDescription = attributeDescription;
247    }
248
249    /**
250     * Returns the first attribute description specified in the list of sort
251     * keys that was in error, or {@code null} if the attribute description was
252     * not included with this control.
253     *
254     * @return The first attribute description specified in the list of sort
255     *         keys that was in error.
256     */
257    public AttributeDescription getAttributeDescription() {
258        return attributeDescription;
259    }
260
261    /** {@inheritDoc} */
262    public String getOID() {
263        return OID;
264    }
265
266    /**
267     * Returns a result code indicating the outcome of the server-side sort
268     * request. This will be {@link ResultCode#SUCCESS} if the search results
269     * were sorted in accordance with the keys specified in the server-side sort
270     * request control, or an error code indicating why the results could not be
271     * sorted (such as {@link ResultCode#NO_SUCH_ATTRIBUTE} or
272     * {@link ResultCode#INAPPROPRIATE_MATCHING}).
273     *
274     * @return The result code indicating the outcome of the server-side sort
275     *         request.
276     */
277    public ResultCode getResult() {
278        return result;
279    }
280
281    /** {@inheritDoc} */
282    public ByteString getValue() {
283        final ByteStringBuilder buffer = new ByteStringBuilder();
284        final ASN1Writer writer = ASN1.getWriter(buffer);
285        try {
286            writer.writeStartSequence();
287            writer.writeEnumerated(result.intValue());
288            if (attributeDescription != null) {
289                writer.writeOctetString(TYPE_ATTRIBUTE_TYPE, attributeDescription.toString());
290            }
291            writer.writeEndSequence();
292            return buffer.toByteString();
293        } catch (final IOException ioe) {
294            // This should never happen unless there is a bug somewhere.
295            throw new RuntimeException(ioe);
296        }
297    }
298
299    /** {@inheritDoc} */
300    public boolean hasValue() {
301        return true;
302    }
303
304    /** {@inheritDoc} */
305    public boolean isCritical() {
306        return isCritical;
307    }
308
309    /** {@inheritDoc} */
310    @Override
311    public String toString() {
312        final StringBuilder builder = new StringBuilder();
313        builder.append("ServerSideSortResponseControl(oid=");
314        builder.append(getOID());
315        builder.append(", criticality=");
316        builder.append(isCritical());
317        builder.append(", result=");
318        builder.append(result);
319        if (attributeDescription != null) {
320            builder.append(", attributeDescription=");
321            builder.append(attributeDescription);
322        }
323        builder.append(")");
324        return builder.toString();
325    }
326}