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.InputStream;
020import java.io.OutputStream;
021import java.security.cert.Certificate;
022import java.util.HashSet;
023import java.util.Map;
024import java.util.Objects;
025import java.util.Set;
026import java.util.stream.Stream;
027
028import org.apache.commons.lang3.ArrayUtils;
029import org.apache.commons.vfs2.FileChangeEvent;
030import org.apache.commons.vfs2.FileContent;
031import org.apache.commons.vfs2.FileContentInfo;
032import org.apache.commons.vfs2.FileListener;
033import org.apache.commons.vfs2.FileName;
034import org.apache.commons.vfs2.FileNotFolderException;
035import org.apache.commons.vfs2.FileObject;
036import org.apache.commons.vfs2.FileSystemException;
037import org.apache.commons.vfs2.FileType;
038import org.apache.commons.vfs2.RandomAccessContent;
039import org.apache.commons.vfs2.util.RandomAccessMode;
040import org.apache.commons.vfs2.util.WeakRefFileListener;
041
042/**
043 * A file backed by another file.
044 * <p>
045 * TODO - Extract subclass that overlays the children.
046 * </p>
047 *
048 * @param <AFS> A subclass of AbstractFileSystem.
049 */
050public class DelegateFileObject<AFS extends AbstractFileSystem> extends AbstractFileObject<AFS> implements FileListener {
051
052    private FileObject fileObject;
053    private final Set<String> children = new HashSet<>();
054    private boolean ignoreEvent;
055
056    /**
057     * Constructs a new instance.
058     *
059     * @param fileName the file name.
060     * @param fileSystem the file system.
061     * @param fileObject My file object.
062     * @throws FileSystemException For subclasses to throw.
063     */
064    public DelegateFileObject(final AbstractFileName fileName, final AFS fileSystem, final FileObject fileObject) throws FileSystemException {
065        super(fileName, fileSystem);
066        this.fileObject = fileObject;
067        if (fileObject != null) {
068            WeakRefFileListener.installListener(fileObject, this);
069        }
070    }
071
072    /**
073     * Adds a child to this file.
074     *
075     * @param baseName The base FileName.
076     * @param type The FileType.
077     * @throws Exception if an error occurs.
078     */
079    public void attachChild(final FileName baseName, final FileType type) throws Exception {
080        final FileType oldType = doGetType();
081        if (children.add(baseName.getBaseName())) {
082            childrenChanged(baseName, type);
083        }
084        maybeTypeChanged(oldType);
085    }
086
087    /**
088     * Close the delegated file.
089     *
090     * @throws FileSystemException if an error occurs.
091     */
092    @Override
093    public void close() throws FileSystemException {
094        super.close();
095
096        if (fileObject != null) {
097            fileObject.close();
098        }
099    }
100
101    /**
102     * Creates this file as a folder.
103     */
104    @Override
105    protected void doCreateFolder() throws Exception {
106        ignoreEvent = true;
107        try {
108            fileObject.createFolder();
109        } finally {
110            ignoreEvent = false;
111        }
112    }
113
114    /**
115     * Deletes the file.
116     */
117    @Override
118    protected void doDelete() throws Exception {
119        ignoreEvent = true;
120        try {
121            fileObject.delete();
122        } finally {
123            ignoreEvent = false;
124        }
125    }
126
127    /**
128     * Returns the attributes of this file.
129     */
130    @Override
131    protected Map<String, Object> doGetAttributes() throws Exception {
132        return getFileContent().getAttributes();
133    }
134
135    /**
136     * Returns the certificates of this file.
137     */
138    @Override
139    protected Certificate[] doGetCertificates() throws Exception {
140        return getFileContent().getCertificates();
141    }
142
143    /**
144     * Gets file content info.
145     *
146     * @return the file content info of the delegee.
147     * @throws Exception Any thrown Exception is wrapped in FileSystemException.
148     * @since 2.0
149     */
150    protected FileContentInfo doGetContentInfo() throws Exception {
151        return getFileContent().getContentInfo();
152    }
153
154    /**
155     * Returns the size of the file content (in bytes). Is only called if {@link #doGetType} returns
156     * {@link FileType#FILE}.
157     */
158    @Override
159    protected long doGetContentSize() throws Exception {
160        return getFileContent().getSize();
161    }
162
163    /**
164     * Creates an input stream to read the file content from.
165     */
166    @Override
167    protected InputStream doGetInputStream(final int bufferSize) throws Exception {
168        return getFileContent().getInputStream(bufferSize);
169    }
170
171    /**
172     * Returns the last-modified time of this file.
173     */
174    @Override
175    protected long doGetLastModifiedTime() throws Exception {
176        return getFileContent().getLastModifiedTime();
177    }
178
179    /**
180     * Creates an output stream to write the file content to.
181     */
182    @Override
183    protected OutputStream doGetOutputStream(final boolean bAppend) throws Exception {
184        return getFileContent().getOutputStream(bAppend);
185    }
186
187    /**
188     * Creates access to the file for random I/O.
189     *
190     * @since 2.0
191     */
192    @Override
193    protected RandomAccessContent doGetRandomAccessContent(final RandomAccessMode mode) throws Exception {
194        return getFileContent().getRandomAccessContent(mode);
195    }
196
197    /**
198     * Determines the type of the file, returns null if the file does not exist.
199     */
200    @Override
201    protected FileType doGetType() throws FileSystemException {
202        if (fileObject != null) {
203            return fileObject.getType();
204        }
205        if (children.isEmpty()) {
206            return FileType.IMAGINARY;
207        }
208        return FileType.FOLDER;
209    }
210
211    /**
212     * Determines if this file is executable.
213     */
214    @Override
215    protected boolean doIsExecutable() throws FileSystemException {
216        if (fileObject != null) {
217            return fileObject.isExecutable();
218        }
219        return false;
220    }
221
222    /**
223     * Determines if this file is hidden.
224     */
225    @Override
226    protected boolean doIsHidden() throws FileSystemException {
227        if (fileObject != null) {
228            return fileObject.isHidden();
229        }
230        return false;
231    }
232
233    /**
234     * Determines if this file can be read.
235     */
236    @Override
237    protected boolean doIsReadable() throws FileSystemException {
238        if (fileObject != null) {
239            return fileObject.isReadable();
240        }
241        return true;
242    }
243
244    /**
245     * Determines if this file can be written to.
246     */
247    @Override
248    protected boolean doIsWriteable() throws FileSystemException {
249        if (fileObject != null) {
250            return fileObject.isWriteable();
251        }
252        return false;
253    }
254
255    /**
256     * Lists the children of the file.
257     */
258    @Override
259    protected String[] doListChildren() throws Exception {
260        if (fileObject != null) {
261            final FileObject[] children;
262
263            try {
264                children = fileObject.getChildren();
265            } catch (final FileNotFolderException e) {
266                // VFS-210
267                throw new FileNotFolderException(getName(), e);
268            }
269
270            return Stream.of(children).filter(Objects::nonNull).map(child -> child.getName().getBaseName()).toArray(String[]::new);
271        }
272        return children.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
273    }
274
275    /**
276     * Removes an attribute of this file.
277     *
278     * @since 2.0
279     */
280    @Override
281    protected void doRemoveAttribute(final String attrName) throws Exception {
282        getFileContent().removeAttribute(attrName);
283    }
284
285    /**
286     * Renames the file.
287     *
288     * @param newFile the new location/name.
289     * @throws Exception Any thrown Exception is wrapped in FileSystemException.
290     * @since 2.0
291     */
292    @Override
293    protected void doRename(final FileObject newFile) throws Exception {
294        fileObject.moveTo(((DelegateFileObject) newFile).fileObject);
295    }
296
297    /**
298     * Sets an attribute of this file.
299     */
300    @Override
301    protected void doSetAttribute(final String attrName, final Object value) throws Exception {
302        getFileContent().setAttribute(attrName, value);
303    }
304
305    /**
306     * Sets the last-modified time of this file.
307     *
308     * @since 2.0
309     */
310    @Override
311    protected boolean doSetLastModifiedTime(final long modtime) throws Exception {
312        getFileContent().setLastModifiedTime(modtime);
313        return true;
314    }
315
316    /**
317     * Called when a file is changed.
318     * <p>
319     * This will only happen if you monitor the file using {@link org.apache.commons.vfs2.FileMonitor}.
320     * </p>
321     *
322     * @param event The FileChangeEvent.
323     * @throws Exception if an error occurs.
324     */
325    @Override
326    public void fileChanged(final FileChangeEvent event) throws Exception {
327        if (event.getFileObject() != fileObject) {
328            return;
329        }
330        if (!ignoreEvent) {
331            handleChanged();
332        }
333    }
334
335    /**
336     * Called when a file is created.
337     *
338     * @param event The FileChangeEvent.
339     * @throws Exception if an error occurs.
340     */
341    @Override
342    public void fileCreated(final FileChangeEvent event) throws Exception {
343        if (event.getFileObject() != fileObject) {
344            return;
345        }
346        if (!ignoreEvent) {
347            handleCreate(fileObject.getType());
348        }
349    }
350
351    /**
352     * Called when a file is deleted.
353     *
354     * @param event The FileChangeEvent.
355     * @throws Exception if an error occurs.
356     */
357    @Override
358    public void fileDeleted(final FileChangeEvent event) throws Exception {
359        if (event.getFileObject() != fileObject) {
360            return;
361        }
362        if (!ignoreEvent) {
363            handleDelete();
364        }
365    }
366
367    /**
368     * Gets access to the delegated file.
369     *
370     * @return The FileObject.
371     * @since 2.0
372     */
373    public FileObject getDelegateFile() {
374        return fileObject;
375    }
376
377    FileContent getFileContent() throws FileSystemException {
378        return fileObject.getContent();
379    }
380
381    /**
382     * Checks whether the file's type has changed, and fires the appropriate events.
383     *
384     * @param oldType The old FileType.
385     * @throws Exception if an error occurs.
386     */
387    private void maybeTypeChanged(final FileType oldType) throws Exception {
388        final FileType newType = doGetType();
389        if (oldType == FileType.IMAGINARY && newType != FileType.IMAGINARY) {
390            handleCreate(newType);
391        } else if (oldType != FileType.IMAGINARY && newType == FileType.IMAGINARY) {
392            handleDelete();
393        }
394    }
395
396    /**
397     * Refresh file information.
398     *
399     * @throws FileSystemException if an error occurs.
400     * @since 2.0
401     */
402    @Override
403    public void refresh() throws FileSystemException {
404        super.refresh();
405        if (fileObject != null) {
406            fileObject.refresh();
407        }
408    }
409
410    /**
411     * Attaches or detaches the target file.
412     *
413     * @param fileObject The FileObject.
414     * @throws Exception if an error occurs.
415     */
416    public void setFile(final FileObject fileObject) throws Exception {
417        final FileType oldType = doGetType();
418        if (fileObject != null) {
419            WeakRefFileListener.installListener(fileObject, this);
420        }
421        this.fileObject = fileObject;
422        maybeTypeChanged(oldType);
423    }
424}