View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors. 
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   *
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  
25  package org.modeshape.connector.filesystem;
26  
27  import java.io.File;
28  import java.io.FilenameFilter;
29  import java.util.Collections;
30  import java.util.HashMap;
31  import java.util.Hashtable;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.regex.Pattern;
35  import javax.naming.Context;
36  import javax.naming.Reference;
37  import javax.naming.StringRefAddr;
38  import javax.naming.spi.ObjectFactory;
39  import net.jcip.annotations.ThreadSafe;
40  import org.modeshape.common.annotation.Category;
41  import org.modeshape.common.annotation.Description;
42  import org.modeshape.common.annotation.Label;
43  import org.modeshape.common.i18n.I18n;
44  import org.modeshape.common.util.CheckArg;
45  import org.modeshape.common.util.Logger;
46  import org.modeshape.common.util.StringUtil;
47  import org.modeshape.connector.filesystem.FileSystemRepository.FileSystemTransaction;
48  import org.modeshape.graph.ExecutionContext;
49  import org.modeshape.graph.connector.RepositoryConnection;
50  import org.modeshape.graph.connector.RepositorySource;
51  import org.modeshape.graph.connector.RepositorySourceCapabilities;
52  import org.modeshape.graph.connector.RepositorySourceException;
53  import org.modeshape.graph.connector.base.AbstractRepositorySource;
54  import org.modeshape.graph.connector.base.Connection;
55  import org.modeshape.graph.connector.base.PathNode;
56  import org.modeshape.graph.property.Binary;
57  import org.modeshape.graph.request.CreateWorkspaceRequest.CreateConflictBehavior;
58  
59  /**
60   * The {@link RepositorySource} for the connector that exposes an area of the local file system as content in a repository. This
61   * source considers a workspace name to be the path to the directory on the file system that represents the root of that
62   * workspace. New workspaces can be created, as long as the names represent valid paths to existing directories.
63   */
64  @ThreadSafe
65  public class FileSystemSource extends AbstractRepositorySource implements ObjectFactory {
66  
67      /**
68       * The first serialized version of this source. Version {@value} .
69       */
70      private static final long serialVersionUID = 1L;
71  
72      /**
73       * The initial {@link #getDefaultWorkspaceName() name of the default workspace} is "{@value} ", unless otherwise specified.
74       */
75      public static final String DEFAULT_NAME_OF_DEFAULT_WORKSPACE = "default";
76  
77      protected static final String SOURCE_NAME = "sourceName";
78      protected static final String DEFAULT_WORKSPACE = "defaultWorkspace";
79      protected static final String WORKSPACE_ROOT = "workspaceRootPath";
80      protected static final String PREDEFINED_WORKSPACE_NAMES = "predefinedWorkspaceNames";
81      protected static final String ALLOW_CREATING_WORKSPACES = "allowCreatingWorkspaces";
82      protected static final String MAX_PATH_LENGTH = "maxPathLength";
83      protected static final String EXCLUSION_PATTERN = "exclusionPattern";
84      protected static final String INCLUSION_PATTERN = "inclusionPattern";
85      protected static final String FILENAME_FILTER = "filenameFilter";
86      protected static final String CUSTOM_PROPERTY_FACTORY = "customPropertyFactory";
87      protected static final String EAGER_FILE_LOADING = "eagerFileLoading";
88      protected static final String DETERMINE_MIME_TYPE_USING_CONTENT = "determineMimeTypeUsingContent";
89      protected static final String EXTRA_PROPERTIES = "extraProperties";
90  
91      /**
92       * This source supports events.
93       */
94      protected static final boolean SUPPORTS_EVENTS = true;
95      /**
96       * This source supports same-name-siblings.
97       */
98      protected static final boolean SUPPORTS_SAME_NAME_SIBLINGS = true;
99      /**
100      * This source does support creating workspaces.
101      */
102     protected static final boolean DEFAULT_SUPPORTS_CREATING_WORKSPACES = true;
103     /**
104      * This source does not support updates by default, but each instance may be configured to be read-only or updateable}.
105      */
106     public static final boolean DEFAULT_SUPPORTS_UPDATES = false;
107 
108     /**
109      * The default behavior for dealing with extra properties on 'nt:file', 'nt:folder' and 'nt:resource' nodes is to log them.
110      */
111     public static final String DEFAULT_EXTRA_PROPERTIES = "log";
112 
113     /**
114      * This source supports creating references.
115      */
116     protected static final boolean SUPPORTS_REFERENCES = false;
117     /**
118      * This source by default does not eagerly read the file content into {@link Binary} values, but instead does it only when
119      * necessary.
120      */
121     public static final boolean DEFAULT_EAGER_FILE_LOADING = false;
122 
123     /**
124      * This source by default does not use the file content to determine the MIME type, but instead just uses the filename.
125      */
126     public static final boolean DEFAULT_DETERMINE_MIME_TYPE_USING_CONTENT = false;
127 
128     public static final int DEFAULT_MAX_PATH_LENGTH = 255; // 255 for windows users
129     public static final String DEFAULT_EXCLUSION_PATTERN = null;
130     public static final String DEFAULT_INCLUSION_PATTERN = null;
131     public static final FilenameFilter DEFAULT_FILENAME_FILTER = null;
132     private static final FilenameFilter ACCEPT_ALL_FILTER = new InclusionExclusionFilenameFilter();
133 
134     protected static Map<String, CustomPropertiesFactory> EXTRA_PROPERTIES_CLASSNAME_BY_KEY;
135 
136     static {
137         Map<String, CustomPropertiesFactory> byKey = new HashMap<String, CustomPropertiesFactory>();
138         byKey.put(DEFAULT_EXTRA_PROPERTIES, new LogProperties(Logger.getLogger(FileSystemSource.class)));
139         byKey.put("store", new StoreProperties());
140         byKey.put("error", new ThrowProperties());
141         byKey.put("ignore", new IgnoreProperties());
142         EXTRA_PROPERTIES_CLASSNAME_BY_KEY = Collections.unmodifiableMap(byKey);
143     }
144 
145     @Description( i18n = FileSystemI18n.class, value = "defaultWorkspaceNamePropertyDescription" )
146     @Label( i18n = FileSystemI18n.class, value = "defaultWorkspaceNamePropertyLabel" )
147     @Category( i18n = FileSystemI18n.class, value = "defaultWorkspaceNamePropertyCategory" )
148     private volatile String defaultWorkspaceName = DEFAULT_NAME_OF_DEFAULT_WORKSPACE;
149 
150     @Description( i18n = FileSystemI18n.class, value = "workspaceRootPathPropertyDescription" )
151     @Label( i18n = FileSystemI18n.class, value = "workspaceRootPathPropertyLabel" )
152     @Category( i18n = FileSystemI18n.class, value = "workspaceRootPathPropertyCategory" )
153     private volatile String workspaceRootPath;
154 
155     @Description( i18n = FileSystemI18n.class, value = "predefinedWorkspacesPropertyDescription" )
156     @Label( i18n = FileSystemI18n.class, value = "predefinedWorkspacesPropertyLabel" )
157     @Category( i18n = FileSystemI18n.class, value = "predefinedWorkspacesPropertyCategory" )
158     private volatile String[] predefinedWorkspaces = new String[] {};
159 
160     @Description( i18n = FileSystemI18n.class, value = "maxPathLengthPropertyDescription" )
161     @Label( i18n = FileSystemI18n.class, value = "maxPathLengthPropertyLabel" )
162     @Category( i18n = FileSystemI18n.class, value = "maxPathLengthPropertyCategory" )
163     private volatile int maxPathLength = DEFAULT_MAX_PATH_LENGTH;
164 
165     @Description( i18n = FileSystemI18n.class, value = "eagerFileLoadingPropertyDescription" )
166     @Label( i18n = FileSystemI18n.class, value = "eagerFileLoadingPropertyLabel" )
167     @Category( i18n = FileSystemI18n.class, value = "eagerFileLoadingPropertyCategory" )
168     private volatile boolean eagerFileLoading = DEFAULT_EAGER_FILE_LOADING;
169 
170     @Description( i18n = FileSystemI18n.class, value = "determineMimeTypeUsingContentPropertyDescription" )
171     @Label( i18n = FileSystemI18n.class, value = "determineMimeTypeUsingContentPropertyLabel" )
172     @Category( i18n = FileSystemI18n.class, value = "determineMimeTypeUsingContentPropertyCategory" )
173     private volatile boolean determineMimeTypeUsingContent = DEFAULT_DETERMINE_MIME_TYPE_USING_CONTENT;
174 
175     @Description( i18n = FileSystemI18n.class, value = "extraPropertiesPropertyDescription" )
176     @Label( i18n = FileSystemI18n.class, value = "extraPropertiesPropertyLabel" )
177     @Category( i18n = FileSystemI18n.class, value = "extraPropertiesPropertyCategory" )
178     private volatile String extraProperties = DEFAULT_EXTRA_PROPERTIES;
179 
180     private volatile FilenameFilter filenameFilter = DEFAULT_FILENAME_FILTER;
181     private volatile InclusionExclusionFilenameFilter inclusionExclusionFilenameFilter = new InclusionExclusionFilenameFilter();
182 
183     private volatile RepositorySourceCapabilities capabilities = new RepositorySourceCapabilities(
184                                                                                                   SUPPORTS_SAME_NAME_SIBLINGS,
185                                                                                                   DEFAULT_SUPPORTS_UPDATES,
186                                                                                                   SUPPORTS_EVENTS,
187                                                                                                   DEFAULT_SUPPORTS_CREATING_WORKSPACES,
188                                                                                                   SUPPORTS_REFERENCES);
189     private transient FileSystemRepository repository;
190     private volatile CustomPropertiesFactory customPropertiesFactory;
191 
192     private ExecutionContext defaultContext = new ExecutionContext();
193 
194     /**
195      * 
196      */
197     public FileSystemSource() {
198     }
199 
200     /**
201      * {@inheritDoc}
202      * 
203      * @see org.modeshape.graph.connector.RepositorySource#getCapabilities()
204      */
205     public RepositorySourceCapabilities getCapabilities() {
206         return capabilities;
207     }
208 
209     /**
210      * Get whether this source supports updates.
211      * 
212      * @return true if this source supports updates, or false if this source only supports reading content.
213      */
214     public boolean getUpdatesAllowed() {
215         return capabilities.supportsUpdates();
216     }
217 
218     /**
219      * Get the relative root directory for the workspaces. If this property is set, workspaces can be given as relative paths from
220      * this directory and all workspace paths must be ancestors of this path.
221      * 
222      * @return the root directory for workspaces
223      */
224     public String getWorkspaceRootPath() {
225         return workspaceRootPath;
226     }
227 
228     /**
229      * Sets the relative root directory for workspaces
230      * 
231      * @param workspaceRootPath the relative root directory for workspaces. If this value is non-null, all workspace paths will be
232      *        treated as paths relative to this directory
233      */
234     public synchronized void setWorkspaceRootPath( String workspaceRootPath ) {
235         this.workspaceRootPath = workspaceRootPath;
236     }
237 
238     /**
239      * Get the regular expression that, if matched by a file or folder, indicates that the file or folder should be ignored
240      * 
241      * @return the regular expression that, if matched by a file or folder, indicates that the file or folder should be ignored;
242      *         may be null
243      */
244     @Description( i18n = FileSystemI18n.class, value = "exclusionPatternPropertyDescription" )
245     @Label( i18n = FileSystemI18n.class, value = "exclusionPatternPropertyLabel" )
246     @Category( i18n = FileSystemI18n.class, value = "exclusionPatternPropertyCategory" )
247     public String getExclusionPattern() {
248         return this.inclusionExclusionFilenameFilter.getExclusionPattern();
249     }
250 
251     /**
252      * Sets the regular expression that, if matched by a file or folder, indicates that the file or folder should be ignored
253      * <p>
254      * Only one of FilenameFilter or Inclusion/Exclusion Pattern are used at a given time. If Inclusion/exclusion are set, then
255      * FilenameFilter is ignored.
256      * </p>
257      * 
258      * @param exclusionPattern the regular expression that, if matched by a file or folder, indicates that the file or folder
259      *        should be ignored. If this pattern is {@code null}, no files will be excluded.
260      */
261     public synchronized void setExclusionPattern( String exclusionPattern ) {
262         this.inclusionExclusionFilenameFilter.setExclusionPattern(exclusionPattern);
263     }
264 
265     /**
266      * Get the regular expression that, if matched by a file or folder, indicates that the file or folder should be included
267      * 
268      * @return the regular expression that, if matched by a file or folder, indicates that the file or folder should be included;
269      *         may be null
270      */
271     @Description( i18n = FileSystemI18n.class, value = "inclusionPatternPropertyDescription" )
272     @Label( i18n = FileSystemI18n.class, value = "inclusionPatternPropertyLabel" )
273     @Category( i18n = FileSystemI18n.class, value = "inclusionPatternPropertyCategory" )
274     public String getInclusionPattern() {
275         return this.inclusionExclusionFilenameFilter.getInclusionPattern();
276     }
277 
278     /**
279      * Sets the regular expression that, if matched by a file or folder, indicates that the file or folder should be included
280      * <p>
281      * Only one of FilenameFilter or Inclusion/Exclusion Pattern are used at a given time. If Inclusion/exclusion are set, then
282      * FilenameFilter is ignored.
283      * </p>
284      * 
285      * @param inclusionPattern the regular expression that, if matched by a file or folder, indicates that the file or folder
286      *        should be ignored. If this pattern is {@code null}, no files will be excluded.
287      */
288     public synchronized void setInclusionPattern( String inclusionPattern ) {
289         this.inclusionExclusionFilenameFilter.setInclusionPattern(inclusionPattern);
290     }
291 
292     /**
293      * @return the {@FilenameFilter filename filter} (if any) that is used to restrict which content can be
294      *         accessed by this connector; may be null
295      */
296     public FilenameFilter getFilenameFilter() {
297         return this.filenameFilter;
298     }
299 
300     /**
301      * Sets the filename filter that is used to restrict which content can be accessed by this connector
302      * <p>
303      * Only one of FilenameFilter or Inclusion/Exclusion Pattern are used at a given time. If Inclusion/exclusion are set, then
304      * FilenameFilter is ignored.
305      * </p>
306      * 
307      * @param filenameFilter the filename filter that is used to restrict which content can be accessed by this connector. If this
308      *        parameter is {@code null}, no files will be excluded.
309      */
310     public synchronized void setFilenameFilter( FilenameFilter filenameFilter ) {
311         this.filenameFilter = filenameFilter;
312     }
313 
314     /**
315      * Sets the filename filter that is used to restrict which content can be accessed by this connector by specifying the name of
316      * a class that implements the {@code FilenameFilter} interface and has a public, no-argument constructor.
317      * <p>
318      * Only one of the {@code exclusionPattern} and {@code filenameFilter} properties may be non-null at any one time. Calling
319      * this method automatically sets the {@code exclusionPattern} property to {@code null}.
320      * </p>
321      * 
322      * @param filenameFilterClassName the class name of the filter implementation or null if no filename filter should be used
323      * @throws ClassNotFoundException if the the class for the {@code FilenameFilter} implementation cannot be located
324      * @throws IllegalAccessException if the filename filter class or its nullary constructor is not accessible.
325      * @throws InstantiationException if the filename filter represents an abstract class, an interface, an array class, a
326      *         primitive type, or void; or if the class has no nullary constructor; or if the instantiation fails for some other
327      *         reason.
328      * @throws ClassCastException if the class named by {@code filenameFilterClassName} does not implement the {@code
329      *         FilenameFilter} interface
330      */
331     public synchronized void setFilenameFilter( String filenameFilterClassName )
332         throws ClassCastException, ClassNotFoundException, IllegalAccessException, InstantiationException {
333         if (filenameFilterClassName == null) {
334             this.filenameFilter = null;
335             return;
336         }
337 
338         Class<?> filenameFilterClass = Class.forName(filenameFilterClassName);
339 
340         this.filenameFilter = (FilenameFilter)filenameFilterClass.newInstance();
341     }
342 
343     FilenameFilter filenameFilter( boolean hideFilesForCustomProperties ) {
344         if (this.filenameFilter != null) return this.filenameFilter;
345         if (this.getInclusionPattern() != null || this.getExclusionPattern() != null) return this.inclusionExclusionFilenameFilter;
346 
347         // Otherwise, create one that take into account the exclusion pattern ...
348         FilenameFilter filenameFilter = null;
349         final String filterPattern = this.inclusionExclusionFilenameFilter.getExclusionPattern();
350         if (filterPattern != null) {
351             filenameFilter = new FilenameFilter() {
352                 Pattern filter = Pattern.compile(filterPattern);
353 
354                 public boolean accept( File dir,
355                                        String name ) {
356                     return !filter.matcher(name).matches();
357                 }
358             };
359         }
360 
361         if (hideFilesForCustomProperties) {
362             // And the properties factory ...
363             CustomPropertiesFactory customPropsFactory = customPropertiesFactory();
364             if (customPropsFactory instanceof BasePropertiesFactory) {
365                 filenameFilter = ((BasePropertiesFactory)customPropsFactory).getFilenameFilter(filenameFilter);
366             }
367         }
368 
369         // If there are no criteria that would allow us to build a filter, then accept any file.
370         if (filenameFilter == null) filenameFilter = ACCEPT_ALL_FILTER;
371 
372         return filenameFilter;
373     }
374 
375     /**
376      * Get the maximum path length (in characters) allowed by the underlying file system
377      * 
378      * @return the maximum path length (in characters) allowed by the underlying file system
379      */
380     public int getMaxPathLength() {
381         return maxPathLength;
382     }
383 
384     /**
385      * Set the maximum absolute path length supported by this source.
386      * <p>
387      * The length of any path is calculated relative to the file system root, NOT the repository root. That is, if a workspace
388      * {@code foo} is mapped to the {@code /tmp/foo/bar} directory on the file system, then the path {@code /node1/node2} in the
389      * {@code foo} workspace has an effective length of 23 for the purposes of the {@code maxPathLength} calculation ({@code
390      * /tmp/foo/bar} has length 11, {@code /node1/node2} has length 12, 11 + 12 = 23).
391      * </p>
392      * 
393      * @param maxPathLength the maximum absolute path length supported by this source; must be non-negative
394      */
395     public synchronized void setMaxPathLength( int maxPathLength ) {
396         CheckArg.isNonNegative(maxPathLength, "maxPathLength");
397         this.maxPathLength = maxPathLength;
398     }
399 
400     /**
401      * Get the name of the default workspace.
402      * 
403      * @return the name of the workspace that should be used by default; never null
404      */
405     public String getDefaultWorkspaceName() {
406         return defaultWorkspaceName;
407     }
408 
409     /**
410      * Set the name of the workspace that should be used when clients don't specify a workspace.
411      * 
412      * @param nameOfDefaultWorkspace the name of the workspace that should be used by default, or null if the
413      *        {@link #DEFAULT_NAME_OF_DEFAULT_WORKSPACE default name} should be used
414      */
415     public synchronized void setDefaultWorkspaceName( String nameOfDefaultWorkspace ) {
416         this.defaultWorkspaceName = nameOfDefaultWorkspace != null ? nameOfDefaultWorkspace : DEFAULT_NAME_OF_DEFAULT_WORKSPACE;
417     }
418 
419     /**
420      * Gets the names of the workspaces that are available when this source is created. Each workspace name corresponds to a path
421      * to a directory on the file system.
422      * 
423      * @return the names of the workspaces that this source starts with, or null if there are no such workspaces
424      * @see #setPredefinedWorkspaceNames(String[])
425      * @see #setCreatingWorkspacesAllowed(boolean)
426      */
427     public synchronized String[] getPredefinedWorkspaceNames() {
428         String[] copy = new String[predefinedWorkspaces.length];
429         System.arraycopy(predefinedWorkspaces, 0, copy, 0, predefinedWorkspaces.length);
430         return copy;
431     }
432 
433     /**
434      * Sets the names of the workspaces that are available when this source is created. Each workspace name corresponds to a path
435      * to a directory on the file system.
436      * 
437      * @param predefinedWorkspaceNames the names of the workspaces that this source should start with, or null if there are no
438      *        such workspaces
439      * @see #setCreatingWorkspacesAllowed(boolean)
440      * @see #getPredefinedWorkspaceNames()
441      */
442     public synchronized void setPredefinedWorkspaceNames( String[] predefinedWorkspaceNames ) {
443         this.predefinedWorkspaces = predefinedWorkspaceNames;
444     }
445 
446     /**
447      * Get whether this source allows workspaces to be created dynamically.
448      * 
449      * @return true if this source allows workspaces to be created by clients, or false if the set of workspaces is fixed
450      * @see #setPredefinedWorkspaceNames(String[])
451      * @see #getPredefinedWorkspaceNames()
452      * @see #setCreatingWorkspacesAllowed(boolean)
453      */
454     public boolean isCreatingWorkspacesAllowed() {
455         return capabilities.supportsCreatingWorkspaces();
456     }
457 
458     /**
459      * Set whether this source allows workspaces to be created dynamically.
460      * 
461      * @param allowWorkspaceCreation true if this source allows workspaces to be created by clients, or false if the set of
462      *        workspaces is fixed
463      * @see #setPredefinedWorkspaceNames(String[])
464      * @see #getPredefinedWorkspaceNames()
465      * @see #isCreatingWorkspacesAllowed()
466      */
467     public synchronized void setCreatingWorkspacesAllowed( boolean allowWorkspaceCreation ) {
468         capabilities = new RepositorySourceCapabilities(capabilities.supportsSameNameSiblings(), capabilities.supportsUpdates(),
469                                                         capabilities.supportsEvents(), allowWorkspaceCreation,
470                                                         capabilities.supportsReferences());
471     }
472 
473     /**
474      * Get whether this source allows updates.
475      * 
476      * @return true if this source allows updates by clients, or false if no updates are allowed
477      * @see #setUpdatesAllowed(boolean)
478      */
479     @Description( i18n = FileSystemI18n.class, value = "updatesAllowedPropertyDescription" )
480     @Label( i18n = FileSystemI18n.class, value = "updatesAllowedPropertyLabel" )
481     @Category( i18n = FileSystemI18n.class, value = "updatesAllowedPropertyCategory" )
482     @Override
483     public boolean areUpdatesAllowed() {
484         return capabilities.supportsUpdates();
485     }
486 
487     /**
488      * Set whether this source allows updates to data within workspaces
489      * 
490      * @param allowUpdates true if this source allows updates to data within workspaces clients, or false if updates are not
491      *        allowed.
492      * @see #areUpdatesAllowed()
493      */
494     public synchronized void setUpdatesAllowed( boolean allowUpdates ) {
495         capabilities = new RepositorySourceCapabilities(capabilities.supportsSameNameSiblings(), allowUpdates,
496                                                         capabilities.supportsEvents(), capabilities.supportsCreatingWorkspaces(),
497                                                         capabilities.supportsReferences());
498     }
499 
500     /**
501      * Get whether this source should use file content (and file name) to determine the MIME type.
502      * 
503      * @return true if the file content should be used to determine the MIME type, or false if only the filename should be used
504      */
505     public boolean isContentUsedToDetermineMimeType() {
506         return determineMimeTypeUsingContent;
507     }
508 
509     /**
510      * Set whether this source should use file content (and file name) to determine the MIME type.
511      * 
512      * @param contentUsedToDetermineMimeType true if the file content should be used to determine the MIME type
513      */
514     public void setContentUsedToDetermineMimeType( boolean contentUsedToDetermineMimeType ) {
515         determineMimeTypeUsingContent = contentUsedToDetermineMimeType;
516     }
517 
518     /**
519      * Get the desired behavior for handling extra properties on "nt:foldeR", "nt:file", and "nt:resource" nodes.
520      * 
521      * @return one of "log", "ignore", "error", or "store"
522      * @see #getCustomPropertiesFactory()
523      */
524     public String getExtraPropertiesBehavior() {
525         return extraProperties;
526     }
527 
528     /**
529      * Set the desired behavior for handling extra properties on "nt:foldeR", "nt:file", and "nt:resource" nodes.
530      * 
531      * @param behavior "log", "ignore", "error", or "store"
532      * @see #setCustomPropertiesFactory(CustomPropertiesFactory)
533      * @see #setCustomPropertiesFactory(String)
534      */
535     public void setExtraPropertiesBehavior( String behavior ) {
536         if (behavior != null) behavior = behavior.trim().toLowerCase();
537         if (EXTRA_PROPERTIES_CLASSNAME_BY_KEY.containsKey(behavior)) {
538             this.extraProperties = behavior;
539         } else {
540             this.extraProperties = DEFAULT_EXTRA_PROPERTIES;
541         }
542     }
543 
544     /**
545      * Get the factory that is used to create custom properties on "nt:folder", "nt:file", and "nt:resource" nodes.
546      * 
547      * @return the factory, or null if no custom properties are to be created
548      */
549     public synchronized CustomPropertiesFactory getCustomPropertiesFactory() {
550         return customPropertiesFactory;
551     }
552 
553     CustomPropertiesFactory customPropertiesFactory() {
554         if (customPropertiesFactory == null) {
555             customPropertiesFactory = EXTRA_PROPERTIES_CLASSNAME_BY_KEY.get(extraProperties);
556             if (customPropertiesFactory == null) {
557                 customPropertiesFactory = EXTRA_PROPERTIES_CLASSNAME_BY_KEY.get(DEFAULT_EXTRA_PROPERTIES);
558             }
559             assert customPropertiesFactory != null;
560         }
561         return customPropertiesFactory;
562     }
563 
564     /**
565      * Set the factory that is used to create custom properties on "nt:folder", "nt:file", and "nt:resource" nodes.
566      * 
567      * @param customPropertiesFactory the factory reference, or null if no custom properties will be created
568      * @see #setExtraPropertiesBehavior(String)
569      */
570     public synchronized void setCustomPropertiesFactory( CustomPropertiesFactory customPropertiesFactory ) {
571         this.customPropertiesFactory = customPropertiesFactory;
572     }
573 
574     /**
575      * Set the factory that is used to create custom properties on "nt:folder", "nt:file", and "nt:resource" nodes by specifying
576      * the name of a class that implements the {@code CustomPropertiesFactory} interface and has a public, no-argument
577      * constructor.
578      * 
579      * @param customPropertiesFactoryClassName the class name of the factory implementation or null if no custom properties will
580      *        be created
581      * @throws ClassNotFoundException if the the class for the {@code CustomPropertiesFactory} implementation cannot be located
582      * @throws IllegalAccessException if the custom properties factory class or its nullary constructor is not accessible.
583      * @throws InstantiationException if the custom properties factory represents an abstract class, an interface, an array class,
584      *         a primitive type, or void; or if the class has no nullary constructor; or if the instantiation fails for some other
585      *         reason.
586      * @throws ClassCastException if the class named by {@code customPropertiesFactoryClassName} does not implement the {@code
587      *         CustomPropertiesFactory} interface
588      * @see #setExtraPropertiesBehavior(String)
589      */
590     public synchronized void setCustomPropertiesFactory( String customPropertiesFactoryClassName )
591         throws ClassCastException, ClassNotFoundException, IllegalAccessException, InstantiationException {
592         if (customPropertiesFactoryClassName == null) {
593             this.customPropertiesFactory = null;
594             return;
595         }
596 
597         Class<?> customPropertiesFactoryClass = Class.forName(customPropertiesFactoryClassName);
598         this.customPropertiesFactory = (CustomPropertiesFactory)customPropertiesFactoryClass.newInstance();
599     }
600 
601     /**
602      * Optional flag that defines whether the connector should eagerly read file content even before it is needed, guaranteeing
603      * access to the content. A value of "true" may result in the file content being loaded even when it is not needed, and may
604      * increase the memory footprint; a value of "false" will delay reading the file content until it is needed, but changes to
605      * the underlying files may leak into the JCR sessions. The default value is "false".
606      * 
607      * @return 'true' if the file is to be read eagerly and preemptively, or false if the file content is to be loaded lazily.
608      */
609     public boolean isEagerFileLoading() {
610         return eagerFileLoading;
611     }
612 
613     /**
614      * @param eagerFileLoading Sets eagerFileLoading to the specified value.
615      */
616     public void setEagerFileLoading( boolean eagerFileLoading ) {
617         this.eagerFileLoading = eagerFileLoading;
618     }
619 
620     /**
621      * {@inheritDoc}
622      * 
623      * @see javax.naming.Referenceable#getReference()
624      */
625     public synchronized Reference getReference() {
626         String className = getClass().getName();
627         String factoryClassName = this.getClass().getName();
628         Reference ref = new Reference(className, factoryClassName, null);
629 
630         if (getName() != null) {
631             ref.add(new StringRefAddr(SOURCE_NAME, getName()));
632         }
633         ref.add(new StringRefAddr(DEFAULT_WORKSPACE, getDefaultWorkspaceName()));
634         ref.add(new StringRefAddr(ALLOW_CREATING_WORKSPACES, Boolean.toString(isCreatingWorkspacesAllowed())));
635         ref.add(new StringRefAddr(MAX_PATH_LENGTH, String.valueOf(maxPathLength)));
636         ref.add(new StringRefAddr(EXTRA_PROPERTIES, String.valueOf(extraProperties)));
637         String[] workspaceNames = getPredefinedWorkspaceNames();
638         if (workspaceNames != null && workspaceNames.length != 0) {
639             ref.add(new StringRefAddr(PREDEFINED_WORKSPACE_NAMES, StringUtil.combineLines(workspaceNames)));
640         }
641         if (getCustomPropertiesFactory() != null) {
642             ref.add(new StringRefAddr(CUSTOM_PROPERTY_FACTORY, getCustomPropertiesFactory().getClass().getName()));
643         }
644         if (this.inclusionExclusionFilenameFilter.getExclusionPattern() != null) {
645             ref.add(new StringRefAddr(EXCLUSION_PATTERN, this.inclusionExclusionFilenameFilter.getExclusionPattern()));
646         }
647         if (this.inclusionExclusionFilenameFilter.getInclusionPattern() != null) {
648             ref.add(new StringRefAddr(INCLUSION_PATTERN, this.inclusionExclusionFilenameFilter.getInclusionPattern()));
649         }
650         if (filenameFilter != null) {
651             ref.add(new StringRefAddr(FILENAME_FILTER, filenameFilter.getClass().getName()));
652         }
653         ref.add(new StringRefAddr(EAGER_FILE_LOADING, Boolean.toString(isEagerFileLoading())));
654         ref.add(new StringRefAddr(DETERMINE_MIME_TYPE_USING_CONTENT, Boolean.toString(isContentUsedToDetermineMimeType())));
655 
656         return ref;
657     }
658 
659     /**
660      * {@inheritDoc}
661      */
662     public Object getObjectInstance( Object obj,
663                                      javax.naming.Name name,
664                                      Context nameCtx,
665                                      Hashtable<?, ?> environment ) throws Exception {
666         if (obj instanceof Reference) {
667             Map<String, Object> values = valuesFrom((Reference)obj);
668 
669             String sourceName = (String)values.get(SOURCE_NAME);
670             String defaultWorkspace = (String)values.get(DEFAULT_WORKSPACE);
671             String createWorkspaces = (String)values.get(ALLOW_CREATING_WORKSPACES);
672             String exclusionPattern = (String)values.get(EXCLUSION_PATTERN);
673             String inclusionPattern = (String)values.get(INCLUSION_PATTERN);
674             String filenameFilterClassName = (String)values.get(FILENAME_FILTER);
675             String maxPathLength = (String)values.get(DEFAULT_MAX_PATH_LENGTH);
676             String customPropertiesFactoryClassName = (String)values.get(CUSTOM_PROPERTY_FACTORY);
677             String extraPropertiesBehavior = (String)values.get(EXTRA_PROPERTIES);
678             String eagerFileLoading = (String)values.get(EAGER_FILE_LOADING);
679             String useContentForMimeType = (String)values.get(DETERMINE_MIME_TYPE_USING_CONTENT);
680 
681             String combinedWorkspaceNames = (String)values.get(PREDEFINED_WORKSPACE_NAMES);
682             String[] workspaceNames = null;
683             if (combinedWorkspaceNames != null) {
684                 List<String> paths = StringUtil.splitLines(combinedWorkspaceNames);
685                 workspaceNames = paths.toArray(new String[paths.size()]);
686             }
687 
688             // Create the source instance ...
689             FileSystemSource source = new FileSystemSource();
690             if (sourceName != null) source.setName(sourceName);
691             if (defaultWorkspace != null) source.setDefaultWorkspaceName(defaultWorkspace);
692             if (createWorkspaces != null) source.setCreatingWorkspacesAllowed(Boolean.parseBoolean(createWorkspaces));
693             if (workspaceNames != null && workspaceNames.length != 0) source.setPredefinedWorkspaceNames(workspaceNames);
694             if (exclusionPattern != null) source.setExclusionPattern(exclusionPattern);
695             if (inclusionPattern != null) source.setInclusionPattern(inclusionPattern);
696             if (filenameFilterClassName != null) source.setFilenameFilter(filenameFilterClassName);
697             if (maxPathLength != null) source.setMaxPathLength(Integer.valueOf(maxPathLength));
698             if (extraPropertiesBehavior != null) source.setExtraPropertiesBehavior(extraPropertiesBehavior);
699             if (customPropertiesFactoryClassName != null) source.setCustomPropertiesFactory(customPropertiesFactoryClassName);
700             if (eagerFileLoading != null) source.setEagerFileLoading(Boolean.parseBoolean(eagerFileLoading));
701             if (useContentForMimeType != null) source.setContentUsedToDetermineMimeType(Boolean.parseBoolean(useContentForMimeType));
702             return source;
703         }
704         return null;
705     }
706 
707     /**
708      * {@inheritDoc}
709      * 
710      * @see org.modeshape.graph.connector.RepositorySource#getConnection()
711      */
712     public synchronized RepositoryConnection getConnection() throws RepositorySourceException {
713         String sourceName = getName();
714         if (sourceName == null || sourceName.trim().length() == 0) {
715             I18n msg = FileSystemI18n.propertyIsRequired;
716             throw new RepositorySourceException(getName(), msg.text("name"));
717         }
718 
719         if (repository == null) {
720             repository = new FileSystemRepository(this);
721 
722             ExecutionContext context = repositoryContext != null ? repositoryContext.getExecutionContext() : defaultContext;
723             FileSystemTransaction txn = repository.startTransaction(context, false);
724             try {
725                 // Create the set of initial workspaces ...
726                 for (String initialName : getPredefinedWorkspaceNames()) {
727                     repository.createWorkspace(txn, initialName, CreateConflictBehavior.DO_NOT_CREATE, null);
728                 }
729             } finally {
730                 txn.commit();
731             }
732 
733         }
734         return new Connection<PathNode, FileSystemWorkspace>(this, repository);
735     }
736 }