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.byteToHex;
020import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
021import static com.forgerock.opendj.ldap.CoreMessages.ERR_VLVREQ_CONTROL_BAD_OID;
022import static com.forgerock.opendj.ldap.CoreMessages.INFO_VLVREQ_CONTROL_CANNOT_DECODE_VALUE;
023import static com.forgerock.opendj.ldap.CoreMessages.INFO_VLVREQ_CONTROL_INVALID_TARGET_TYPE;
024import static com.forgerock.opendj.ldap.CoreMessages.INFO_VLVREQ_CONTROL_NO_VALUE;
025
026import java.io.IOException;
027
028import org.forgerock.i18n.LocalizableMessage;
029import org.forgerock.opendj.io.ASN1;
030import org.forgerock.opendj.io.ASN1Reader;
031import org.forgerock.opendj.io.ASN1Writer;
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.util.Reject;
037
038/**
039 * The virtual list view request control as defined in
040 * draft-ietf-ldapext-ldapv3-vlv. This control allows a client to specify that
041 * the server return, for a given search request with associated sort keys, a
042 * contiguous subset of the search result set. This subset is specified in terms
043 * of offsets into the ordered list, or in terms of a greater than or equal
044 * assertion value.
045 * <p>
046 * This control must be used in conjunction with the server-side sort request
047 * control in order to ensure that results are returned in a consistent order.
048 * <p>
049 * This control is similar to the simple paged results request control, except
050 * that it allows the client to move backwards and forwards in the result set.
051 * <p>
052 * The following example demonstrates use of the virtual list view controls.
053 *
054 * <pre>
055 * ByteString contextID = ByteString.empty();
056 *
057 * // Add a window of 2 entries on either side of the first sn=Jensen entry.
058 * SearchRequest request = Requests.newSearchRequest("ou=People,dc=example,dc=com",
059 *          SearchScope.WHOLE_SUBTREE, "(sn=*)", "sn", "givenName")
060 *          .addControl(ServerSideSortRequestControl.newControl(true, new SortKey("sn")))
061 *          .addControl(VirtualListViewRequestControl.newAssertionControl(
062 *                  true, ByteString.valueOf("Jensen"), 2, 2, contextID));
063 *
064 * SearchResultHandler resultHandler = new MySearchResultHandler();
065 * Result result = connection.search(request, resultHandler);
066 *
067 * ServerSideSortResponseControl sssControl =
068 *         result.getControl(ServerSideSortResponseControl.DECODER, new DecodeOptions());
069 * if (sssControl != null &amp;&amp; sssControl.getResult() == ResultCode.SUCCESS) {
070 *     // Entries are sorted.
071 * } else {
072 *     // Entries not necessarily sorted
073 * }
074 *
075 * VirtualListViewResponseControl vlvControl =
076 *         result.getControl(VirtualListViewResponseControl.DECODER, new DecodeOptions());
077 * // Position in list: vlvControl.getTargetPosition()/vlvControl.getContentCount()
078 * </pre>
079 *
080 * The search result handler in this case displays pages of results as LDIF on
081 * standard out.
082 *
083 * <pre>
084 * private static class MySearchResultHandler implements SearchResultHandler {
085 *
086 *     {@literal @}Override
087 *     public void handleExceptionResult(LdapException error) {
088 *         // Ignore.
089 *     }
090 *
091 *     {@literal @}Override
092 *     public void handleResult(Result result) {
093 *         // Ignore.
094 *     }
095 *
096 *     {@literal @}Override
097 *     public boolean handleEntry(SearchResultEntry entry) {
098 *         final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
099 *         try {
100 *             writer.writeEntry(entry);
101 *             writer.flush();
102 *         } catch (final IOException e) {
103 *             // The writer could not write to System.out.
104 *         }
105 *         return true;
106 *     }
107 *
108 *     {@literal @}Override
109 *     public boolean handleReference(SearchResultReference reference) {
110 *         System.out.println("Got a reference: " + reference.toString());
111 *         return false;
112 *     }
113 * }
114 * </pre>
115 *
116 * @see VirtualListViewResponseControl
117 * @see ServerSideSortRequestControl
118 * @see <a href="http://tools.ietf.org/html/draft-ietf-ldapext-ldapv3-vlv">
119 *      draft-ietf-ldapext-ldapv3-vlv - LDAP Extensions for Scrolling View
120 *      Browsing of Search Results </a>
121 */
122public final class VirtualListViewRequestControl implements Control {
123    /** The OID for the virtual list view request control. */
124    public static final String OID = "2.16.840.1.113730.3.4.9";
125
126    /** A decoder which can be used for decoding the virtual list view request control. */
127    public static final ControlDecoder<VirtualListViewRequestControl> DECODER =
128            new ControlDecoder<VirtualListViewRequestControl>() {
129
130                @Override
131                public VirtualListViewRequestControl decodeControl(final Control control,
132                        final DecodeOptions options) throws DecodeException {
133                    Reject.ifNull(control);
134
135                    if (control instanceof VirtualListViewRequestControl) {
136                        return (VirtualListViewRequestControl) control;
137                    }
138
139                    if (!control.getOID().equals(OID)) {
140                        final LocalizableMessage message =
141                                ERR_VLVREQ_CONTROL_BAD_OID.get(control.getOID(), OID);
142                        throw DecodeException.error(message);
143                    }
144
145                    if (!control.hasValue()) {
146                        // The request control must always have a value.
147                        final LocalizableMessage message = INFO_VLVREQ_CONTROL_NO_VALUE.get();
148                        throw DecodeException.error(message);
149                    }
150
151                    final ASN1Reader reader = ASN1.getReader(control.getValue());
152                    try {
153                        reader.readStartSequence();
154
155                        final int beforeCount = (int) reader.readInteger();
156                        final int afterCount = (int) reader.readInteger();
157
158                        int offset = -1;
159                        int contentCount = -1;
160                        ByteString assertionValue = null;
161                        final byte targetType = reader.peekType();
162                        switch (targetType) {
163                        case TYPE_TARGET_BYOFFSET:
164                            reader.readStartSequence();
165                            offset = (int) reader.readInteger();
166                            contentCount = (int) reader.readInteger();
167                            reader.readEndSequence();
168                            break;
169                        case TYPE_TARGET_GREATERTHANOREQUAL:
170                            assertionValue = reader.readOctetString();
171                            break;
172                        default:
173                            final LocalizableMessage message =
174                                    INFO_VLVREQ_CONTROL_INVALID_TARGET_TYPE
175                                            .get(byteToHex(targetType));
176                            throw DecodeException.error(message);
177                        }
178
179                        ByteString contextID = null;
180                        if (reader.hasNextElement()) {
181                            contextID = reader.readOctetString();
182                        }
183
184                        return new VirtualListViewRequestControl(control.isCritical(), beforeCount,
185                                afterCount, contentCount, offset, assertionValue, contextID);
186                    } catch (final IOException e) {
187                        final LocalizableMessage message =
188                                INFO_VLVREQ_CONTROL_CANNOT_DECODE_VALUE.get(getExceptionMessage(e));
189                        throw DecodeException.error(message, e);
190                    }
191                }
192
193                @Override
194                public String getOID() {
195                    return OID;
196                }
197            };
198
199    /** The BER type to use when encoding the byOffset target element. */
200    private static final byte TYPE_TARGET_BYOFFSET = (byte) 0xA0;
201
202    /** The BER type to use when encoding the greaterThanOrEqual target element. */
203    private static final byte TYPE_TARGET_GREATERTHANOREQUAL = (byte) 0x81;
204
205    /**
206     * Creates a new virtual list view request control that will identify the
207     * target entry by an assertion value. The assertion value is encoded
208     * according to the ORDERING matching rule for the attribute description in
209     * the sort control. The assertion value is used to determine the target
210     * entry by comparison with the values of the attribute specified as the
211     * primary sort key. The first list entry who's value is no less than (less
212     * than or equal to when the sort order is reversed) the supplied value is
213     * the target entry.
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 assertionValue
220     *            The assertion value that will be used to locate the target
221     *            entry.
222     * @param beforeCount
223     *            The number of entries before the target entry to be included
224     *            in the search results.
225     * @param afterCount
226     *            The number of entries after the target entry to be included in
227     *            the search results.
228     * @param contextID
229     *            The context ID provided by the server in the last virtual list
230     *            view response for the same set of criteria, or {@code null} if
231     *            there was no previous virtual list view response or the server
232     *            did not include a context ID in the last response.
233     * @return The new control.
234     * @throws IllegalArgumentException
235     *             If {@code beforeCount} or {@code afterCount} were less than
236     *             {@code 0}.
237     * @throws NullPointerException
238     *             If {@code assertionValue} was {@code null}.
239     */
240    public static VirtualListViewRequestControl newAssertionControl(final boolean isCritical,
241            final ByteString assertionValue, final int beforeCount, final int afterCount,
242            final ByteString contextID) {
243        Reject.ifNull(assertionValue);
244        Reject.ifFalse(beforeCount >= 0, "beforeCount is less than 0");
245        Reject.ifFalse(afterCount >= 0, "afterCount is less than 0");
246
247        return new VirtualListViewRequestControl(isCritical, beforeCount, afterCount, -1, -1,
248                assertionValue, contextID);
249    }
250
251    /**
252     * Creates a new virtual list view request control that will identify the
253     * target entry by a positional offset within the complete result set.
254     *
255     * @param isCritical
256     *            {@code true} if it is unacceptable to perform the operation
257     *            without applying the semantics of this control, or
258     *            {@code false} if it can be ignored.
259     * @param offset
260     *            The positional offset of the target entry in the result set,
261     *            where {@code 1} is the first entry.
262     * @param contentCount
263     *            The content count returned by the server in the last virtual
264     *            list view response, or {@code 0} if this is the first virtual
265     *            list view request.
266     * @param beforeCount
267     *            The number of entries before the target entry to be included
268     *            in the search results.
269     * @param afterCount
270     *            The number of entries after the target entry to be included in
271     *            the search results.
272     * @param contextID
273     *            The context ID provided by the server in the last virtual list
274     *            view response for the same set of criteria, or {@code null} if
275     *            there was no previous virtual list view response or the server
276     *            did not include a context ID in the last response.
277     * @return The new control.
278     * @throws IllegalArgumentException
279     *             If {@code beforeCount}, {@code afterCount}, or
280     *             {@code contentCount} were less than {@code 0}, or if
281     *             {@code offset} was less than {@code 1}.
282     */
283    public static VirtualListViewRequestControl newOffsetControl(final boolean isCritical,
284            final int offset, final int contentCount, final int beforeCount, final int afterCount,
285            final ByteString contextID) {
286        Reject.ifFalse(beforeCount >= 0, "beforeCount is less than 0");
287        Reject.ifFalse(afterCount >= 0, "afterCount is less than 0");
288        Reject.ifFalse(offset > 0, "offset is less than 1");
289        Reject.ifFalse(contentCount >= 0, "contentCount is less than 0");
290
291        return new VirtualListViewRequestControl(isCritical, beforeCount, afterCount, contentCount,
292                offset, null, contextID);
293    }
294
295    private final int beforeCount;
296
297    private final int afterCount;
298
299    private final ByteString contextID;
300
301    private final boolean isCritical;
302
303    private final int contentCount;
304
305    private final int offset;
306
307    private final ByteString assertionValue;
308
309    private VirtualListViewRequestControl(final boolean isCritical, final int beforeCount,
310            final int afterCount, final int contentCount, final int offset,
311            final ByteString assertionValue, final ByteString contextID) {
312        this.isCritical = isCritical;
313        this.beforeCount = beforeCount;
314        this.afterCount = afterCount;
315        this.contentCount = contentCount;
316        this.offset = offset;
317        this.assertionValue = assertionValue;
318        this.contextID = contextID;
319    }
320
321    /**
322     * Returns the number of entries after the target entry to be included in
323     * the search results.
324     *
325     * @return The number of entries after the target entry to be included in
326     *         the search results.
327     */
328    public int getAfterCount() {
329        return afterCount;
330    }
331
332    /**
333     * Returns the assertion value that will be used to locate the target entry,
334     * if applicable.
335     *
336     * @return The assertion value that will be used to locate the target entry,
337     *         or {@code null} if this control is using a target offset.
338     */
339    public ByteString getAssertionValue() {
340        return assertionValue;
341    }
342
343    /**
344     * Returns the assertion value that will be used to locate the target entry,
345     * if applicable, decoded as a UTF-8 string.
346     *
347     * @return The assertion value that will be used to locate the target entry
348     *         decoded as a UTF-8 string, or {@code null} if this control is
349     *         using a target offset.
350     */
351    public String getAssertionValueAsString() {
352        return assertionValue != null ? assertionValue.toString() : null;
353    }
354
355    /**
356     * Returns the number of entries before the target entry to be included in
357     * the search results.
358     *
359     * @return The number of entries before the target entry to be included in
360     *         the search results.
361     */
362    public int getBeforeCount() {
363        return beforeCount;
364    }
365
366    /**
367     * Returns the content count returned by the server in the last virtual list
368     * view response, if applicable.
369     *
370     * @return The content count returned by the server in the last virtual list
371     *         view response, which may be {@code 0} if this is the first
372     *         virtual list view request, or {@code -1} if this control is using
373     *         a target assertion.
374     */
375    public int getContentCount() {
376        return contentCount;
377    }
378
379    /**
380     * Returns the context ID provided by the server in the last virtual list
381     * view response for the same set of criteria, or {@code null} if there was
382     * no previous virtual list view response or the server did not include a
383     * context ID in the last response.
384     *
385     * @return The context ID provided by the server in the last virtual list
386     *         view response, or {@code null} if unavailable.
387     */
388    public ByteString getContextID() {
389        return contextID;
390    }
391
392    /**
393     * Returns the positional offset of the target entry in the result set, if
394     * applicable, where {@code 1} is the first entry.
395     *
396     * @return The positional offset of the target entry in the result set, or
397     *         {@code -1} if this control is using a target assertion.
398     */
399    public int getOffset() {
400        return offset;
401    }
402
403    @Override
404    public String getOID() {
405        return OID;
406    }
407
408    @Override
409    public ByteString getValue() {
410        final ByteStringBuilder buffer = new ByteStringBuilder();
411        final ASN1Writer writer = ASN1.getWriter(buffer);
412        try {
413            writer.writeStartSequence();
414            writer.writeInteger(beforeCount);
415            writer.writeInteger(afterCount);
416            if (hasTargetOffset()) {
417                writer.writeStartSequence(TYPE_TARGET_BYOFFSET);
418                writer.writeInteger(offset);
419                writer.writeInteger(contentCount);
420                writer.writeEndSequence();
421            } else {
422                writer.writeOctetString(TYPE_TARGET_GREATERTHANOREQUAL, assertionValue);
423            }
424            if (contextID != null) {
425                writer.writeOctetString(contextID);
426            }
427            writer.writeEndSequence();
428            return buffer.toByteString();
429        } catch (final IOException ioe) {
430            // This should never happen unless there is a bug somewhere.
431            throw new RuntimeException(ioe);
432        }
433    }
434
435    /**
436     * Returns {@code true} if this control is using a target offset, or
437     * {@code false} if this control is using a target assertion.
438     *
439     * @return {@code true} if this control is using a target offset, or
440     *         {@code false} if this control is using a target assertion.
441     */
442    public boolean hasTargetOffset() {
443        return assertionValue == null;
444    }
445
446    @Override
447    public boolean hasValue() {
448        return true;
449    }
450
451    @Override
452    public boolean isCritical() {
453        return isCritical;
454    }
455
456    @Override
457    public String toString() {
458        final StringBuilder builder = new StringBuilder();
459        builder.append("VirtualListViewRequestControl(oid=");
460        builder.append(getOID());
461        builder.append(", criticality=");
462        builder.append(isCritical());
463        builder.append(", beforeCount=");
464        builder.append(beforeCount);
465        builder.append(", afterCount=");
466        builder.append(afterCount);
467        if (hasTargetOffset()) {
468            builder.append(", offset=");
469            builder.append(offset);
470            builder.append(", contentCount=");
471            builder.append(contentCount);
472        } else {
473            builder.append(", greaterThanOrEqual=");
474            builder.append(assertionValue);
475        }
476        if (contextID != null) {
477            builder.append(", contextID=");
478            builder.append(contextID);
479        }
480        builder.append(")");
481        return builder.toString();
482    }
483}