001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 * 019 */ 020package org.apache.directory.server.ldap.replication.provider; 021 022 023import org.apache.directory.api.ldap.extras.controls.syncrepl.syncState.SyncStateTypeEnum; 024import org.apache.directory.api.ldap.extras.controls.syncrepl.syncState.SyncStateValue; 025import org.apache.directory.api.ldap.extras.controls.syncrepl.syncState.SyncStateValueImpl; 026import org.apache.directory.api.ldap.model.constants.SchemaConstants; 027import org.apache.directory.api.ldap.model.entry.Entry; 028import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException; 029import org.apache.directory.api.ldap.model.message.AbandonListener; 030import org.apache.directory.api.ldap.model.message.AbandonableRequest; 031import org.apache.directory.api.ldap.model.message.SearchRequest; 032import org.apache.directory.api.ldap.model.message.SearchResultEntry; 033import org.apache.directory.api.ldap.model.message.SearchResultEntryImpl; 034import org.apache.directory.api.ldap.model.message.controls.ChangeType; 035import org.apache.directory.api.util.Strings; 036import org.apache.directory.server.constants.ServerDNConstants; 037import org.apache.directory.server.core.api.DirectoryService; 038import org.apache.directory.server.core.api.entry.ClonedServerEntry; 039import org.apache.directory.server.core.api.event.DirectoryListener; 040import org.apache.directory.server.core.api.event.EventType; 041import org.apache.directory.server.core.api.interceptor.context.AbstractChangeOperationContext; 042import org.apache.directory.server.core.api.interceptor.context.AddOperationContext; 043import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext; 044import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext; 045import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext; 046import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext; 047import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext; 048import org.apache.directory.server.i18n.I18n; 049import org.apache.directory.server.ldap.LdapProtocolUtils; 050import org.apache.directory.server.ldap.LdapSession; 051import org.apache.directory.server.ldap.replication.ReplicaEventMessage; 052import org.apache.mina.core.future.WriteFuture; 053import org.slf4j.Logger; 054import org.slf4j.LoggerFactory; 055 056 057/** 058 * A listener associated with the replication system. It does send the modifications to the 059 * consumer, if it's connected, or store the data into a queue for a later transmission. 060 * 061 * Note: we always log the entry irrespective of the client's connection status for guaranteed delivery 062 * 063 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 064 */ 065public class SyncReplSearchListener implements DirectoryListener, AbandonListener 066{ 067 /** Logger for this class */ 068 private static final Logger LOG = LoggerFactory.getLogger( SyncReplSearchListener.class ); 069 070 /** The ldap session */ 071 private LdapSession session; 072 073 /** The search request we are processing */ 074 private SearchRequest searchRequest; 075 076 /** A flag telling if we push the response to the consumer or if we store them in a queue */ 077 private volatile boolean pushInRealTime; 078 079 /** The consumer configuration */ 080 private final ReplicaEventLog consumerMsgLog; 081 082 private static String replConsumerConfigDn = Strings.toLowerCaseAscii( ServerDNConstants.REPL_CONSUMER_CONFIG_DN ); 083 private static String schemaDn = Strings.toLowerCaseAscii( SchemaConstants.OU_SCHEMA ); 084 private static String replConsumerDn = Strings.toLowerCaseAscii( ServerDNConstants.REPL_CONSUMER_DN_STR ); 085 086 /** 087 * Create a new instance of a consumer listener 088 * 089 * @param session The LDAP session to use for this listener 090 * @param searchRequest The searchRequest to process 091 * @param consumerMsgLog The consumer configuration 092 * @param pushInRealTime Tells if we push the results to the consumer in real time 093 */ 094 SyncReplSearchListener( LdapSession session, SearchRequest searchRequest, ReplicaEventLog consumerMsgLog, 095 boolean pushInRealTime ) 096 { 097 this.pushInRealTime = pushInRealTime; 098 setSession( session ); 099 setSearchRequest( searchRequest ); 100 this.consumerMsgLog = consumerMsgLog; 101 } 102 103 104 /** 105 * Store the Ldap session to use 106 * @param session The Ldap Session to use 107 */ 108 public void setSession( LdapSession session ) 109 { 110 this.session = session; 111 } 112 113 114 /** 115 * Stores the SearchRequest, and associate a AbandonListener to it 116 * 117 * @param searchRequest The SearchRequest instance to store 118 */ 119 public void setSearchRequest( SearchRequest searchRequest ) 120 { 121 this.searchRequest = searchRequest; 122 123 if ( searchRequest != null ) 124 { 125 searchRequest.addAbandonListener( this ); 126 } 127 } 128 129 130 @Override 131 public boolean isSynchronous() 132 { 133 return true; // always synchronous 134 } 135 136 137 /** 138 * Abandon a SearchRequest 139 * 140 * @param searchRequest The SearchRequest to abandon 141 */ 142 public void requestAbandoned( AbandonableRequest searchRequest ) 143 { 144 try 145 { 146 if ( session != null ) 147 { 148 // We first remove the Listener from the session's chain 149 session.getCoreSession().getDirectoryService().getEventService().removeListener( this ); 150 } 151 152 /* 153 * From RFC 2251 Section 4.11: 154 * 155 * In the event that a server receives an Abandon Request on a Search 156 * operation in the midst of transmitting responses to the Search, that 157 * server MUST cease transmitting entry responses to the abandoned 158 * request immediately, and MUST NOT send the SearchResultDone. Of 159 * course, the server MUST ensure that only properly encoded LDAPMessage 160 * PDUs are transmitted. 161 * 162 * SO DON'T SEND BACK ANYTHING!!!!! 163 */ 164 } 165 catch ( Exception e ) 166 { 167 LOG.error( I18n.err( I18n.ERR_164 ), e ); 168 } 169 } 170 171 172 /** 173 * Create the SyncStateValue control 174 */ 175 private SyncStateValue createControl( DirectoryService directoryService, SyncStateTypeEnum operation, Entry entry ) 176 throws LdapInvalidAttributeValueException 177 { 178 SyncStateValue syncStateValue = new SyncStateValueImpl(); 179 180 syncStateValue.setSyncStateType( operation ); 181 String uuidStr = entry.get( SchemaConstants.ENTRY_UUID_AT ).getString(); 182 syncStateValue.setEntryUUID( Strings.uuidToBytes( uuidStr ) ); 183 syncStateValue.setCookie( getCookie( entry ) ); 184 185 return syncStateValue; 186 } 187 188 189 /** 190 * Send the result to the consumer. If the consumer has disconnected, we fail back to the queue. 191 */ 192 private void sendResult( SearchResultEntry searchResultEntry, Entry entry, EventType eventType, 193 SyncStateValue syncStateValue ) 194 { 195 searchResultEntry.addControl( syncStateValue ); 196 197 LOG.debug( "sending event {} of entry {}", eventType, entry.getDn() ); 198 WriteFuture future = session.getIoSession().write( searchResultEntry ); 199 200 // Now, send the entry to the consumer 201 handleWriteFuture( future, entry, eventType ); 202 } 203 204 205 /** 206 * Process a ADD operation. The added entry is pushed to the consumer if it's connected, 207 * or stored in the consumer's queue if it's not. 208 * 209 * @param addContext The Addition operation context 210 */ 211 public void entryAdded( AddOperationContext addContext ) 212 { 213 Entry entry = addContext.getEntry(); 214 215 if ( isConfigEntry( entry ) || isNotValidForReplication( addContext ) ) 216 { 217 return; 218 } 219 220 try 221 { 222 //System.out.println( "ADD Listener : log " + entry.getDn() ); 223 // we log it first 224 consumerMsgLog.log( new ReplicaEventMessage( ChangeType.ADD, entry ) ); 225 226 // We send the added entry directly to the consumer if it's connected 227 if ( pushInRealTime ) 228 { 229 // Construct a new SearchResultEntry 230 SearchResultEntry resultEntry = new SearchResultEntryImpl( searchRequest.getMessageId() ); 231 resultEntry.setObjectName( entry.getDn() ); 232 resultEntry.setEntry( entry ); 233 234 // Create the control which will be added to the response. 235 SyncStateValue syncAdd = createControl( session.getCoreSession().getDirectoryService(), SyncStateTypeEnum.ADD, entry ); 236 237 sendResult( resultEntry, entry, EventType.ADD, syncAdd ); 238 } 239 } 240 catch ( LdapInvalidAttributeValueException e ) 241 { 242 // shouldn't happen 243 LOG.error( e.getMessage(), e ); 244 } 245 } 246 247 248 /** 249 * Process a Delete operation. A delete event is send to the consumer, or stored in its 250 * queue if the consumer is not connected. 251 * 252 * @param deleteContext The delete operation context 253 */ 254 public void entryDeleted( DeleteOperationContext deleteContext ) 255 { 256 Entry entry = deleteContext.getEntry(); 257 258 if ( isConfigEntry( entry ) || isNotValidForReplication( deleteContext ) ) 259 { 260 return; 261 } 262 263 sendDeletedEntry( ( ( ClonedServerEntry ) entry ).getClonedEntry() ); 264 } 265 266 267 /** 268 * A helper method, as the delete opertaionis used by the ModDN operations. 269 */ 270 private void sendDeletedEntry( Entry entry ) 271 { 272 try 273 { 274 //System.out.println( "DELETE Listener : log " + entry.getDn() ); 275 consumerMsgLog.log( new ReplicaEventMessage( ChangeType.DELETE, entry ) ); 276 277 if ( pushInRealTime ) 278 { 279 SearchResultEntry resultEntry = new SearchResultEntryImpl( searchRequest.getMessageId() ); 280 resultEntry.setObjectName( entry.getDn() ); 281 resultEntry.setEntry( entry ); 282 283 SyncStateValue syncDelete = createControl( session.getCoreSession().getDirectoryService(), SyncStateTypeEnum.DELETE, entry ); 284 285 sendResult( resultEntry, entry, EventType.DELETE, syncDelete ); 286 } 287 } 288 catch ( LdapInvalidAttributeValueException e ) 289 { 290 // shouldn't happen 291 LOG.error( e.getMessage(), e ); 292 } 293 } 294 295 296 /** 297 * Process a Modify operation. A modify event is send to the consumer, or stored in its 298 * queue if the consumer is not connected. 299 * 300 * @param modifyContext The modify operation context 301 */ 302 public void entryModified( ModifyOperationContext modifyContext ) 303 { 304 Entry alteredEntry = modifyContext.getAlteredEntry(); 305 306 if ( isConfigEntry( alteredEntry ) || isNotValidForReplication( modifyContext ) ) 307 { 308 return; 309 } 310 311 try 312 { 313 //System.out.println( "MODIFY Listener : log " + alteredEntry.getDn() ); 314 consumerMsgLog.log( new ReplicaEventMessage( ChangeType.MODIFY, alteredEntry ) ); 315 316 if ( pushInRealTime ) 317 { 318 319 SearchResultEntry resultEntry = new SearchResultEntryImpl( searchRequest.getMessageId() ); 320 resultEntry.setObjectName( modifyContext.getDn() ); 321 resultEntry.setEntry( alteredEntry ); 322 323 SyncStateValue syncModify = createControl( session.getCoreSession().getDirectoryService(), SyncStateTypeEnum.MODIFY, alteredEntry ); 324 325 sendResult( resultEntry, alteredEntry, EventType.MODIFY, syncModify ); 326 } 327 } 328 catch ( Exception e ) 329 { 330 LOG.error( e.getMessage(), e ); 331 } 332 } 333 334 335 /** 336 * Process a Move operation. A MODDN event is send to the consumer, or stored in its 337 * queue if the consumer is not connected. 338 * 339 * @param moveContext The move operation context 340 */ 341 public void entryMoved( MoveOperationContext moveContext ) 342 { 343 // should always send the modified entry cause the consumer perform the modDn operation locally 344 Entry entry = moveContext.getModifiedEntry(); 345 346 if ( isConfigEntry( entry ) || isNotValidForReplication( moveContext ) ) 347 { 348 return; 349 } 350 351 try 352 { 353 if ( !moveContext.getNewSuperior().isDescendantOf( consumerMsgLog.getSearchCriteria().getBase() ) ) 354 { 355 sendDeletedEntry( moveContext.getOriginalEntry() ); 356 return; 357 } 358 359 //System.out.println( "MOVE Listener : log " + moveContext.getDn() + " moved to " + moveContext.getNewSuperior() ); 360 consumerMsgLog.log( new ReplicaEventMessage( ChangeType.MODDN, entry ) ); 361 362 if ( pushInRealTime ) 363 { 364 SearchResultEntry resultEntry = new SearchResultEntryImpl( searchRequest.getMessageId() ); 365 resultEntry.setObjectName( moveContext.getDn() ); 366 resultEntry.setEntry( entry ); 367 368 SyncStateValue syncModify = createControl( session.getCoreSession().getDirectoryService(), SyncStateTypeEnum.MODDN, entry ); 369 370 sendResult( resultEntry, entry, EventType.MOVE, syncModify ); 371 } 372 } 373 catch ( Exception e ) 374 { 375 LOG.error( e.getMessage(), e ); 376 } 377 } 378 379 380 /** 381 * Process a MoveAndRename operation. A MODDN event is send to the consumer, or stored in its 382 * queue if the consumer is not connected. 383 * 384 * @param moveAndRenameContext The move and rename operation context 385 */ 386 public void entryMovedAndRenamed( MoveAndRenameOperationContext moveAndRenameContext ) 387 { 388 // should always send the modified entry cause the consumer perform the modDn operation locally 389 Entry entry = moveAndRenameContext.getModifiedEntry(); 390 391 if ( isConfigEntry( entry ) || isNotValidForReplication( moveAndRenameContext ) ) 392 { 393 return; 394 } 395 396 try 397 { 398 if ( !moveAndRenameContext.getNewSuperiorDn().isDescendantOf( consumerMsgLog.getSearchCriteria().getBase() ) ) 399 { 400 sendDeletedEntry( entry ); 401 return; 402 } 403 404 405 //System.out.println( "MOVE AND RENAME Listener : log " + moveAndRenameContext.getDn() + 406 // " moved to " + moveAndRenameContext.getNewSuperiorDn() + " renamed to " + moveAndRenameContext.getNewRdn() ); 407 consumerMsgLog.log( new ReplicaEventMessage( ChangeType.MODDN, entry ) ); 408 409 if ( pushInRealTime ) 410 { 411 SearchResultEntry resultEntry = new SearchResultEntryImpl( searchRequest.getMessageId() ); 412 resultEntry.setObjectName( entry.getDn() ); 413 resultEntry.setEntry( entry ); 414 415 SyncStateValue syncModify = createControl( session.getCoreSession().getDirectoryService(), SyncStateTypeEnum.MODDN, entry ); 416 417 sendResult( resultEntry, entry, EventType.MOVE_AND_RENAME, syncModify ); 418 } 419 } 420 catch ( Exception e ) 421 { 422 LOG.error( e.getMessage(), e ); 423 } 424 } 425 426 427 /** 428 * Process a Rename operation. A MODDN event is send to the consumer, or stored in its 429 * queue if the consumer is not connected. 430 * 431 * @param renameContext The rename operation context 432 */ 433 public void entryRenamed( RenameOperationContext renameContext ) 434 { 435 // should always send the modified entry cause the consumer perform the modDn operation locally 436 Entry entry = renameContext.getModifiedEntry(); 437 438 if ( isConfigEntry( entry ) || isNotValidForReplication( renameContext ) ) 439 { 440 return; 441 } 442 443 try 444 { 445 // should always send the original entry cause the consumer perform the modDn operation there 446 //System.out.println( "RENAME Listener : log " + renameContext.getDn() + " renamed to " + renameContext.getNewRdn() ); 447 consumerMsgLog.log( new ReplicaEventMessage( ChangeType.MODDN, entry ) ); 448 449 if ( pushInRealTime ) 450 { 451 SearchResultEntry resultEntry = new SearchResultEntryImpl( searchRequest.getMessageId() ); 452 resultEntry.setObjectName( entry.getDn() ); 453 resultEntry.setEntry( entry ); 454 455 SyncStateValue syncModify = createControl( session.getCoreSession().getDirectoryService(), SyncStateTypeEnum.MODDN, entry ); 456 457 // In this case, the cookie is different 458 syncModify.setCookie( getCookie( entry ) ); 459 460 sendResult( resultEntry, entry, EventType.RENAME, syncModify ); 461 } 462 } 463 catch ( Exception e ) 464 { 465 LOG.error( e.getMessage(), e ); 466 } 467 } 468 469 470 /** 471 * @return true if the entries are sent to the consumer in real time 472 */ 473 public boolean isPushInRealTime() 474 { 475 return pushInRealTime; 476 } 477 478 479 /** 480 * Set the pushInRealTime parameter 481 * @param pushInRealTime true if the entries must be push to the consumer directly 482 */ 483 public void setPushInRealTime( boolean pushInRealTime ) 484 { 485 this.pushInRealTime = pushInRealTime; 486 } 487 488 489 /** 490 * Get the cookie from the entry 491 */ 492 private byte[] getCookie( Entry entry ) throws LdapInvalidAttributeValueException 493 { 494 String csn = entry.get( SchemaConstants.ENTRY_CSN_AT ).getString(); 495 496 return LdapProtocolUtils.createCookie( consumerMsgLog.getId(), csn ); 497 } 498 499 500 /** 501 * Process the writing of the replicated entry to the consumer 502 */ 503 private void handleWriteFuture( WriteFuture future, Entry entry, EventType event ) 504 { 505 // Let the operation be executed. 506 // Note : we wait 10 seconds max 507 future.awaitUninterruptibly( 10000L ); 508 509 if ( !future.isWritten() ) 510 { 511 LOG.error( "Failed to write to the consumer {} during the event {} on entry {}", new Object[] { 512 consumerMsgLog.getId(), event, entry.getDn() } ); 513 LOG.error( "", future.getException() ); 514 515 // set realtime push to false, will be set back to true when the client 516 // comes back and sends another request this flag will be set to true 517 pushInRealTime = false; 518 } 519 else 520 { 521 try 522 { 523 // if successful update the last sent CSN 524 consumerMsgLog.setLastSentCsn( entry.get( SchemaConstants.ENTRY_CSN_AT ).getString() ); 525 } 526 catch ( Exception e ) 527 { 528 //should never happen 529 LOG.error( "No entry CSN attribute found", e ); 530 } 531 } 532 } 533 534 535 /** 536 * checks if the given entry belongs to the ou=config or ou=schema partition 537 * We don't replicate those two partitions 538 * @param entry the entry 539 * @return true if the entry belongs to ou=config partition, false otherwise 540 */ 541 private boolean isConfigEntry( Entry entry ) 542 { 543 // we can do Dn.isDescendantOf but in this part of the 544 // server the DNs are all normalized and a simple string compare should 545 // do the trick 546 547 String name = Strings.toLowerCase( entry.getDn().getName() ); 548 549 if ( name.endsWith( replConsumerConfigDn ) 550 || name.endsWith( schemaDn ) 551 || name.endsWith( replConsumerDn ) ) 552 { 553 return true; 554 } 555 556 // do not replicate the changes made to transport config entries 557 return name.startsWith( "ads-transportid" ) && name.endsWith( ServerDNConstants.CONFIG_DN ); 558 } 559 560 561 private boolean isNotValidForReplication( AbstractChangeOperationContext ctx ) 562 { 563 if ( ctx.isGenerateNoReplEvt() ) 564 { 565 return true; 566 } 567 568 return isMmrConfiguredToReceiver( ctx ); 569 } 570 571 572 /** 573 * checks if the sender of this replication event is setup with MMR 574 * (Note: this method is used to prevent sending a replicated event back to the sender after 575 * performing local update) 576 * @param ctx the operation's context 577 * @return true if the rid present in operation context is same as the event log's ID, false otherwise 578 */ 579 private boolean isMmrConfiguredToReceiver( AbstractChangeOperationContext ctx ) 580 { 581 if ( ctx.isReplEvent() ) 582 { 583 boolean skip = ( ctx.getRid() == consumerMsgLog.getId() ); 584 585 if ( skip ) 586 { 587 LOG.debug( "RID in operation context matches with the ID of replication event log {} for host {}", consumerMsgLog.getName(), consumerMsgLog.getHostName() ); 588 } 589 590 return skip; 591 } 592 593 return false; 594 } 595 596 597 /** 598 * {@inheritDoc} 599 */ 600 public String toString() 601 { 602 StringBuilder sb = new StringBuilder(); 603 604 sb.append( "SyncReplSearchListener : \n" ); 605 sb.append( '\'' ).append( searchRequest ).append( "', " ); 606 sb.append( '\'' ).append( pushInRealTime ).append( "', \n" ); 607 sb.append( consumerMsgLog ); 608 sb.append( '\n' ); 609 610 return sb.toString(); 611 } 612}