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 java.util.ArrayList;
025import java.util.List;
026import java.util.Map;
027import java.util.concurrent.ConcurrentHashMap;
028
029import org.apache.directory.api.ldap.model.constants.Loggers;
030import org.apache.directory.api.ldap.model.constants.SchemaConstants;
031import org.apache.directory.api.ldap.model.cursor.Cursor;
032import org.apache.directory.api.ldap.model.entry.Attribute;
033import org.apache.directory.api.ldap.model.entry.DefaultEntry;
034import org.apache.directory.api.ldap.model.entry.DefaultModification;
035import org.apache.directory.api.ldap.model.entry.Entry;
036import org.apache.directory.api.ldap.model.entry.Modification;
037import org.apache.directory.api.ldap.model.entry.ModificationOperation;
038import org.apache.directory.api.ldap.model.entry.Value;
039import org.apache.directory.api.ldap.model.exception.LdapEntryAlreadyExistsException;
040import org.apache.directory.api.ldap.model.exception.LdapException;
041import org.apache.directory.api.ldap.model.filter.EqualityNode;
042import org.apache.directory.api.ldap.model.filter.ExprNode;
043import org.apache.directory.api.ldap.model.message.AliasDerefMode;
044import org.apache.directory.api.ldap.model.message.SearchRequest;
045import org.apache.directory.api.ldap.model.message.SearchRequestImpl;
046import org.apache.directory.api.ldap.model.message.SearchScope;
047import org.apache.directory.api.ldap.model.name.Dn;
048import org.apache.directory.api.ldap.model.schema.AttributeType;
049import org.apache.directory.api.ldap.model.schema.SchemaManager;
050import org.apache.directory.server.core.api.CoreSession;
051import org.apache.directory.server.core.api.DirectoryService;
052import org.apache.directory.server.core.api.event.EventType;
053import org.apache.directory.server.core.api.event.NotificationCriteria;
054import org.apache.directory.server.core.api.partition.Partition;
055import org.apache.directory.server.core.api.partition.PartitionTxn;
056import org.slf4j.Logger;
057import org.slf4j.LoggerFactory;
058
059
060/**
061 * Manage the consumers on the provider : add them, and remove them.
062 * 
063 * All the consumers configuration will be stored in the 'ou=consumers,ou=system' branch.
064 *
065 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
066 */
067public class ReplConsumerManager
068{
069    /** Logger for this class */
070    private static final Logger LOG = LoggerFactory.getLogger( ReplConsumerManager.class );
071
072    /** A logger for the replication provider */
073    private static final Logger PROVIDER_LOG = LoggerFactory.getLogger( Loggers.PROVIDER_LOG.getName() );
074
075    /** The admin session used to commuicate with the backend */
076    private CoreSession adminSession;
077
078    /** The DirectoryService instance */
079    private DirectoryService directoryService;
080
081    /** The schema manager instance */
082    private SchemaManager schemaManager;
083
084    /** The replication factory DN */
085    private static final String REPL_CONSUMER_DN_STR = "ou=consumers,ou=system";
086    private Dn replConsumerDn;
087
088    /** The consumers' ou value */
089    private static final String CONSUMERS = "consumers";
090
091    /** An AdsReplLastSentCsn AT instance */
092    private AttributeType adsReplLastSentCsn;
093
094    /** A map containing the last sent CSN for every connected consumer */
095    private Map<Integer, Modification> modMap = new ConcurrentHashMap<>();
096
097
098    /**
099     * Create a new instance of the producer replication manager.
100     * 
101     * @param directoryService The directoryService instance
102     * @throws Exception if we add an error while creating the configuration
103     */
104    public ReplConsumerManager( DirectoryService directoryService ) throws Exception
105    {
106        this.directoryService = directoryService;
107        adminSession = directoryService.getAdminSession();
108        schemaManager = directoryService.getSchemaManager();
109        replConsumerDn = directoryService.getDnFactory().create( REPL_CONSUMER_DN_STR );
110        adsReplLastSentCsn = schemaManager.lookupAttributeTypeRegistry( SchemaConstants.ADS_REPL_LAST_SENT_CSN );
111
112        PROVIDER_LOG.debug( "Starting the replication consumer manager" );
113        createConsumersBranch();
114    }
115
116
117    /**
118     * Initialize the replication Store, creating the ou=consumers,ou=system entry (only if it does not exist yet)
119     */
120    private void createConsumersBranch() throws Exception
121    {
122        if ( !adminSession.exists( replConsumerDn ) )
123        {
124            LOG.debug( "creating the entry for storing replication consumers' details" );
125            PROVIDER_LOG
126                .debug( "Creating the entry for storing replication consumers' details in {}", replConsumerDn );
127
128            Entry entry = new DefaultEntry( schemaManager, replConsumerDn,
129                SchemaConstants.OBJECT_CLASS_AT, SchemaConstants.ORGANIZATIONAL_UNIT_OC,
130                SchemaConstants.OU_AT, CONSUMERS );
131
132            adminSession.add( entry );
133        }
134    }
135
136
137    /**
138     * Add a new consumer entry in ou=consumers,ou=system
139     * 
140     * @param replica The added consumer replica
141     * @throws Exception If the addition failed
142     */
143    public void addConsumerEntry( ReplicaEventLog replica ) throws Exception
144    {
145        if ( replica == null )
146        {
147            // No consumer ? Get out...
148            return;
149        }
150
151        PROVIDER_LOG.debug( "Adding a consumer for replica {}", replica );
152
153        // Check that we don't already have an entry for this consumer
154        Dn consumerDn = directoryService.getDnFactory().create(
155            SchemaConstants.ADS_DS_REPLICA_ID + "=" + replica.getId() + "," + replConsumerDn );
156
157        if ( adminSession.exists( consumerDn ) )
158        {
159            // Error...
160            String message = "The replica " + consumerDn.getName() + " already exists";
161            LOG.error( message );
162            PROVIDER_LOG.error( message );
163            throw new LdapEntryAlreadyExistsException( message );
164        }
165
166        // Create the new consumer entry
167        Entry entry = new DefaultEntry( schemaManager, consumerDn,
168            SchemaConstants.OBJECT_CLASS_AT, SchemaConstants.ADS_REPL_EVENT_LOG,
169            SchemaConstants.ADS_DS_REPLICA_ID, String.valueOf( replica.getId() ),
170            SchemaConstants.ADS_REPL_ALIAS_DEREF_MODE, replica.getSearchCriteria().getAliasDerefMode().getJndiValue(),
171            SchemaConstants.ADS_SEARCH_BASE_DN, replica.getSearchCriteria().getBase().getName(),
172            SchemaConstants.ADS_REPL_LAST_SENT_CSN, replica.getLastSentCsn(),
173            SchemaConstants.ADS_REPL_SEARCH_SCOPE, replica.getSearchCriteria().getScope().getLdapUrlValue(),
174            SchemaConstants.ADS_REPL_REFRESH_N_PERSIST, String.valueOf( replica.isRefreshNPersist() ),
175            SchemaConstants.ADS_REPL_SEARCH_FILTER, replica.getSearchFilter(),
176            SchemaConstants.ADS_REPL_LOG_MAX_IDLE, String.valueOf( replica.getMaxIdlePeriod() ),
177            SchemaConstants.ADS_REPL_LOG_PURGE_THRESHOLD_COUNT, String.valueOf( replica.getPurgeThresholdCount() ) );
178
179        adminSession.add( entry );
180
181        replica.setConsumerEntryDn( consumerDn );
182
183        LOG.debug( "stored replication consumer entry {}", consumerDn );
184    }
185
186
187    /**
188     * Delete an existing consumer entry from ou=consumers,ou=system
189     * 
190     * @param replica The added consumer replica
191     * @throws LdapException If the addition failed
192     */
193    public void deleteConsumerEntry( ReplicaEventLog replica ) throws LdapException
194    {
195        if ( replica == null )
196        {
197            // No consumer ? Get out...
198            return;
199        }
200
201        // Check that we have an entry for this consumer
202        Dn consumerDn = directoryService.getDnFactory().create(
203            SchemaConstants.ADS_DS_REPLICA_ID + "=" + replica.getId() + "," + replConsumerDn );
204
205        PROVIDER_LOG.debug( "Trying to delete the consumer entry {}", consumerDn );
206
207        if ( !adminSession.exists( consumerDn ) )
208        {
209            // Error...
210            String message = "The replica " + consumerDn.getName() + " does not exist";
211            LOG.error( message );
212            PROVIDER_LOG.debug( message );
213            return;
214        }
215
216        // Delete the consumer entry
217        adminSession.delete( consumerDn );
218
219        LOG.debug( "Deleted replication consumer entry {}", consumerDn );
220    }
221
222
223    /**
224     * Store the new CSN sent by the consumer in place of the previous one.
225     * 
226     * @param replica The consumer informations
227     * @throws Exception If the update failed
228     */
229    public void updateReplicaLastSentCsn( ReplicaEventLog replica ) throws Exception
230    {
231        Modification mod = modMap.get( replica.getId() );
232        Attribute lastSentCsnAt = null;
233
234        if ( mod == null )
235        {
236            mod = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE, adsReplLastSentCsn,
237                replica.getLastSentCsn() );
238
239            modMap.put( replica.getId(), mod );
240        }
241        else
242        {
243            lastSentCsnAt = mod.getAttribute();
244            lastSentCsnAt.clear(); // clearing is mandatory
245            lastSentCsnAt.add( replica.getLastSentCsn() );
246        }
247
248        Dn dn = directoryService.getDnFactory().create(
249            SchemaConstants.ADS_DS_REPLICA_ID + "=" + replica.getId() + "," + replConsumerDn );
250        adminSession.modify( dn, mod );
251
252        LOG.debug( "updated last sent CSN of consumer entry {}", dn );
253        PROVIDER_LOG.debug( "updated the LastSentCSN of consumer entry {}", dn );
254    }
255
256
257    /**
258     * Get the list of consumers' configuration
259     * 
260     * @return A list of all the consumer configuration stored on the provider
261     * @throws Exception If we had an error while building this list
262     */
263    public List<ReplicaEventLog> getReplicaEventLogs() throws Exception
264    {
265        List<ReplicaEventLog> replicas = new ArrayList<>();
266        
267        Partition partition = directoryService.getPartitionNexus().getPartition( replConsumerDn );
268        
269
270        // Search for all the consumers
271        ExprNode filter = new EqualityNode<String>( directoryService.getAtProvider().getObjectClass(), 
272            new Value( directoryService.getAtProvider().getObjectClass(), SchemaConstants.ADS_REPL_EVENT_LOG ) );
273        SearchRequest searchRequest = new SearchRequestImpl();
274        searchRequest.setBase( replConsumerDn );
275        searchRequest.setScope( SearchScope.ONELEVEL );
276        searchRequest.setFilter( filter );
277        searchRequest.addAttributes( SchemaConstants.ALL_ATTRIBUTES_ARRAY );
278
279        Cursor<Entry> cursor = adminSession.search( searchRequest );
280
281        // Now loop on each consumer configuration
282        while ( cursor.next() )
283        {
284            Entry entry = cursor.get();
285            
286            try ( PartitionTxn partitionTxn = partition.beginReadTransaction() )
287            {
288                ReplicaEventLog replica = convertEntryToReplica( partitionTxn, entry );
289                replicas.add( replica );
290            }
291        }
292
293        cursor.close();
294
295        // Now, we can return the list of replicas
296        return replicas;
297    }
298
299
300    /**
301     * Convert the stored entry to a valid ReplicaEventLog structure
302     */
303    private ReplicaEventLog convertEntryToReplica( PartitionTxn partitionTxn, Entry entry ) throws Exception
304    {
305        String id = entry.get( SchemaConstants.ADS_DS_REPLICA_ID ).getString();
306        ReplicaEventLog replica = new ReplicaEventLog( partitionTxn, directoryService, Integer.parseInt( id ) );
307
308        NotificationCriteria searchCriteria = new NotificationCriteria( schemaManager );
309
310        String aliasMode = entry.get( SchemaConstants.ADS_REPL_ALIAS_DEREF_MODE ).getString();
311        searchCriteria.setAliasDerefMode( AliasDerefMode.getDerefMode( aliasMode ) );
312
313        String baseDn = entry.get( SchemaConstants.ADS_SEARCH_BASE_DN ).getString();
314        searchCriteria.setBase( new Dn( schemaManager, baseDn ) );
315
316        Attribute lastSentCsnAt = entry.get( SchemaConstants.ADS_REPL_LAST_SENT_CSN );
317
318        if ( lastSentCsnAt != null )
319        {
320            replica.setLastSentCsn( lastSentCsnAt.getString() );
321        }
322
323        String scope = entry.get( SchemaConstants.ADS_REPL_SEARCH_SCOPE ).getString();
324        int scopeIntVal = SearchScope.getSearchScope( scope );
325        searchCriteria.setScope( SearchScope.getSearchScope( scopeIntVal ) );
326
327        String filter = entry.get( SchemaConstants.ADS_REPL_SEARCH_FILTER ).getString();
328        searchCriteria.setFilter( filter );
329        replica.setSearchFilter( filter );
330
331        replica.setRefreshNPersist( Boolean.parseBoolean( entry.get( SchemaConstants.ADS_REPL_REFRESH_N_PERSIST )
332            .getString() ) );
333
334        searchCriteria.setEventMask( EventType.ALL_EVENT_TYPES_MASK );
335        replica.setSearchCriteria( searchCriteria );
336
337        int maxIdlePeriod = Integer.parseInt( entry.get( SchemaConstants.ADS_REPL_LOG_MAX_IDLE ).getString() );
338        replica.setMaxIdlePeriod( maxIdlePeriod );
339
340        int purgeThreshold = Integer.parseInt( entry.get( SchemaConstants.ADS_REPL_LOG_PURGE_THRESHOLD_COUNT )
341            .getString() );
342        replica.setPurgeThresholdCount( purgeThreshold );
343
344        // explicitly mark the replica as not-dirty, cause we just loaded it from 
345        // the store, this prevents updating the replica info immediately after loading
346        replica.setDirty( false );
347
348        replica.setConsumerEntryDn( entry.getDn() );
349
350        return replica;
351    }
352}