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      // https://datatracker.ietf.org/doc/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      /**
46       * Checks whether a path fits in a particular scope of another path.
47       *
48       * @param basePath An absolute, normalized path.
49       * @param path An absolute, normalized path.
50       * @param scope The NameScope.
51       * @return true if the path fits in the scope, false otherwise.
52       */
53      public static boolean checkName(final String basePath, final String path, final NameScope scope) {
54          if (scope == NameScope.FILE_SYSTEM) {
55              // All good
56              return true;
57          }
58          if (!path.startsWith(basePath)) {
59              return false;
60          }
61          int baseLen = basePath.length();
62          if (VFS.isUriStyle()) {
63              // strip the trailing "/"
64              baseLen--;
65          }
66          if (scope != null) {
67              switch (scope) {
68              case CHILD:
69                  return path.length() != baseLen && (baseLen <= 1 || path.charAt(baseLen) == SEPARATOR_CHAR) && path.indexOf(SEPARATOR_CHAR, baseLen + 1) == -1;
70              case DESCENDENT:
71                  return path.length() != baseLen && (baseLen <= 1 || path.charAt(baseLen) == SEPARATOR_CHAR);
72              case DESCENDENT_OR_SELF:
73                  return baseLen <= 1 || path.length() <= baseLen || path.charAt(baseLen) == SEPARATOR_CHAR;
74              default:
75                  break;
76              }
77          }
78          throw new IllegalArgumentException();
79      }
80      private final String scheme;
81      private final String absolutePath;
82  
83      private FileType type;
84      // Cached attributes
85      private String uriString;
86      private String baseName;
87      private String rootUri;
88      private String extension;
89  
90      private String decodedAbsPath;
91  
92      private String key;
93  
94      /**
95       * Constructs a new instance for subclasses.
96       *
97       * @param scheme The scheme.
98       * @param absolutePath the absolute path, maybe empty or null.
99       * @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 }