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