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