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-2014 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_VLVRES_CONTROL_BAD_OID;
031import static com.forgerock.opendj.ldap.CoreMessages.INFO_VLVRES_CONTROL_CANNOT_DECODE_VALUE;
032import static com.forgerock.opendj.ldap.CoreMessages.INFO_VLVRES_CONTROL_NO_VALUE;
033
034import java.io.IOException;
035
036import org.forgerock.i18n.LocalizableMessage;
037import org.forgerock.opendj.io.ASN1;
038import org.forgerock.opendj.io.ASN1Reader;
039import org.forgerock.opendj.io.ASN1Writer;
040import org.forgerock.opendj.ldap.ByteString;
041import org.forgerock.opendj.ldap.ByteStringBuilder;
042import org.forgerock.opendj.ldap.DecodeException;
043import org.forgerock.opendj.ldap.DecodeOptions;
044import org.forgerock.opendj.ldap.ResultCode;
045import org.forgerock.util.Reject;
046
047/**
048 * The virtual list view response control as defined in
049 * draft-ietf-ldapext-ldapv3-vlv. This control is included with a search result
050 * in response to a virtual list view request included with a search request.
051 * <p>
052 * If the result code included with this control indicates that the virtual list
053 * view request succeeded then the content count and target position give
054 * sufficient information for the client to update a list box slider position to
055 * match the newly retrieved entries and identify the target entry.
056 * <p>
057 * The content count and context ID should be used in a subsequent virtual list
058 * view requests.
059 * <p>
060 * The following example demonstrates use of the virtual list view controls.
061 *
062 * <pre>
063 * ByteString contextID = ByteString.empty();
064 *
065 * // Add a window of 2 entries on either side of the first sn=Jensen entry.
066 * SearchRequest request = Requests.newSearchRequest("ou=People,dc=example,dc=com",
067 *          SearchScope.WHOLE_SUBTREE, "(sn=*)", "sn", "givenName")
068 *          .addControl(ServerSideSortRequestControl.newControl(true, new SortKey("sn")))
069 *          .addControl(VirtualListViewRequestControl.newAssertionControl(
070 *                  true, ByteString.valueOf("Jensen"), 2, 2, contextID));
071 *
072 * SearchResultHandler resultHandler = new MySearchResultHandler();
073 * Result result = connection.search(request, resultHandler);
074 *
075 * ServerSideSortResponseControl sssControl =
076 *         result.getControl(ServerSideSortResponseControl.DECODER, new DecodeOptions());
077 * if (sssControl != null &amp;&amp; sssControl.getResult() == ResultCode.SUCCESS) {
078 *     // Entries are sorted.
079 * } else {
080 *     // Entries not necessarily sorted
081 * }
082 *
083 * VirtualListViewResponseControl vlvControl =
084 *         result.getControl(VirtualListViewResponseControl.DECODER, new DecodeOptions());
085 * // Position in list: vlvControl.getTargetPosition()/vlvControl.getContentCount()
086 * </pre>
087 *
088 * The search result handler in this case displays pages of results as LDIF on
089 * standard out.
090 *
091 * <pre>
092 * private static class MySearchResultHandler implements SearchResultHandler {
093 *
094 *     {@literal @}Override
095 *     public void handleExceptionResult(LdapException error) {
096 *         // Ignore.
097 *     }
098 *
099 *     {@literal @}Override
100 *     public void handleResult(Result result) {
101 *         // Ignore.
102 *     }
103 *
104 *     {@literal @}Override
105 *     public boolean handleEntry(SearchResultEntry entry) {
106 *         final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
107 *         try {
108 *             writer.writeEntry(entry);
109 *             writer.flush();
110 *         } catch (final IOException e) {
111 *             // The writer could not write to System.out.
112 *         }
113 *         return true;
114 *     }
115 *
116 *     {@literal @}Override
117 *     public boolean handleReference(SearchResultReference reference) {
118 *         System.out.println("Got a reference: " + reference.toString());
119 *         return false;
120 *     }
121 * }
122 * </pre>
123 *
124 * @see VirtualListViewRequestControl
125 * @see <a href="http://tools.ietf.org/html/draft-ietf-ldapext-ldapv3-vlv">
126 *      draft-ietf-ldapext-ldapv3-vlv - LDAP Extensions for Scrolling View
127 *      Browsing of Search Results </a>
128 */
129public final class VirtualListViewResponseControl implements Control {
130    /**
131     * The OID for the virtual list view request control.
132     */
133    public static final String OID = "2.16.840.1.113730.3.4.10";
134
135    /**
136     * A decoder which can be used for decoding the virtual list view response
137     * control.
138     */
139    public static final ControlDecoder<VirtualListViewResponseControl> DECODER =
140            new ControlDecoder<VirtualListViewResponseControl>() {
141
142                public VirtualListViewResponseControl decodeControl(final Control control,
143                        final DecodeOptions options) throws DecodeException {
144                    Reject.ifNull(control);
145
146                    if (control instanceof VirtualListViewResponseControl) {
147                        return (VirtualListViewResponseControl) control;
148                    }
149
150                    if (!control.getOID().equals(OID)) {
151                        final LocalizableMessage message =
152                                ERR_VLVRES_CONTROL_BAD_OID.get(control.getOID(), OID);
153                        throw DecodeException.error(message);
154                    }
155
156                    if (!control.hasValue()) {
157                        // The response control must always have a value.
158                        final LocalizableMessage message = INFO_VLVRES_CONTROL_NO_VALUE.get();
159                        throw DecodeException.error(message);
160                    }
161
162                    final ASN1Reader reader = ASN1.getReader(control.getValue());
163                    try {
164                        reader.readStartSequence();
165
166                        final int targetPosition = (int) reader.readInteger();
167                        final int contentCount = (int) reader.readInteger();
168                        final ResultCode result = ResultCode.valueOf(reader.readEnumerated());
169                        ByteString contextID = null;
170                        if (reader.hasNextElement()) {
171                            contextID = reader.readOctetString();
172                        }
173
174                        return new VirtualListViewResponseControl(control.isCritical(),
175                                targetPosition, contentCount, result, contextID);
176                    } catch (final IOException e) {
177                        final LocalizableMessage message =
178                                INFO_VLVRES_CONTROL_CANNOT_DECODE_VALUE.get(getExceptionMessage(e));
179                        throw DecodeException.error(message, e);
180                    }
181                }
182
183                public String getOID() {
184                    return OID;
185                }
186            };
187
188    /**
189     * Creates a new virtual list view response control.
190     *
191     * @param targetPosition
192     *            The position of the target entry in the result set.
193     * @param contentCount
194     *            An estimate of the total number of entries in the result set.
195     * @param result
196     *            The result code indicating the outcome of the virtual list
197     *            view request.
198     * @param contextID
199     *            A server-defined octet string. If present, the contextID
200     *            should be sent back to the server by the client in a
201     *            subsequent virtual list request.
202     * @return The new control.
203     * @throws IllegalArgumentException
204     *             If {@code targetPosition} or {@code contentCount} were less
205     *             than {@code 0}.
206     * @throws NullPointerException
207     *             If {@code result} was {@code null}.
208     */
209    public static VirtualListViewResponseControl newControl(final int targetPosition,
210            final int contentCount, final ResultCode result, final ByteString contextID) {
211        Reject.ifNull(result);
212        Reject.ifFalse(targetPosition >= 0, "targetPosition is less than 0");
213        Reject.ifFalse(contentCount >= 0, "contentCount is less than 0");
214
215        return new VirtualListViewResponseControl(false, targetPosition, contentCount, result,
216                contextID);
217    }
218
219    private final int targetPosition;
220
221    private final int contentCount;
222
223    private final ResultCode result;
224
225    private final ByteString contextID;
226
227    private final boolean isCritical;
228
229    private VirtualListViewResponseControl(final boolean isCritical, final int targetPosition,
230            final int contentCount, final ResultCode result, final ByteString contextID) {
231        this.isCritical = isCritical;
232        this.targetPosition = targetPosition;
233        this.contentCount = contentCount;
234        this.result = result;
235        this.contextID = contextID;
236    }
237
238    /**
239     * Returns the estimated total number of entries in the result set.
240     *
241     * @return The estimated total number of entries in the result set.
242     */
243    public int getContentCount() {
244        return contentCount;
245    }
246
247    /**
248     * Returns a server-defined octet string which, if present, should be sent
249     * back to the server by the client in a subsequent virtual list request.
250     *
251     * @return A server-defined octet string which, if present, should be sent
252     *         back to the server by the client in a subsequent virtual list
253     *         request, or {@code null} if there is no context ID.
254     */
255    public ByteString getContextID() {
256        return contextID;
257    }
258
259    /** {@inheritDoc} */
260    public String getOID() {
261        return OID;
262    }
263
264    /**
265     * Returns result code indicating the outcome of the virtual list view
266     * request.
267     *
268     * @return The result code indicating the outcome of the virtual list view
269     *         request.
270     */
271    public ResultCode getResult() {
272        return result;
273    }
274
275    /**
276     * Returns the position of the target entry in the result set.
277     *
278     * @return The position of the target entry in the result set.
279     */
280    public int getTargetPosition() {
281        return targetPosition;
282    }
283
284    /** {@inheritDoc} */
285    public ByteString getValue() {
286        final ByteStringBuilder buffer = new ByteStringBuilder();
287        final ASN1Writer writer = ASN1.getWriter(buffer);
288        try {
289            writer.writeStartSequence();
290            writer.writeInteger(targetPosition);
291            writer.writeInteger(contentCount);
292            writer.writeEnumerated(result.intValue());
293            if (contextID != null) {
294                writer.writeOctetString(contextID);
295            }
296            writer.writeEndSequence();
297            return buffer.toByteString();
298        } catch (final IOException ioe) {
299            // This should never happen unless there is a bug somewhere.
300            throw new RuntimeException(ioe);
301        }
302    }
303
304    /** {@inheritDoc} */
305    public boolean hasValue() {
306        return true;
307    }
308
309    /** {@inheritDoc} */
310    public boolean isCritical() {
311        return isCritical;
312    }
313
314    /** {@inheritDoc} */
315    @Override
316    public String toString() {
317        final StringBuilder builder = new StringBuilder();
318        builder.append("VirtualListViewResponseControl(oid=");
319        builder.append(getOID());
320        builder.append(", criticality=");
321        builder.append(isCritical());
322        builder.append(", targetPosition=");
323        builder.append(targetPosition);
324        builder.append(", contentCount=");
325        builder.append(contentCount);
326        builder.append(", result=");
327        builder.append(result);
328        if (contextID != null) {
329            builder.append(", contextID=");
330            builder.append(contextID);
331        }
332        builder.append(")");
333        return builder.toString();
334    }
335
336}