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