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 2006-2010 Sun Microsystems, Inc. 015 * Portions Copyright 2014-2016 ForgeRock AS. 016 */ 017package org.opends.server.core; 018 019import java.util.Collections; 020import java.util.List; 021import java.util.Set; 022import java.util.concurrent.CopyOnWriteArrayList; 023 024import org.forgerock.i18n.slf4j.LocalizedLogger; 025import org.forgerock.opendj.ldap.ResultCode; 026import org.opends.server.controls.EntryChangeNotificationControl; 027import org.opends.server.controls.PersistentSearchChangeType; 028import org.opends.server.types.CancelResult; 029import org.opends.server.types.Control; 030import org.forgerock.opendj.ldap.DN; 031import org.opends.server.types.DirectoryException; 032import org.opends.server.types.Entry; 033 034import static org.opends.server.controls.PersistentSearchChangeType.*; 035 036/** 037 * This class defines a data structure that will be used to hold the 038 * information necessary for processing a persistent search. 039 * <p> 040 * Work flow element implementations are responsible for managing the 041 * persistent searches that they are currently handling. 042 * <p> 043 * Typically, a work flow element search operation will first decode 044 * the persistent search control and construct a new {@code 045 * PersistentSearch}. 046 * <p> 047 * Once the initial search result set has been returned and no errors 048 * encountered, the work flow element implementation should register a 049 * cancellation callback which will be invoked when the persistent 050 * search is cancelled. This is achieved using 051 * {@link #registerCancellationCallback(CancellationCallback)}. The 052 * callback should make sure that any resources associated with the 053 * {@code PersistentSearch} are released. This may included removing 054 * the {@code PersistentSearch} from a list, or abandoning a 055 * persistent search operation that has been sent to a remote server. 056 * <p> 057 * Finally, the {@code PersistentSearch} should be enabled using 058 * {@link #enable()}. This method will register the {@code 059 * PersistentSearch} with the client connection and notify the 060 * underlying search operation that no result should be sent to the 061 * client. 062 * <p> 063 * Work flow element implementations should {@link #cancel()} active 064 * persistent searches when the work flow element fails or is shut 065 * down. 066 */ 067public final class PersistentSearch 068{ 069 /** 070 * A cancellation call-back which can be used by work-flow element 071 * implementations in order to register for resource cleanup when a 072 * persistent search is cancelled. 073 */ 074 public static interface CancellationCallback 075 { 076 /** 077 * The provided persistent search has been cancelled. Any 078 * resources associated with the persistent search should be 079 * released. 080 * 081 * @param psearch 082 * The persistent search which has just been cancelled. 083 */ 084 void persistentSearchCancelled(PersistentSearch psearch); 085 } 086 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 087 088 /** Cancel a persistent search. */ 089 private static synchronized void cancel(PersistentSearch psearch) 090 { 091 if (!psearch.isCancelled) 092 { 093 psearch.isCancelled = true; 094 095 // The persistent search can no longer be cancelled. 096 psearch.searchOperation.getClientConnection().deregisterPersistentSearch(psearch); 097 098 DirectoryServer.deregisterPersistentSearch(); 099 100 // Notify any cancellation callbacks. 101 for (CancellationCallback callback : psearch.cancellationCallbacks) 102 { 103 try 104 { 105 callback.persistentSearchCancelled(psearch); 106 } 107 catch (Exception e) 108 { 109 logger.traceException(e); 110 } 111 } 112 } 113 } 114 115 /** Cancellation callbacks which should be run when this persistent search is cancelled. */ 116 private final List<CancellationCallback> cancellationCallbacks = new CopyOnWriteArrayList<>(); 117 118 /** The set of change types to send to the client. */ 119 private final Set<PersistentSearchChangeType> changeTypes; 120 121 /** Indicates whether this persistent search has already been aborted. */ 122 private boolean isCancelled; 123 124 /** Indicates whether entries returned should include the entry change notification control. */ 125 private final boolean returnECs; 126 127 /** The reference to the associated search operation. */ 128 private final SearchOperation searchOperation; 129 130 /** 131 * Indicates whether to only return entries that have been updated since the 132 * beginning of the search. 133 */ 134 private final boolean changesOnly; 135 136 /** 137 * Creates a new persistent search object with the provided information. 138 * 139 * @param searchOperation 140 * The search operation for this persistent search. 141 * @param changeTypes 142 * The change types for which changes should be examined. 143 * @param changesOnly 144 * whether to only return entries that have been updated since the 145 * beginning of the search 146 * @param returnECs 147 * Indicates whether to include entry change notification controls in 148 * search result entries sent to the client. 149 */ 150 public PersistentSearch(SearchOperation searchOperation, 151 Set<PersistentSearchChangeType> changeTypes, boolean changesOnly, 152 boolean returnECs) 153 { 154 this.searchOperation = searchOperation; 155 this.changeTypes = changeTypes; 156 this.changesOnly = changesOnly; 157 this.returnECs = returnECs; 158 } 159 160 /** 161 * Cancels this persistent search operation. On exit this persistent 162 * search will no longer be valid and any resources associated with 163 * it will have been released. In addition, any other persistent 164 * searches that are associated with this persistent search will 165 * also be canceled. 166 * 167 * @return The result of the cancellation. 168 */ 169 public synchronized CancelResult cancel() 170 { 171 if (!isCancelled) 172 { 173 // Cancel this persistent search. 174 cancel(this); 175 176 // Cancel any other persistent searches which are associated 177 // with this one. For example, a persistent search may be 178 // distributed across multiple proxies. 179 for (PersistentSearch psearch : searchOperation.getClientConnection() 180 .getPersistentSearches()) 181 { 182 if (psearch.getMessageID() == getMessageID()) 183 { 184 cancel(psearch); 185 } 186 } 187 } 188 189 return new CancelResult(ResultCode.CANCELLED, null); 190 } 191 192 /** 193 * Gets the message ID associated with this persistent search. 194 * 195 * @return The message ID associated with this persistent search. 196 */ 197 public int getMessageID() 198 { 199 return searchOperation.getMessageID(); 200 } 201 202 /** 203 * Get the search operation associated with this persistent search. 204 * 205 * @return The search operation associated with this persistent search. 206 */ 207 public SearchOperation getSearchOperation() 208 { 209 return searchOperation; 210 } 211 212 /** 213 * Returns whether only entries updated after the beginning of this persistent 214 * search should be returned. 215 * 216 * @return true if only entries updated after the beginning of this search 217 * should be returned, false otherwise 218 */ 219 public boolean isChangesOnly() 220 { 221 return changesOnly; 222 } 223 224 /** 225 * Notifies the persistent searches that an entry has been added. 226 * 227 * @param entry 228 * The entry that was added. 229 */ 230 public void processAdd(Entry entry) 231 { 232 if (changeTypes.contains(ADD) 233 && isInScope(entry.getName()) 234 && matchesFilter(entry)) 235 { 236 sendEntry(entry, createControls(ADD, null)); 237 } 238 } 239 240 private boolean isInScope(final DN dn) 241 { 242 final DN baseDN = searchOperation.getBaseDN(); 243 switch (searchOperation.getScope().asEnum()) 244 { 245 case BASE_OBJECT: 246 return baseDN.equals(dn); 247 case SINGLE_LEVEL: 248 return baseDN.equals(DirectoryServer.getParentDNInSuffix(dn)); 249 case WHOLE_SUBTREE: 250 return baseDN.isSuperiorOrEqualTo(dn); 251 case SUBORDINATES: 252 return !baseDN.equals(dn) && baseDN.isSuperiorOrEqualTo(dn); 253 default: 254 return false; 255 } 256 } 257 258 private boolean matchesFilter(Entry entry) 259 { 260 try 261 { 262 final boolean filterMatchesEntry = searchOperation.getFilter().matchesEntry(entry); 263 if (logger.isTraceEnabled()) 264 { 265 logger.trace(this + " " + entry + " filter=" + filterMatchesEntry); 266 } 267 return filterMatchesEntry; 268 } 269 catch (DirectoryException de) 270 { 271 logger.traceException(de); 272 273 // FIXME -- Do we need to do anything here? 274 return false; 275 } 276 } 277 278 /** 279 * Notifies the persistent searches that an entry has been deleted. 280 * 281 * @param entry 282 * The entry that was deleted. 283 */ 284 public void processDelete(Entry entry) 285 { 286 if (changeTypes.contains(DELETE) 287 && isInScope(entry.getName()) 288 && matchesFilter(entry)) 289 { 290 sendEntry(entry, createControls(DELETE, null)); 291 } 292 } 293 294 /** 295 * Notifies the persistent searches that an entry has been modified. 296 * 297 * @param entry 298 * The entry after it was modified. 299 */ 300 public void processModify(Entry entry) 301 { 302 processModify(entry, entry); 303 } 304 305 /** 306 * Notifies persistent searches that an entry has been modified. 307 * 308 * @param entry 309 * The entry after it was modified. 310 * @param oldEntry 311 * The entry before it was modified. 312 */ 313 public void processModify(Entry entry, Entry oldEntry) 314 { 315 if (changeTypes.contains(MODIFY) 316 && isInScopeForModify(oldEntry.getName()) 317 && anyMatchesFilter(entry, oldEntry)) 318 { 319 sendEntry(entry, createControls(MODIFY, null)); 320 } 321 } 322 323 private boolean isInScopeForModify(final DN dn) 324 { 325 final DN baseDN = searchOperation.getBaseDN(); 326 switch (searchOperation.getScope().asEnum()) 327 { 328 case BASE_OBJECT: 329 return baseDN.equals(dn); 330 case SINGLE_LEVEL: 331 return baseDN.equals(dn.parent()); 332 case WHOLE_SUBTREE: 333 return baseDN.isSuperiorOrEqualTo(dn); 334 case SUBORDINATES: 335 return !baseDN.equals(dn) && baseDN.isSuperiorOrEqualTo(dn); 336 default: 337 return false; 338 } 339 } 340 341 private boolean anyMatchesFilter(Entry entry, Entry oldEntry) 342 { 343 return matchesFilter(oldEntry) || matchesFilter(entry); 344 } 345 346 /** 347 * Notifies the persistent searches that an entry has been renamed. 348 * 349 * @param entry 350 * The entry after it was modified. 351 * @param oldDN 352 * The DN of the entry before it was renamed. 353 */ 354 public void processModifyDN(Entry entry, DN oldDN) 355 { 356 if (changeTypes.contains(MODIFY_DN) 357 && isAnyInScopeForModify(entry, oldDN) 358 && matchesFilter(entry)) 359 { 360 sendEntry(entry, createControls(MODIFY_DN, oldDN)); 361 } 362 } 363 364 private boolean isAnyInScopeForModify(Entry entry, DN oldDN) 365 { 366 return isInScopeForModify(oldDN) || isInScopeForModify(entry.getName()); 367 } 368 369 /** 370 * The entry is one that should be sent to the client. See if we also need to 371 * construct an entry change notification control. 372 */ 373 private List<Control> createControls(PersistentSearchChangeType changeType, 374 DN previousDN) 375 { 376 if (returnECs) 377 { 378 final Control c = previousDN != null 379 ? new EntryChangeNotificationControl(changeType, previousDN, -1) 380 : new EntryChangeNotificationControl(changeType, -1); 381 return Collections.singletonList(c); 382 } 383 return Collections.emptyList(); 384 } 385 386 private void sendEntry(Entry entry, List<Control> entryControls) 387 { 388 try 389 { 390 if (!searchOperation.returnEntry(entry, entryControls)) 391 { 392 cancel(); 393 searchOperation.sendSearchResultDone(); 394 } 395 } 396 catch (Exception e) 397 { 398 logger.traceException(e); 399 400 cancel(); 401 402 try 403 { 404 searchOperation.sendSearchResultDone(); 405 } 406 catch (Exception e2) 407 { 408 logger.traceException(e2); 409 } 410 } 411 } 412 413 /** 414 * Registers a cancellation callback with this persistent search. 415 * The cancellation callback will be notified when this persistent 416 * search has been cancelled. 417 * 418 * @param callback 419 * The cancellation callback. 420 */ 421 public void registerCancellationCallback(CancellationCallback callback) 422 { 423 cancellationCallbacks.add(callback); 424 } 425 426 /** 427 * Enable this persistent search. The persistent search will be 428 * registered with the client connection and will be prevented from 429 * sending responses to the client. 430 */ 431 public void enable() 432 { 433 searchOperation.getClientConnection().registerPersistentSearch(this); 434 searchOperation.setSendResponse(false); 435 //Register itself with the Core. 436 DirectoryServer.registerPersistentSearch(); 437 } 438 439 /** 440 * Retrieves a string representation of this persistent search. 441 * 442 * @return A string representation of this persistent search. 443 */ 444 @Override 445 public String toString() 446 { 447 StringBuilder buffer = new StringBuilder(); 448 toString(buffer); 449 return buffer.toString(); 450 } 451 452 /** 453 * Appends a string representation of this persistent search to the 454 * provided buffer. 455 * 456 * @param buffer 457 * The buffer to which the information should be appended. 458 */ 459 public void toString(StringBuilder buffer) 460 { 461 buffer.append("PersistentSearch(connID="); 462 buffer.append(searchOperation.getConnectionID()); 463 buffer.append(",opID="); 464 buffer.append(searchOperation.getOperationID()); 465 buffer.append(",baseDN=\""); 466 buffer.append(searchOperation.getBaseDN()); 467 buffer.append("\",scope="); 468 buffer.append(searchOperation.getScope()); 469 buffer.append(",filter=\""); 470 searchOperation.getFilter().toString(buffer); 471 buffer.append("\")"); 472 } 473}