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    
025    package org.jboss.dna.connector.filesystem;
026    
027    import java.io.File;
028    import java.io.FilenameFilter;
029    import java.util.Enumeration;
030    import java.util.HashMap;
031    import java.util.Hashtable;
032    import java.util.List;
033    import java.util.Map;
034    import java.util.concurrent.CopyOnWriteArraySet;
035    import javax.naming.Context;
036    import javax.naming.RefAddr;
037    import javax.naming.Reference;
038    import javax.naming.StringRefAddr;
039    import javax.naming.spi.ObjectFactory;
040    import net.jcip.annotations.Immutable;
041    import net.jcip.annotations.ThreadSafe;
042    import org.jboss.dna.common.i18n.I18n;
043    import org.jboss.dna.common.util.Logger;
044    import org.jboss.dna.common.util.StringUtil;
045    import org.jboss.dna.graph.cache.CachePolicy;
046    import org.jboss.dna.graph.connector.RepositoryConnection;
047    import org.jboss.dna.graph.connector.RepositoryContext;
048    import org.jboss.dna.graph.connector.RepositorySource;
049    import org.jboss.dna.graph.connector.RepositorySourceCapabilities;
050    import org.jboss.dna.graph.connector.RepositorySourceException;
051    
052    /**
053     * The {@link RepositorySource} for the connector that exposes an area of the local file system as content in a repository. This
054     * source considers a workspace name to be the path to the directory on the file system that represents the root of that
055     * workspace. New workspaces can be created, as long as the names represent valid paths to existing directories.
056     * 
057     * @author Randall Hauch
058     */
059    @ThreadSafe
060    public class FileSystemSource implements RepositorySource, ObjectFactory {
061    
062        /**
063         * The first serialized version of this source. Version {@value} .
064         */
065        private static final long serialVersionUID = 1L;
066    
067        protected static final String SOURCE_NAME = "sourceName";
068        protected static final String CACHE_TIME_TO_LIVE_IN_MILLISECONDS = "cacheTimeToLiveInMilliseconds";
069        protected static final String RETRY_LIMIT = "retryLimit";
070        protected static final String DEFAULT_WORKSPACE = "defaultWorkspace";
071        protected static final String PREDEFINED_WORKSPACE_NAMES = "predefinedWorkspaceNames";
072        protected static final String ALLOW_CREATING_WORKSPACES = "allowCreatingWorkspaces";
073    
074        /**
075         * This source supports events.
076         */
077        protected static final boolean SUPPORTS_EVENTS = true;
078        /**
079         * This source supports same-name-siblings.
080         */
081        protected static final boolean SUPPORTS_SAME_NAME_SIBLINGS = true;
082        /**
083         * This source does support creating workspaces.
084         */
085        protected static final boolean DEFAULT_SUPPORTS_CREATING_WORKSPACES = true;
086        /**
087         * This source does not support udpates by default, but each instance may be configured to be read-only or updateable}.
088         */
089        public static final boolean DEFAULT_SUPPORTS_UPDATES = false;
090    
091        /**
092         * This source supports creating references.
093         */
094        protected static final boolean SUPPORTS_REFERENCES = false;
095    
096        public static final int DEFAULT_RETRY_LIMIT = 0;
097        public static final int DEFAULT_CACHE_TIME_TO_LIVE_IN_SECONDS = 60 * 5; // 5 minutes
098    
099        private volatile String name;
100        private volatile int retryLimit = DEFAULT_RETRY_LIMIT;
101        private volatile int cacheTimeToLiveInMilliseconds = DEFAULT_CACHE_TIME_TO_LIVE_IN_SECONDS * 1000;
102        private volatile String defaultWorkspace;
103        private volatile String[] predefinedWorkspaces = new String[] {};
104        private volatile RepositorySourceCapabilities capabilities = new RepositorySourceCapabilities(
105                                                                                                      SUPPORTS_SAME_NAME_SIBLINGS,
106                                                                                                      DEFAULT_SUPPORTS_UPDATES,
107                                                                                                      SUPPORTS_EVENTS,
108                                                                                                      DEFAULT_SUPPORTS_CREATING_WORKSPACES,
109                                                                                                      SUPPORTS_REFERENCES);
110        private transient CachePolicy cachePolicy;
111        private transient CopyOnWriteArraySet<String> availableWorkspaceNames;
112    
113        /**
114         * 
115         */
116        public FileSystemSource() {
117        }
118    
119        /**
120         * {@inheritDoc}
121         * 
122         * @see org.jboss.dna.graph.connector.RepositorySource#getCapabilities()
123         */
124        public RepositorySourceCapabilities getCapabilities() {
125            return capabilities;
126        }
127    
128        /**
129         * {@inheritDoc}
130         * 
131         * @see org.jboss.dna.graph.connector.RepositorySource#getName()
132         */
133        public String getName() {
134            return name;
135        }
136    
137        /**
138         * Set the name for the source
139         * 
140         * @param name the new name for the source
141         */
142        public synchronized void setName( String name ) {
143            if (name != null) {
144                name = name.trim();
145                if (name.length() == 0) name = null;
146            }
147            this.name = name;
148        }
149    
150        /**
151         * Get whether this source supports updates.
152         * 
153         * @return true if this source supports updates, or false if this source only supports reading content.
154         */
155        public boolean getSupportsUpdates() {
156            return capabilities.supportsUpdates();
157        }
158    
159        // /**
160        // * Set whether this source supports updates.
161        // *
162        // * @param supportsUpdates true if this source supports updating content, or false if this source only supports reading
163        // * content.
164        // */
165        // public synchronized void setSupportsUpdates( boolean supportsUpdates ) {
166        // capabilities = new RepositorySourceCapabilities(SUPPORTS_SAME_NAME_SIBLINGS,
167        // supportsUpdates,
168        // SUPPORTS_EVENTS,
169        // capabilities.supportsCreatingWorkspaces());
170        // }
171    
172        /**
173         * Get the file system path to the existing directory that should be used for the default workspace. If the default is
174         * specified as a null String or is not a valid and resolvable path, this source will consider the default to be the current
175         * working directory of this virtual machine, as defined by the <code>new File(".")</code>.
176         * 
177         * @return the file system path to the directory representing the default workspace, or null if the default should be the
178         *         current working directory
179         */
180        public String getDirectoryForDefaultWorkspace() {
181            return defaultWorkspace;
182        }
183    
184        /**
185         * Set the file system path to the existing directory that should be used for the default workspace. If the default is
186         * specified as a null String or is not a valid and resolvable path, this source will consider the default to be the current
187         * working directory of this virtual machine, as defined by the <code>new File(".")</code>.
188         * 
189         * @param pathToDirectoryForDefaultWorkspace the valid and resolvable file system path to the directory representing the
190         *        default workspace, or null if the current working directory should be used as the default workspace
191         */
192        public synchronized void setDirectoryForDefaultWorkspace( String pathToDirectoryForDefaultWorkspace ) {
193            this.defaultWorkspace = pathToDirectoryForDefaultWorkspace;
194        }
195    
196        /**
197         * Gets the names of the workspaces that are available when this source is created. Each workspace name corresponds to a path
198         * to a directory on the file system.
199         * 
200         * @return the names of the workspaces that this source starts with, or null if there are no such workspaces
201         * @see #setPredefinedWorkspaceNames(String[])
202         * @see #setCreatingWorkspacesAllowed(boolean)
203         */
204        public synchronized String[] getPredefinedWorkspaceNames() {
205            String[] copy = new String[predefinedWorkspaces.length];
206            System.arraycopy(predefinedWorkspaces, 0, copy, 0, predefinedWorkspaces.length);
207            return copy;
208        }
209    
210        /**
211         * Sets the names of the workspaces that are available when this source is created. Each workspace name corresponds to a path
212         * to a directory on the file system.
213         * 
214         * @param predefinedWorkspaceNames the names of the workspaces that this source should start with, or null if there are no
215         *        such workspaces
216         * @see #setCreatingWorkspacesAllowed(boolean)
217         * @see #getPredefinedWorkspaceNames()
218         */
219        public synchronized void setPredefinedWorkspaceNames( String[] predefinedWorkspaceNames ) {
220            this.predefinedWorkspaces = predefinedWorkspaceNames;
221        }
222    
223        /**
224         * Get whether this source allows workspaces to be created dynamically.
225         * 
226         * @return true if this source allows workspaces to be created by clients, or false if the set of workspaces is fixed
227         * @see #setPredefinedWorkspaceNames(String[])
228         * @see #getPredefinedWorkspaceNames()
229         * @see #setCreatingWorkspacesAllowed(boolean)
230         */
231        public boolean isCreatingWorkspacesAllowed() {
232            return capabilities.supportsCreatingWorkspaces();
233        }
234    
235        /**
236         * Set whether this source allows workspaces to be created dynamically.
237         * 
238         * @param allowWorkspaceCreation true if this source allows workspaces to be created by clients, or false if the set of
239         *        workspaces is fixed
240         * @see #setPredefinedWorkspaceNames(String[])
241         * @see #getPredefinedWorkspaceNames()
242         * @see #isCreatingWorkspacesAllowed()
243         */
244        public synchronized void setCreatingWorkspacesAllowed( boolean allowWorkspaceCreation ) {
245            capabilities = new RepositorySourceCapabilities(capabilities.supportsSameNameSiblings(), capabilities.supportsUpdates(),
246                                                            capabilities.supportsEvents(), allowWorkspaceCreation,
247                                                            capabilities.supportsReferences());
248        }
249    
250        /**
251         * {@inheritDoc}
252         * 
253         * @see org.jboss.dna.graph.connector.RepositorySource#getRetryLimit()
254         */
255        public int getRetryLimit() {
256            return retryLimit;
257        }
258    
259        /**
260         * {@inheritDoc}
261         * 
262         * @see org.jboss.dna.graph.connector.RepositorySource#setRetryLimit(int)
263         */
264        public synchronized void setRetryLimit( int limit ) {
265            this.retryLimit = limit < 0 ? 0 : limit;
266        }
267    
268        /**
269         * Get the time in milliseconds that content returned from this source may used while in the cache.
270         * 
271         * @return the time to live, in milliseconds, or 0 if the time to live is not specified by this source
272         */
273        public int getCacheTimeToLiveInMilliseconds() {
274            return cacheTimeToLiveInMilliseconds;
275        }
276    
277        /**
278         * Set the time in milliseconds that content returned from this source may used while in the cache.
279         * 
280         * @param cacheTimeToLive the time to live, in milliseconds; 0 if the time to live is not specified by this source; or a
281         *        negative number for the default value
282         */
283        public synchronized void setCacheTimeToLiveInMilliseconds( int cacheTimeToLive ) {
284            if (cacheTimeToLive < 0) cacheTimeToLive = DEFAULT_CACHE_TIME_TO_LIVE_IN_SECONDS;
285            this.cacheTimeToLiveInMilliseconds = cacheTimeToLive;
286            this.cachePolicy = cacheTimeToLiveInMilliseconds > 0 ? new FileSystemCachePolicy(cacheTimeToLiveInMilliseconds) : null;
287        }
288    
289        /**
290         * {@inheritDoc}
291         * 
292         * @see org.jboss.dna.graph.connector.RepositorySource#initialize(org.jboss.dna.graph.connector.RepositoryContext)
293         */
294        public synchronized void initialize( RepositoryContext context ) throws RepositorySourceException {
295            // No need to do anything
296        }
297    
298        /**
299         * {@inheritDoc}
300         * 
301         * @see javax.naming.Referenceable#getReference()
302         */
303        public synchronized Reference getReference() {
304            String className = getClass().getName();
305            String factoryClassName = this.getClass().getName();
306            Reference ref = new Reference(className, factoryClassName, null);
307    
308            if (getName() != null) {
309                ref.add(new StringRefAddr(SOURCE_NAME, getName()));
310            }
311            ref.add(new StringRefAddr(CACHE_TIME_TO_LIVE_IN_MILLISECONDS, Integer.toString(getCacheTimeToLiveInMilliseconds())));
312            ref.add(new StringRefAddr(RETRY_LIMIT, Integer.toString(getRetryLimit())));
313            ref.add(new StringRefAddr(DEFAULT_WORKSPACE, getDirectoryForDefaultWorkspace()));
314            ref.add(new StringRefAddr(ALLOW_CREATING_WORKSPACES, Boolean.toString(isCreatingWorkspacesAllowed())));
315            String[] workspaceNames = getPredefinedWorkspaceNames();
316            if (workspaceNames != null && workspaceNames.length != 0) {
317                ref.add(new StringRefAddr(PREDEFINED_WORKSPACE_NAMES, StringUtil.combineLines(workspaceNames)));
318            }
319            return ref;
320        }
321    
322        /**
323         * {@inheritDoc}
324         */
325        public Object getObjectInstance( Object obj,
326                                         javax.naming.Name name,
327                                         Context nameCtx,
328                                         Hashtable<?, ?> environment ) throws Exception {
329            if (obj instanceof Reference) {
330                Map<String, String> values = new HashMap<String, String>();
331                Reference ref = (Reference)obj;
332                Enumeration<?> en = ref.getAll();
333                while (en.hasMoreElements()) {
334                    RefAddr subref = (RefAddr)en.nextElement();
335                    if (subref instanceof StringRefAddr) {
336                        String key = subref.getType();
337                        Object value = subref.getContent();
338                        if (value != null) values.put(key, value.toString());
339                    }
340                }
341                String sourceName = values.get(SOURCE_NAME);
342                String cacheTtlInMillis = values.get(CACHE_TIME_TO_LIVE_IN_MILLISECONDS);
343                String retryLimit = values.get(RETRY_LIMIT);
344                String defaultWorkspace = values.get(DEFAULT_WORKSPACE);
345                String createWorkspaces = values.get(ALLOW_CREATING_WORKSPACES);
346    
347                String combinedWorkspaceNames = values.get(PREDEFINED_WORKSPACE_NAMES);
348                String[] workspaceNames = null;
349                if (combinedWorkspaceNames != null) {
350                    List<String> paths = StringUtil.splitLines(combinedWorkspaceNames);
351                    workspaceNames = paths.toArray(new String[paths.size()]);
352                }
353    
354                // Create the source instance ...
355                FileSystemSource source = new FileSystemSource();
356                if (sourceName != null) source.setName(sourceName);
357                if (cacheTtlInMillis != null) source.setCacheTimeToLiveInMilliseconds(Integer.parseInt(cacheTtlInMillis));
358                if (retryLimit != null) source.setRetryLimit(Integer.parseInt(retryLimit));
359                if (defaultWorkspace != null) source.setDirectoryForDefaultWorkspace(defaultWorkspace);
360                if (createWorkspaces != null) source.setCreatingWorkspacesAllowed(Boolean.parseBoolean(createWorkspaces));
361                if (workspaceNames != null && workspaceNames.length != 0) source.setPredefinedWorkspaceNames(workspaceNames);
362                return source;
363            }
364            return null;
365        }
366    
367        /**
368         * {@inheritDoc}
369         * 
370         * @see org.jboss.dna.graph.connector.RepositorySource#getConnection()
371         */
372        public synchronized RepositoryConnection getConnection() throws RepositorySourceException {
373            String sourceName = getName();
374            if (sourceName == null || sourceName.trim().length() == 0) {
375                I18n msg = FileSystemI18n.propertyIsRequired;
376                throw new RepositorySourceException(getName(), msg.text("name"));
377            }
378    
379            boolean reportWarnings = false;
380            if (this.availableWorkspaceNames == null) {
381                // Set up the predefined workspace names ...
382                this.availableWorkspaceNames = new CopyOnWriteArraySet<String>();
383                for (String predefined : this.predefinedWorkspaces) {
384                    this.availableWorkspaceNames.add(predefined);
385                }
386    
387                // Report the warnings for non-existant predefined workspaces
388                reportWarnings = true;
389                for (String path : this.availableWorkspaceNames) {
390                    // Look for the file at this path ...
391                    File file = new File(path);
392                    if (!file.exists()) {
393                        Logger.getLogger(getClass()).warn(FileSystemI18n.pathForPredefinedWorkspaceDoesNotExist, path, name);
394                    } else if (!file.isDirectory()) {
395                        Logger.getLogger(getClass()).warn(FileSystemI18n.pathForPredefinedWorkspaceIsNotDirectory, path, name);
396                    } else if (!file.canRead()) {
397                        Logger.getLogger(getClass()).warn(FileSystemI18n.pathForPredefinedWorkspaceCannotBeRead, path, name);
398                    }
399                }
400            }
401    
402            FilenameFilter filenameFilter = null;
403            boolean supportsUpdates = getSupportsUpdates();
404            File defaultWorkspace = new File(".");
405            String path = getDirectoryForDefaultWorkspace();
406            if (path != null) {
407                // Look for the file at this path ...
408                File file = new File(path);
409                I18n warning = null;
410                if (!file.exists()) {
411                    warning = FileSystemI18n.pathForDefaultWorkspaceDoesNotExist;
412                } else if (!file.isDirectory()) {
413                    warning = FileSystemI18n.pathForDefaultWorkspaceIsNotDirectory;
414                } else if (!file.canRead()) {
415                    warning = FileSystemI18n.pathForDefaultWorkspaceCannotBeRead;
416                } else {
417                    // good to use!
418                    defaultWorkspace = file;
419                }
420                if (reportWarnings && warning != null) {
421                    Logger.getLogger(getClass()).warn(warning, path, name);
422                }
423            }
424            this.availableWorkspaceNames.add(defaultWorkspace.getPath());
425            return new FileSystemConnection(name, defaultWorkspace, availableWorkspaceNames, isCreatingWorkspacesAllowed(),
426                                            cachePolicy, filenameFilter, supportsUpdates);
427        }
428    
429        @Immutable
430        /*package*/class FileSystemCachePolicy implements CachePolicy {
431            private static final long serialVersionUID = 1L;
432            private final int ttl;
433    
434            /*package*/FileSystemCachePolicy( int ttl ) {
435                this.ttl = ttl;
436            }
437    
438            public long getTimeToLive() {
439                return ttl;
440            }
441    
442        }
443    
444    }