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 */
020
021package org.apache.directory.server.ldap.replication.provider;
022
023
024import static java.lang.Math.min;
025import static org.apache.directory.server.ldap.LdapServer.NO_SIZE_LIMIT;
026import static org.apache.directory.server.ldap.LdapServer.NO_TIME_LIMIT;
027
028import java.io.File;
029import java.io.FilenameFilter;
030import java.io.IOException;
031import java.net.InetSocketAddress;
032import java.util.HashSet;
033import java.util.List;
034import java.util.Map;
035import java.util.Set;
036import java.util.concurrent.ConcurrentHashMap;
037import java.util.concurrent.CountDownLatch;
038import java.util.concurrent.TimeUnit;
039import java.util.concurrent.atomic.AtomicInteger;
040
041import org.apache.directory.api.ldap.extras.controls.SynchronizationModeEnum;
042import org.apache.directory.api.ldap.extras.controls.syncrepl.syncDone.SyncDoneValue;
043import org.apache.directory.api.ldap.extras.controls.syncrepl.syncDone.SyncDoneValueImpl;
044import org.apache.directory.api.ldap.extras.controls.syncrepl.syncRequest.SyncRequestValue;
045import org.apache.directory.api.ldap.extras.controls.syncrepl.syncState.SyncStateTypeEnum;
046import org.apache.directory.api.ldap.extras.controls.syncrepl.syncState.SyncStateValue;
047import org.apache.directory.api.ldap.extras.controls.syncrepl.syncState.SyncStateValueImpl;
048import org.apache.directory.api.ldap.extras.intermediate.syncrepl.SyncInfoValue;
049import org.apache.directory.api.ldap.extras.intermediate.syncrepl.SyncInfoValueImpl;
050import org.apache.directory.api.ldap.extras.intermediate.syncrepl.SynchronizationInfoEnum;
051import org.apache.directory.api.ldap.model.constants.Loggers;
052import org.apache.directory.api.ldap.model.constants.SchemaConstants;
053import org.apache.directory.api.ldap.model.cursor.Cursor;
054import org.apache.directory.api.ldap.model.entry.Attribute;
055import org.apache.directory.api.ldap.model.entry.Entry;
056import org.apache.directory.api.ldap.model.entry.Modification;
057import org.apache.directory.api.ldap.model.entry.Value;
058import org.apache.directory.api.ldap.model.exception.LdapException;
059import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
060import org.apache.directory.api.ldap.model.exception.LdapURLEncodingException;
061import org.apache.directory.api.ldap.model.filter.AndNode;
062import org.apache.directory.api.ldap.model.filter.EqualityNode;
063import org.apache.directory.api.ldap.model.filter.ExprNode;
064import org.apache.directory.api.ldap.model.filter.GreaterEqNode;
065import org.apache.directory.api.ldap.model.filter.LessEqNode;
066import org.apache.directory.api.ldap.model.filter.OrNode;
067import org.apache.directory.api.ldap.model.filter.PresenceNode;
068import org.apache.directory.api.ldap.model.message.LdapResult;
069import org.apache.directory.api.ldap.model.message.ReferralImpl;
070import org.apache.directory.api.ldap.model.message.Response;
071import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
072import org.apache.directory.api.ldap.model.message.SearchRequest;
073import org.apache.directory.api.ldap.model.message.SearchResultDone;
074import org.apache.directory.api.ldap.model.message.SearchResultEntry;
075import org.apache.directory.api.ldap.model.message.SearchResultEntryImpl;
076import org.apache.directory.api.ldap.model.message.SearchResultReference;
077import org.apache.directory.api.ldap.model.message.SearchResultReferenceImpl;
078import org.apache.directory.api.ldap.model.message.SearchScope;
079import org.apache.directory.api.ldap.model.message.controls.ChangeType;
080import org.apache.directory.api.ldap.model.message.controls.ManageDsaIT;
081import org.apache.directory.api.ldap.model.message.controls.SortKey;
082import org.apache.directory.api.ldap.model.message.controls.SortRequest;
083import org.apache.directory.api.ldap.model.message.controls.SortRequestImpl;
084import org.apache.directory.api.ldap.model.name.Dn;
085import org.apache.directory.api.ldap.model.schema.AttributeType;
086import org.apache.directory.api.ldap.model.url.LdapUrl;
087import org.apache.directory.api.util.Strings;
088import org.apache.directory.server.constants.ServerDNConstants;
089import org.apache.directory.server.core.api.DirectoryService;
090import org.apache.directory.server.core.api.event.DirectoryListenerAdapter;
091import org.apache.directory.server.core.api.event.EventService;
092import org.apache.directory.server.core.api.event.EventType;
093import org.apache.directory.server.core.api.event.NotificationCriteria;
094import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
095import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
096import org.apache.directory.server.core.api.interceptor.context.OperationContext;
097import org.apache.directory.server.core.api.partition.Partition;
098import org.apache.directory.server.core.api.partition.PartitionTxn;
099import org.apache.directory.server.i18n.I18n;
100import org.apache.directory.server.ldap.LdapProtocolUtils;
101import org.apache.directory.server.ldap.LdapServer;
102import org.apache.directory.server.ldap.LdapSession;
103import org.apache.directory.server.ldap.handlers.SearchAbandonListener;
104import org.apache.directory.server.ldap.handlers.SearchTimeLimitingMonitor;
105import org.apache.directory.server.ldap.replication.ReplicaEventMessage;
106import org.slf4j.Logger;
107import org.slf4j.LoggerFactory;
108
109
110/**
111 * Class used to process the incoming synchronization request from the consumers.
112 *
113 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
114 */
115public class SyncReplRequestHandler implements ReplicationRequestHandler
116{
117    /** A logger for the replication provider */
118    private static final Logger PROVIDER_LOG = LoggerFactory.getLogger( Loggers.PROVIDER_LOG.getName() );
119
120    /** Tells if the replication handler is already started */
121    private boolean initialized = false;
122
123    /** The directory service instance */
124    private DirectoryService dirService;
125
126    /** The reference on the Ldap server instance */
127    protected LdapServer ldapServer;
128
129    /** An ObjectClass AT instance */
130    private AttributeType objectClassAT;
131
132    /** The CSN AttributeType instance */
133    private AttributeType csnAT;
134
135    private Map<Integer, ReplicaEventLog> replicaLogMap = new ConcurrentHashMap<>();
136
137    private File syncReplData;
138
139    private AtomicInteger replicaCount = new AtomicInteger( 0 );
140
141    private ReplConsumerManager replicaUtil;
142
143    private ConsumerLogEntryChangeListener cledListener;
144
145    private ReplicaEventLogJanitor logJanitor;
146
147    private AttributeType replLogMaxIdleAT;
148
149    private AttributeType replLogPurgeThresholdCountAT;
150
151    /** thread used for updating consumer infor */
152    private Thread consumerInfoUpdateThread;
153
154    /**
155     * Create a SyncReplRequestHandler empty instance
156     */
157    public SyncReplRequestHandler()
158    {
159    }
160
161
162    /**
163     * {@inheritDoc}
164     */
165    public void start( LdapServer server )
166    {
167        // Check that the handler is not already started : we don't want to start it twice...
168        if ( initialized )
169        {
170            PROVIDER_LOG.warn( "syncrepl provider was already initialized" );
171
172            return;
173        }
174
175        try
176        {
177            PROVIDER_LOG.debug( "initializing the syncrepl provider" );
178
179            this.ldapServer = server;
180            this.dirService = server.getDirectoryService();
181
182            csnAT = dirService.getSchemaManager()
183                .lookupAttributeTypeRegistry( SchemaConstants.ENTRY_CSN_AT );
184
185            objectClassAT = dirService.getSchemaManager()
186                .lookupAttributeTypeRegistry( SchemaConstants.OBJECT_CLASS_AT );
187
188            replLogMaxIdleAT = dirService.getSchemaManager()
189                .lookupAttributeTypeRegistry( SchemaConstants.ADS_REPL_LOG_MAX_IDLE );
190
191            replLogPurgeThresholdCountAT = dirService.getSchemaManager()
192                .lookupAttributeTypeRegistry( SchemaConstants.ADS_REPL_LOG_PURGE_THRESHOLD_COUNT );
193
194            // Get and create the replication directory if it does not exist
195            syncReplData = dirService.getInstanceLayout().getReplDirectory();
196
197            if ( !syncReplData.exists() && !syncReplData.mkdirs() )
198            {
199                throw new IOException( I18n.err( I18n.ERR_112_COULD_NOT_CREATE_DIRECTORY, syncReplData ) );
200            }
201
202            // Create the replication manager
203            replicaUtil = new ReplConsumerManager( dirService );
204
205            loadReplicaInfo();
206
207            logJanitor = new ReplicaEventLogJanitor( dirService, replicaLogMap );
208            logJanitor.start();
209
210            registerPersistentSearches();
211
212            cledListener = new ConsumerLogEntryChangeListener();
213            NotificationCriteria criteria = new NotificationCriteria( dirService.getSchemaManager() );
214            criteria.setBase( new Dn( dirService.getSchemaManager(), ServerDNConstants.REPL_CONSUMER_DN_STR ) );
215            criteria.setEventMask( EventType.DELETE );
216
217            dirService.getEventService().addListener( cledListener, criteria );
218
219            CountDownLatch latch = new CountDownLatch( 1 );
220
221            consumerInfoUpdateThread = new Thread( createConsumerInfoUpdateTask( latch ) );
222            consumerInfoUpdateThread.setDaemon( true );
223            consumerInfoUpdateThread.start();
224
225            // Wait for the thread to be ready. We wait 5 minutes, it should be way more
226            // than necessary
227            boolean threadInitDone = latch.await( 5, TimeUnit.MINUTES );
228
229            if ( !threadInitDone )
230            {
231                // We have had a time out : just get out
232                PROVIDER_LOG.error( "The consumer replica thread has not been initialized in time" );
233                throw new RuntimeException( "Cannot initialize the Provider replica listener" );
234            }
235
236            initialized = true;
237            PROVIDER_LOG.debug( "syncrepl provider initialized successfully" );
238        }
239        catch ( Exception e )
240        {
241            PROVIDER_LOG.error( "Failed to initialize the log files required by the syncrepl provider", e );
242            throw new RuntimeException( e );
243        }
244    }
245
246
247    /**
248     * {@inheritDoc}
249     */
250    public void stop()
251    {
252        EventService evtSrv = dirService.getEventService();
253
254        evtSrv.removeListener( cledListener );
255        //first set the 'stop' flag
256        logJanitor.stopCleaning();
257        //then interrupt the janitor
258        logJanitor.interrupt();
259
260        //then stop the consumerInfoUpdateThread
261        consumerInfoUpdateThread.interrupt();
262        
263        for ( ReplicaEventLog log : replicaLogMap.values() )
264        {
265            try
266            {
267                PROVIDER_LOG.debug( "Stopping the logging for replica {}", log.getId() );
268                evtSrv.removeListener( log.getPersistentListener() );
269                log.stop();
270            }
271            catch ( Exception e )
272            {
273                PROVIDER_LOG.error( "Failed to close the event log {}", log.getId(), e );
274            }
275        }
276
277        // flush the dirty repos
278        storeReplicaInfo();
279
280        initialized = false;
281    }
282
283
284    /**
285     * Process the incoming search request sent by a remote server when trying to replicate.
286     *
287     * @param session The used LdapSession. Should be the dedicated user
288     * @param request The search request
289     */
290    public void handleSyncRequest( LdapSession session, SearchRequest request ) throws LdapException
291    {
292        PROVIDER_LOG.debug( "Received a Syncrepl request : {} from {}", request, session );
293        try
294        {
295            if ( !request.getAttributes().contains( SchemaConstants.ALL_OPERATIONAL_ATTRIBUTES ) )
296            {
297                // this is needed for accessing entryUUID and entryCSN attributes for internal purpose
298                request.addAttributes( SchemaConstants.ALL_OPERATIONAL_ATTRIBUTES );
299            }
300
301            // First extract the Sync control from the request
302            SyncRequestValue syncControl = ( SyncRequestValue ) request.getControls().get(
303                SyncRequestValue.OID );
304
305            // cookie is in the format <replicaId>;<Csn value>
306            byte[] cookieBytes = syncControl.getCookie();
307
308            if ( cookieBytes == null )
309            {
310                PROVIDER_LOG.debug( "Received a replication request with no cookie" );
311                // No cookie ? We have to get all the entries from the provider
312                // This is an initiate Content Poll action (RFC 4533, 3.3.1)
313                doInitialRefresh( session, request );
314            }
315            else
316            {
317                String cookieString = Strings.utf8ToString( cookieBytes );
318
319                PROVIDER_LOG.debug( "Received a replication request {} with a cookie '{}'", request, cookieString );
320
321                if ( !LdapProtocolUtils.isValidCookie( cookieString ) )
322                {
323                    PROVIDER_LOG.error( "received an invalid cookie {} from the consumer with session {}",
324                        cookieString,
325                        session );
326                    sendESyncRefreshRequired( session, request );
327                }
328                else
329                {
330                    ReplicaEventLog clientMsgLog = getReplicaEventLog( cookieString );
331
332                    if ( clientMsgLog == null )
333                    {
334                        PROVIDER_LOG.debug(
335                            "received a valid cookie {} but there is no event log associated with this replica",
336                            cookieString );
337                        sendESyncRefreshRequired( session, request );
338                    }
339                    else
340                    {
341                        String consumerCsn = LdapProtocolUtils.getCsn( cookieString );
342                        doContentUpdate( session, request, clientMsgLog, consumerCsn );
343                    }
344                }
345            }
346        }
347        catch ( Exception e )
348        {
349            PROVIDER_LOG.error( "Failed to handle the syncrepl request", e );
350
351            throw new LdapException( e.getMessage(), e );
352        }
353    }
354
355
356    /**
357     * Send all the stored modifications to the consumer
358     */
359    private void sendContentFromLog( LdapSession session, SearchRequest req, ReplicaEventLog clientMsgLog,
360        String fromCsn )
361        throws Exception
362    {
363        // do the search from the log
364        String lastSentCsn = fromCsn;
365
366        ReplicaJournalCursor cursor = clientMsgLog.getCursor( fromCsn );
367
368        PROVIDER_LOG.debug( "Processing the log for replica {}", clientMsgLog.getId() );
369
370        try
371        {
372            while ( cursor.next() )
373            {
374                ReplicaEventMessage replicaEventMessage = cursor.get();
375                Entry entry = replicaEventMessage.getEntry();
376                PROVIDER_LOG.debug( "Read message from the queue {}", entry );
377
378                lastSentCsn = entry.get( csnAT ).getString();
379
380                ChangeType changeType = replicaEventMessage.getChangeType();
381
382                SyncStateTypeEnum syncStateType = null;
383
384                switch ( changeType )
385                {
386                    case ADD:
387                        syncStateType = SyncStateTypeEnum.ADD;
388                        break;
389
390                    case MODIFY:
391                        syncStateType = SyncStateTypeEnum.MODIFY;
392                        break;
393
394                    case MODDN:
395                        syncStateType = SyncStateTypeEnum.MODDN;
396                        break;
397
398                    case DELETE:
399                        syncStateType = SyncStateTypeEnum.DELETE;
400                        break;
401
402                    default:
403                        throw new IllegalStateException( I18n.err( I18n.ERR_686 ) );
404                }
405
406                sendSearchResultEntry( session, req, entry, syncStateType );
407
408                clientMsgLog.setLastSentCsn( lastSentCsn );
409
410                PROVIDER_LOG.debug( "The latest entry sent to the consumer {} has this CSN : {}", clientMsgLog.getId(),
411                    lastSentCsn );
412            }
413
414            PROVIDER_LOG.debug( "All pending modifciations for replica {} processed", clientMsgLog.getId() );
415        }
416        finally
417        {
418            cursor.close();
419        }
420    }
421
422
423    /**
424     * process the update of the consumer, starting from the given LastEntryCSN the consumer
425     * has sent with the sync request.
426     */
427    private void doContentUpdate( LdapSession session, SearchRequest req, ReplicaEventLog replicaLog, String consumerCsn )
428        throws Exception
429    {
430        synchronized ( replicaLog )
431        {
432            boolean refreshNPersist = isRefreshNPersist( req );
433
434            // if this method is called with refreshAndPersist
435            // means the client was offline after it initiated a persistent synch session
436            // we need to update the handler's session
437            if ( refreshNPersist )
438            {
439                SyncReplSearchListener handler = replicaLog.getPersistentListener();
440                handler.setSearchRequest( req );
441                handler.setSession( session );
442            }
443
444            sendContentFromLog( session, req, replicaLog, consumerCsn );
445
446            String lastSentCsn = replicaLog.getLastSentCsn();
447
448            byte[] cookie = LdapProtocolUtils.createCookie( replicaLog.getId(), lastSentCsn );
449
450            if ( refreshNPersist )
451            {
452                SyncInfoValue syncInfoValue = new SyncInfoValueImpl();
453                syncInfoValue.setSyncInfoValueType( SynchronizationInfoEnum.NEW_COOKIE );
454                syncInfoValue.setMessageId( req.getMessageId() );
455                syncInfoValue.setCookie( cookie );
456
457                PROVIDER_LOG.debug( "Sent the intermediate response to the {} consumer, {}", replicaLog.getId(),
458                    syncInfoValue );
459                session.getIoSession().write( syncInfoValue );
460
461                replicaLog.getPersistentListener().setPushInRealTime( refreshNPersist );
462            }
463            else
464            {
465                SearchResultDone searchDoneResp = ( SearchResultDone ) req.getResultResponse();
466                searchDoneResp.getLdapResult().setResultCode( ResultCodeEnum.SUCCESS );
467                SyncDoneValue syncDone = new SyncDoneValueImpl();
468                syncDone.setCookie( cookie );
469                searchDoneResp.addControl( syncDone );
470
471                PROVIDER_LOG.debug( "Send a SearchResultDone response to the {} consumer", replicaLog.getId(),
472                    searchDoneResp );
473
474                session.getIoSession().write( searchDoneResp );
475            }
476        }
477    }
478
479
480    /**
481     * Process the initial refresh : we will send all the entries
482     */
483    private void doInitialRefresh( LdapSession session, SearchRequest request ) throws Exception
484    {
485        PROVIDER_LOG.debug( "Starting an initial refresh" );
486
487        SortRequest ctrl = ( SortRequest ) request.getControl( SortRequest.OID );
488
489        if ( ctrl != null )
490        {
491            PROVIDER_LOG
492                .warn( "Removing the received sort control from the syncrepl search request during initial refresh" );
493            request.removeControl( ctrl );
494        }
495
496        PROVIDER_LOG
497            .debug( "Adding sort control to sort the entries by entryDn attribute to preserve order of insertion" );
498        SortKey sk = new SortKey( SchemaConstants.ENTRY_DN_AT );
499        // matchingrule for "entryDn"
500        sk.setMatchingRuleId( SchemaConstants.DISTINGUISHED_NAME_MATCH_MR_OID );
501        sk.setReverseOrder( true );
502
503        ctrl = new SortRequestImpl();
504        ctrl.addSortKey( sk );
505
506        request.addControl( ctrl );
507
508        String originalFilter = request.getFilter().toString();
509        InetSocketAddress address = ( InetSocketAddress ) session.getIoSession().getRemoteAddress();
510        String hostName = address.getAddress().getHostName();
511
512        ExprNode modifiedFilter = modifyFilter( session, request );
513
514        Partition partition = dirService.getPartitionNexus().getPartition( request.getBase() );
515        String contextCsn;
516        
517        try ( PartitionTxn partitionTxn = partition.beginReadTransaction() )
518        {
519            contextCsn = partition.getContextCsn( partitionTxn );
520        }
521
522        boolean refreshNPersist = isRefreshNPersist( request );
523
524        // first register a ReplicaEventLog before starting the initial content refresh
525        // this is to log all the operations happen on DIT during initial content refresh
526        ReplicaEventLog replicaLog = null;
527        
528        try ( PartitionTxn partitionTxn = partition.beginReadTransaction() )
529        {
530            replicaLog = createReplicaEventLog( partitionTxn, hostName, originalFilter );
531        }
532
533        replicaLog.setRefreshNPersist( refreshNPersist );
534        Value contexCsnValue = new Value( dirService.getAtProvider().getEntryCSN(), contextCsn );
535
536        // modify the filter to include the context Csn
537        GreaterEqNode csnGeNode = new GreaterEqNode( csnAT, contexCsnValue );
538        ExprNode postInitContentFilter = new AndNode( modifiedFilter, csnGeNode );
539        request.setFilter( postInitContentFilter );
540
541        // now we process entries forever as they change
542        // irrespective of the sync mode set the 'isRealtimePush' to false initially so that we can
543        // store the modifications in the queue and later if it is a persist mode
544        PROVIDER_LOG.debug( "Starting the replicaLog {}", replicaLog );
545
546        // we push this queue's content and switch to realtime mode
547        SyncReplSearchListener replicationListener = new SyncReplSearchListener( session, request, replicaLog, false );
548        replicaLog.setPersistentListener( replicationListener );
549
550        // compose notification criteria and add the listener to the event
551        // service using that notification criteria to determine which events
552        // are to be delivered to the persistent search issuing client
553        NotificationCriteria criteria = new NotificationCriteria( dirService.getSchemaManager() );
554        criteria.setAliasDerefMode( request.getDerefAliases() );
555        criteria.setBase( request.getBase() );
556        criteria.setFilter( request.getFilter() );
557        criteria.setScope( request.getScope() );
558        criteria.setEventMask( EventType.ALL_EVENT_TYPES_MASK );
559
560        replicaLog.setSearchCriteria( criteria );
561
562        dirService.getEventService().addListener( replicationListener, criteria );
563
564        // then start pushing initial content
565        LessEqNode csnNode = new LessEqNode( csnAT, contexCsnValue );
566
567        // modify the filter to include the context Csn
568        ExprNode initialContentFilter = new AndNode( modifiedFilter, csnNode );
569        request.setFilter( initialContentFilter );
570
571        // Now, do a search to get all the entries
572        SearchResultDone searchDoneResp = doSimpleSearch( session, request, replicaLog );
573
574        if ( searchDoneResp.getLdapResult().getResultCode() == ResultCodeEnum.SUCCESS )
575        {
576            if ( replicaLog.getLastSentCsn() == null )
577            {
578                replicaLog.setLastSentCsn( contextCsn );
579            }
580
581            if ( refreshNPersist ) // refreshAndPersist mode
582            {
583                PROVIDER_LOG
584                    .debug( "Refresh&Persist requested : send the data being modified since the initial refresh" );
585                // Now, send the modified entries since the search has started
586                sendContentFromLog( session, request, replicaLog, contextCsn );
587
588                byte[] cookie = LdapProtocolUtils.createCookie( replicaLog.getId(), replicaLog.getLastSentCsn() );
589
590                SyncInfoValue syncInfoValue = new SyncInfoValueImpl();
591                syncInfoValue.setSyncInfoValueType( SynchronizationInfoEnum.NEW_COOKIE );
592                syncInfoValue.setMessageId( request.getMessageId() );
593                syncInfoValue.setCookie( cookie );
594
595                PROVIDER_LOG.info( "Sending the intermediate response to consumer {}, {}", 
596                    replicaLog, syncInfoValue );
597
598                session.getIoSession().write( syncInfoValue );
599
600                // switch the handler mode to realtime push
601                replicationListener.setPushInRealTime( refreshNPersist );
602                PROVIDER_LOG.debug( "e waiting for any modification for {}", replicaLog );
603            }
604            else
605            {
606                PROVIDER_LOG.debug( "RefreshOnly requested" );
607                byte[] cookie = LdapProtocolUtils.createCookie( replicaLog.getId(), contextCsn );
608
609                // no need to send from the log, that will be done in the next refreshOnly session
610                SyncDoneValue syncDone = new SyncDoneValueImpl();
611                syncDone.setCookie( cookie );
612                searchDoneResp.addControl( syncDone );
613                PROVIDER_LOG.info( "Sending the searchResultDone response to consumer {}, {}", replicaLog,
614                    searchDoneResp );
615
616                session.getIoSession().write( searchDoneResp );
617            }
618        }
619        else
620        // if not succeeded return
621        {
622            PROVIDER_LOG.warn( "initial content refresh didn't succeed due to {}", searchDoneResp.getLdapResult()
623                .getResultCode() );
624            replicaLog.stop();
625            replicaLog = null;
626
627            // remove the listener
628            dirService.getEventService().removeListener( replicationListener );
629
630            return;
631        }
632
633        // if all is well then store the consumer information
634        replicaUtil.addConsumerEntry( replicaLog );
635
636        // add to the map only after storing in the DIT, else the Replica update thread barfs
637        replicaLogMap.put( replicaLog.getId(), replicaLog );
638    }
639
640
641    /**
642     * Process a search on the provider to get all the modified entries. We then send all
643     * of them to the consumer
644     */
645    private SearchResultDone doSimpleSearch( LdapSession session, SearchRequest req, ReplicaEventLog replicaLog )
646        throws Exception
647    {
648        PROVIDER_LOG.debug( "Simple Search {} for {}", req, session );
649        SearchResultDone searchDoneResp = ( SearchResultDone ) req.getResultResponse();
650        LdapResult ldapResult = searchDoneResp.getLdapResult();
651
652        // A normal search
653        // Check that we have a cursor or not.
654        // No cursor : do a search.
655        Cursor<Entry> cursor = session.getCoreSession().search( req );
656
657        // Position the cursor at the beginning
658        cursor.beforeFirst();
659
660        /*
661         * Iterate through all search results building and sending back responses
662         * for each search result returned.
663         */
664        try
665        {
666            // Get the size limits
667            // Don't bother setting size limits for administrators that don't ask for it
668            long serverLimit = getServerSizeLimit( session, req );
669
670            long requestLimit = req.getSizeLimit() == 0L ? Long.MAX_VALUE : req.getSizeLimit();
671
672            req.addAbandonListener( new SearchAbandonListener( ldapServer, cursor ) );
673            setTimeLimitsOnCursor( req, session, cursor );
674            PROVIDER_LOG.debug( "search operation requested size limit {}, server size limit {}", requestLimit,
675                serverLimit );
676            long sizeLimit = min( requestLimit, serverLimit );
677
678            readResults( session, req, ldapResult, cursor, sizeLimit, replicaLog );
679        }
680        finally
681        {
682            if ( cursor != null )
683            {
684                try
685                {
686                    cursor.close();
687                }
688                catch ( Exception e )
689                {
690                    PROVIDER_LOG.error( I18n.err( I18n.ERR_168 ), e );
691                }
692            }
693        }
694
695        PROVIDER_LOG.debug( "Search done" );
696
697        return searchDoneResp;
698    }
699
700
701    /**
702     * Process the results get from a search request. We will send them to the client.
703     */
704    private void readResults( LdapSession session, SearchRequest req, LdapResult ldapResult,
705        Cursor<Entry> cursor, long sizeLimit, ReplicaEventLog replicaLog ) throws Exception
706    {
707        long count = 0;
708
709        while ( ( count < sizeLimit ) && cursor.next() )
710        {
711            // Handle closed session
712            if ( session.getIoSession().isClosing() )
713            {
714                // The client has closed the connection
715                PROVIDER_LOG.debug( "Request terminated for message {}, the client has closed the session",
716                    req.getMessageId() );
717                break;
718            }
719
720            if ( req.isAbandoned() )
721            {
722                // The cursor has been closed by an abandon request.
723                PROVIDER_LOG.debug( "Request terminated by an AbandonRequest for message {}", req.getMessageId() );
724                break;
725            }
726
727            Entry entry = cursor.get();
728
729            sendSearchResultEntry( session, req, entry, SyncStateTypeEnum.ADD );
730
731            String lastSentCsn = entry.get( csnAT ).getString();
732            replicaLog.setLastSentCsn( lastSentCsn );
733
734            count++;
735        }
736
737        PROVIDER_LOG.debug( "Sent {} entries for {}", count, replicaLog );
738
739        // DO NOT WRITE THE RESPONSE - JUST RETURN IT
740        ldapResult.setResultCode( ResultCodeEnum.SUCCESS );
741
742        if ( ( count >= sizeLimit ) && ( cursor.next() ) )
743        {
744            // We have reached the limit
745            // Move backward on the cursor to restore the previous position, as we moved forward
746            // to check if there is one more entry available
747            cursor.previous();
748            // Special case if the user has requested more elements than the request size limit
749            ldapResult.setResultCode( ResultCodeEnum.SIZE_LIMIT_EXCEEDED );
750        }
751    }
752
753
754    /**
755     * Prepare and send a search result entry response, with the associated
756     * SyncState control.
757     */
758    private void sendSearchResultEntry( LdapSession session, SearchRequest req, Entry entry,
759        SyncStateTypeEnum syncStateType ) throws Exception
760    {
761        Attribute uuid = entry.get( SchemaConstants.ENTRY_UUID_AT );
762
763        // Create the SyncState control
764        SyncStateValue syncStateControl = new SyncStateValueImpl();
765        syncStateControl.setSyncStateType( syncStateType );
766        syncStateControl.setEntryUUID( Strings.uuidToBytes( uuid.getString() ) );
767
768        if ( syncStateType == SyncStateTypeEnum.DELETE )
769        {
770            // clear the entry's all attributes except the Dn and entryUUID
771            entry.clear();
772            entry.add( uuid );
773        }
774
775        Response resp = generateResponse( session, req, entry );
776        resp.addControl( syncStateControl );
777
778        PROVIDER_LOG.debug( "Sending the entry:\n {}", resp );
779        session.getIoSession().write( resp );
780    }
781
782
783    /**
784     * Build the response to be sent to the client
785     */
786    private Response generateResponse( LdapSession session, SearchRequest req, Entry entry ) throws Exception
787    {
788        Attribute ref = entry.get( SchemaConstants.REF_AT );
789        boolean hasManageDsaItControl = req.getControls().containsKey( ManageDsaIT.OID );
790
791        if ( ( ref != null ) && !hasManageDsaItControl )
792        {
793            // The entry is a referral.
794            SearchResultReference respRef;
795            respRef = new SearchResultReferenceImpl( req.getMessageId() );
796            respRef.setReferral( new ReferralImpl() );
797
798            for ( Value val : ref )
799            {
800                String url = val.getString();
801
802                if ( !url.startsWith( "ldap" ) )
803                {
804                    respRef.getReferral().addLdapUrl( url );
805                }
806
807                LdapUrl ldapUrl = null;
808
809                try
810                {
811                    ldapUrl = new LdapUrl( url );
812                    ldapUrl.setForceScopeRendering( true );
813                }
814                catch ( LdapURLEncodingException e )
815                {
816                    PROVIDER_LOG.error( I18n.err( I18n.ERR_165, url, entry ) );
817                }
818
819                switch ( req.getScope() )
820                {
821                    case SUBTREE:
822                        ldapUrl.setScope( SearchScope.SUBTREE.getScope() );
823                        break;
824
825                    case ONELEVEL: // one level here is object level on remote server
826                        ldapUrl.setScope( SearchScope.OBJECT.getScope() );
827                        break;
828
829                    default:
830                        throw new IllegalStateException( I18n.err( I18n.ERR_686 ) );
831                }
832
833                respRef.getReferral().addLdapUrl( ldapUrl.toString() );
834            }
835
836            return respRef;
837        }
838        else
839        {
840            // The entry is not a referral, or the ManageDsaIt control is set
841            SearchResultEntry respEntry;
842            respEntry = new SearchResultEntryImpl( req.getMessageId() );
843            respEntry.setEntry( entry );
844            respEntry.setObjectName( entry.getDn() );
845
846            return respEntry;
847        }
848    }
849
850
851    /**
852     * Return the server size limit
853     */
854    private long getServerSizeLimit( LdapSession session, SearchRequest request )
855    {
856        if ( session.getCoreSession().isAnAdministrator() )
857        {
858            if ( request.getSizeLimit() == NO_SIZE_LIMIT )
859            {
860                return Long.MAX_VALUE;
861            }
862            else
863            {
864                return request.getSizeLimit();
865            }
866        }
867        else
868        {
869            if ( ldapServer.getMaxSizeLimit() == NO_SIZE_LIMIT )
870            {
871                return Long.MAX_VALUE;
872            }
873            else
874            {
875                return ldapServer.getMaxSizeLimit();
876            }
877        }
878    }
879
880
881    private void setTimeLimitsOnCursor( SearchRequest req, LdapSession session,
882        final Cursor<Entry> cursor )
883    {
884        // Don't bother setting time limits for administrators
885        if ( session.getCoreSession().isAnAdministrator() && req.getTimeLimit() == NO_TIME_LIMIT )
886        {
887            return;
888        }
889
890        /*
891         * Non administrator based searches are limited by time if the server
892         * has been configured with unlimited time and the request specifies
893         * unlimited search time
894         */
895        if ( ldapServer.getMaxTimeLimit() == NO_TIME_LIMIT && req.getTimeLimit() == NO_TIME_LIMIT )
896        {
897            return;
898        }
899
900        /*
901         * If the non-administrator user specifies unlimited time but the server
902         * is configured to limit the search time then we limit by the max time
903         * allowed by the configuration
904         */
905        if ( req.getTimeLimit() == 0 )
906        {
907            cursor.setClosureMonitor( new SearchTimeLimitingMonitor( ldapServer.getMaxTimeLimit(), TimeUnit.SECONDS ) );
908            return;
909        }
910
911        /*
912         * If the non-administrative user specifies a time limit equal to or
913         * less than the maximum limit configured in the server then we
914         * constrain search by the amount specified in the request
915         */
916        if ( ldapServer.getMaxTimeLimit() >= req.getTimeLimit() )
917        {
918            cursor.setClosureMonitor( new SearchTimeLimitingMonitor( req.getTimeLimit(), TimeUnit.SECONDS ) );
919            return;
920        }
921
922        /*
923         * Here the non-administrative user's requested time limit is greater
924         * than what the server's configured maximum limit allows so we limit
925         * the search to the configured limit
926         */
927        cursor.setClosureMonitor( new SearchTimeLimitingMonitor( ldapServer.getMaxTimeLimit(), TimeUnit.SECONDS ) );
928    }
929
930
931    public ExprNode modifyFilter( LdapSession session, SearchRequest req ) throws Exception
932    {
933        /*
934         * Most of the time the search filter is just (objectClass=*) and if
935         * this is the case then there's no reason at all to OR this with an
936         * (objectClass=referral).  If we detect this case then we leave it
937         * as is to represent the OR condition:
938         *
939         *  (| (objectClass=referral)(objectClass=*)) == (objectClass=*)
940         */
941        boolean isOcPresenceFilter = false;
942
943        if ( req.getFilter() instanceof PresenceNode )
944        {
945            PresenceNode presenceNode = ( PresenceNode ) req.getFilter();
946
947            AttributeType at = session.getCoreSession().getDirectoryService().getSchemaManager()
948                .lookupAttributeTypeRegistry( presenceNode.getAttribute() );
949
950            if ( at.getOid().equals( SchemaConstants.OBJECT_CLASS_AT_OID ) )
951            {
952                isOcPresenceFilter = true;
953            }
954        }
955
956        ExprNode filter = req.getFilter();
957
958        if ( !req.hasControl( ManageDsaIT.OID ) && !isOcPresenceFilter )
959        {
960            filter = new OrNode( req.getFilter(), newIsReferralEqualityNode( session ) );
961        }
962
963        return filter;
964    }
965
966
967    public ReplicaEventLogJanitor getLogJanitor()
968    {
969        return logJanitor;
970    }
971
972
973    public Map<Integer, ReplicaEventLog> getReplicaLogMap()
974    {
975        return replicaLogMap;
976    }
977
978
979    private EqualityNode<String> newIsReferralEqualityNode( LdapSession session ) throws Exception
980    {
981        return new EqualityNode<>( SchemaConstants.OBJECT_CLASS_AT, 
982            new Value( objectClassAT, SchemaConstants.REFERRAL_OC ).getString() );
983    }
984
985
986    /**
987     * Update the consumer configuration entries if they are 'dirty' (ie, if
988     * the consumer lastCSN is not up to date)
989     */
990    private void storeReplicaInfo()
991    {
992        try
993        {
994            for ( Map.Entry<Integer, ReplicaEventLog> e : replicaLogMap.entrySet() )
995            {
996                ReplicaEventLog replica = e.getValue();
997
998                if ( replica.isDirty() )
999                {
1000                    PROVIDER_LOG.debug( "updating the details of replica {}", replica );
1001                    replicaUtil.updateReplicaLastSentCsn( replica );
1002                    replica.setDirty( false );
1003                }
1004            }
1005        }
1006        catch ( Exception e )
1007        {
1008            PROVIDER_LOG.error( "Failed to store the replica information", e );
1009        }
1010    }
1011
1012
1013    /**
1014     * Read and store the consumer's informations
1015     */
1016    private void loadReplicaInfo()
1017    {
1018        try
1019        {
1020            List<ReplicaEventLog> eventLogs = replicaUtil.getReplicaEventLogs();
1021            Set<String> eventLogNames = new HashSet<>();
1022
1023            if ( !eventLogs.isEmpty() )
1024            {
1025                for ( ReplicaEventLog replica : eventLogs )
1026                {
1027                    PROVIDER_LOG.debug( "initializing the replica log from {}", replica.getId() );
1028                    replicaLogMap.put( replica.getId(), replica );
1029                    eventLogNames.add( replica.getName() );
1030
1031                    // update the replicaCount's value to assign a correct value to the new replica(s)
1032                    if ( replicaCount.get() < replica.getId() )
1033                    {
1034                        replicaCount.set( replica.getId() );
1035                    }
1036                }
1037            }
1038            else
1039            {
1040                PROVIDER_LOG.debug( "no replica logs found to initialize" );
1041            }
1042
1043            // remove unused logs
1044            for ( File f : getAllReplJournalNames() )
1045            {
1046                if ( !eventLogNames.contains( f.getName() ) )
1047                {
1048                    f.delete();
1049                    PROVIDER_LOG.info( "removed unused replication event log {}", f );
1050                }
1051            }
1052        }
1053        catch ( Exception e )
1054        {
1055            PROVIDER_LOG.error( "Failed to load the replica information", e );
1056        }
1057    }
1058
1059
1060    /**
1061     * Register the listeners for each existing consumers
1062     */
1063    private void registerPersistentSearches() throws Exception
1064    {
1065        for ( Map.Entry<Integer, ReplicaEventLog> e : replicaLogMap.entrySet() )
1066        {
1067            ReplicaEventLog log = e.getValue();
1068
1069            if ( log.getSearchCriteria() != null )
1070            {
1071                PROVIDER_LOG.debug( "registering persistent search for the replica {}", log.getId() );
1072                SyncReplSearchListener handler = new SyncReplSearchListener( null, null, log, false );
1073                log.setPersistentListener( handler );
1074
1075                dirService.getEventService().addListener( handler, log.getSearchCriteria() );
1076            }
1077            else
1078            {
1079                PROVIDER_LOG.warn( "invalid persistent search criteria {} for the replica {}", log.getSearchCriteria(),
1080                    log
1081                        .getId() );
1082            }
1083        }
1084    }
1085
1086
1087    /**
1088     * Create a thread to process replication communication with a consumer
1089     */
1090    private Runnable createConsumerInfoUpdateTask( final CountDownLatch latch )
1091    {
1092        return new Runnable()
1093        {
1094            public void run()
1095            {
1096                try
1097                {
1098                    while ( true )
1099                    {
1100                        storeReplicaInfo();
1101                        
1102                        latch.countDown();
1103                        Thread.sleep( 10000 );
1104                    }
1105                }
1106                catch ( InterruptedException e )
1107                {
1108                    // log at debug level, this will be interrupted during stop
1109                    PROVIDER_LOG.debug( "thread storing the replica information was interrupted", e );
1110                }
1111            }
1112        };
1113    }
1114
1115
1116    /**
1117     * Get the Replica event log from the replica ID found in the cookie
1118     */
1119    private ReplicaEventLog getReplicaEventLog( String cookieString )
1120    {
1121        ReplicaEventLog replicaLog = null;
1122
1123        if ( LdapProtocolUtils.isValidCookie( cookieString ) )
1124        {
1125            int clientId = LdapProtocolUtils.getReplicaId( cookieString );
1126            replicaLog = replicaLogMap.get( clientId );
1127        }
1128
1129        return replicaLog;
1130    }
1131
1132
1133    /**
1134     * Create a new ReplicaEventLog. Each replica will have a unique ID, created by the provider.
1135     */
1136    private ReplicaEventLog createReplicaEventLog( PartitionTxn partitionTxn, String hostName, String filter ) throws Exception
1137    {
1138        int replicaId = replicaCount.incrementAndGet();
1139
1140        PROVIDER_LOG.debug( "creating a new event log for the replica with id {}", replicaId );
1141
1142        ReplicaEventLog replicaLog = new ReplicaEventLog( partitionTxn, dirService, replicaId );
1143        replicaLog.setHostName( hostName );
1144        replicaLog.setSearchFilter( filter );
1145
1146        return replicaLog;
1147    }
1148
1149
1150    /**
1151     * Send an error response to he consue r: it has to send a SYNC_REFRESH request first.
1152     */
1153    private void sendESyncRefreshRequired( LdapSession session, SearchRequest req )
1154    {
1155        SearchResultDone searchDoneResp = ( SearchResultDone ) req.getResultResponse();
1156        searchDoneResp.getLdapResult().setResultCode( ResultCodeEnum.E_SYNC_REFRESH_REQUIRED );
1157        SyncDoneValue syncDone = new SyncDoneValueImpl();
1158        searchDoneResp.addControl( syncDone );
1159
1160        session.getIoSession().write( searchDoneResp );
1161    }
1162
1163
1164    /**
1165     * Tells if the control contains the REFRESHNPERSIST mode
1166     */
1167    private boolean isRefreshNPersist( SearchRequest req )
1168    {
1169        SyncRequestValue control = ( SyncRequestValue ) req.getControls().get( SyncRequestValue.OID );
1170
1171        return control.getMode() == SynchronizationModeEnum.REFRESH_AND_PERSIST;
1172    }
1173
1174
1175    private File[] getAllReplJournalNames()
1176    {
1177        File replDir = dirService.getInstanceLayout().getReplDirectory();
1178        FilenameFilter filter = new FilenameFilter()
1179        {
1180            @Override
1181            public boolean accept( File dir, String name )
1182            {
1183                return name.startsWith( ReplicaEventLog.REPLICA_EVENT_LOG_NAME_PREFIX );
1184            }
1185        };
1186
1187        return replDir.listFiles( filter );
1188    }
1189
1190    /**
1191     * an event listener for handling deletions and updates of replication event log entries present under ou=consumers,ou=system
1192     */
1193    private class ConsumerLogEntryChangeListener extends DirectoryListenerAdapter
1194    {
1195
1196        private ReplicaEventLog getEventLog( OperationContext opCtx )
1197        {
1198            Dn consumerLogDn = opCtx.getDn();
1199            String name = ReplicaEventLog.REPLICA_EVENT_LOG_NAME_PREFIX + consumerLogDn.getRdn().getValue();
1200
1201            for ( ReplicaEventLog log : replicaLogMap.values() )
1202            {
1203                if ( name.equalsIgnoreCase( log.getName() ) )
1204                {
1205                    return log;
1206                }
1207            } // end of for
1208
1209            return null;
1210        }
1211
1212
1213        @Override
1214        public void entryDeleted( DeleteOperationContext deleteContext )
1215        {
1216            // lock this listener instance
1217            synchronized ( this )
1218            {
1219                ReplicaEventLog log = getEventLog( deleteContext );
1220                if ( log != null )
1221                {
1222                    logJanitor.removeEventLog( log );
1223                }
1224            } // end of synchronized block
1225        } // end of delete method
1226
1227
1228        @Override
1229        public void entryModified( ModifyOperationContext modifyContext )
1230        {
1231            List<Modification> mods = modifyContext.getModItems();
1232
1233            // lock this listener instance
1234            synchronized ( this )
1235            {
1236                for ( Modification m : mods )
1237                {
1238                    try
1239                    {
1240                        Attribute at = m.getAttribute();
1241
1242                        if ( at.isInstanceOf( replLogMaxIdleAT ) )
1243                        {
1244                            ReplicaEventLog log = getEventLog( modifyContext );
1245                            if ( log != null )
1246                            {
1247                                int maxIdlePeriod = Integer.parseInt( m.getAttribute().getString() );
1248                                log.setMaxIdlePeriod( maxIdlePeriod );
1249                            }
1250                        }
1251                        else if ( at.isInstanceOf( replLogPurgeThresholdCountAT ) )
1252                        {
1253                            ReplicaEventLog log = getEventLog( modifyContext );
1254                            if ( log != null )
1255                            {
1256                                int purgeThreshold = Integer.parseInt( m.getAttribute().getString() );
1257                                log.setPurgeThresholdCount( purgeThreshold );
1258                            }
1259                        }
1260                    }
1261                    catch ( LdapInvalidAttributeValueException e )
1262                    {
1263                        PROVIDER_LOG.warn( "Invalid attribute type", e );
1264                    }
1265                }
1266            }
1267        }
1268    } // end of listener class
1269}