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}