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