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