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.*; 031 032import java.io.IOException; 033import java.util.ArrayList; 034import java.util.Arrays; 035import java.util.Collection; 036import java.util.Collections; 037import java.util.LinkedList; 038import java.util.List; 039import java.util.StringTokenizer; 040 041import org.forgerock.i18n.LocalizableMessage; 042import org.forgerock.i18n.LocalizedIllegalArgumentException; 043import org.forgerock.opendj.io.ASN1; 044import org.forgerock.opendj.io.ASN1Reader; 045import org.forgerock.opendj.io.ASN1Writer; 046import org.forgerock.opendj.ldap.ByteString; 047import org.forgerock.opendj.ldap.ByteStringBuilder; 048import org.forgerock.opendj.ldap.DecodeException; 049import org.forgerock.opendj.ldap.DecodeOptions; 050import org.forgerock.opendj.ldap.SortKey; 051import org.forgerock.util.Reject; 052 053/** 054 * The server-side sort request control as defined in RFC 2891. This control may 055 * be included in a search request to indicate that search result entries should 056 * be sorted by the server before being returned. The sort order is specified 057 * using one or more sort keys, the first being the primary key, and so on. 058 * <p> 059 * This controls may be useful when the client has limited functionality or for 060 * some other reason cannot sort the results but still needs them sorted. In 061 * cases where the client can sort the results client-side sorting is 062 * recommended in order to reduce load on the server. See {@link SortKey} for an 063 * example of client-side sorting. 064 * <p> 065 * The following example demonstrates how to work with a server-side sort. 066 * 067 * <pre> 068 * Connection connection = ...; 069 * 070 * SearchRequest request = Requests.newSearchRequest( 071 * "ou=People,dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(sn=Jensen)", "cn") 072 * .addControl(ServerSideSortRequestControl.newControl(true, new SortKey("cn"))); 073 * 074 * SearchResultHandler resultHandler = new MySearchResultHandler(); 075 * Result result = connection.search(request, resultHandler); 076 * 077 * ServerSideSortResponseControl control = result.getControl( 078 * ServerSideSortResponseControl.DECODER, new DecodeOptions()); 079 * if (control != null && control.getResult() == ResultCode.SUCCESS) { 080 * // Entries are sorted. 081 * } else { 082 * // Entries not sorted. 083 * } 084 * </pre> 085 * 086 * @see ServerSideSortResponseControl 087 * @see SortKey 088 * @see <a href="http://tools.ietf.org/html/rfc2891">RFC 2891 - LDAP Control 089 * Extension for Server Side Sorting of Search Results </a> 090 */ 091public final class ServerSideSortRequestControl implements Control { 092 /** 093 * The OID for the server-side sort request control. 094 */ 095 public static final String OID = "1.2.840.113556.1.4.473"; 096 097 /** 098 * The BER type to use when encoding the orderingRule element. 099 */ 100 private static final byte TYPE_ORDERING_RULE_ID = (byte) 0x80; 101 102 /** 103 * The BER type to use when encoding the reverseOrder element. 104 */ 105 private static final byte TYPE_REVERSE_ORDER = (byte) 0x81; 106 107 /** 108 * A decoder which can be used for decoding the server side sort request 109 * control. 110 */ 111 public static final ControlDecoder<ServerSideSortRequestControl> DECODER = 112 new ControlDecoder<ServerSideSortRequestControl>() { 113 114 public ServerSideSortRequestControl decodeControl(final Control control, 115 final DecodeOptions options) throws DecodeException { 116 Reject.ifNull(control); 117 118 if (control instanceof ServerSideSortRequestControl) { 119 return (ServerSideSortRequestControl) control; 120 } 121 122 if (!control.getOID().equals(OID)) { 123 final LocalizableMessage message = 124 ERR_SORTREQ_CONTROL_BAD_OID.get(control.getOID(), OID); 125 throw DecodeException.error(message); 126 } 127 128 if (!control.hasValue()) { 129 // The request control must always have a value. 130 final LocalizableMessage message = INFO_SORTREQ_CONTROL_NO_VALUE.get(); 131 throw DecodeException.error(message); 132 } 133 134 final ASN1Reader reader = ASN1.getReader(control.getValue()); 135 try { 136 reader.readStartSequence(); 137 if (!reader.hasNextElement()) { 138 final LocalizableMessage message = 139 INFO_SORTREQ_CONTROL_NO_SORT_KEYS.get(); 140 throw DecodeException.error(message); 141 } 142 143 final List<SortKey> keys = new LinkedList<>(); 144 while (reader.hasNextElement()) { 145 reader.readStartSequence(); 146 final String attrName = reader.readOctetStringAsString(); 147 148 String orderingRule = null; 149 boolean reverseOrder = false; 150 if (reader.hasNextElement() 151 && (reader.peekType() == TYPE_ORDERING_RULE_ID)) { 152 orderingRule = reader.readOctetStringAsString(); 153 } 154 if (reader.hasNextElement() 155 && (reader.peekType() == TYPE_REVERSE_ORDER)) { 156 reverseOrder = reader.readBoolean(); 157 } 158 reader.readEndSequence(); 159 160 keys.add(new SortKey(attrName, reverseOrder, orderingRule)); 161 } 162 reader.readEndSequence(); 163 164 return new ServerSideSortRequestControl(control.isCritical(), Collections 165 .unmodifiableList(keys)); 166 } catch (final IOException e) { 167 final LocalizableMessage message = 168 INFO_SORTREQ_CONTROL_CANNOT_DECODE_VALUE 169 .get(getExceptionMessage(e)); 170 throw DecodeException.error(message, e); 171 } 172 } 173 174 public String getOID() { 175 return OID; 176 } 177 }; 178 179 /** 180 * Creates a new server side sort request control with the provided 181 * criticality and list of sort keys. 182 * 183 * @param isCritical 184 * {@code true} if it is unacceptable to perform the operation 185 * without applying the semantics of this control, or 186 * {@code false} if it can be ignored. 187 * @param keys 188 * The list of sort keys. 189 * @return The new control. 190 * @throws IllegalArgumentException 191 * If {@code keys} was empty. 192 * @throws NullPointerException 193 * If {@code keys} was {@code null}. 194 */ 195 public static ServerSideSortRequestControl newControl(final boolean isCritical, 196 final Collection<SortKey> keys) { 197 Reject.ifNull(keys); 198 Reject.ifFalse(!keys.isEmpty(), "keys must not be empty"); 199 200 return new ServerSideSortRequestControl(isCritical, Collections 201 .unmodifiableList(new ArrayList<SortKey>(keys))); 202 } 203 204 /** 205 * Creates a new server side sort request control with the provided 206 * criticality and list of sort keys. 207 * 208 * @param isCritical 209 * {@code true} if it is unacceptable to perform the operation 210 * without applying the semantics of this control, or 211 * {@code false} if it can be ignored. 212 * @param keys 213 * The list of sort keys. 214 * @return The new control. 215 * @throws IllegalArgumentException 216 * If {@code keys} was empty. 217 * @throws NullPointerException 218 * If {@code keys} was {@code null}. 219 */ 220 public static ServerSideSortRequestControl newControl(final boolean isCritical, 221 final SortKey... keys) { 222 return newControl(isCritical, Arrays.asList(keys)); 223 } 224 225 /** 226 * Creates a new server side sort request control with the provided 227 * criticality and string representation of a list of sort keys. The string 228 * representation is comprised of a comma separate list of sort keys as 229 * defined in {@link SortKey#valueOf(String)}. There must be at least one 230 * sort key present in the string representation. 231 * 232 * @param isCritical 233 * {@code true} if it is unacceptable to perform the operation 234 * without applying the semantics of this control, or 235 * {@code false} if it can be ignored. 236 * @param sortKeys 237 * The list of sort keys. 238 * @return The new control. 239 * @throws LocalizedIllegalArgumentException 240 * If {@code sortKeys} is not a valid string representation of a 241 * list of sort keys. 242 * @throws NullPointerException 243 * If {@code sortKeys} was {@code null}. 244 */ 245 public static ServerSideSortRequestControl newControl(final boolean isCritical, 246 final String sortKeys) { 247 Reject.ifNull(sortKeys); 248 249 final List<SortKey> keys = new LinkedList<>(); 250 final StringTokenizer tokenizer = new StringTokenizer(sortKeys, ","); 251 while (tokenizer.hasMoreTokens()) { 252 final String token = tokenizer.nextToken().trim(); 253 keys.add(SortKey.valueOf(token)); 254 } 255 if (keys.isEmpty()) { 256 final LocalizableMessage message = ERR_SORT_KEY_NO_SORT_KEYS.get(sortKeys); 257 throw new LocalizedIllegalArgumentException(message); 258 } 259 return new ServerSideSortRequestControl(isCritical, Collections.unmodifiableList(keys)); 260 } 261 262 private final List<SortKey> sortKeys; 263 264 private final boolean isCritical; 265 266 private ServerSideSortRequestControl(final boolean isCritical, final List<SortKey> keys) { 267 this.isCritical = isCritical; 268 this.sortKeys = keys; 269 } 270 271 /** {@inheritDoc} */ 272 public String getOID() { 273 return OID; 274 } 275 276 /** 277 * Returns an unmodifiable list containing the sort keys associated with 278 * this server side sort request control. The list will contain at least one 279 * sort key. 280 * 281 * @return An unmodifiable list containing the sort keys associated with 282 * this server side sort request control. 283 */ 284 public List<SortKey> getSortKeys() { 285 return sortKeys; 286 } 287 288 /** {@inheritDoc} */ 289 public ByteString getValue() { 290 final ByteStringBuilder buffer = new ByteStringBuilder(); 291 final ASN1Writer writer = ASN1.getWriter(buffer); 292 try { 293 writer.writeStartSequence(); 294 for (final SortKey sortKey : sortKeys) { 295 writer.writeStartSequence(); 296 writer.writeOctetString(sortKey.getAttributeDescription()); 297 298 if (sortKey.getOrderingMatchingRule() != null) { 299 writer.writeOctetString(TYPE_ORDERING_RULE_ID, sortKey 300 .getOrderingMatchingRule()); 301 } 302 303 if (sortKey.isReverseOrder()) { 304 writer.writeBoolean(TYPE_REVERSE_ORDER, true); 305 } 306 307 writer.writeEndSequence(); 308 } 309 writer.writeEndSequence(); 310 return buffer.toByteString(); 311 } catch (final IOException ioe) { 312 // This should never happen unless there is a bug somewhere. 313 throw new RuntimeException(ioe); 314 } 315 } 316 317 /** {@inheritDoc} */ 318 public boolean hasValue() { 319 return true; 320 } 321 322 /** {@inheritDoc} */ 323 public boolean isCritical() { 324 return isCritical; 325 } 326 327 /** {@inheritDoc} */ 328 @Override 329 public String toString() { 330 final StringBuilder buffer = new StringBuilder(); 331 buffer.append("ServerSideSortRequestControl(oid="); 332 buffer.append(getOID()); 333 buffer.append(", criticality="); 334 buffer.append(isCritical()); 335 buffer.append(", sortKeys="); 336 buffer.append(sortKeys); 337 buffer.append(")"); 338 return buffer.toString(); 339 } 340 341}