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