001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.vfs2.provider;
018
019import java.io.File;
020import java.lang.reflect.InvocationTargetException;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.Map;
026import java.util.Objects;
027import java.util.concurrent.atomic.AtomicInteger;
028import java.util.concurrent.atomic.AtomicLong;
029
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032import org.apache.commons.vfs2.CacheStrategy;
033import org.apache.commons.vfs2.Capability;
034import org.apache.commons.vfs2.FileListener;
035import org.apache.commons.vfs2.FileName;
036import org.apache.commons.vfs2.FileObject;
037import org.apache.commons.vfs2.FileSelector;
038import org.apache.commons.vfs2.FileSystem;
039import org.apache.commons.vfs2.FileSystemConfigBuilder;
040import org.apache.commons.vfs2.FileSystemException;
041import org.apache.commons.vfs2.FileSystemManager;
042import org.apache.commons.vfs2.FileSystemOptions;
043import org.apache.commons.vfs2.FilesCache;
044import org.apache.commons.vfs2.VfsLog;
045import org.apache.commons.vfs2.cache.OnCallRefreshFileObject;
046import org.apache.commons.vfs2.events.AbstractFileChangeEvent;
047import org.apache.commons.vfs2.events.ChangedEvent;
048import org.apache.commons.vfs2.events.CreateEvent;
049import org.apache.commons.vfs2.events.DeleteEvent;
050import org.apache.commons.vfs2.impl.DefaultFileSystemConfigBuilder;
051import org.apache.commons.vfs2.util.FileObjectUtils;
052import org.apache.commons.vfs2.util.Messages;
053
054/**
055 * A partial {@link org.apache.commons.vfs2.FileSystem} implementation.
056 */
057public abstract class AbstractFileSystem extends AbstractVfsComponent implements FileSystem {
058
059    private static final FileListener[] EMPTY_FILE_LISTENER_ARRAY = {};
060
061    private static final Log LOG = LogFactory.getLog(AbstractFileSystem.class);
062
063    /**
064     * The "root" of the file system. This is always "/" so it isn't always the "real" root.
065     */
066    private final FileName rootName;
067
068    /**
069     * The root URI of the file system. The base path specified as a file system option when the file system was
070     * created.
071     */
072    private final String rootURI;
073
074    private final Collection<Capability> capabilities = new HashSet<>();
075
076    private final FileObject parentLayer;
077
078    /**
079     * Map from FileName to an ArrayList of listeners for that file.
080     */
081    private final Map<FileName, ArrayList<FileListener>> listenerMap = new HashMap<>();
082
083    /**
084     * FileSystemOptions used for configuration
085     */
086    private final FileSystemOptions fileSystemOptions;
087
088    /**
089     * How many fileObjects are handed out
090     */
091    private final AtomicLong useCount = new AtomicLong();
092
093    private FileSystemKey cacheKey;
094
095    /**
096     * open streams counter for this file system
097     */
098    private final AtomicInteger openStreams = new AtomicInteger();
099
100    /**
101     * Only provided for Serializable subclasses.
102     */
103    AbstractFileSystem() {
104        this(null, null, null);
105    }
106
107    /**
108     * Constructs a new instance.
109     *
110     * @param rootName The root file name of this file system.
111     * @param parentLayer The parent layer of this file system.
112     * @param fileSystemOptions Options to build this file system.
113     */
114    protected AbstractFileSystem(final FileName rootName, final FileObject parentLayer, final FileSystemOptions fileSystemOptions) {
115        this.parentLayer = parentLayer;
116        this.rootName = rootName;
117        this.fileSystemOptions = fileSystemOptions;
118        final FileSystemConfigBuilder builder = DefaultFileSystemConfigBuilder.getInstance();
119        String uri = builder.getRootURI(fileSystemOptions);
120        if (uri == null) {
121            uri = rootName != null ? rootName.getURI() : null;
122        }
123        this.rootURI = uri;
124    }
125
126    /**
127     * Adds the capabilities of this file system.
128     *
129     * @param caps collections of Capabilities, can be immutable.
130     */
131    protected abstract void addCapabilities(Collection<Capability> caps);
132
133    /**
134     * Adds a junction to this file system.
135     *
136     * @param junctionPoint The junction point.
137     * @param targetFile The target to add.
138     * @throws FileSystemException if an error occurs.
139     */
140    @Override
141    public void addJunction(final String junctionPoint, final FileObject targetFile) throws FileSystemException {
142        throw new FileSystemException("vfs.provider/junctions-not-supported.error", rootName);
143    }
144
145    /**
146     * Adds a listener on a file in this file system.
147     *
148     * @param file The FileObject to be monitored.
149     * @param listener The FileListener
150     */
151    @Override
152    public void addListener(final FileObject file, final FileListener listener) {
153        synchronized (listenerMap) {
154            final ArrayList<FileListener> listeners = listenerMap.computeIfAbsent(file.getName(), k -> new ArrayList<>());
155            listeners.add(listener);
156        }
157    }
158
159    /**
160     * Closes this component.
161     */
162    @Override
163    public void close() {
164        closeCommunicationLink();
165    }
166
167    /**
168     * Closes the underlying link used to access the files.
169     */
170    public void closeCommunicationLink() {
171        synchronized (this) {
172            doCloseCommunicationLink();
173        }
174    }
175
176    /**
177     * Creates a file object.
178     * <p>
179     * This method is called only if the requested file is not cached.
180     * </p>
181     *
182     * @param name name referencing the new file.
183     * @return new created FileObject.
184     * @throws Exception might throw an Exception, which is then wrapped in FileSystemException.
185     */
186    protected abstract FileObject createFile(AbstractFileName name) throws Exception;
187
188    /**
189     * Decorates the given file object.
190     *
191     * @param file the file object.
192     * @return the decorated file object.
193     * @throws FileSystemException if a file system error occurs.
194     */
195    protected FileObject decorateFileObject(FileObject file) throws FileSystemException {
196        if (getFileSystemManager().getCacheStrategy().equals(CacheStrategy.ON_CALL)) {
197            file = new OnCallRefreshFileObject(file);
198        }
199
200        if (getFileSystemManager().getFileObjectDecoratorConst() != null) {
201            try {
202                file = (FileObject) getFileSystemManager().getFileObjectDecoratorConst().newInstance(file);
203            } catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) {
204                throw new FileSystemException("vfs.impl/invalid-decorator.error",
205                        getFileSystemManager().getFileObjectDecorator().getName(), e);
206            }
207        }
208
209        return file;
210    }
211
212    /**
213     * Closes the underlying link used to access the files.
214     */
215    protected void doCloseCommunicationLink() {
216        // default is noop.
217    }
218
219    /**
220     * Creates a temporary local copy of a file and its descendants.
221     *
222     * @param file the start of the tree.
223     * @param selector selection what to do with children.
224     * @return replicated root file.
225     * @throws Exception any Exception is wrapped as FileSystemException.
226     */
227    protected File doReplicateFile(final FileObject file, final FileSelector selector) throws Exception {
228        return getContext().getReplicator().replicateFile(file, selector);
229    }
230
231    void fileObjectDestroyed(final FileObject fileObject) {
232        useCount.decrementAndGet();
233    }
234
235    void fileObjectHanded(final FileObject fileObject) {
236        useCount.incrementAndGet();
237    }
238
239    /**
240     * Fires an event.
241     */
242    private void fireEvent(final AbstractFileChangeEvent event) {
243        FileListener[] fileListeners = null;
244        final FileObject fileObject = event.getFileObject();
245
246        synchronized (listenerMap) {
247            final ArrayList<?> listeners = listenerMap.get(fileObject.getName());
248            if (listeners != null) {
249                fileListeners = listeners.toArray(EMPTY_FILE_LISTENER_ARRAY);
250            }
251        }
252
253        if (fileListeners != null) {
254            for (final FileListener fileListener : fileListeners) {
255                try {
256                    event.notify(fileListener);
257                } catch (final Exception e) {
258                    final String message = Messages.getString("vfs.provider/notify-listener.warn", fileObject);
259                    // getLogger().warn(message, e);
260                    VfsLog.warn(getLogger(), LOG, message, e);
261                }
262            }
263        }
264    }
265
266    /**
267     * Fires a file changed event.
268     * <p>
269     * This will only happen if you monitor the file using {@link org.apache.commons.vfs2.FileMonitor}.
270     * </p>
271     *
272     * @param file The FileObject that changed.
273     */
274    public void fireFileChanged(final FileObject file) {
275        fireEvent(new ChangedEvent(file));
276    }
277
278    /**
279     * Fires a file create event.
280     *
281     * @param file The FileObject that was created.
282     */
283    public void fireFileCreated(final FileObject file) {
284        fireEvent(new CreateEvent(file));
285    }
286
287    /**
288     * Fires a file delete event.
289     *
290     * @param file The FileObject that was deleted.
291     */
292    public void fireFileDeleted(final FileObject file) {
293        fireEvent(new DeleteEvent(file));
294    }
295
296    void freeResources() {
297        // default is noop.
298    }
299
300    /**
301     * Gets the attribute with the specified name. The default implementation simply throws an exception.
302     *
303     * @param attrName The name of the attribute.
304     * @return the Object associated with the attribute or null if no object is.
305     * @throws FileSystemException if an error occurs.
306     */
307    @Override
308    public Object getAttribute(final String attrName) throws FileSystemException {
309        throw new FileSystemException("vfs.provider/get-attribute-not-supported.error");
310    }
311
312    FileSystemKey getCacheKey() {
313        return cacheKey;
314    }
315
316    /**
317     * Gets a cached file.
318     *
319     * @param name name to search for.
320     * @return file object or null if not found.
321     */
322    protected FileObject getFileFromCache(final FileName name) {
323        return getFilesCache().getFile(this, name);
324    }
325
326    private FilesCache getFilesCache() {
327        final FilesCache filesCache = getContext().getFileSystemManager().getFilesCache();
328        return Objects.requireNonNull(filesCache, () -> Messages.getString("vfs.provider/files-cache-missing.error"));
329    }
330
331    /**
332     * Gets the FileSystemManager used to instantiate this file system.
333     *
334     * @return the FileSystemManager.
335     */
336    @Override
337    public FileSystemManager getFileSystemManager() {
338        return getContext().getFileSystemManager();
339    }
340
341    /**
342     * Gets the FileSystemOptions used to instantiate this file system.
343     *
344     * @return the FileSystemOptions.
345     */
346    @Override
347    public FileSystemOptions getFileSystemOptions() {
348        return fileSystemOptions;
349    }
350
351    /**
352     * Gets the accuracy of the last modification time.
353     *
354     * @return milliseconds, 0 means perfectly accurate, {@code > 0} might be off by this value, for examnple, sftp is 1000 milliseconds.
355     */
356    @Override
357    public double getLastModTimeAccuracy() {
358        return 0;
359    }
360
361    /**
362     * Gets the parent layer if this is a layered file system.
363     *
364     * @return The FileObject for the parent layer.
365     * @throws FileSystemException if an error occurs.
366     */
367    @Override
368    public FileObject getParentLayer() throws FileSystemException {
369        return parentLayer;
370    }
371
372    /**
373     * Gets the root file of this file system.
374     *
375     * @return The root FileObject of the FileSystem
376     * @throws FileSystemException if an error occurs.
377     */
378    @Override
379    public FileObject getRoot() throws FileSystemException {
380        return resolveFile(rootName);
381    }
382
383    /**
384     * Gets the name of the root of this file system.
385     *
386     * @return the root FileName.
387     */
388    @Override
389    public FileName getRootName() {
390        return rootName;
391    }
392
393    /**
394     * Gets the root URI specified for this file System.
395     *
396     * @return The root URI used in this file system.
397     * @since 2.0
398     */
399    @Override
400    public String getRootURI() {
401        return rootURI;
402    }
403
404    /**
405     * Tests whether this file system has a particular capability.
406     *
407     * @param capability the Capability to check for.
408     * @return true if the FileSystem has the Capability, false otherwise.
409     */
410    @Override
411    public boolean hasCapability(final Capability capability) {
412        return capabilities.contains(capability);
413    }
414
415    /**
416     * Initializes this component.
417     *
418     * @throws FileSystemException if an error occurs.
419     */
420    @Override
421    public void init() throws FileSystemException {
422        addCapabilities(capabilities);
423    }
424
425    /**
426     * Tests whether this file system has open streams.
427     *
428     * @return true if the FileSystem has open streams.
429     */
430    public boolean isOpen() {
431        return openStreams.get() > 0;
432    }
433
434    /**
435     * Tests whether any files are using this FileSystem.
436     *
437     * @return whether any files are using this FileSystem.
438     */
439    public boolean isReleaseable() {
440        return useCount.get() < 1;
441    }
442
443    /**
444     * Called after all file-objects closed their streams.
445     */
446    protected void notifyAllStreamsClosed() {
447        // default is noop.
448    }
449
450    /**
451     * Adds a file object to the cache.
452     *
453     * @param file the file to add.
454     */
455    protected void putFileToCache(final FileObject file) {
456        getFilesCache().putFile(file);
457    }
458
459    /**
460     * Removes a cached file.
461     *
462     * @param name The file name to remove.
463     */
464    protected void removeFileFromCache(final FileName name) {
465        getFilesCache().removeFile(this, name);
466    }
467
468    /**
469     * Removes a junction from this file system.
470     *
471     * @param junctionPoint The junction point.
472     * @throws FileSystemException if an error occurs
473     */
474    @Override
475    public void removeJunction(final String junctionPoint) throws FileSystemException {
476        throw new FileSystemException("vfs.provider/junctions-not-supported.error", rootName);
477    }
478
479    /**
480     * Removes a listener from a file in this file system.
481     *
482     * @param file The FileObject to be monitored.
483     * @param listener The FileListener
484     */
485    @Override
486    public void removeListener(final FileObject file, final FileListener listener) {
487        synchronized (listenerMap) {
488            final ArrayList<?> listeners = listenerMap.get(file.getName());
489            if (listeners != null) {
490                listeners.remove(listener);
491                if (listeners.isEmpty()) {
492                    listenerMap.remove(file.getName());
493                }
494            }
495        }
496    }
497
498    /**
499     * Creates a temporary local copy of a file and its descendants.
500     *
501     * @param file The FileObject to replicate.
502     * @param selector The FileSelector.
503     * @return The replicated File.
504     * @throws FileSystemException if an error occurs.
505     */
506    @Override
507    public File replicateFile(final FileObject file, final FileSelector selector) throws FileSystemException {
508        if (!FileObjectUtils.exists(file)) {
509            throw new FileSystemException("vfs.provider/replicate-missing-file.error", file.getName());
510        }
511
512        try {
513            return doReplicateFile(file, selector);
514        } catch (final Exception e) {
515            throw new FileSystemException("vfs.provider/replicate-file.error", file.getName(), e);
516        }
517    }
518
519    /**
520     * Finds a file in this file system.
521     *
522     * @param name The name of the file to locate.
523     * @return The located FileObject or null if none could be located.
524     * @throws FileSystemException if an error occurs.
525     */
526    @Override
527    public FileObject resolveFile(final FileName name) throws FileSystemException {
528        return resolveFile(name, true);
529    }
530
531    private synchronized FileObject resolveFile(final FileName name, final boolean useCache)
532            throws FileSystemException {
533        if (!rootName.getRootURI().equals(name.getRootURI())) {
534            throw new FileSystemException("vfs.provider/mismatched-fs-for-name.error", name, rootName,
535                    name.getRootURI());
536        }
537
538        // imario@apache.org ==> use getFileFromCache
539        FileObject file;
540        if (useCache) {
541            file = getFileFromCache(name);
542        } else {
543            file = null;
544        }
545
546        if (file == null) {
547            try {
548                file = createFile((AbstractFileName) name);
549            } catch (final Exception e) {
550                throw new FileSystemException("vfs.provider/resolve-file.error", name, e);
551            }
552
553            file = decorateFileObject(file);
554
555            // imario@apache.org ==> use putFileToCache
556            if (useCache) {
557                putFileToCache(file);
558            }
559        }
560
561        /*
562          resync the file information if requested
563         */
564        if (getFileSystemManager().getCacheStrategy().equals(CacheStrategy.ON_RESOLVE)) {
565            file.refresh();
566        }
567        return file;
568    }
569
570    /**
571     * Finds a file in this file system.
572     *
573     * @param nameStr The name of the file to resolve.
574     * @return The located FileObject or null if none could be located.
575     * @throws FileSystemException if an error occurs.
576     */
577    @Override
578    public FileObject resolveFile(final String nameStr) throws FileSystemException {
579        // Resolve the name, and create the file
580        return resolveFile(getFileSystemManager().resolveName(rootName, nameStr));
581    }
582
583    /**
584     * Sets the attribute with the specified name. The default implementation simply throws an exception.
585     *
586     * @param attrName the attribute name.
587     * @param value The object to associate with the attribute.
588     * @throws FileSystemException if an error occurs.
589     */
590    @Override
591    public void setAttribute(final String attrName, final Object value) throws FileSystemException {
592        throw new FileSystemException("vfs.provider/set-attribute-not-supported.error");
593    }
594
595    void setCacheKey(final FileSystemKey cacheKey) {
596        this.cacheKey = cacheKey;
597    }
598
599    void streamClosed() {
600        if (openStreams.decrementAndGet() == 0) {
601            notifyAllStreamsClosed();
602        }
603    }
604
605    void streamOpened() {
606        openStreams.incrementAndGet();
607    }
608}