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     * Unless otherwise indicated, all code in JBoss DNA is licensed
010     * 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.connector.jbosscache;
025    
026    import java.net.URI;
027    import java.net.URISyntaxException;
028    import java.util.Collections;
029    import java.util.HashSet;
030    import java.util.Set;
031    import java.util.concurrent.ConcurrentHashMap;
032    import java.util.concurrent.locks.Lock;
033    import java.util.concurrent.locks.ReentrantLock;
034    import javax.naming.Context;
035    import net.jcip.annotations.GuardedBy;
036    import net.jcip.annotations.ThreadSafe;
037    import org.jboss.cache.Cache;
038    import org.jboss.cache.CacheFactory;
039    import org.jboss.cache.config.ConfigurationException;
040    import org.jboss.dna.common.i18n.I18n;
041    import org.jboss.dna.common.util.Logger;
042    import org.jboss.dna.graph.connector.RepositorySourceException;
043    import org.jboss.dna.graph.property.Name;
044    
045    /**
046     * This class represents a set of workspaces used by the {@link JBossCacheSource JBoss Cache connector}.
047     */
048    @ThreadSafe
049    public class JBossCacheWorkspaces {
050    
051        private final String sourceName;
052        private final ConcurrentHashMap<String, Cache<Name, Object>> caches = new ConcurrentHashMap<String, Cache<Name, Object>>();
053        private final Set<String> initialNames;
054        private final CacheFactory<Name, Object> cacheFactory;
055        private final String defaultCacheFactoryConfigurationName;
056        private final Context jndi;
057        private final Set<String> workspaceNamesForJndiClassCastProblems = new HashSet<String>();
058        private final Set<String> workspaceNamesForConfigurationNameProblems = new HashSet<String>();
059        private final Lock writeLock = new ReentrantLock();
060    
061        /**
062         * Create a new instance of the workspace and cache manager for the JBoss Cache connector.
063         * 
064         * @param sourceName the name of the source that uses this object; may not be null
065         * @param cacheFactory the factory that should be used to create new caches; may not be null
066         * @param defaultCacheFactoryConfigurationName the name of the configuration that is supplied to the {@link CacheFactory cache
067         *        factory} to {@link CacheFactory#createCache(String) create the new cache} if the workspace name does not correspond
068         *        to a configuration; may be null
069         * @param initialNames the initial names for the workspaces; may be null or empty
070         * @param jndiContext the JNDI context that should be used, or null if JNDI should not be used at all
071         */
072        public JBossCacheWorkspaces( String sourceName,
073                                     CacheFactory<Name, Object> cacheFactory,
074                                     String defaultCacheFactoryConfigurationName,
075                                     Set<String> initialNames,
076                                     Context jndiContext ) {
077            assert sourceName != null;
078            this.sourceName = sourceName;
079            if (initialNames == null) initialNames = Collections.emptySet();
080            this.initialNames = initialNames;
081            this.cacheFactory = cacheFactory;
082            this.defaultCacheFactoryConfigurationName = defaultCacheFactoryConfigurationName;
083            this.jndi = jndiContext;
084        }
085    
086        /**
087         * Attempt to create a new workspace with the supplied name.
088         * 
089         * @param workspaceName the name of the new workspace, which may be a valid URI if the cache is to be found in JNDI
090         * @return the new workspace, or null if there is already a workspace with the name
091         */
092        public Cache<Name, Object> createWorkspace( String workspaceName ) {
093            try {
094                writeLock.lock();
095                // First, see if there is already an existing cache ...
096                Cache<Name, Object> cache = caches.get(workspaceName);
097                if (cache != null) {
098                    // There is already a workspace, so we can't create ...
099                    return null;
100                }
101    
102                // There isn't already a cache, but next check the list of initial names ...
103                if (initialNames.contains(workspaceName)) {
104                    // The workspace already exists, but we just haven't accessed it yet
105                    return null;
106                }
107    
108                // Time to create a new cache. First see if we're supposed to use a cache already in JNDI ...
109                cache = findCacheInJndi(workspaceName);
110                if (cache == null) {
111                    // Try to create one ...
112                    cache = createNewCache(workspaceName);
113                }
114    
115                if (cache != null) {
116                    // Manage this cache ...
117                    Cache<Name, Object> existing = caches.putIfAbsent(workspaceName, cache);
118                    if (existing != null) cache = existing;
119                }
120                return cache; // may still be null if we couldn't create a new cache
121    
122            } finally {
123                writeLock.unlock();
124            }
125        }
126    
127        /**
128         * Get the cache that corresponds to the supplied workspace name, and optionally create a new cache if no such cache already
129         * exists. This method first checks for {@link Cache} instances previously found for the same workspace name. If no cache is
130         * found, this method then checks whether the supplied workspace name is a valid URI, and if so the method looks for a
131         * {@link Cache} instance in JNDI at that URI. If none is found (or the name is not a valid URI), this method then creates a
132         * new {@link Cache} instance using the {@link CacheFactory} supplied in the constructor.
133         * 
134         * @param workspaceName the name of the workspace, which may be a valid URI if the cache is to be found in JNDI
135         * @param createIfMissing true if the cache should be created if no such cache already exists
136         * @return the cache that corresponds to the workspace with the supplied name, or null if there is no cache for that workspace
137         *         (and one could not be or was not created)
138         */
139        public Cache<Name, Object> getWorkspace( String workspaceName,
140                                                 boolean createIfMissing ) {
141            // First, see if there is already an existing cache ...
142            Cache<Name, Object> cache = caches.get(workspaceName);
143            if (cache != null) return cache;
144    
145            try {
146                writeLock.lock();
147                // Ensure one didn't get created while we waited for the lock ...
148                cache = caches.get(workspaceName);
149                if (cache != null) return cache;
150    
151                // We've not yet come across the cache for the workspace.
152    
153                // Check whether the workspace name was one of the initial set of names...
154                if (this.initialNames.contains(workspaceName)) {
155                    // This workspace/cache was one of those defined at startup to be available,
156                    // so we really don't consider this to be "creating a new cache"; it's just the first time we've used it
157                    // and we're lazily finding the instances. So, just mark 'createIfMissing' to true and continue ...
158                    createIfMissing = true;
159                }
160    
161                if (!createIfMissing) return null;
162    
163                // First see if we can find a cache in JNDI ...
164                cache = findCacheInJndi(workspaceName);
165    
166                if (cache == null) {
167                    // Try to create one ...
168                    cache = createNewCache(workspaceName);
169                }
170    
171                if (cache != null) {
172                    Cache<Name, Object> existing = caches.putIfAbsent(workspaceName, cache);
173                    if (existing != null) cache = existing;
174                }
175                return cache; // may still be null if we couldn't create a new cache
176            } finally {
177                writeLock.unlock();
178            }
179        }
180    
181        /**
182         * Attempt to find an existing {@link Cache} object in JNDI, using the supplied workspace name as the JNDI name.
183         * 
184         * @param workspaceName the name of the workspace
185         * @return the cache found in JNDI that corresponds to the workspace name
186         */
187        @SuppressWarnings( "unchecked" )
188        @GuardedBy( "writeLock" )
189        protected Cache<Name, Object> findCacheInJndi( String workspaceName ) {
190            assert workspaceName != null;
191            if (jndi == null) return null;
192    
193            // Try to look up the cache instance in JDNI ...
194            workspaceName = workspaceName.trim();
195            if (workspaceName.length() != 0) {
196                try {
197                    new URI(workspaceName.trim());
198                    Object object = null;
199                    try {
200                        object = jndi.lookup(workspaceName);
201                        if (object != null && object instanceof Cache) {
202                            return (Cache<Name, Object>)object;
203                        }
204                    } catch (ClassCastException err) {
205                        // The object found in JNDI was not a JBoss Cache instance ...
206                        if (this.workspaceNamesForJndiClassCastProblems.add(workspaceName)) {
207                            // Log this problem only the first time ...
208                            String className = object != null ? object.getClass().getName() : "null";
209                            I18n msg = JBossCacheConnectorI18n.objectFoundInJndiWasNotCache;
210                            Logger.getLogger(getClass()).warn(msg, workspaceName, sourceName, className);
211                        }
212                    } catch (Throwable error) {
213                        // try loading
214                        if (error instanceof RuntimeException) throw (RuntimeException)error;
215                        throw new RepositorySourceException(sourceName, error);
216                    }
217    
218                } catch (URISyntaxException err) {
219                    // Not a valid URI, so just continue ...
220                }
221            }
222            return null;
223        }
224    
225        /**
226         * Method that is responsible for attempting to create a new cache given the supplied workspace name. Note that this is
227         * probably called at most once for each workspace name (except if this method fails to create a cache for a given workspace
228         * name).
229         * 
230         * @param workspaceName the name of the workspace
231         * @return the new cache that corresponds to the workspace name
232         */
233        @GuardedBy( "writeLock" )
234        protected Cache<Name, Object> createNewCache( String workspaceName ) {
235            assert workspaceName != null;
236            if (this.cacheFactory == null) return null;
237    
238            // Try to create the cache using the workspace name as the configuration ...
239            try {
240                return this.cacheFactory.createCache(workspaceName);
241            } catch (ConfigurationException error) {
242                // The workspace name is probably not the name of a configuration ...
243                I18n msg = JBossCacheConnectorI18n.workspaceNameWasNotValidConfiguration;
244                Logger.getLogger(getClass()).debug(msg.text(workspaceName, error.getMessage()));
245            }
246    
247            if (this.defaultCacheFactoryConfigurationName != null) {
248                // Try to create the cache using the default configuration name ...
249                try {
250                    return this.cacheFactory.createCache(this.defaultCacheFactoryConfigurationName);
251                } catch (ConfigurationException error) {
252                    // The default configuration name is not valid ...
253                    if (this.workspaceNamesForConfigurationNameProblems.add(workspaceName)) {
254                        // Log this problem only the first time ...
255                        I18n msg = JBossCacheConnectorI18n.defaultCacheFactoryConfigurationNameWasNotValidConfiguration;
256                        Logger.getLogger(getClass()).debug(msg.text(workspaceName));
257                    }
258                }
259            }
260    
261            // Just create a new cache with the default configuration ...
262            return this.cacheFactory.createCache();
263        }
264    
265        /**
266         * Return an immutable set of names for the currently available workspaces.
267         * 
268         * @return the immutable set of workspace names; never null
269         */
270        public Set<String> getWorkspaceNames() {
271            Set<String> names = new HashSet<String>();
272            if (!initialNames.isEmpty()) names.addAll(initialNames);
273            names.addAll(caches.keySet());
274            return Collections.unmodifiableSet(names);
275        }
276    
277        /**
278         * Remove the cache that corresponds to the supplied workspace name as no longer being available. This will remove the cache
279         * even if the workspace name is one of the "initial names" provided to this object's constructor.
280         * 
281         * @param workspaceName the name of the existing workspace that is to be removed
282         * @return true if there was an existing workspace that was removed by this call, or false if there was no workspace with the
283         *         supplied name
284         */
285        public boolean removeWorkspace( String workspaceName ) {
286            try {
287                writeLock.lock();
288    
289                // Remove this from both the cache and initialNames ...
290                boolean removed = initialNames.remove(workspaceName);
291                if (caches.remove(workspaceName) != null) removed = true;
292                return removed;
293            } finally {
294                writeLock.unlock();
295            }
296        }
297    }