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