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.*; 031 032import java.io.IOException; 033import java.util.Arrays; 034import java.util.Collection; 035import java.util.Collections; 036import java.util.EnumSet; 037import java.util.Set; 038 039import org.forgerock.i18n.LocalizableMessage; 040import org.forgerock.i18n.slf4j.LocalizedLogger; 041import org.forgerock.opendj.io.ASN1; 042import org.forgerock.opendj.io.ASN1Reader; 043import org.forgerock.opendj.io.ASN1Writer; 044import org.forgerock.opendj.ldap.ByteString; 045import org.forgerock.opendj.ldap.ByteStringBuilder; 046import org.forgerock.opendj.ldap.DecodeException; 047import org.forgerock.opendj.ldap.DecodeOptions; 048import org.forgerock.util.Reject; 049 050/** 051 * The persistent search request control as defined in 052 * draft-ietf-ldapext-psearch. This control allows a client to receive 053 * notification of changes that occur in an LDAP server. 054 * <p> 055 * You can examine the entry change notification response control to get more 056 * information about a change returned by the persistent search. 057 * 058 * <pre> 059 * Connection connection = ...; 060 * 061 * SearchRequest request = 062 * Requests.newSearchRequest( 063 * "dc=example,dc=com", SearchScope.WHOLE_SUBTREE, 064 * "(objectclass=inetOrgPerson)", "cn") 065 * .addControl(PersistentSearchRequestControl.newControl( 066 * true, true, true, // critical,changesOnly,returnECs 067 * PersistentSearchChangeType.ADD, 068 * PersistentSearchChangeType.DELETE, 069 * PersistentSearchChangeType.MODIFY, 070 * PersistentSearchChangeType.MODIFY_DN)); 071 * 072 * ConnectionEntryReader reader = connection.search(request); 073 * 074 * while (reader.hasNext()) { 075 * if (!reader.isReference()) { 076 * SearchResultEntry entry = reader.readEntry(); // Entry that changed 077 * 078 * EntryChangeNotificationResponseControl control = entry.getControl( 079 * EntryChangeNotificationResponseControl.DECODER, 080 * new DecodeOptions()); 081 * 082 * PersistentSearchChangeType type = control.getChangeType(); 083 * if (type.equals(PersistentSearchChangeType.MODIFY_DN)) { 084 * // Previous DN: control.getPreviousName() 085 * } 086 * // Change number: control.getChangeNumber()); 087 * } 088 * } 089 * 090 * </pre> 091 * 092 * @see EntryChangeNotificationResponseControl 093 * @see PersistentSearchChangeType 094 * @see <a 095 * href="http://tools.ietf.org/html/draft-ietf-ldapext-psearch">draft-ietf-ldapext-psearch 096 * - Persistent Search: A Simple LDAP Change Notification Mechanism </a> 097 */ 098public final class PersistentSearchRequestControl implements Control { 099 100 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 101 /** 102 * The OID for the persistent search request control. 103 */ 104 public static final String OID = "2.16.840.1.113730.3.4.3"; 105 106 /** 107 * A decoder which can be used for decoding the persistent search request 108 * control. 109 */ 110 public static final ControlDecoder<PersistentSearchRequestControl> DECODER = 111 new ControlDecoder<PersistentSearchRequestControl>() { 112 113 public PersistentSearchRequestControl decodeControl(final Control control, 114 final DecodeOptions options) throws DecodeException { 115 Reject.ifNull(control); 116 117 if (control instanceof PersistentSearchRequestControl) { 118 return (PersistentSearchRequestControl) control; 119 } 120 121 if (!control.getOID().equals(OID)) { 122 final LocalizableMessage message = 123 ERR_PSEARCH_CONTROL_BAD_OID.get(control.getOID(), OID); 124 throw DecodeException.error(message); 125 } 126 127 if (!control.hasValue()) { 128 // The control must always have a value. 129 final LocalizableMessage message = ERR_PSEARCH_NO_CONTROL_VALUE.get(); 130 throw DecodeException.error(message); 131 } 132 133 final ASN1Reader reader = ASN1.getReader(control.getValue()); 134 boolean changesOnly; 135 boolean returnECs; 136 int changeTypes; 137 138 try { 139 reader.readStartSequence(); 140 141 changeTypes = (int) reader.readInteger(); 142 changesOnly = reader.readBoolean(); 143 returnECs = reader.readBoolean(); 144 145 reader.readEndSequence(); 146 } catch (final IOException e) { 147 logger.debug(LocalizableMessage.raw("Unable to read sequence", e)); 148 149 final LocalizableMessage message = 150 ERR_PSEARCH_CANNOT_DECODE_VALUE.get(getExceptionMessage(e)); 151 throw DecodeException.error(message, e); 152 } 153 154 final Set<PersistentSearchChangeType> changeTypeSet = 155 EnumSet.noneOf(PersistentSearchChangeType.class); 156 157 if ((changeTypes & 15) != 0) { 158 final LocalizableMessage message = 159 ERR_PSEARCH_BAD_CHANGE_TYPES.get(changeTypes); 160 throw DecodeException.error(message); 161 } 162 163 if ((changeTypes & 1) != 0) { 164 changeTypeSet.add(PersistentSearchChangeType.ADD); 165 } 166 167 if ((changeTypes & 2) != 0) { 168 changeTypeSet.add(PersistentSearchChangeType.DELETE); 169 } 170 171 if ((changeTypes & 4) != 0) { 172 changeTypeSet.add(PersistentSearchChangeType.MODIFY); 173 } 174 175 if ((changeTypes & 8) != 0) { 176 changeTypeSet.add(PersistentSearchChangeType.MODIFY_DN); 177 } 178 179 return new PersistentSearchRequestControl(control.isCritical(), changesOnly, 180 returnECs, Collections.unmodifiableSet(changeTypeSet)); 181 } 182 183 public String getOID() { 184 return OID; 185 } 186 }; 187 188 /** 189 * Creates a new persistent search request control. 190 * 191 * @param isCritical 192 * {@code true} if it is unacceptable to perform the operation 193 * without applying the semantics of this control, or 194 * {@code false} if it can be ignored 195 * @param changesOnly 196 * Indicates whether or not only updated entries should be 197 * returned (added, modified, deleted, or subject to a modifyDN 198 * operation). If this parameter is {@code false} then the search 199 * will initially return all the existing entries which match the 200 * filter. 201 * @param returnECs 202 * Indicates whether or not the entry change notification control 203 * should be included in updated entries that match the 204 * associated search criteria. 205 * @param changeTypes 206 * The types of update operation for which change notifications 207 * should be returned. 208 * @return The new control. 209 * @throws NullPointerException 210 * If {@code changeTypes} was {@code null}. 211 */ 212 public static PersistentSearchRequestControl newControl(final boolean isCritical, 213 final boolean changesOnly, final boolean returnECs, 214 final Collection<PersistentSearchChangeType> changeTypes) { 215 Reject.ifNull(changeTypes); 216 217 final Set<PersistentSearchChangeType> copyOfChangeTypes = 218 EnumSet.noneOf(PersistentSearchChangeType.class); 219 copyOfChangeTypes.addAll(changeTypes); 220 return new PersistentSearchRequestControl(isCritical, changesOnly, returnECs, Collections 221 .unmodifiableSet(copyOfChangeTypes)); 222 } 223 224 /** 225 * Creates a new persistent search request control. 226 * 227 * @param isCritical 228 * {@code true} if it is unacceptable to perform the operation 229 * without applying the semantics of this control, or 230 * {@code false} if it can be ignored 231 * @param changesOnly 232 * Indicates whether or not only updated entries should be 233 * returned (added, modified, deleted, or subject to a modifyDN 234 * operation). If this parameter is {@code false} then the search 235 * will initially return all the existing entries which match the 236 * filter. 237 * @param returnECs 238 * Indicates whether or not the entry change notification control 239 * should be included in updated entries that match the 240 * associated search criteria. 241 * @param changeTypes 242 * The types of update operation for which change notifications 243 * should be returned. 244 * @return The new control. 245 * @throws NullPointerException 246 * If {@code changeTypes} was {@code null}. 247 */ 248 public static PersistentSearchRequestControl newControl(final boolean isCritical, 249 final boolean changesOnly, final boolean returnECs, 250 final PersistentSearchChangeType... changeTypes) { 251 Reject.ifNull((Object) changeTypes); 252 253 return newControl(isCritical, changesOnly, returnECs, Arrays.asList(changeTypes)); 254 } 255 256 /** 257 * Indicates whether to only return entries that have been updated 258 * since the beginning of the search. 259 */ 260 private final boolean changesOnly; 261 262 /** 263 * Indicates whether entries returned as a result of changes to 264 * directory data should include the entry change notification control. 265 */ 266 private final boolean returnECs; 267 268 /** The logical OR of change types associated with this control. */ 269 private final Set<PersistentSearchChangeType> changeTypes; 270 271 private final boolean isCritical; 272 273 private PersistentSearchRequestControl(final boolean isCritical, final boolean changesOnly, 274 final boolean returnECs, final Set<PersistentSearchChangeType> changeTypes) { 275 this.isCritical = isCritical; 276 this.changesOnly = changesOnly; 277 this.returnECs = returnECs; 278 this.changeTypes = changeTypes; 279 } 280 281 /** 282 * Returns an unmodifiable set containing the types of update operation for 283 * which change notifications should be returned. 284 * 285 * @return An unmodifiable set containing the types of update operation for 286 * which change notifications should be returned. 287 */ 288 public Set<PersistentSearchChangeType> getChangeTypes() { 289 return changeTypes; 290 } 291 292 /** {@inheritDoc} */ 293 public String getOID() { 294 return OID; 295 } 296 297 /** {@inheritDoc} */ 298 public ByteString getValue() { 299 final ByteStringBuilder buffer = new ByteStringBuilder(); 300 final ASN1Writer writer = ASN1.getWriter(buffer); 301 try { 302 writer.writeStartSequence(); 303 304 int changeTypesInt = 0; 305 for (final PersistentSearchChangeType changeType : changeTypes) { 306 changeTypesInt |= changeType.intValue(); 307 } 308 writer.writeInteger(changeTypesInt); 309 310 writer.writeBoolean(changesOnly); 311 writer.writeBoolean(returnECs); 312 writer.writeEndSequence(); 313 return buffer.toByteString(); 314 } catch (final IOException ioe) { 315 // This should never happen unless there is a bug somewhere. 316 throw new RuntimeException(ioe); 317 } 318 } 319 320 /** {@inheritDoc} */ 321 public boolean hasValue() { 322 return true; 323 } 324 325 /** 326 * Returns {@code true} if only updated entries should be returned (added, 327 * modified, deleted, or subject to a modifyDN operation), otherwise 328 * {@code false} if the search will initially return all the existing 329 * entries which match the filter. 330 * 331 * @return {@code true} if only updated entries should be returned (added, 332 * modified, deleted, or subject to a modifyDN operation), otherwise 333 * {@code false} if the search will initially return all the 334 * existing entries which match the filter. 335 */ 336 public boolean isChangesOnly() { 337 return changesOnly; 338 } 339 340 /** {@inheritDoc} */ 341 public boolean isCritical() { 342 return isCritical; 343 } 344 345 /** 346 * Returns {@code true} if the entry change notification control should be 347 * included in updated entries that match the associated search criteria. 348 * 349 * @return {@code true} if the entry change notification control should be 350 * included in updated entries that match the associated search 351 * criteria. 352 */ 353 public boolean isReturnECs() { 354 return returnECs; 355 } 356 357 /** {@inheritDoc} */ 358 @Override 359 public String toString() { 360 final StringBuilder builder = new StringBuilder(); 361 builder.append("PersistentSearchRequestControl(oid="); 362 builder.append(getOID()); 363 builder.append(", criticality="); 364 builder.append(isCritical()); 365 builder.append(", changeTypes=["); 366 367 boolean comma = false; 368 for (final PersistentSearchChangeType type : changeTypes) { 369 if (comma) { 370 builder.append(", "); 371 } 372 builder.append(type); 373 comma = true; 374 } 375 376 builder.append("]("); 377 builder.append(changeTypes); 378 builder.append("), changesOnly="); 379 builder.append(changesOnly); 380 builder.append(", returnECs="); 381 builder.append(returnECs); 382 builder.append(")"); 383 return builder.toString(); 384 } 385}