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