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 }