View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs2.provider;
18  
19  import org.apache.commons.lang3.StringUtils;
20  import org.apache.commons.vfs2.FileName;
21  import org.apache.commons.vfs2.FileSystemException;
22  import org.apache.commons.vfs2.FileType;
23  import org.apache.commons.vfs2.NameScope;
24  import org.apache.commons.vfs2.VFS;
25  
26  /**
27   * A default file name implementation.
28   */
29  public abstract class AbstractFileName implements FileName {
30  
31      // URI Characters that are possible in local file names, but must be escaped
32      // for proper URI handling.
33      //
34      // How reserved URI chars were selected:
35      //
36      // URIs can contain :, /, ?, #, @
37      // See https://docs.oracle.com/javase/8/docs/api/java/net/URI.html
38      // http://tools.ietf.org/html/rfc3986#section-2.2
39      //
40      // Since : and / occur before the path, only chars after path are escaped (i.e., # and ?)
41      // ? is a reserved filesystem character for Windows and Unix, so can't be part of a file name.
42      // Therefore only # is a reserved char in a URI as part of the path that can be in the file name.
43      private static final char[] RESERVED_URI_CHARS = { '#', ' ' };
44  
45      private final String scheme;
46      private final String absPath;
47      private FileType type;
48  
49      // Cached attributes
50      private String uriString;
51      private String baseName;
52      private String rootUri;
53      private String extension;
54      private String decodedAbsPath;
55  
56      private String key;
57  
58      public AbstractFileName(final String scheme, final String absPath, final FileType type) {
59          this.rootUri = null;
60          this.scheme = scheme;
61          this.type = type;
62          if (StringUtils.isEmpty(absPath)) {
63              this.absPath = ROOT_PATH;
64          } else {
65              if (absPath.length() > 1 && absPath.endsWith("/")) {
66                  this.absPath = absPath.substring(0, absPath.length() - 1);
67              } else {
68                  this.absPath = absPath;
69              }
70          }
71      }
72  
73      @Override
74      public boolean equals(final Object o) {
75          if (this == o) {
76              return true;
77          }
78          if (o == null || getClass() != o.getClass()) {
79              return false;
80          }
81  
82          final AbstractFileName/../../org/apache/commons/vfs2/provider/AbstractFileName.html#AbstractFileName">AbstractFileName that = (AbstractFileName) o;
83  
84          return getKey().equals(that.getKey());
85      }
86  
87      @Override
88      public int hashCode() {
89          return getKey().hashCode();
90      }
91  
92      /**
93       * Implement Comparable.
94       *
95       * @param obj another abstract file name
96       * @return negative number if less than, 0 if equal, positive if greater than.
97       */
98      @Override
99      public int compareTo(final FileName obj) {
100         final AbstractFileName/../../org/apache/commons/vfs2/provider/AbstractFileName.html#AbstractFileName">AbstractFileName name = (AbstractFileName) obj;
101         return getKey().compareTo(name.getKey());
102     }
103 
104     /**
105      * Returns the URI of the file.
106      *
107      * @return the FileName as a URI.
108      */
109     @Override
110     public String toString() {
111         return getURI();
112     }
113 
114     /**
115      * Factory method for creating name instances.
116      *
117      * @param absolutePath The absolute path.
118      * @param fileType The FileType.
119      * @return The FileName.
120      */
121     public abstract FileName createName(String absolutePath, FileType fileType);
122 
123     /**
124      * Builds the root URI for this file name. Note that the root URI must not end with a separator character.
125      *
126      * @param buffer A StringBuilder to use to construct the URI.
127      * @param addPassword true if the password should be added, false otherwise.
128      */
129     protected abstract void appendRootUri(StringBuilder buffer, boolean addPassword);
130 
131     /**
132      * Returns the base name of the file.
133      *
134      * @return The base name of the file.
135      */
136     @Override
137     public String getBaseName() {
138         if (baseName == null) {
139             final int idx = getPath().lastIndexOf(SEPARATOR_CHAR);
140             if (idx == -1) {
141                 baseName = getPath();
142             } else {
143                 baseName = getPath().substring(idx + 1);
144             }
145         }
146 
147         return baseName;
148     }
149 
150     /**
151      * Returns the absolute path of the file, relative to the root of the file system that the file belongs to.
152      *
153      * @return The path String.
154      */
155     @Override
156     public String getPath() {
157         if (VFS.isUriStyle()) {
158             return absPath + getUriTrailer();
159         }
160         return absPath;
161     }
162 
163     protected String getUriTrailer() {
164         return getType().hasChildren() ? "/" : "";
165     }
166 
167     /**
168      * Returns the decoded path.
169      *
170      * @return The decoded path String.
171      * @throws FileSystemException If an error occurs.
172      */
173     @Override
174     public String getPathDecoded() throws FileSystemException {
175         if (decodedAbsPath == null) {
176             decodedAbsPath = UriParser.decode(getPath());
177         }
178 
179         return decodedAbsPath;
180     }
181 
182     /**
183      * Returns the name of the parent of the file.
184      *
185      * @return the FileName of the parent.
186      */
187     @Override
188     public FileName getParent() {
189         final String parentPath;
190         final int idx = getPath().lastIndexOf(SEPARATOR_CHAR);
191         if (idx == -1 || idx == getPath().length() - 1) {
192             // No parent
193             return null;
194         }
195         if (idx == 0) {
196             // Root is the parent
197             parentPath = SEPARATOR;
198         } else {
199             parentPath = getPath().substring(0, idx);
200         }
201         return createName(parentPath, FileType.FOLDER);
202     }
203 
204     /**
205      * find the root of the file system.
206      *
207      * @return The root FileName.
208      */
209     @Override
210     public FileName getRoot() {
211         FileName root = this;
212         while (root.getParent() != null) {
213             root = root.getParent();
214         }
215 
216         return root;
217     }
218 
219     /**
220      * Returns the URI scheme of this file.
221      *
222      * @return The protocol used to access the file.
223      */
224     @Override
225     public String getScheme() {
226         return scheme;
227     }
228 
229     /**
230      * Returns the absolute URI of the file.
231      *
232      * @return The absolute URI of the file.
233      */
234     @Override
235     public String getURI() {
236         if (uriString == null) {
237             uriString = createURI();
238         }
239         return uriString;
240     }
241 
242     protected String createURI() {
243         return createURI(false, true);
244     }
245 
246     /**
247      * Create a path that does not use the FileType since that field is not immutable.
248      *
249      * @return The key.
250      */
251     private String getKey() {
252         if (key == null) {
253             key = getURI();
254         }
255         return key;
256     }
257 
258     /**
259      * Returns the URI without a password.
260      *
261      * @return Returns the URI without a password.
262      */
263     @Override
264     public String getFriendlyURI() {
265         return createURI(false, false);
266     }
267 
268     private String createURI(final boolean useAbsolutePath, final boolean usePassword) {
269         final StringBuilder buffer = new StringBuilder();
270         appendRootUri(buffer, usePassword);
271         buffer.append(handleURISpecialCharacters(useAbsolutePath ? absPath : getPath()));
272         return buffer.toString();
273     }
274 
275     private String handleURISpecialCharacters(String uri) {
276         if (!StringUtils.isEmpty(uri)) {
277             try {
278                 // VFS-325: Handle URI special characters in file name
279                 // Decode the base URI and re-encode with URI special characters
280                 uri = UriParser.decode(uri);
281 
282                 return UriParser.encode(uri, RESERVED_URI_CHARS);
283             } catch (final FileSystemException e) {
284                 // Default to base URI value
285             }
286         }
287 
288         return uri;
289     }
290 
291     /**
292      * Converts a file name to a relative name, relative to this file name.
293      *
294      * @param name The FileName.
295      * @return The relative path to the file.
296      * @throws FileSystemException if an error occurs.
297      */
298     @Override
299     public String getRelativeName(final FileName name) throws FileSystemException {
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             return ".";
309         }
310         if (basePathLen == 1) {
311             return path.substring(1);
312         }
313 
314         final int maxlen = Math.min(basePathLen, pathLen);
315         int pos = 0;
316         for (; pos < maxlen && getPath().charAt(pos) == path.charAt(pos); pos++) {
317             // empty
318         }
319 
320         if (pos == basePathLen && pos == pathLen) {
321             // Same names
322             return ".";
323         }
324         if (pos == basePathLen && pos < pathLen && path.charAt(pos) == SEPARATOR_CHAR) {
325             // A descendent of the base path
326             return path.substring(pos + 1);
327         }
328 
329         // Strip the common prefix off the path
330         final StringBuilder buffer = new StringBuilder();
331         if (pathLen > 1 && (pos < pathLen || getPath().charAt(pos) != SEPARATOR_CHAR)) {
332             // Not a direct ancestor, need to back up
333             pos = getPath().lastIndexOf(SEPARATOR_CHAR, pos);
334             buffer.append(path.substring(pos));
335         }
336 
337         // Prepend a '../' for each element in the base path past the common
338         // prefix
339         buffer.insert(0, "..");
340         pos = getPath().indexOf(SEPARATOR_CHAR, pos + 1);
341         while (pos != -1) {
342             buffer.insert(0, "../");
343             pos = getPath().indexOf(SEPARATOR_CHAR, pos + 1);
344         }
345 
346         return buffer.toString();
347     }
348 
349     /**
350      * Returns the root URI of the file system this file belongs to.
351      *
352      * @return The URI of the root.
353      */
354     @Override
355     public String getRootURI() {
356         if (rootUri == null) {
357             final StringBuilder buffer = new StringBuilder();
358             appendRootUri(buffer, true);
359             buffer.append(SEPARATOR_CHAR);
360             rootUri = buffer.toString().intern();
361         }
362         return rootUri;
363     }
364 
365     /**
366      * Returns the depth of this file name, within its file system.
367      *
368      * @return The depth of the file name.
369      */
370     @Override
371     public int getDepth() {
372         final int len = getPath().length();
373         if (len == 0 || len == 1 && getPath().charAt(0) == SEPARATOR_CHAR) {
374             return 0;
375         }
376         int depth = 1;
377         for (int pos = 0; pos > -1 && pos < len; depth++) {
378             pos = getPath().indexOf(SEPARATOR_CHAR, pos + 1);
379         }
380         return depth;
381     }
382 
383     /**
384      * Returns the extension of this file name.
385      *
386      * @return The file extension.
387      */
388     @Override
389     public String getExtension() {
390         if (extension == null) {
391             getBaseName();
392             final int pos = baseName.lastIndexOf('.');
393             // if ((pos == -1) || (pos == baseName.length() - 1))
394             // imario@ops.co.at: Review of patch from adagoubard@chello.nl
395             // do not treat file names like
396             // .bashrc c:\windows\.java c:\windows\.javaws c:\windows\.jedit c:\windows\.appletviewer
397             // as extension
398             if (pos < 1 || pos == baseName.length() - 1) {
399                 // No extension
400                 extension = "";
401             } else {
402                 extension = baseName.substring(pos + 1).intern();
403             }
404         }
405         return extension;
406     }
407 
408     /**
409      * Determines if another file name is an ancestor of this file name.
410      *
411      * @param ancestor The FileName to check.
412      * @return true if the FileName is an ancestor, false otherwise.
413      */
414     @Override
415     public boolean isAncestor(final FileName ancestor) {
416         if (!ancestor.getRootURI().equals(getRootURI())) {
417             return false;
418         }
419         return checkName(ancestor.getPath(), getPath(), NameScope.DESCENDENT);
420     }
421 
422     /**
423      * Determines if another file name is a descendent of this file name.
424      *
425      * @param descendent The FileName to check.
426      * @return true if the FileName is a descendent, false otherwise.
427      */
428     @Override
429     public boolean isDescendent(final FileName descendent) {
430         return isDescendent(descendent, NameScope.DESCENDENT);
431     }
432 
433     /**
434      * Determines if another file name is a descendent of this file name.
435      *
436      * @param descendent The FileName to check.
437      * @param scope The NameScope.
438      * @return true if the FileName is a descendent, false otherwise.
439      */
440     @Override
441     public boolean isDescendent(final FileName descendent, final NameScope scope) {
442         if (!descendent.getRootURI().equals(getRootURI())) {
443             return false;
444         }
445         return checkName(getPath(), descendent.getPath(), scope);
446     }
447 
448     /**
449      * Checks if this file name is a name for a regular file by using its type.
450      *
451      * @return true if this file is a regular file.
452      * @throws FileSystemException may be thrown by subclasses.
453      * @see #getType()
454      * @see FileType#FILE
455      */
456     @Override
457     public boolean isFile() throws FileSystemException {
458         // Use equals instead of == to avoid any class loader worries.
459         return FileType.FILE.equals(this.getType());
460     }
461 
462     /**
463      * Returns the requested or current type of this name.
464      * <p>
465      * The "requested" type is the one determined during resolving the name. n this case the name is a
466      * {@link FileType#FOLDER} if it ends with an "/" else it will be a {@link FileType#FILE}.
467      * </p>
468      * <p>
469      * Once attached it will be changed to reflect the real type of this resource.
470      * </p>
471      *
472      * @return {@link FileType#FOLDER} or {@link FileType#FILE}
473      */
474     @Override
475     public FileType getType() {
476         return type;
477     }
478 
479     /**
480      * Sets the type of this file e.g. when it will be attached.
481      *
482      * @param type {@link FileType#FOLDER} or {@link FileType#FILE}
483      * @throws FileSystemException if an error occurs.
484      */
485     void setType(final FileType type) throws FileSystemException {
486         if (type != FileType.FOLDER && type != FileType.FILE && type != FileType.FILE_OR_FOLDER) {
487             throw new FileSystemException("vfs.provider/filename-type.error");
488         }
489 
490         this.type = type;
491     }
492 
493     /**
494      * Checks whether a path fits in a particular scope of another path.
495      *
496      * @param basePath An absolute, normalised path.
497      * @param path An absolute, normalised path.
498      * @param scope The NameScope.
499      * @return true if the path fits in the scope, false otherwise.
500      */
501     public static boolean checkName(final String basePath, final String path, final NameScope scope) {
502         if (scope == NameScope.FILE_SYSTEM) {
503             // All good
504             return true;
505         }
506 
507         if (!path.startsWith(basePath)) {
508             return false;
509         }
510 
511         int baseLen = basePath.length();
512         if (VFS.isUriStyle()) {
513             // strip the trailing "/"
514             baseLen--;
515         }
516 
517         if (scope == NameScope.CHILD) {
518             return path.length() != baseLen && (baseLen <= 1 || path.charAt(baseLen) == SEPARATOR_CHAR)
519                     && path.indexOf(SEPARATOR_CHAR, baseLen + 1) == -1;
520         }
521         if (scope == NameScope.DESCENDENT) {
522             return path.length() != baseLen && (baseLen <= 1 || path.charAt(baseLen) == SEPARATOR_CHAR);
523         }
524         if (scope == NameScope.DESCENDENT_OR_SELF) {
525             return baseLen <= 1 || path.length() <= baseLen || path.charAt(baseLen) == SEPARATOR_CHAR;
526         }
527         throw new IllegalArgumentException();
528 
529     }
530 }