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 org.apache.commons.vfs2.FileName;
020import org.apache.commons.vfs2.FileSystemException;
021import org.apache.commons.vfs2.FileType;
022import org.apache.commons.vfs2.NameScope;
023import org.apache.commons.vfs2.VFS;
024
025/**
026 * A default file name implementation.
027 */
028public abstract class AbstractFileName implements FileName {
029
030    private final String scheme;
031    private final String absPath;
032    private FileType type;
033
034    // Cached stuff
035    private String uri;
036    private String baseName;
037    private String rootUri;
038    private String extension;
039    private String decodedAbsPath;
040
041    private String key = null;
042
043    public AbstractFileName(final String scheme, final String absPath, final FileType type) {
044        this.rootUri = null;
045        this.scheme = scheme;
046        this.type = type;
047        if (absPath != null && absPath.length() > 0) {
048            if (absPath.length() > 1 && absPath.endsWith("/")) {
049                this.absPath = absPath.substring(0, absPath.length() - 1);
050            } else {
051                this.absPath = absPath;
052            }
053        } else {
054            this.absPath = ROOT_PATH;
055        }
056    }
057
058    @Override
059    public boolean equals(final Object o) {
060        if (this == o) {
061            return true;
062        }
063        if (o == null || getClass() != o.getClass()) {
064            return false;
065        }
066
067        final AbstractFileName that = (AbstractFileName) o;
068
069        return getKey().equals(that.getKey());
070    }
071
072    @Override
073    public int hashCode() {
074        return getKey().hashCode();
075    }
076
077    /**
078     * Implement Comparable.
079     *
080     * @param obj another abstract filename
081     * @return negative number if less than, 0 if equal, positive if greater than.
082     */
083    @Override
084    public int compareTo(final FileName obj) {
085        final AbstractFileName name = (AbstractFileName) obj;
086        return getKey().compareTo(name.getKey());
087    }
088
089    /**
090     * Returns the URI of the file.
091     *
092     * @return the FileName as a URI.
093     */
094    @Override
095    public String toString() {
096        return getURI();
097    }
098
099    /**
100     * Factory method for creating name instances.
101     *
102     * @param absolutePath The absolute path.
103     * @param fileType The FileType.
104     * @return The FileName.
105     */
106    public abstract FileName createName(String absolutePath, FileType fileType);
107
108    /**
109     * Builds the root URI for this file name. Note that the root URI must not end with a separator character.
110     *
111     * @param buffer A StringBuilder to use to construct the URI.
112     * @param addPassword true if the password should be added, false otherwise.
113     */
114    protected abstract void appendRootUri(StringBuilder buffer, boolean addPassword);
115
116    /**
117     * Returns the base name of the file.
118     *
119     * @return The base name of the file.
120     */
121    @Override
122    public String getBaseName() {
123        if (baseName == null) {
124            final int idx = getPath().lastIndexOf(SEPARATOR_CHAR);
125            if (idx == -1) {
126                baseName = getPath();
127            } else {
128                baseName = getPath().substring(idx + 1);
129            }
130        }
131
132        return baseName;
133    }
134
135    /**
136     * Returns the absolute path of the file, relative to the root of the file system that the file belongs to.
137     *
138     * @return The path String.
139     */
140    @Override
141    public String getPath() {
142        if (VFS.isUriStyle()) {
143            return absPath + getUriTrailer();
144        }
145        return absPath;
146    }
147
148    protected String getUriTrailer() {
149        return getType().hasChildren() ? "/" : "";
150    }
151
152    /**
153     * Returns the decoded path.
154     *
155     * @return The decoded path String.
156     * @throws FileSystemException If an error occurs.
157     */
158    @Override
159    public String getPathDecoded() throws FileSystemException {
160        if (decodedAbsPath == null) {
161            decodedAbsPath = UriParser.decode(getPath());
162        }
163
164        return decodedAbsPath;
165    }
166
167    /**
168     * Returns the name of the parent of the file.
169     *
170     * @return the FileName of the parent.
171     */
172    @Override
173    public FileName getParent() {
174        final String parentPath;
175        final int idx = getPath().lastIndexOf(SEPARATOR_CHAR);
176        if (idx == -1 || idx == getPath().length() - 1) {
177            // No parent
178            return null;
179        } else if (idx == 0) {
180            // Root is the parent
181            parentPath = SEPARATOR;
182        } else {
183            parentPath = getPath().substring(0, idx);
184        }
185        return createName(parentPath, FileType.FOLDER);
186    }
187
188    /**
189     * find the root of the filesystem.
190     *
191     * @return The root FileName.
192     */
193    @Override
194    public FileName getRoot() {
195        FileName root = this;
196        while (root.getParent() != null) {
197            root = root.getParent();
198        }
199
200        return root;
201    }
202
203    /**
204     * Returns the URI scheme of this file.
205     *
206     * @return The protocol used to access the file.
207     */
208    @Override
209    public String getScheme() {
210        return scheme;
211    }
212
213    /**
214     * Returns the absolute URI of the file.
215     *
216     * @return The absolute URI of the file.
217     */
218    @Override
219    public String getURI() {
220        if (uri == null) {
221            uri = createURI();
222        }
223        return uri;
224    }
225
226    protected String createURI() {
227        return createURI(false, true);
228    }
229
230    /**
231     * Create a path that does not use the FileType since that field is not immutable.
232     *
233     * @return The key.
234     */
235    private String getKey() {
236        if (key == null) {
237            key = getURI();
238        }
239        return key;
240    }
241
242    /**
243     * Returns the URI without a password.
244     *
245     * @return Returns the URI without a password.
246     */
247    @Override
248    public String getFriendlyURI() {
249        return createURI(false, false);
250    }
251
252    private String createURI(final boolean useAbsolutePath, final boolean usePassword) {
253        final StringBuilder buffer = new StringBuilder();
254        appendRootUri(buffer, usePassword);
255        buffer.append(useAbsolutePath ? absPath : getPath());
256        return buffer.toString();
257    }
258
259    /**
260     * Converts a file name to a relative name, relative to this file name.
261     *
262     * @param name The FileName.
263     * @return The relative path to the file.
264     * @throws FileSystemException if an error occurs.
265     */
266    @Override
267    public String getRelativeName(final FileName name) throws FileSystemException {
268        final String path = name.getPath();
269
270        // Calculate the common prefix
271        final int basePathLen = getPath().length();
272        final int pathLen = path.length();
273
274        // Deal with root
275        if (basePathLen == 1 && pathLen == 1) {
276            return ".";
277        } else if (basePathLen == 1) {
278            return path.substring(1);
279        }
280
281        final int maxlen = Math.min(basePathLen, pathLen);
282        int pos = 0;
283        for (; pos < maxlen && getPath().charAt(pos) == path.charAt(pos); pos++) {
284        }
285
286        if (pos == basePathLen && pos == pathLen) {
287            // Same names
288            return ".";
289        } else if (pos == basePathLen && pos < pathLen && path.charAt(pos) == SEPARATOR_CHAR) {
290            // A descendent of the base path
291            return path.substring(pos + 1);
292        }
293
294        // Strip the common prefix off the path
295        final StringBuilder buffer = new StringBuilder();
296        if (pathLen > 1 && (pos < pathLen || getPath().charAt(pos) != SEPARATOR_CHAR)) {
297            // Not a direct ancestor, need to back up
298            pos = getPath().lastIndexOf(SEPARATOR_CHAR, pos);
299            buffer.append(path.substring(pos));
300        }
301
302        // Prepend a '../' for each element in the base path past the common
303        // prefix
304        buffer.insert(0, "..");
305        pos = getPath().indexOf(SEPARATOR_CHAR, pos + 1);
306        while (pos != -1) {
307            buffer.insert(0, "../");
308            pos = getPath().indexOf(SEPARATOR_CHAR, pos + 1);
309        }
310
311        return buffer.toString();
312    }
313
314    /**
315     * Returns the root URI of the file system this file belongs to.
316     *
317     * @return The URI of the root.
318     */
319    @Override
320    public String getRootURI() {
321        if (rootUri == null) {
322            final StringBuilder buffer = new StringBuilder();
323            appendRootUri(buffer, true);
324            buffer.append(SEPARATOR_CHAR);
325            rootUri = buffer.toString().intern();
326        }
327        return rootUri;
328    }
329
330    /**
331     * Returns the depth of this file name, within its file system.
332     *
333     * @return The depth of the file name.
334     */
335    @Override
336    public int getDepth() {
337        final int len = getPath().length();
338        if (len == 0 || len == 1 && getPath().charAt(0) == SEPARATOR_CHAR) {
339            return 0;
340        }
341        int depth = 1;
342        for (int pos = 0; pos > -1 && pos < len; depth++) {
343            pos = getPath().indexOf(SEPARATOR_CHAR, pos + 1);
344        }
345        return depth;
346    }
347
348    /**
349     * Returns the extension of this file name.
350     *
351     * @return The file extension.
352     */
353    @Override
354    public String getExtension() {
355        if (extension == null) {
356            getBaseName();
357            final int pos = baseName.lastIndexOf('.');
358            // if ((pos == -1) || (pos == baseName.length() - 1))
359            // imario@ops.co.at: Review of patch from adagoubard@chello.nl
360            // do not treat filenames like
361            // .bashrc c:\windows\.java c:\windows\.javaws c:\windows\.jedit c:\windows\.appletviewer
362            // as extension
363            if (pos < 1 || pos == baseName.length() - 1) {
364                // No extension
365                extension = "";
366            } else {
367                extension = baseName.substring(pos + 1).intern();
368            }
369        }
370        return extension;
371    }
372
373    /**
374     * Determines if another file name is an ancestor of this file name.
375     *
376     * @param ancestor The FileName to check.
377     * @return true if the FileName is an ancestor, false otherwise.
378     */
379    @Override
380    public boolean isAncestor(final FileName ancestor) {
381        if (!ancestor.getRootURI().equals(getRootURI())) {
382            return false;
383        }
384        return checkName(ancestor.getPath(), getPath(), NameScope.DESCENDENT);
385    }
386
387    /**
388     * Determines if another file name is a descendent of this file name.
389     *
390     * @param descendent The FileName to check.
391     * @return true if the FileName is a descendent, false otherwise.
392     */
393    @Override
394    public boolean isDescendent(final FileName descendent) {
395        return isDescendent(descendent, NameScope.DESCENDENT);
396    }
397
398    /**
399     * Determines if another file name is a descendent of this file name.
400     *
401     * @param descendent The FileName to check.
402     * @param scope The NameScope.
403     * @return true if the FileName is a descendent, false otherwise.
404     */
405    @Override
406    public boolean isDescendent(final FileName descendent, final NameScope scope) {
407        if (!descendent.getRootURI().equals(getRootURI())) {
408            return false;
409        }
410        return checkName(getPath(), descendent.getPath(), scope);
411    }
412
413    /**
414     * Checks if this file name is a name for a regular file by using its type.
415     *
416     * @return true if this file is a regular file.
417     * @throws FileSystemException may be thrown by subclasses.
418     * @see #getType()
419     * @see FileType#FILE
420     */
421    @Override
422    public boolean isFile() throws FileSystemException {
423        // Use equals instead of == to avoid any class loader worries.
424        return FileType.FILE.equals(this.getType());
425    }
426
427    /**
428     * Returns the requested or current type of this name.
429     * <p>
430     * The "requested" type is the one determined during resolving the name. n this case the name is a
431     * {@link FileType#FOLDER} if it ends with an "/" else it will be a {@link FileType#FILE}.
432     * <p>
433     * Once attached it will be changed to reflect the real type of this resource.
434     *
435     * @return {@link FileType#FOLDER} or {@link FileType#FILE}
436     */
437    @Override
438    public FileType getType() {
439        return type;
440    }
441
442    /**
443     * Sets the type of this file e.g. when it will be attached.
444     *
445     * @param type {@link FileType#FOLDER} or {@link FileType#FILE}
446     * @throws FileSystemException if an error occurs.
447     */
448    void setType(final FileType type) throws FileSystemException {
449        if (type != FileType.FOLDER && type != FileType.FILE && type != FileType.FILE_OR_FOLDER) {
450            throw new FileSystemException("vfs.provider/filename-type.error");
451        }
452
453        this.type = type;
454    }
455
456    /**
457     * Checks whether a path fits in a particular scope of another path.
458     *
459     * @param basePath An absolute, normalised path.
460     * @param path An absolute, normalised path.
461     * @param scope The NameScope.
462     * @return true if the path fits in the scope, false otherwise.
463     */
464    public static boolean checkName(final String basePath, final String path, final NameScope scope) {
465        if (scope == NameScope.FILE_SYSTEM) {
466            // All good
467            return true;
468        }
469
470        if (!path.startsWith(basePath)) {
471            return false;
472        }
473
474        int baseLen = basePath.length();
475        if (VFS.isUriStyle()) {
476            // strip the trailing "/"
477            baseLen--;
478        }
479
480        if (scope == NameScope.CHILD) {
481            if (path.length() == baseLen || baseLen > 1 && path.charAt(baseLen) != SEPARATOR_CHAR
482                    || path.indexOf(SEPARATOR_CHAR, baseLen + 1) != -1) {
483                return false;
484            }
485        } else if (scope == NameScope.DESCENDENT) {
486            if (path.length() == baseLen || baseLen > 1 && path.charAt(baseLen) != SEPARATOR_CHAR) {
487                return false;
488            }
489        } else if (scope == NameScope.DESCENDENT_OR_SELF) {
490            if (baseLen > 1 && path.length() > baseLen && path.charAt(baseLen) != SEPARATOR_CHAR) {
491                return false;
492            }
493        } else if (scope != NameScope.FILE_SYSTEM) {
494            throw new IllegalArgumentException();
495        }
496
497        return true;
498    }
499}