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; 033 034import org.forgerock.i18n.LocalizableMessage; 035import org.forgerock.i18n.LocalizedIllegalArgumentException; 036import org.forgerock.i18n.slf4j.LocalizedLogger; 037import org.forgerock.opendj.io.ASN1; 038import org.forgerock.opendj.io.ASN1Reader; 039import org.forgerock.opendj.io.ASN1Writer; 040import org.forgerock.opendj.ldap.ByteString; 041import org.forgerock.opendj.ldap.ByteStringBuilder; 042import org.forgerock.opendj.ldap.DN; 043import org.forgerock.opendj.ldap.DecodeException; 044import org.forgerock.opendj.ldap.DecodeOptions; 045import org.forgerock.opendj.ldap.schema.Schema; 046import org.forgerock.util.Reject; 047 048/** 049 * The entry change notification response control as defined in 050 * draft-ietf-ldapext-psearch. This control provides additional information 051 * about the change that caused a particular entry to be returned as the result 052 * of a persistent search. 053 * 054 * <pre> 055 * Connection connection = ...; 056 * 057 * SearchRequest request = 058 * Requests.newSearchRequest( 059 * "dc=example,dc=com", SearchScope.WHOLE_SUBTREE, 060 * "(objectclass=inetOrgPerson)", "cn") 061 * .addControl(PersistentSearchRequestControl.newControl( 062 * true, true, true, // critical,changesOnly,returnECs 063 * PersistentSearchChangeType.ADD, 064 * PersistentSearchChangeType.DELETE, 065 * PersistentSearchChangeType.MODIFY, 066 * PersistentSearchChangeType.MODIFY_DN)); 067 * 068 * ConnectionEntryReader reader = connection.search(request); 069 * 070 * while (reader.hasNext()) { 071 * if (!reader.isReference()) { 072 * SearchResultEntry entry = reader.readEntry(); // Entry that changed 073 * 074 * EntryChangeNotificationResponseControl control = entry.getControl( 075 * EntryChangeNotificationResponseControl.DECODER, 076 * new DecodeOptions()); 077 * 078 * PersistentSearchChangeType type = control.getChangeType(); 079 * if (type.equals(PersistentSearchChangeType.MODIFY_DN)) { 080 * // Previous DN: control.getPreviousName() 081 * } 082 * // Change number: control.getChangeNumber()); 083 * } 084 * } 085 * 086 * </pre> 087 * 088 * @see PersistentSearchRequestControl 089 * @see PersistentSearchChangeType 090 * @see <a 091 * href="http://tools.ietf.org/html/draft-ietf-ldapext-psearch">draft-ietf-ldapext-psearch 092 * - Persistent Search: A Simple LDAP Change Notification Mechanism </a> 093 */ 094public final class EntryChangeNotificationResponseControl implements Control { 095 096 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 097 /** 098 * The OID for the entry change notification response control. 099 */ 100 public static final String OID = "2.16.840.1.113730.3.4.7"; 101 102 /** 103 * A decoder which can be used for decoding the entry change notification 104 * response control. 105 */ 106 public static final ControlDecoder<EntryChangeNotificationResponseControl> DECODER = 107 new ControlDecoder<EntryChangeNotificationResponseControl>() { 108 109 public EntryChangeNotificationResponseControl decodeControl(final Control control, 110 final DecodeOptions options) throws DecodeException { 111 Reject.ifNull(control, options); 112 113 if (control instanceof EntryChangeNotificationResponseControl) { 114 return (EntryChangeNotificationResponseControl) control; 115 } 116 117 if (!control.getOID().equals(OID)) { 118 final LocalizableMessage message = 119 ERR_ECN_CONTROL_BAD_OID.get(control.getOID(), OID); 120 throw DecodeException.error(message); 121 } 122 123 if (!control.hasValue()) { 124 // The response control must always have a value. 125 final LocalizableMessage message = ERR_ECN_NO_CONTROL_VALUE.get(); 126 throw DecodeException.error(message); 127 } 128 129 String previousDNString = null; 130 long changeNumber = -1; 131 PersistentSearchChangeType changeType; 132 final ASN1Reader reader = ASN1.getReader(control.getValue()); 133 try { 134 reader.readStartSequence(); 135 136 final int changeTypeInt = reader.readEnumerated(); 137 switch (changeTypeInt) { 138 case 1: 139 changeType = PersistentSearchChangeType.ADD; 140 break; 141 case 2: 142 changeType = PersistentSearchChangeType.DELETE; 143 break; 144 case 4: 145 changeType = PersistentSearchChangeType.MODIFY; 146 break; 147 case 8: 148 changeType = PersistentSearchChangeType.MODIFY_DN; 149 break; 150 default: 151 final LocalizableMessage message = 152 ERR_ECN_BAD_CHANGE_TYPE.get(changeTypeInt); 153 throw DecodeException.error(message); 154 } 155 156 if (reader.hasNextElement() 157 && (reader.peekType() == ASN1.UNIVERSAL_OCTET_STRING_TYPE)) { 158 if (changeType != PersistentSearchChangeType.MODIFY_DN) { 159 final LocalizableMessage message = 160 ERR_ECN_ILLEGAL_PREVIOUS_DN.get(String.valueOf(changeType)); 161 throw DecodeException.error(message); 162 } 163 164 previousDNString = reader.readOctetStringAsString(); 165 } 166 if (reader.hasNextElement() 167 && (reader.peekType() == ASN1.UNIVERSAL_INTEGER_TYPE)) { 168 changeNumber = reader.readInteger(); 169 } 170 } catch (final IOException e) { 171 logger.debug(LocalizableMessage.raw("%s", e)); 172 173 final LocalizableMessage message = 174 ERR_ECN_CANNOT_DECODE_VALUE.get(getExceptionMessage(e)); 175 throw DecodeException.error(message, e); 176 } 177 178 final Schema schema = 179 options.getSchemaResolver().resolveSchema(previousDNString); 180 DN previousDN = null; 181 if (previousDNString != null) { 182 try { 183 previousDN = DN.valueOf(previousDNString, schema); 184 } catch (final LocalizedIllegalArgumentException e) { 185 final LocalizableMessage message = 186 ERR_ECN_INVALID_PREVIOUS_DN.get(getExceptionMessage(e)); 187 throw DecodeException.error(message, e); 188 } 189 } 190 191 return new EntryChangeNotificationResponseControl(control.isCritical(), 192 changeType, previousDN, changeNumber); 193 } 194 195 public String getOID() { 196 return OID; 197 } 198 }; 199 200 /** 201 * Creates a new entry change notification response control with the 202 * provided change type and optional previous distinguished name and change 203 * number. 204 * 205 * @param type 206 * The change type for this change notification control. 207 * @param previousName 208 * The distinguished name that the entry had prior to a modify DN 209 * operation, or <CODE>null</CODE> if the operation was not a 210 * modify DN. 211 * @param changeNumber 212 * The change number for the associated change, or a negative 213 * value if no change number is available. 214 * @return The new control. 215 * @throws NullPointerException 216 * If {@code type} was {@code null}. 217 */ 218 public static EntryChangeNotificationResponseControl newControl( 219 final PersistentSearchChangeType type, final DN previousName, final long changeNumber) { 220 return new EntryChangeNotificationResponseControl(false, type, previousName, changeNumber); 221 } 222 223 /** 224 * Creates a new entry change notification response control with the 225 * provided change type and optional previous distinguished name and change 226 * number. The previous distinguished name, if provided, will be decoded 227 * using the default schema. 228 * 229 * @param type 230 * The change type for this change notification control. 231 * @param previousName 232 * The distinguished name that the entry had prior to a modify DN 233 * operation, or <CODE>null</CODE> if the operation was not a 234 * modify DN. 235 * @param changeNumber 236 * The change number for the associated change, or a negative 237 * value if no change number is available. 238 * @return The new control. 239 * @throws LocalizedIllegalArgumentException 240 * If {@code previousName} is not a valid LDAP string 241 * representation of a DN. 242 * @throws NullPointerException 243 * If {@code type} was {@code null}. 244 */ 245 public static EntryChangeNotificationResponseControl newControl( 246 final PersistentSearchChangeType type, final String previousName, 247 final long changeNumber) { 248 return new EntryChangeNotificationResponseControl(false, type, DN.valueOf(previousName), 249 changeNumber); 250 } 251 252 /** The previous DN for this change notification control. */ 253 private final DN previousName; 254 255 /** The change number for this change notification control. */ 256 private final long changeNumber; 257 258 /** The change type for this change notification control. */ 259 private final PersistentSearchChangeType changeType; 260 261 private final boolean isCritical; 262 263 private EntryChangeNotificationResponseControl(final boolean isCritical, 264 final PersistentSearchChangeType changeType, final DN previousName, 265 final long changeNumber) { 266 Reject.ifNull(changeType); 267 this.isCritical = isCritical; 268 this.changeType = changeType; 269 this.previousName = previousName; 270 this.changeNumber = changeNumber; 271 } 272 273 /** 274 * Returns the change number for this entry change notification control. 275 * 276 * @return The change number for this entry change notification control, or 277 * a negative value if no change number is available. 278 */ 279 public long getChangeNumber() { 280 return changeNumber; 281 } 282 283 /** 284 * Returns the change type for this entry change notification control. 285 * 286 * @return The change type for this entry change notification control. 287 */ 288 public PersistentSearchChangeType getChangeType() { 289 return changeType; 290 } 291 292 /** {@inheritDoc} */ 293 public String getOID() { 294 return OID; 295 } 296 297 /** 298 * Returns the distinguished name that the entry had prior to a modify DN 299 * operation, or <CODE>null</CODE> if the operation was not a modify DN. 300 * 301 * @return The distinguished name that the entry had prior to a modify DN 302 * operation. 303 */ 304 public DN getPreviousName() { 305 return previousName; 306 } 307 308 /** {@inheritDoc} */ 309 public ByteString getValue() { 310 final ByteStringBuilder buffer = new ByteStringBuilder(); 311 final ASN1Writer writer = ASN1.getWriter(buffer); 312 try { 313 writer.writeStartSequence(); 314 writer.writeInteger(changeType.intValue()); 315 316 if (previousName != null) { 317 writer.writeOctetString(previousName.toString()); 318 } 319 320 if (changeNumber > 0) { 321 writer.writeInteger(changeNumber); 322 } 323 writer.writeEndSequence(); 324 return buffer.toByteString(); 325 } catch (final IOException ioe) { 326 // This should never happen unless there is a bug somewhere. 327 throw new RuntimeException(ioe); 328 } 329 } 330 331 /** {@inheritDoc} */ 332 public boolean hasValue() { 333 return true; 334 } 335 336 /** {@inheritDoc} */ 337 public boolean isCritical() { 338 return isCritical; 339 } 340 341 /** {@inheritDoc} */ 342 @Override 343 public String toString() { 344 final StringBuilder builder = new StringBuilder(); 345 builder.append("EntryChangeNotificationResponseControl(oid="); 346 builder.append(getOID()); 347 builder.append(", criticality="); 348 builder.append(isCritical()); 349 builder.append(", changeType="); 350 builder.append(changeType); 351 builder.append(", previousDN=\""); 352 builder.append(previousName); 353 builder.append("\""); 354 builder.append(", changeNumber="); 355 builder.append(changeNumber); 356 builder.append(")"); 357 return builder.toString(); 358 } 359 360}