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 * Gets 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 * Gets 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 * Gets 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 * Gets 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 * Gets 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 * Gets 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 * Gets 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 * Gets 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 * Gets 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 * Gets 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 * Gets 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 * Gets 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 * Gets 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 * Gets 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 }