001    /*
002     * JBoss DNA (http://www.jboss.org/dna)
003     * See the COPYRIGHT.txt file distributed with this work for information
004     * regarding copyright ownership.  Some portions may be licensed
005     * to Red Hat, Inc. under one or more contributor license agreements.
006     * See the AUTHORS.txt file in the distribution for a full listing of 
007     * individual contributors. 
008     *
009     * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
010     * is licensed to you under the terms of the GNU Lesser General Public License as
011     * published by the Free Software Foundation; either version 2.1 of
012     * the License, or (at your option) any later version.
013     *
014     * JBoss DNA is distributed in the hope that it will be useful,
015     * but WITHOUT ANY WARRANTY; without even the implied warranty of
016     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017     * Lesser General Public License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this software; if not, write to the Free
021     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
022     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
023     */
024    package org.jboss.dna.repository;
025    
026    import java.util.Collection;
027    import java.util.Collections;
028    import java.util.HashSet;
029    import java.util.LinkedList;
030    import java.util.List;
031    import java.util.Set;
032    import java.util.concurrent.CopyOnWriteArrayList;
033    import java.util.concurrent.TimeUnit;
034    import java.util.concurrent.locks.ReadWriteLock;
035    import java.util.concurrent.locks.ReentrantReadWriteLock;
036    import net.jcip.annotations.ThreadSafe;
037    import org.jboss.dna.common.util.CheckArg;
038    import org.jboss.dna.graph.ExecutionContext;
039    import org.jboss.dna.graph.connector.RepositoryConnection;
040    import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
041    import org.jboss.dna.graph.connector.RepositoryConnectionPool;
042    import org.jboss.dna.graph.connector.RepositoryContext;
043    import org.jboss.dna.graph.connector.RepositorySource;
044    import org.jboss.dna.repository.mimetype.MimeTypeDetectors;
045    import org.jboss.dna.repository.service.AbstractServiceAdministrator;
046    import org.jboss.dna.repository.service.ServiceAdministrator;
047    
048    /**
049     * A library of {@link RepositorySource} instances and the {@link RepositoryConnectionPool} used to manage the connections for
050     * each.
051     * 
052     * @author Randall Hauch
053     */
054    @ThreadSafe
055    public class RepositoryLibrary implements RepositoryConnectionFactory {
056    
057        /**
058         * The administrative component for this service.
059         * 
060         * @author Randall Hauch
061         */
062        protected class Administrator extends AbstractServiceAdministrator {
063    
064            protected Administrator() {
065                super(RepositoryI18n.federationServiceName, State.STARTED);
066            }
067    
068            /**
069             * {@inheritDoc}
070             */
071            @Override
072            protected void doStart( State fromState ) {
073                super.doStart(fromState);
074                RepositoryLibrary.this.start();
075            }
076    
077            /**
078             * {@inheritDoc}
079             */
080            @Override
081            protected void doShutdown( State fromState ) {
082                super.doShutdown(fromState);
083                RepositoryLibrary.this.shutdown();
084            }
085    
086            /**
087             * {@inheritDoc}
088             */
089            public boolean awaitTermination( long timeout,
090                                             TimeUnit unit ) throws InterruptedException {
091                return RepositoryLibrary.this.awaitTermination(timeout, unit);
092            }
093    
094            /**
095             * {@inheritDoc}
096             */
097            @Override
098            protected boolean doCheckIsTerminated() {
099                return RepositoryLibrary.this.isTerminated();
100            }
101    
102        }
103    
104        private final MimeTypeDetectors mimeTypeDetectors = new MimeTypeDetectors();
105        private final ServiceAdministrator administrator = new Administrator();
106        private final ReadWriteLock sourcesLock = new ReentrantReadWriteLock();
107        private final CopyOnWriteArrayList<RepositoryConnectionPool> pools = new CopyOnWriteArrayList<RepositoryConnectionPool>();
108        private RepositoryConnectionFactory delegate;
109        protected final ExecutionContext executionContext;
110        private final RepositoryContext repositoryContext;
111    
112        /**
113         * Create a new manager instance.
114         */
115        public RepositoryLibrary() {
116            this(new ExecutionContext(), null);
117        }
118    
119        /**
120         * Create a new manager instance.
121         * 
122         * @param delegate the connection factory to which this instance should delegate in the event that a source is not found in
123         *        this manager; may be null if there is no delegate
124         */
125        public RepositoryLibrary( RepositoryConnectionFactory delegate ) {
126            this(new ExecutionContext(), delegate);
127        }
128    
129        /**
130         * Create a new manager instance.
131         * 
132         * @param executionContext the execution context, which can be used used by sources to create other {@link ExecutionContext}
133         *        instances with different JAAS security contexts
134         * @throws IllegalArgumentException if the <code>executionContextFactory</code> reference is null
135         */
136        public RepositoryLibrary( ExecutionContext executionContext ) {
137            this(executionContext, null);
138        }
139    
140        /**
141         * Create a new manager instance.
142         * 
143         * @param executionContext the execution context, which can be used used by sources to create other {@link ExecutionContext}
144         *        instances with different JAAS security contexts
145         * @param delegate the connection factory to which this instance should delegate in the event that a source is not found in
146         *        this manager; may be null if there is no delegate
147         * @throws IllegalArgumentException if the <code>executionContextFactory</code> reference is null
148         */
149        public RepositoryLibrary( ExecutionContext executionContext,
150                                  RepositoryConnectionFactory delegate ) {
151            CheckArg.isNotNull(executionContext, "executionContext");
152            this.delegate = delegate;
153            this.executionContext = executionContext;
154            this.repositoryContext = new RepositoryContext() {
155                /**
156                 * {@inheritDoc}
157                 * 
158                 * @see org.jboss.dna.graph.connector.RepositoryContext#getExecutionContext()
159                 */
160                public ExecutionContext getExecutionContext() {
161                    return RepositoryLibrary.this.executionContext;
162                }
163    
164                /**
165                 * {@inheritDoc}
166                 * 
167                 * @see org.jboss.dna.graph.connector.RepositoryContext#getRepositoryConnectionFactory()
168                 */
169                public RepositoryConnectionFactory getRepositoryConnectionFactory() {
170                    return RepositoryLibrary.this;
171                }
172    
173            };
174        }
175    
176        /**
177         * @return executionContextFactory
178         */
179        public ExecutionContext getExecutionContext() {
180            return executionContext;
181        }
182    
183        /**
184         * @return mimeTypeDetectors
185         */
186        public MimeTypeDetectors getMimeTypeDetectors() {
187            return mimeTypeDetectors;
188        }
189    
190        /**
191         * Get the delegate connection factory.
192         * 
193         * @return the connection factory to which this instance should delegate in the event that a source is not found in this
194         *         manager, or null if there is no delegate
195         */
196        public RepositoryConnectionFactory getDelegate() {
197            return delegate;
198        }
199    
200        /**
201         * Set the delegate connection factory.
202         * 
203         * @param delegate the connection factory to which this instance should delegate in the event that a source is not found in
204         *        this manager; may be null if there is no delegate
205         */
206        public void setDelegate( RepositoryConnectionFactory delegate ) {
207            this.delegate = delegate;
208        }
209    
210        /**
211         * @return administrator
212         */
213        public ServiceAdministrator getAdministrator() {
214            return this.administrator;
215        }
216    
217        /**
218         * Utility method called by the administrator.
219         */
220        protected void start() {
221            // Do not establish connections to the pools; these will be established as needed
222    
223        }
224    
225        /**
226         * Utility method called by the administrator.
227         */
228        protected void shutdown() {
229            // Close all connections to the pools. This is done inside the pools write lock.
230            try {
231                this.sourcesLock.readLock().lock();
232                for (RepositoryConnectionPool pool : this.pools) {
233                    pool.shutdown();
234                }
235            } finally {
236                this.sourcesLock.readLock().unlock();
237            }
238        }
239    
240        /**
241         * Utility method called by the administrator.
242         * 
243         * @param timeout
244         * @param unit
245         * @return true if all pools were terminated in the supplied time (or were already terminated), or false if the timeout
246         *         occurred before all the connections were closed
247         * @throws InterruptedException
248         */
249        protected boolean awaitTermination( long timeout,
250                                            TimeUnit unit ) throws InterruptedException {
251            // Check whether all source pools are shut down. This is done inside the pools write lock.
252            try {
253                this.sourcesLock.readLock().lock();
254                for (RepositoryConnectionPool pool : this.pools) {
255                    if (!pool.awaitTermination(timeout, unit)) return false;
256                }
257                return true;
258            } finally {
259                this.sourcesLock.readLock().unlock();
260            }
261        }
262    
263        /**
264         * Returns true if this federated repository is in the process of terminating after {@link ServiceAdministrator#shutdown()}
265         * has been called on the {@link #getAdministrator() administrator}, but the federated repository has connections that have
266         * not yet normally been {@link RepositoryConnection#close() closed}. This method may be useful for debugging. A return of
267         * <tt>true</tt> reported a sufficient period after shutdown may indicate that connection users have ignored or suppressed
268         * interruption, causing this repository not to properly terminate.
269         * 
270         * @return true if terminating but not yet terminated, or false otherwise
271         * @see #isTerminated()
272         */
273        public boolean isTerminating() {
274            try {
275                this.sourcesLock.readLock().lock();
276                for (RepositoryConnectionPool pool : this.pools) {
277                    if (pool.isTerminating()) return true;
278                }
279                return false;
280            } finally {
281                this.sourcesLock.readLock().unlock();
282            }
283        }
284    
285        /**
286         * Return true if this federated repository has completed its termination and no longer has any open connections.
287         * 
288         * @return true if terminated, or false otherwise
289         * @see #isTerminating()
290         */
291        public boolean isTerminated() {
292            try {
293                this.sourcesLock.readLock().lock();
294                for (RepositoryConnectionPool pool : this.pools) {
295                    if (!pool.isTerminated()) return false;
296                }
297                return true;
298            } finally {
299                this.sourcesLock.readLock().unlock();
300            }
301        }
302    
303        /**
304         * Get an unmodifiable collection of {@link RepositorySource} names.
305         * 
306         * @return the pools
307         */
308        public Collection<String> getSourceNames() {
309            Set<String> sourceNames = new HashSet<String>();
310            for (RepositoryConnectionPool pool : this.pools) {
311                sourceNames.add(pool.getRepositorySource().getName());
312            }
313            return Collections.unmodifiableCollection(sourceNames);
314        }
315    
316        /**
317         * Get an unmodifiable collection of {@link RepositorySource} instances managed by this instance.
318         * 
319         * @return the pools
320         */
321        public Collection<RepositorySource> getSources() {
322            List<RepositorySource> sources = new LinkedList<RepositorySource>();
323            for (RepositoryConnectionPool pool : this.pools) {
324                sources.add(pool.getRepositorySource());
325            }
326            return Collections.unmodifiableCollection(sources);
327        }
328    
329        /**
330         * Get the RepositorySource with the specified name managed by this instance.
331         * 
332         * @param sourceName the name of the source
333         * @return the source, or null if no such source exists in this instance
334         */
335        public RepositorySource getSource( String sourceName ) {
336            try {
337                this.sourcesLock.readLock().lock();
338                for (RepositoryConnectionPool existingPool : this.pools) {
339                    RepositorySource source = existingPool.getRepositorySource();
340                    if (source.getName().equals(sourceName)) return source;
341                }
342            } finally {
343                this.sourcesLock.readLock().unlock();
344            }
345            return null;
346        }
347    
348        /**
349         * Get the connection pool managing the {@link RepositorySource} with the specified name managed by this instance.
350         * 
351         * @param sourceName the name of the source
352         * @return the pool, or null if no such pool exists in this instance
353         */
354        public RepositoryConnectionPool getConnectionPool( String sourceName ) {
355            try {
356                this.sourcesLock.readLock().lock();
357                for (RepositoryConnectionPool existingPool : this.pools) {
358                    RepositorySource source = existingPool.getRepositorySource();
359                    if (source.getName().equals(sourceName)) return existingPool;
360                }
361            } finally {
362                this.sourcesLock.readLock().unlock();
363            }
364            return null;
365        }
366    
367        /**
368         * Add the supplied federated source. This method returns false if the source is null.
369         * 
370         * @param source the source to add
371         * @return true if the source is added, or false if the reference is null or if there is already an existing source with the
372         *         supplied name.
373         */
374        public boolean addSource( RepositorySource source ) {
375            if (source == null) return false;
376            try {
377                this.sourcesLock.writeLock().lock();
378                for (RepositoryConnectionPool existingPool : this.pools) {
379                    if (existingPool.getRepositorySource().getName().equals(source.getName())) return false;
380                }
381                source.initialize(repositoryContext);
382                RepositoryConnectionPool pool = new RepositoryConnectionPool(source);
383                this.pools.add(pool);
384                return true;
385            } finally {
386                this.sourcesLock.writeLock().unlock();
387            }
388        }
389    
390        /**
391         * Remove from this federated repository the supplied source (or a source with the same name as that supplied). This call
392         * shuts down the connections in the source in an orderly fashion, allowing those connection currently in use to be used and
393         * closed normally, but preventing further connections from being used.
394         * <p>
395         * This method can safely be called while the federation repository is in use.
396         * </p>
397         * 
398         * @param source the source to be removed
399         * @param timeToAwait the amount of time to wait while all of the source's connections are closed, or non-positive if the call
400         *        should not wait at all
401         * @param unit the time unit to be used for <code>timeToAwait</code>
402         * @return true if the source was removed, or false if the source was not a source for this repository.
403         * @throws InterruptedException if the thread is interrupted while awaiting closing of the connections
404         */
405        public boolean removeSource( RepositorySource source,
406                                     long timeToAwait,
407                                     TimeUnit unit ) throws InterruptedException {
408            // Use the name; don't use the object equality ...
409            return removeSource(source.getName(), timeToAwait, unit) != null;
410        }
411    
412        /**
413         * Remove from this federated repository the source with the supplied name. This call shuts down the connections in the source
414         * in an orderly fashion, allowing those connection currently in use to be used and closed normally, but preventing further
415         * connections from being used.
416         * 
417         * @param name the name of the source to be removed
418         * @param timeToAwait the amount of time to wait while all of the source's connections are closed, or non-positive if the call
419         *        should not wait at all
420         * @param unit the time unit to be used for <code>timeToAwait</code>
421         * @return the source with the supplied name that was removed, or null if no existing source matching the supplied name could
422         *         be found
423         * @throws InterruptedException if the thread is interrupted while awaiting closing of the connections
424         */
425        public RepositorySource removeSource( String name,
426                                              long timeToAwait,
427                                              TimeUnit unit ) throws InterruptedException {
428            try {
429                this.sourcesLock.writeLock().lock();
430                for (RepositoryConnectionPool existingPool : this.pools) {
431                    if (existingPool.getRepositorySource().getName().equals(name)) {
432                        // Shut down the source ...
433                        existingPool.shutdown();
434                        if (timeToAwait > 0L) existingPool.awaitTermination(timeToAwait, unit);
435                    }
436                    return existingPool.getRepositorySource();
437                }
438            } finally {
439                this.sourcesLock.writeLock().unlock();
440            }
441            return null;
442        }
443    
444        /**
445         * {@inheritDoc}
446         * 
447         * @see org.jboss.dna.graph.connector.RepositoryConnectionFactory#createConnection(java.lang.String)
448         */
449        public RepositoryConnection createConnection( String sourceName ) {
450            try {
451                this.sourcesLock.readLock().lock();
452                for (RepositoryConnectionPool existingPool : this.pools) {
453                    RepositorySource source = existingPool.getRepositorySource();
454                    if (source.getName().equals(sourceName)) return existingPool.getConnection();
455                }
456                RepositoryConnectionFactory delegate = this.delegate;
457                if (delegate != null) {
458                    return delegate.createConnection(sourceName);
459                }
460            } finally {
461                this.sourcesLock.readLock().unlock();
462            }
463            return null;
464        }
465    }