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