001package jmri.util; 002 003import static jmri.util.FileUtil.HOME; 004import static jmri.util.FileUtil.PREFERENCES; 005import static jmri.util.FileUtil.PROFILE; 006import static jmri.util.FileUtil.PROGRAM; 007import static jmri.util.FileUtil.SCRIPTS; 008import static jmri.util.FileUtil.SEPARATOR; 009import static jmri.util.FileUtil.SETTINGS; 010 011import com.sun.jna.platform.win32.KnownFolders; 012import com.sun.jna.platform.win32.Shell32Util; 013import com.sun.jna.platform.win32.ShlObj; 014import java.io.BufferedReader; 015import java.io.File; 016import java.io.FileNotFoundException; 017import java.io.FileOutputStream; 018import java.io.IOException; 019import java.io.InputStream; 020import java.io.InputStreamReader; 021import java.io.OutputStreamWriter; 022import java.io.PrintWriter; 023import java.net.MalformedURLException; 024import java.net.URI; 025import java.net.URISyntaxException; 026import java.net.URL; 027import java.nio.charset.StandardCharsets; 028import java.nio.file.FileVisitResult; 029import java.nio.file.Files; 030import java.nio.file.Path; 031import java.nio.file.SimpleFileVisitor; 032import java.nio.file.StandardCopyOption; 033import java.nio.file.attribute.BasicFileAttributes; 034import java.security.CodeSource; 035import java.util.Arrays; 036import java.util.HashMap; 037import java.util.HashSet; 038import java.util.Objects; 039import java.util.Set; 040import java.util.jar.JarFile; 041import java.util.regex.Matcher; 042import java.util.stream.Collectors; 043import javax.annotation.CheckReturnValue; 044import javax.annotation.CheckForNull; 045import javax.annotation.Nonnull; 046 047import jmri.Version; 048import jmri.beans.Bean; 049import jmri.profile.Profile; 050import jmri.profile.ProfileManager; 051import jmri.util.FileUtil.Location; 052import jmri.util.FileUtil.Property; 053import org.slf4j.Logger; 054import org.slf4j.LoggerFactory; 055 056/** 057 * Support the {@link jmri.util.FileUtil} static API while providing 058 * {@link java.beans.PropertyChangeSupport} for listening to changes in the 059 * paths. Also provides the underlying implementation of all FileUtil methods so 060 * they can be exposed to scripts as an object methods instead of as static 061 * methods of a class. 062 * 063 * @author Randall Wood (C) 2015, 2016, 2019, 2020 064 */ 065public class FileUtilSupport extends Bean { 066 067 /* User's home directory */ 068 private static final String HOME_PATH = System.getProperty("user.home") + File.separator; // NOI18N 069 // 070 // Settable directories 071 // 072 /* JMRI program path, defaults to directory JMRI is executed from */ 073 private String programPath = null; 074 /* path to jmri.jar */ 075 private String jarPath = null; 076 /* path to the jython scripts directory */ 077 private final HashMap<Profile, String> scriptsPaths = new HashMap<>(); 078 /* path to the user's files directory */ 079 private final HashMap<Profile, String> userFilesPaths = new HashMap<>(); 080 /* path to profiles in use */ 081 private final HashMap<Profile, String> profilePaths = new HashMap<>(); 082 083 // initialize logging 084 private static final Logger log = LoggerFactory.getLogger(FileUtilSupport.class); 085 // default instance 086 volatile private static FileUtilSupport defaultInstance = null; 087 088 private FileUtilSupport() { 089 super(false); 090 } 091 092 /** 093 * Get the {@link java.io.File} that path refers to. Throws a 094 * {@link java.io.FileNotFoundException} if the file cannot be found instead 095 * of returning null (as File would). Use {@link #getURI(java.lang.String) } 096 * or {@link #getURL(java.lang.String) } instead of this method if possible. 097 * 098 * @param path the path to find 099 * @return {@link java.io.File} at path 100 * @throws java.io.FileNotFoundException if path cannot be found 101 * @see #getURI(java.lang.String) 102 * @see #getURL(java.lang.String) 103 */ 104 @Nonnull 105 @CheckReturnValue 106 public File getFile(@Nonnull String path) throws FileNotFoundException { 107 return getFile(ProfileManager.getDefault().getActiveProfile(), path); 108 } 109 110 /** 111 * Get the {@link java.io.File} that path refers to. Throws a 112 * {@link java.io.FileNotFoundException} if the file cannot be found instead 113 * of returning null (as File would). Use {@link #getURI(java.lang.String) } 114 * or {@link #getURL(java.lang.String) } instead of this method if possible. 115 * 116 * @param profile the profile to use as a base 117 * @param path the path to find 118 * @return {@link java.io.File} at path 119 * @throws java.io.FileNotFoundException if path cannot be found 120 * @see #getURI(java.lang.String) 121 * @see #getURL(java.lang.String) 122 */ 123 @Nonnull 124 @CheckReturnValue 125 public File getFile(@CheckForNull Profile profile, @Nonnull String path) throws FileNotFoundException { 126 try { 127 return new File(this.pathFromPortablePath(profile, path)); 128 } catch (NullPointerException ex) { 129 throw new FileNotFoundException("Cannot find file at " + path); 130 } 131 } 132 133 /** 134 * Get the {@link java.io.File} that path refers to. Throws a 135 * {@link java.io.FileNotFoundException} if the file cannot be found instead 136 * of returning null (as File would). 137 * 138 * @param path the path to find 139 * @return {@link java.io.File} at path 140 * @throws java.io.FileNotFoundException if path cannot be found 141 * @see #getFile(java.lang.String) 142 * @see #getURL(java.lang.String) 143 */ 144 @Nonnull 145 @CheckReturnValue 146 public URI getURI(@Nonnull String path) throws FileNotFoundException { 147 return this.getFile(path).toURI(); 148 } 149 150 /** 151 * Get the {@link java.net.URL} that path refers to. Throws a 152 * {@link java.io.FileNotFoundException} if the URL cannot be found instead 153 * of returning null. 154 * 155 * @param path the path to find 156 * @return {@link java.net.URL} at path 157 * @throws java.io.FileNotFoundException if path cannot be found 158 * @see #getFile(java.lang.String) 159 * @see #getURI(java.lang.String) 160 */ 161 @Nonnull 162 @CheckReturnValue 163 public URL getURL(@Nonnull String path) throws FileNotFoundException { 164 try { 165 return this.getURI(path).toURL(); 166 } catch (MalformedURLException ex) { 167 throw new FileNotFoundException("Cannot create URL for file at " + path); 168 } 169 } 170 171 /** 172 * Convenience method to get the {@link java.net.URL} from a 173 * {@link java.net.URI}. Logs errors and returns null if any exceptions are 174 * thrown by the conversion. 175 * 176 * @param uri The URI to convert. 177 * @return URL or null if any errors exist. 178 */ 179 @CheckForNull 180 @CheckReturnValue 181 public URL getURL(@Nonnull URI uri) { 182 try { 183 return uri.toURL(); 184 } catch (MalformedURLException | IllegalArgumentException ex) { 185 log.warn("Unable to get URL from {}", uri); 186 return null; 187 } catch (NullPointerException ex) { 188 log.warn("Unable to get URL from null object.", ex); 189 return null; 190 } 191 } 192 193 /** 194 * Find all files matching the given name under the given root directory 195 * within both the user and installed file locations. 196 * 197 * @param name the name of the file to find 198 * @param root the relative path to a directory in either or both of the 199 * user or installed file locations; use a single period 200 * character to refer to the root of the user or installed file 201 * locations 202 * @return a set of found files or an empty set if no matching files were 203 * found 204 * @throws IllegalArgumentException if the name is not a relative path, is 205 * empty, or contains path separators; or 206 * if the root is not a relative path, is 207 * empty, or contains a parent directory 208 * (..) 209 * @throws NullPointerException if any parameter is null 210 */ 211 @Nonnull 212 @CheckReturnValue 213 public Set<File> findFiles(@Nonnull String name, @Nonnull String root) throws IllegalArgumentException { 214 return this.findFiles(name, root, Location.ALL); 215 } 216 217 /** 218 * Find all files matching the given name under the given root directory 219 * within the specified location. 220 * 221 * @param name the name of the file to find 222 * @param root the relative path to a directory in either or both of the 223 * user or installed file locations; use a single period 224 * character to refer to the root of the location 225 * @param location the location to search within 226 * @return a set of found files or an empty set if no matching files were 227 * found 228 * @throws IllegalArgumentException if the name is not a relative path, is 229 * empty, or contains path separators; if 230 * the root is not a relative path, is 231 * empty, or contains a parent directory 232 * (..); or if the location is 233 * {@link Location#NONE} 234 * @throws NullPointerException if any parameter is null 235 */ 236 @Nonnull 237 @CheckReturnValue 238 public Set<File> findFiles(@Nonnull String name, @Nonnull String root, @Nonnull Location location) { 239 Objects.requireNonNull(name, "name must be nonnull"); 240 Objects.requireNonNull(root, "root must be nonnull"); 241 Objects.requireNonNull(location, "location must be nonnull"); 242 if (location == Location.NONE) { 243 throw new IllegalArgumentException("location must not be NONE"); 244 } 245 if (root.isEmpty() || root.contains("..") || root.startsWith("/")) { 246 throw new IllegalArgumentException("root is invalid"); 247 } 248 if (name.isEmpty() || name.contains(File.pathSeparator) || name.contains("/")) { 249 throw new IllegalArgumentException("name is invalid"); 250 } 251 Set<File> files = new HashSet<>(); 252 if (location == Location.INSTALLED || location == Location.ALL) { 253 files.addAll(this.findFiles(name, new File(this.findURI(PROGRAM + root, Location.NONE)))); 254 } 255 if (location == Location.USER || location == Location.ALL) { 256 try { 257 files.addAll(this.findFiles(name, new File(this.findURI(PREFERENCES + root, Location.NONE)))); 258 } catch (NullPointerException ex) { 259 // expected if path PREFERENCES + root does not exist 260 log.trace("{} does not exist in {}", root, PREFERENCES); 261 } 262 try { 263 files.addAll(this.findFiles(name, new File(this.findURI(PROFILE + root, Location.NONE)))); 264 } catch (NullPointerException ex) { 265 // expected if path PROFILE + root does not exist 266 log.trace("{} does not exist in {}", root, PROFILE); 267 } 268 try { 269 files.addAll(this.findFiles(name, new File(this.findURI(SETTINGS + root, Location.NONE)))); 270 } catch (NullPointerException ex) { 271 // expected if path SETTINGS + root does not exist 272 log.trace("{} does not exist in {}", root, SETTINGS); 273 } 274 } 275 return files; 276 } 277 278 private Set<File> findFiles(String name, File root) { 279 Set<File> files = new HashSet<>(); 280 if (root.isDirectory()) { 281 try { 282 Files.walkFileTree(root.toPath(), new SimpleFileVisitor<Path>() { 283 @Override 284 public FileVisitResult preVisitDirectory(final Path dir, 285 final BasicFileAttributes attrs) throws IOException { 286 287 Path fn = dir.getFileName(); 288 if (fn != null && name.equals(fn.toString())) { 289 files.add(dir.toFile().getCanonicalFile()); 290 } 291 return FileVisitResult.CONTINUE; 292 } 293 294 @Override 295 public FileVisitResult visitFile(final Path file, 296 final BasicFileAttributes attrs) throws IOException { 297 // TODO: accept glob patterns 298 Path fn = file.getFileName(); 299 if (fn != null && name.equals(fn.toString())) { 300 files.add(file.toFile().getCanonicalFile()); 301 } 302 return FileVisitResult.CONTINUE; 303 } 304 }); 305 } catch (IOException ex) { 306 log.warn("Exception while finding file {} in {}", name, root, ex); 307 } 308 } 309 return files; 310 } 311 312 /** 313 * Get the resource file corresponding to a name. There are five cases: 314 * <ul> 315 * <li>Starts with "resource:", treat the rest as a pathname relative to the 316 * program directory (deprecated; see "program:" below)</li> 317 * <li>Starts with "program:", treat the rest as a relative pathname below 318 * the program directory</li> 319 * <li>Starts with "preference:", treat the rest as a relative path below 320 * the user's files directory</li> 321 * <li>Starts with "settings:", treat the rest as a relative path below the 322 * JMRI system preferences directory</li> 323 * <li>Starts with "home:", treat the rest as a relative path below the 324 * user.home directory</li> 325 * <li>Starts with "file:", treat the rest as a relative path below the 326 * resource directory in the preferences directory (deprecated; see 327 * "preference:" above)</li> 328 * <li>Starts with "profile:", treat the rest as a relative path below the 329 * profile directory as specified in the 330 * active{@link jmri.profile.Profile}</li> 331 * <li>Starts with "scripts:", treat the rest as a relative path below the 332 * scripts directory</li> 333 * <li>Otherwise, treat the name as a relative path below the program 334 * directory</li> 335 * </ul> 336 * In any case, absolute pathnames will work. Uses the Profile returned by 337 * {@link ProfileManager#getActiveProfile()} as the base. 338 * 339 * @param pName the name, possibly starting with file:, home:, profile:, 340 * program:, preference:, scripts:, settings, or resource: 341 * @return Absolute file name to use. This will include system-specific file 342 * separators. 343 * @since 2.7.2 344 */ 345 @Nonnull 346 @CheckReturnValue 347 public String getExternalFilename(@Nonnull String pName) { 348 return getExternalFilename(ProfileManager.getDefault().getActiveProfile(), pName); 349 } 350 351 /** 352 * Get the resource file corresponding to a name. There are five cases: 353 * <ul> 354 * <li>Starts with "resource:", treat the rest as a pathname relative to the 355 * program directory (deprecated; see "program:" below)</li> 356 * <li>Starts with "program:", treat the rest as a relative pathname below 357 * the program directory</li> 358 * <li>Starts with "preference:", treat the rest as a relative path below 359 * the user's files directory</li> 360 * <li>Starts with "settings:", treat the rest as a relative path below the 361 * JMRI system preferences directory</li> 362 * <li>Starts with "home:", treat the rest as a relative path below the 363 * user.home directory</li> 364 * <li>Starts with "file:", treat the rest as a relative path below the 365 * resource directory in the preferences directory (deprecated; see 366 * "preference:" above)</li> 367 * <li>Starts with "profile:", treat the rest as a relative path below the 368 * profile directory as specified in the 369 * active{@link jmri.profile.Profile}</li> 370 * <li>Starts with "scripts:", treat the rest as a relative path below the 371 * scripts directory</li> 372 * <li>Otherwise, treat the name as a relative path below the program 373 * directory</li> 374 * </ul> 375 * In any case, absolute pathnames will work. 376 * 377 * @param profile the Profile to use as a base 378 * @param pName the name, possibly starting with file:, home:, profile:, 379 * program:, preference:, scripts:, settings, or resource: 380 * @return Absolute file name to use. This will include system-specific file 381 * separators. 382 * @since 4.17.3 383 */ 384 @Nonnull 385 @CheckReturnValue 386 public String getExternalFilename(@CheckForNull Profile profile, @Nonnull String pName) { 387 String filename = this.pathFromPortablePath(profile, pName); 388 return (filename != null) ? filename : pName.replace(SEPARATOR, File.separatorChar); 389 } 390 391 /** 392 * Convert a portable filename into an absolute filename, using 393 * {@link jmri.profile.ProfileManager#getActiveProfile()} as the base. 394 * 395 * @param path the portable filename 396 * @return An absolute filename 397 */ 398 @Nonnull 399 @CheckReturnValue 400 public String getAbsoluteFilename(@Nonnull String path) { 401 return this.getAbsoluteFilename(ProfileManager.getDefault().getActiveProfile(), path); 402 } 403 404 /** 405 * Convert a portable filename into an absolute filename. 406 * 407 * @param profile the profile to use as the base 408 * @param path the portable filename 409 * @return An absolute filename 410 */ 411 @Nonnull 412 @CheckReturnValue 413 public String getAbsoluteFilename(@CheckForNull Profile profile, @Nonnull String path) { 414 return this.pathFromPortablePath(profile, path); 415 } 416 417 /** 418 * Convert a File object's path to our preferred storage form. 419 * <p> 420 * This is the inverse of {@link #getFile(String pName)}. Deprecated forms 421 * are not created. 422 * 423 * @param file File at path to be represented 424 * @return Filename for storage in a portable manner. This will include 425 * portable, not system-specific, file separators. 426 * @since 2.7.2 427 */ 428 @Nonnull 429 @CheckReturnValue 430 public String getPortableFilename(@Nonnull File file) { 431 return this.getPortableFilename(ProfileManager.getDefault().getActiveProfile(), file, false, false); 432 } 433 434 /** 435 * Convert a File object's path to our preferred storage form. 436 * <p> 437 * This is the inverse of {@link #getFile(String pName)}. Deprecated forms 438 * are not created. 439 * <p> 440 * This method supports a specific use case concerning profiles and other 441 * portable paths that are stored within the User files directory, which 442 * will cause the {@link jmri.profile.ProfileManager} to write an incorrect 443 * path for the current profile or 444 * {@link apps.configurexml.FileLocationPaneXml} to write an incorrect path 445 * for the Users file directory. In most cases, the use of 446 * {@link #getPortableFilename(java.io.File)} is preferable. 447 * 448 * @param file File at path to be represented 449 * @param ignoreUserFilesPath true if paths in the User files path should be 450 * stored as absolute paths, which is often not 451 * desirable. 452 * @param ignoreProfilePath true if paths in the profile should be stored 453 * as absolute paths, which is often not 454 * desirable. 455 * @return Storage format representation 456 * @since 3.5.5 457 */ 458 @Nonnull 459 @CheckReturnValue 460 public String getPortableFilename(@Nonnull File file, boolean ignoreUserFilesPath, boolean ignoreProfilePath) { 461 return getPortableFilename(ProfileManager.getDefault().getActiveProfile(), file, ignoreUserFilesPath, ignoreProfilePath); 462 } 463 464 /** 465 * Convert a filename string to our preferred storage form. 466 * <p> 467 * This is the inverse of {@link #getExternalFilename(String pName)}. 468 * Deprecated forms are not created. 469 * 470 * @param filename Filename to be represented 471 * @return Filename for storage in a portable manner 472 * @since 2.7.2 473 */ 474 @Nonnull 475 @CheckReturnValue 476 public String getPortableFilename(@Nonnull String filename) { 477 return getPortableFilename(ProfileManager.getDefault().getActiveProfile(), filename, false, false); 478 } 479 480 /** 481 * Convert a filename string to our preferred storage form. 482 * <p> 483 * This is the inverse of {@link #getExternalFilename(String pName)}. 484 * Deprecated forms are not created. 485 * <p> 486 * This method supports a specific use case concerning profiles and other 487 * portable paths that are stored within the User files directory, which 488 * will cause the {@link jmri.profile.ProfileManager} to write an incorrect 489 * path for the current profile or 490 * {@link apps.configurexml.FileLocationPaneXml} to write an incorrect path 491 * for the Users file directory. In most cases, the use of 492 * {@link #getPortableFilename(java.io.File)} is preferable. 493 * 494 * @param filename Filename to be represented 495 * @param ignoreUserFilesPath true if paths in the User files path should be 496 * stored as absolute paths, which is often not 497 * desirable. 498 * @param ignoreProfilePath true if paths in the profile path should be 499 * stored as absolute paths, which is often not 500 * desirable. 501 * @return Storage format representation 502 * @since 3.5.5 503 */ 504 @Nonnull 505 @CheckReturnValue 506 public String getPortableFilename(@Nonnull String filename, boolean ignoreUserFilesPath, boolean ignoreProfilePath) { 507 if (this.isPortableFilename(filename)) { 508 // if this already contains prefix, run through conversion to normalize 509 return getPortableFilename(getExternalFilename(filename), ignoreUserFilesPath, ignoreProfilePath); 510 } else { 511 // treat as pure filename 512 return getPortableFilename(new File(filename), ignoreUserFilesPath, ignoreProfilePath); 513 } 514 } 515 516 /** 517 * Convert a File object's path to our preferred storage form. 518 * <p> 519 * This is the inverse of {@link #getFile(String pName)}. Deprecated forms 520 * are not created. 521 * 522 * @param profile Profile to use as base 523 * @param file File at path to be represented 524 * @return Filename for storage in a portable manner. This will include 525 * portable, not system-specific, file separators. 526 * @since 4.17.3 527 */ 528 @Nonnull 529 @CheckReturnValue 530 public String getPortableFilename(@CheckForNull Profile profile, @Nonnull File file) { 531 return this.getPortableFilename(profile, file, false, false); 532 } 533 534 /** 535 * Convert a File object's path to our preferred storage form. 536 * <p> 537 * This is the inverse of {@link #getFile(String pName)}. Deprecated forms 538 * are not created. 539 * <p> 540 * This method supports a specific use case concerning profiles and other 541 * portable paths that are stored within the User files directory, which 542 * will cause the {@link jmri.profile.ProfileManager} to write an incorrect 543 * path for the current profile or 544 * {@link apps.configurexml.FileLocationPaneXml} to write an incorrect path 545 * for the Users file directory. In most cases, the use of 546 * {@link #getPortableFilename(java.io.File)} is preferable. 547 * 548 * @param profile Profile to use as base 549 * @param file File at path to be represented 550 * @param ignoreUserFilesPath true if paths in the User files path should be 551 * stored as absolute paths, which is often not 552 * desirable. 553 * @param ignoreProfilePath true if paths in the profile should be stored 554 * as absolute paths, which is often not 555 * desirable. 556 * @return Storage format representation 557 * @since 3.5.5 558 */ 559 @Nonnull 560 @CheckReturnValue 561 public String getPortableFilename(@CheckForNull Profile profile, @Nonnull File file, boolean ignoreUserFilesPath, boolean ignoreProfilePath) { 562 // compare full path name to see if same as preferences 563 String filename = file.getAbsolutePath(); 564 565 // append separator if file is a directory 566 if (file.isDirectory()) { 567 filename = filename + File.separator; 568 } 569 570 if (filename == null) { 571 throw new IllegalArgumentException("File \"" + file + "\" has a null absolute path which is not allowed"); 572 } 573 574 // compare full path name to see if same as preferences 575 if (!ignoreUserFilesPath) { 576 if (filename.startsWith(getUserFilesPath(profile))) { 577 return PREFERENCES 578 + filename.substring(getUserFilesPath(profile).length(), filename.length()).replace(File.separatorChar, 579 SEPARATOR); 580 } 581 } 582 583 if (!ignoreProfilePath) { 584 // compare full path name to see if same as profile 585 if (filename.startsWith(getProfilePath(profile))) { 586 return PROFILE 587 + filename.substring(getProfilePath(profile).length(), filename.length()).replace(File.separatorChar, 588 SEPARATOR); 589 } 590 } 591 592 // compare full path name to see if same as settings 593 if (filename.startsWith(getPreferencesPath())) { 594 return SETTINGS 595 + filename.substring(getPreferencesPath().length(), filename.length()).replace(File.separatorChar, 596 SEPARATOR); 597 } 598 599 if (!ignoreUserFilesPath) { 600 /* 601 * The tests for any portatable path that could be within the 602 * UserFiles locations needs to be within this block. This prevents 603 * the UserFiles or Profile path from being set to another portable 604 * path that is user settable. 605 * 606 * Note that this test should be after the UserFiles, Profile, and 607 * Preferences tests. 608 */ 609 // check for relative to scripts dir 610 if (filename.startsWith(getScriptsPath(profile)) && !filename.equals(getScriptsPath(profile))) { 611 return SCRIPTS 612 + filename.substring(getScriptsPath(profile).length(), filename.length()).replace(File.separatorChar, 613 SEPARATOR); 614 } 615 } 616 617 // now check for relative to program dir 618 if (filename.startsWith(getProgramPath())) { 619 return PROGRAM 620 + filename.substring(getProgramPath().length(), filename.length()).replace(File.separatorChar, 621 SEPARATOR); 622 } 623 624 // compare full path name to see if same as home directory 625 // do this last, in case preferences or program dir are in home directory 626 if (filename.startsWith(getHomePath())) { 627 return HOME 628 + filename.substring(getHomePath().length(), filename.length()).replace(File.separatorChar, 629 SEPARATOR); 630 } 631 632 return filename.replace(File.separatorChar, SEPARATOR); // absolute, and doesn't match; not really portable... 633 } 634 635 /** 636 * Convert a filename string to our preferred storage form. 637 * <p> 638 * This is the inverse of {@link #getExternalFilename(String pName)}. 639 * Deprecated forms are not created. 640 * 641 * @param profile Profile to use as base 642 * @param filename Filename to be represented 643 * @return Filename for storage in a portable manner 644 * @since 4.17.3 645 */ 646 @Nonnull 647 @CheckReturnValue 648 public String getPortableFilename(@CheckForNull Profile profile, @Nonnull String filename) { 649 return getPortableFilename(profile, filename, false, false); 650 } 651 652 /** 653 * Convert a filename string to our preferred storage form. 654 * <p> 655 * This is the inverse of {@link #getExternalFilename(String pName)}. 656 * Deprecated forms are not created. 657 * <p> 658 * This method supports a specific use case concerning profiles and other 659 * portable paths that are stored within the User files directory, which 660 * will cause the {@link jmri.profile.ProfileManager} to write an incorrect 661 * path for the current profile or 662 * {@link apps.configurexml.FileLocationPaneXml} to write an incorrect path 663 * for the Users file directory. In most cases, the use of 664 * {@link #getPortableFilename(java.io.File)} is preferable. 665 * 666 * @param profile Profile to use as base 667 * @param filename Filename to be represented 668 * @param ignoreUserFilesPath true if paths in the User files path should be 669 * stored as absolute paths, which is often not 670 * desirable. 671 * @param ignoreProfilePath true if paths in the profile path should be 672 * stored as absolute paths, which is often not 673 * desirable. 674 * @return Storage format representation 675 * @since 4.17.3 676 */ 677 @Nonnull 678 @CheckReturnValue 679 public String getPortableFilename(@CheckForNull Profile profile, @Nonnull String filename, boolean ignoreUserFilesPath, 680 boolean ignoreProfilePath) { 681 if (isPortableFilename(filename)) { 682 // if this already contains prefix, run through conversion to normalize 683 return getPortableFilename(profile, getExternalFilename(filename), ignoreUserFilesPath, ignoreProfilePath); 684 } else { 685 // treat as pure filename 686 return getPortableFilename(profile, new File(filename), ignoreUserFilesPath, ignoreProfilePath); 687 } 688 } 689 690 /** 691 * Test if the given filename is a portable filename. 692 * <p> 693 * Note that this method may return a false positive if the filename is a 694 * file: URL. 695 * 696 * @param filename the name to test 697 * @return true if filename is portable 698 */ 699 public boolean isPortableFilename(@Nonnull String filename) { 700 return (filename.startsWith(PROGRAM) 701 || filename.startsWith(HOME) 702 || filename.startsWith(PREFERENCES) 703 || filename.startsWith(SCRIPTS) 704 || filename.startsWith(PROFILE) 705 || filename.startsWith(SETTINGS)); 706 } 707 708 /** 709 * Get the user's home directory. 710 * 711 * @return User's home directory as a String 712 */ 713 @Nonnull 714 @CheckReturnValue 715 public String getHomePath() { 716 return HOME_PATH; 717 } 718 719 /** 720 * Get the user's files directory. If not set by the user, this is the same 721 * as the profile path returned by 722 * {@link ProfileManager#getActiveProfile()}. Note that if the profile path 723 * has been set to null, that returns the preferences directory, see 724 * {@link #getProfilePath()}. 725 * 726 * @see #getProfilePath() 727 * @return User's files directory 728 */ 729 @Nonnull 730 @CheckReturnValue 731 public String getUserFilesPath() { 732 return getUserFilesPath(ProfileManager.getDefault().getActiveProfile()); 733 } 734 735 /** 736 * Get the user's files directory. If not set by the user, this is the same 737 * as the profile path. Note that if the profile path has been set to null, 738 * that returns the preferences directory, see {@link #getProfilePath()}. 739 * 740 * @param profile the profile to use 741 * @see #getProfilePath() 742 * @return User's files directory 743 */ 744 @Nonnull 745 @CheckReturnValue 746 public String getUserFilesPath(@CheckForNull Profile profile) { 747 String path = userFilesPaths.get(profile); 748 return path != null ? path : getProfilePath(profile); 749 } 750 751 /** 752 * Set the user's files directory. 753 * 754 * @see #getUserFilesPath() 755 * @param profile the profile to set the user's files directory for 756 * @param path The path to the user's files directory using 757 * system-specific separators 758 */ 759 public void setUserFilesPath(@CheckForNull Profile profile, @Nonnull String path) { 760 String old = userFilesPaths.get(profile); 761 if (!path.endsWith(File.separator)) { 762 path = path + File.separator; 763 } 764 userFilesPaths.put(profile, path); 765 if ((old != null && !old.equals(path)) || (!path.equals(old))) { 766 this.firePropertyChange(FileUtil.PREFERENCES, new Property(profile, old), new Property(profile, path)); 767 } 768 } 769 770 /** 771 * Get the profile directory. If not set, provide the preferences path. 772 * 773 * @param profile the Profile to use as a base 774 * @see #getPreferencesPath() 775 * @return Profile directory using system-specific separators 776 */ 777 @Nonnull 778 @CheckReturnValue 779 public String getProfilePath(@CheckForNull Profile profile) { 780 String path = profilePaths.get(profile); 781 if (path == null) { 782 File f = profile != null ? profile.getPath() : null; 783 if (f != null) { 784 path = f.getAbsolutePath(); 785 if (!path.endsWith(File.separator)) { 786 path = path + File.separator; 787 } 788 profilePaths.put(profile, path); 789 } 790 } 791 return (path != null) ? path : this.getPreferencesPath(); 792 } 793 794 /** 795 * Get the profile directory. If not set, provide the preferences path. Uses 796 * the Profile returned by {@link ProfileManager#getActiveProfile()} as a 797 * base. 798 * 799 * @see #getPreferencesPath() 800 * @return Profile directory using system-specific separators 801 */ 802 @Nonnull 803 @CheckReturnValue 804 public String getProfilePath() { 805 return getProfilePath(ProfileManager.getDefault().getActiveProfile()); 806 } 807 808 /** 809 * Get the preferences directory. This directory is set based on the OS and 810 * is not normally settable by the user. 811 * <ul> 812 * <li>On Microsoft Windows systems, this is {@code JMRI} in the User's home 813 * directory.</li> 814 * <li>On OS X systems, this is {@code Library/Preferences/JMRI} in the 815 * User's home directory.</li> 816 * <li>On Linux, Solaris, and other UNIXes, this is {@code .jmri} in the 817 * User's home directory.</li> 818 * <li>This can be overridden with by setting the {@code jmri.prefsdir} Java 819 * property when starting JMRI.</li> 820 * </ul> 821 * Use {@link #getHomePath()} to get the User's home directory. 822 * 823 * @see #getHomePath() 824 * @return Path to the preferences directory using system-specific 825 * separators. 826 */ 827 @Nonnull 828 @CheckReturnValue 829 public String getPreferencesPath() { 830 // return jmri.prefsdir property if present 831 String jmriPrefsDir = System.getProperty("jmri.prefsdir", ""); // NOI18N 832 if (!jmriPrefsDir.isEmpty()) { 833 try { 834 return new File(jmriPrefsDir).getCanonicalPath() + File.separator; 835 } catch (IOException ex) { 836 // use System.err because logging at this point will fail 837 // since this method is called to setup logging 838 System.err.println("Unable to locate settings dir \"" + jmriPrefsDir + "\""); 839 if (!jmriPrefsDir.endsWith(File.separator)) { 840 return jmriPrefsDir + File.separator; 841 } 842 } 843 } 844 String result; 845 switch (SystemType.getType()) { 846 case SystemType.MACOSX: 847 // Mac OS X 848 result = this.getHomePath() + "Library" + File.separator + "Preferences" + File.separator + "JMRI" + File.separator; // NOI18N 849 break; 850 case SystemType.LINUX: 851 case SystemType.UNIX: 852 // Linux, so use an invisible file 853 result = this.getHomePath() + ".jmri" + File.separator; // NOI18N 854 break; 855 case SystemType.WINDOWS: 856 default: 857 // Could be Windows, other 858 result = this.getHomePath() + "JMRI" + File.separator; // NOI18N 859 break; 860 } 861 // logging here merely throws warnings since we call this method to set up logging 862 // uncomment below to print OS default to console 863 // System.out.println("preferencesPath defined as \"" + result + "\" based on os.name=\"" + SystemType.getOSName() + "\""); 864 return result; 865 } 866 867 /** 868 * Get the JMRI cache location, ensuring its existence. 869 * <p> 870 * This is <strong>not</strong> part of the {@link jmri.util.FileUtil} API 871 * since it should generally be accessed using 872 * {@link jmri.profile.ProfileUtils#getCacheDirectory(jmri.profile.Profile, java.lang.Class)}. 873 * <p> 874 * Uses the following locations (where [version] is from 875 * {@link jmri.Version#getCanonicalVersion()}): 876 * <dl> 877 * <dt>System Property (if set)</dt><dd>value of 878 * <em>jmri_default_cachedir</em></dd> 879 * <dt>macOS</dt><dd>~/Library/Caches/JMRI/[version]</dd> 880 * <dt>Windows</dt><dd>%Local AppData%/JMRI/[version]</dd> 881 * <dt>UNIX/Linux/POSIX</dt><dd>${XDG_CACHE_HOME}/JMRI/[version] or 882 * $HOME/.cache/JMRI/[version]</dd> 883 * <dt>Fallback</dt><dd>JMRI portable path 884 * <em>setting:cache/[version]</em></dd> 885 * </dl> 886 * 887 * @return the cache directory for this version of JMRI 888 */ 889 @Nonnull 890 public File getCacheDirectory() { 891 File cache; 892 String property = System.getProperty("jmri_default_cachedir"); 893 if (property != null) { 894 cache = new File(property); 895 } else { 896 switch (SystemType.getType()) { 897 case SystemType.MACOSX: 898 cache = new File(new File(this.getHomePath(), "Library/Caches/JMRI"), Version.getCanonicalVersion()); 899 break; 900 case SystemType.LINUX: 901 case SystemType.UNIX: 902 property = System.getenv("XDG_CACHE_HOME"); 903 if (property != null) { 904 cache = new File(new File(property, "JMRI"), Version.getCanonicalVersion()); 905 } else { 906 cache = new File(new File(this.getHomePath(), ".cache/JMRI"), Version.getCanonicalVersion()); 907 } 908 break; 909 case SystemType.WINDOWS: 910 try { 911 cache = new File(new File(Shell32Util.getKnownFolderPath(KnownFolders.FOLDERID_LocalAppData), "JMRI/cache"), Version.getCanonicalVersion()); 912 } catch (UnsatisfiedLinkError er) { 913 // Needed only on Windows XP 914 cache = new File(new File(Shell32Util.getFolderPath(ShlObj.CSIDL_LOCAL_APPDATA), "JMRI/cache"), Version.getCanonicalVersion()); 915 } 916 break; 917 default: 918 // fallback 919 cache = new File(new File(this.getPreferencesPath(), "cache"), Version.getCanonicalVersion()); 920 break; 921 } 922 } 923 this.createDirectory(cache); 924 return cache; 925 } 926 927 /** 928 * Get the JMRI program directory. 929 * <p> 930 * If the program directory has not been 931 * previously set, first sets the program directory to the value specified 932 * in the Java System property <code>jmri.path.program</code> 933 * <p> 934 * If this property is unset, finds from jar or class files location. 935 * <p> 936 * If this fails, returns <code>.</code> . 937 * 938 * @return JMRI program directory as a String. 939 */ 940 @Nonnull 941 @CheckReturnValue 942 public String getProgramPath() { 943 // As this method is called in Log4J setup, should not 944 // contain standard logging statements. 945 if (programPath == null) { 946 if (System.getProperty("jmri.path.program") == null) { 947 // find from jar or class files location 948 String path1 = this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath(); 949 String path2 = (new File(path1)).getParentFile().getPath(); 950 path2 = path2.replaceAll("\\+", "%2B"); // convert + chars to UTF-8 to get through the decode 951 try { 952 String loadingDir = java.net.URLDecoder.decode(path2, "UTF-8"); 953 if (loadingDir.endsWith("target")) { 954 loadingDir = loadingDir.substring(0, loadingDir.length()-6); 955 } 956 this.setProgramPath(loadingDir); // NOI18N 957 } catch (java.io.UnsupportedEncodingException e) { 958 System.err.println("Unsupported URL when trying to locate program directory: " + path2 ); 959 // best guess 960 this.setProgramPath("."); // NOI18N 961 } 962 } else { 963 this.setProgramPath(System.getProperty("jmri.path.program", ".")); // NOI18N 964 } 965 } 966 return programPath; 967 } 968 969 /** 970 * Set the JMRI program directory. 971 * <p> 972 * Convenience method that calls {@link #setProgramPath(java.io.File)} with 973 * the passed in path. 974 * 975 * @param path the path to the JMRI installation 976 */ 977 public void setProgramPath(@Nonnull String path) { 978 this.setProgramPath(new File(path)); 979 } 980 981 /** 982 * Set the JMRI program directory. 983 * <p> 984 * If set, allows JMRI to be loaded from locations other than the directory 985 * containing JMRI resources. This must be set very early in the process of 986 * loading JMRI (prior to loading any other JMRI code) to be meaningfully 987 * used. 988 * 989 * @param path the path to the JMRI installation 990 */ 991 public void setProgramPath(@Nonnull File path) { 992 String old = this.programPath; 993 try { 994 this.programPath = (path).getCanonicalPath() + File.separator; 995 } catch (IOException ex) { 996 log.error("Unable to get JMRI program directory.", ex); 997 } 998 if ((old != null && !old.equals(this.programPath)) 999 || (this.programPath != null && !this.programPath.equals(old))) { 1000 this.firePropertyChange(FileUtil.PROGRAM, old, this.programPath); 1001 } 1002 } 1003 1004 /** 1005 * Get the resources directory within the user's files directory. 1006 * 1007 * @return path to [user's file]/resources/ using system-specific separators 1008 */ 1009 @Nonnull 1010 @CheckReturnValue 1011 public String getUserResourcePath() { 1012 return this.getUserFilesPath() + "resources" + File.separator; // NOI18N 1013 } 1014 1015 /** 1016 * Log all paths at the INFO level. 1017 */ 1018 public void logFilePaths() { 1019 log.info("File path {} is {}", FileUtil.PROGRAM, this.getProgramPath()); 1020 log.info("File path {} is {}", FileUtil.PREFERENCES, this.getUserFilesPath()); 1021 log.info("File path {} is {}", FileUtil.PROFILE, this.getProfilePath()); 1022 log.info("File path {} is {}", FileUtil.SETTINGS, this.getPreferencesPath()); 1023 log.info("File path {} is {}", FileUtil.HOME, this.getHomePath()); 1024 log.info("File path {} is {}", FileUtil.SCRIPTS, this.getScriptsPath()); 1025 } 1026 1027 /** 1028 * Get the path to the scripts directory. If not set previously with 1029 * {@link #setScriptsPath}, this is the "jython" subdirectory in the program 1030 * directory. Uses the Profile returned by 1031 * {@link ProfileManager#getActiveProfile()} as the base. 1032 * 1033 * @return the scripts directory using system-specific separators 1034 */ 1035 @Nonnull 1036 @CheckReturnValue 1037 public String getScriptsPath() { 1038 return getScriptsPath(ProfileManager.getDefault().getActiveProfile()); 1039 } 1040 1041 /** 1042 * Get the path to the scripts directory. If not set previously with 1043 * {@link #setScriptsPath}, this is the "jython" subdirectory in the program 1044 * directory. 1045 * 1046 * @param profile the Profile to use as the base 1047 * @return the path to scripts directory using system-specific separators 1048 */ 1049 @Nonnull 1050 @CheckReturnValue 1051 public String getScriptsPath(@CheckForNull Profile profile) { 1052 String path = scriptsPaths.get(profile); 1053 if (path != null) { 1054 return path; 1055 } 1056 // scripts directory not set by user, return default if it exists 1057 File file = new File(this.getProgramPath() + File.separator + "jython" + File.separator); // NOI18N 1058 if (file.exists() && file.isDirectory()) { 1059 return file.getPath() + File.separator; 1060 } 1061 // if default does not exist, return user's files directory 1062 return this.getUserFilesPath(); 1063 } 1064 1065 /** 1066 * Set the path to python scripts. 1067 * 1068 * @param profile the profile to use as a base 1069 * @param path the scriptsPaths to set; null resets to the default, 1070 * defined in {@link #getScriptsPath()} 1071 */ 1072 public void setScriptsPath(@CheckForNull Profile profile, @CheckForNull String path) { 1073 String old = scriptsPaths.get(profile); 1074 if (path != null && !path.endsWith(File.separator)) { 1075 path = path + File.separator; 1076 } 1077 scriptsPaths.put(profile, path); 1078 if ((old != null && !old.equals(path)) || (path != null && !path.equals(old))) { 1079 this.firePropertyChange(FileUtil.SCRIPTS, new Property(profile, old), new Property(profile, path)); 1080 } 1081 } 1082 1083 /** 1084 * Get the URL of a portable filename if it can be located using 1085 * {@link #findURI(java.lang.String)} 1086 * 1087 * @param path the path to find 1088 * @return URL of portable or absolute path 1089 */ 1090 @Nonnull 1091 @CheckReturnValue 1092 public URI findExternalFilename(@Nonnull String path) { 1093 log.debug("Finding external path {}", path); 1094 if (this.isPortableFilename(path)) { 1095 int index = path.indexOf(":") + 1; 1096 String location = path.substring(0, index); 1097 path = path.substring(index); 1098 log.debug("Finding {} and {}", location, path); 1099 switch (location) { 1100 case FileUtil.PROGRAM: 1101 return this.findURI(path, FileUtil.Location.INSTALLED); 1102 case FileUtil.PREFERENCES: 1103 return this.findURI(path, FileUtil.Location.USER); 1104 case FileUtil.PROFILE: 1105 case FileUtil.SETTINGS: 1106 case FileUtil.SCRIPTS: 1107 case FileUtil.HOME: 1108 return this.findURI(this.getExternalFilename(location + path)); 1109 default: 1110 break; 1111 } 1112 } 1113 return this.findURI(path, Location.ALL); 1114 } 1115 1116 /** 1117 * Search for a file or JAR resource by name and return the 1118 * {@link java.io.InputStream} for that file. Search order is defined by 1119 * {@link #findURL(java.lang.String, jmri.util.FileUtil.Location, java.lang.String...) }. 1120 * No limits are placed on search locations. 1121 * 1122 * @param path The relative path of the file or resource 1123 * @return InputStream or null. 1124 * @see #findInputStream(java.lang.String, java.lang.String...) 1125 * @see #findInputStream(java.lang.String, jmri.util.FileUtil.Location, 1126 * java.lang.String...) 1127 * @see #findURL(java.lang.String) 1128 * @see #findURL(java.lang.String, java.lang.String...) 1129 * @see #findURL(java.lang.String, jmri.util.FileUtil.Location, 1130 * java.lang.String...) 1131 */ 1132 public InputStream findInputStream(@Nonnull String path) { 1133 return this.findInputStream(path, new String[]{}); 1134 } 1135 1136 /** 1137 * Search for a file or JAR resource by name and return the 1138 * {@link java.io.InputStream} for that file. Search order is defined by 1139 * {@link #findURL(java.lang.String, jmri.util.FileUtil.Location, java.lang.String...) }. 1140 * No limits are placed on search locations. 1141 * 1142 * @param path The relative path of the file or resource 1143 * @param searchPaths a list of paths to search for the path in 1144 * @return InputStream or null. 1145 * @see #findInputStream(java.lang.String) 1146 * @see #findInputStream(java.lang.String, jmri.util.FileUtil.Location, 1147 * java.lang.String...) 1148 */ 1149 public InputStream findInputStream(@Nonnull String path, @Nonnull String... searchPaths) { 1150 return this.findInputStream(path, Location.ALL, searchPaths); 1151 } 1152 1153 /** 1154 * Search for a file or JAR resource by name and return the 1155 * {@link java.io.InputStream} for that file. Search order is defined by 1156 * {@link #findURL(java.lang.String, jmri.util.FileUtil.Location, java.lang.String...) }. 1157 * 1158 * @param path The relative path of the file or resource 1159 * @param locations The type of locations to limit the search to 1160 * @return InputStream or null. 1161 * @see #findInputStream(java.lang.String) 1162 * @see #findInputStream(java.lang.String, jmri.util.FileUtil.Location, 1163 * java.lang.String...) 1164 */ 1165 public InputStream findInputStream(@Nonnull String path, @Nonnull Location locations) { 1166 return this.findInputStream(path, locations, new String[]{}); 1167 } 1168 1169 /** 1170 * Search for a file or JAR resource by name and return the 1171 * {@link java.io.InputStream} for that file. Search order is defined by 1172 * {@link #findURL(java.lang.String, jmri.util.FileUtil.Location, java.lang.String...) }. 1173 * 1174 * @param path The relative path of the file or resource 1175 * @param locations The type of locations to limit the search to 1176 * @param searchPaths a list of paths to search for the path in 1177 * @return InputStream or null. 1178 * @see #findInputStream(java.lang.String) 1179 * @see #findInputStream(java.lang.String, java.lang.String...) 1180 */ 1181 public InputStream findInputStream(@Nonnull String path, @Nonnull Location locations, @Nonnull String... searchPaths) { 1182 URL file = this.findURL(path, locations, searchPaths); 1183 if (file != null) { 1184 try { 1185 return file.openStream(); 1186 } catch (IOException ex) { 1187 log.error("findInputStream IOException", ex); 1188 } 1189 } 1190 return null; 1191 } 1192 1193 /** 1194 * Search for a file or JAR resource by name and return the 1195 * {@link java.net.URI} for that file. Search order is defined by 1196 * {@link #findURI(java.lang.String, jmri.util.FileUtil.Location, java.lang.String...)}. 1197 * No limits are placed on search locations. 1198 * 1199 * @param path The relative path of the file or resource. 1200 * @return The URI or null. 1201 * @see #findURI(java.lang.String, java.lang.String...) 1202 * @see #findURI(java.lang.String, jmri.util.FileUtil.Location) 1203 * @see #findURI(java.lang.String, jmri.util.FileUtil.Location, 1204 * java.lang.String...) 1205 */ 1206 public URI findURI(@Nonnull String path) { 1207 return this.findURI(path, new String[]{}); 1208 } 1209 1210 /** 1211 * Search for a file or JAR resource by name and return the 1212 * {@link java.net.URI} for that file. Search order is defined by 1213 * {@link #findURI(java.lang.String, jmri.util.FileUtil.Location, java.lang.String...)}. 1214 * No limits are placed on search locations. 1215 * <p> 1216 * Note that if the file for path is not found in one of the searchPaths, 1217 * all standard locations are also be searched through to find the file. If 1218 * you need to limit the locations where the file can be found use 1219 * {@link #findURI(java.lang.String, jmri.util.FileUtil.Location, java.lang.String...)}. 1220 * 1221 * @param path The relative path of the file or resource 1222 * @param searchPaths a list of paths to search for the path in 1223 * @return The URI or null 1224 * @see #findURI(java.lang.String) 1225 * @see #findURI(java.lang.String, jmri.util.FileUtil.Location) 1226 * @see #findURI(java.lang.String, jmri.util.FileUtil.Location, 1227 * java.lang.String...) 1228 */ 1229 public URI findURI(@Nonnull String path, @Nonnull String... searchPaths) { 1230 return this.findURI(path, Location.ALL, searchPaths); 1231 } 1232 1233 /** 1234 * Search for a file or JAR resource by name and return the 1235 * {@link java.net.URI} for that file. Search order is defined by 1236 * {@link #findURI(java.lang.String, jmri.util.FileUtil.Location, java.lang.String...)}. 1237 * 1238 * @param path The relative path of the file or resource 1239 * @param locations The types of locations to limit the search to 1240 * @return The URI or null 1241 * @see #findURI(java.lang.String) 1242 * @see #findURI(java.lang.String, java.lang.String...) 1243 * @see #findURI(java.lang.String, jmri.util.FileUtil.Location, 1244 * java.lang.String...) 1245 */ 1246 public URI findURI(@Nonnull String path, @Nonnull Location locations) { 1247 return this.findURI(path, locations, new String[]{}); 1248 } 1249 1250 /** 1251 * Search for a file or JAR resource by name and return the 1252 * {@link java.net.URI} for that file. 1253 * <p> 1254 * Search order is: 1255 * <ol> 1256 * <li>For any provided searchPaths, iterate over the searchPaths by 1257 * prepending each searchPath to the path and following the following search 1258 * order:<ol> 1259 * <li>As a {@link java.io.File} in the user preferences directory</li> 1260 * <li>As a File in the current working directory (usually, but not always 1261 * the JMRI distribution directory)</li> 1262 * <li>As a File in the JMRI distribution directory</li> 1263 * <li>As a resource in jmri.jar</li> 1264 * </ol></li> 1265 * <li>If the file or resource has not been found in the searchPaths, search 1266 * in the four locations listed without prepending any path</li> 1267 * <li>As a File with an absolute path</li> 1268 * </ol> 1269 * <p> 1270 * The <code>locations</code> parameter limits the above logic by limiting 1271 * the location searched. 1272 * <ol> 1273 * <li>{@link Location#ALL} will not place any limits on the search</li> 1274 * <li>{@link Location#NONE} effectively requires that <code>path</code> be 1275 * a portable pathname</li> 1276 * <li>{@link Location#INSTALLED} limits the search to the 1277 * {@link FileUtil#PROGRAM} directory and JARs in the class path</li> 1278 * <li>{@link Location#USER} limits the search to the 1279 * {@link FileUtil#PREFERENCES}, {@link FileUtil#PROFILE}, and 1280 * {@link FileUtil#SETTINGS} directories (in that order)</li> 1281 * </ol> 1282 * 1283 * @param path The relative path of the file or resource 1284 * @param locations The types of locations to limit the search to 1285 * @param searchPaths a list of paths to search for the path in 1286 * @return The URI or null 1287 * @see #findURI(java.lang.String) 1288 * @see #findURI(java.lang.String, jmri.util.FileUtil.Location) 1289 * @see #findURI(java.lang.String, java.lang.String...) 1290 */ 1291 public URI findURI(@Nonnull String path, @Nonnull Location locations, @Nonnull String... searchPaths) { 1292 if (log.isDebugEnabled()) { // avoid the Arrays.toString call unless debugging 1293 log.debug("Attempting to find {} in {}", path, Arrays.toString(searchPaths)); 1294 } 1295 if (this.isPortableFilename(path)) { 1296 try { 1297 return this.findExternalFilename(path); 1298 } catch (NullPointerException ex) { 1299 // do nothing 1300 } 1301 } 1302 URI resource = null; 1303 for (String searchPath : searchPaths) { 1304 resource = this.findURI(searchPath + File.separator + path); 1305 if (resource != null) { 1306 return resource; 1307 } 1308 } 1309 File file; 1310 if (locations == Location.ALL || locations == Location.USER) { 1311 // attempt to return path from preferences directory 1312 file = new File(this.getUserFilesPath(), path); 1313 if (file.exists()) { 1314 return file.toURI(); 1315 } 1316 // attempt to return path from profile directory 1317 file = new File(this.getProfilePath(), path); 1318 if (file.exists()) { 1319 return file.toURI(); 1320 } 1321 // attempt to return path from preferences directory 1322 file = new File(this.getPreferencesPath(), path); 1323 if (file.exists()) { 1324 return file.toURI(); 1325 } 1326 } 1327 if (locations == Location.ALL || locations == Location.INSTALLED) { 1328 // attempt to return path from current working directory 1329 file = new File(path); 1330 if (file.exists()) { 1331 return file.toURI(); 1332 } 1333 // attempt to return path from JMRI distribution directory 1334 file = new File(this.getProgramPath() + path); 1335 if (file.exists()) { 1336 return file.toURI(); 1337 } 1338 } 1339 if (locations == Location.ALL || locations == Location.INSTALLED) { 1340 // return path if in jmri.jar or null 1341 // The ClassLoader needs paths to use / 1342 path = path.replace(File.separatorChar, '/'); 1343 URL url = FileUtilSupport.class.getClassLoader().getResource(path); 1344 if (url == null) { 1345 url = FileUtilSupport.class.getResource(path); 1346 if (url == null) { 1347 log.debug("{} not found in classpath", path); 1348 } 1349 } 1350 try { 1351 resource = (url != null) ? url.toURI() : null; 1352 } catch (URISyntaxException ex) { 1353 log.warn("Unable to get URI for {}", path, ex); 1354 } 1355 } 1356 // if a resource has not been found and path is absolute and exists 1357 // return it 1358 if (resource == null) { 1359 file = new File(path); 1360 if (file.isAbsolute() && file.exists()) { 1361 return file.toURI(); 1362 } 1363 } 1364 return resource; 1365 } 1366 1367 /** 1368 * Search for a file or JAR resource by name and return the 1369 * {@link java.net.URL} for that file. Search order is defined by 1370 * {@link #findURL(java.lang.String, jmri.util.FileUtil.Location, java.lang.String...)}. 1371 * No limits are placed on search locations. 1372 * 1373 * @param path The relative path of the file or resource. 1374 * @return The URL or null. 1375 * @see #findURL(java.lang.String, java.lang.String...) 1376 * @see #findURL(java.lang.String, jmri.util.FileUtil.Location) 1377 * @see #findURL(java.lang.String, jmri.util.FileUtil.Location, 1378 * java.lang.String...) 1379 */ 1380 public URL findURL(@Nonnull String path) { 1381 return this.findURL(path, new String[]{}); 1382 } 1383 1384 /** 1385 * Search for a file or JAR resource by name and return the 1386 * {@link java.net.URL} for that file. Search order is defined by 1387 * {@link #findURL(java.lang.String, jmri.util.FileUtil.Location, java.lang.String...)}. 1388 * No limits are placed on search locations. 1389 * 1390 * @param path The relative path of the file or resource 1391 * @param searchPaths a list of paths to search for the path in 1392 * @return The URL or null 1393 * @see #findURL(java.lang.String) 1394 * @see #findURL(java.lang.String, jmri.util.FileUtil.Location) 1395 * @see #findURL(java.lang.String, jmri.util.FileUtil.Location, 1396 * java.lang.String...) 1397 */ 1398 public URL findURL(@Nonnull String path, @Nonnull String... searchPaths) { 1399 return this.findURL(path, Location.ALL, searchPaths); 1400 } 1401 1402 /** 1403 * Search for a file or JAR resource by name and return the 1404 * {@link java.net.URL} for that file. Search order is defined by 1405 * {@link #findURL(java.lang.String, jmri.util.FileUtil.Location, java.lang.String...)}. 1406 * 1407 * @param path The relative path of the file or resource 1408 * @param locations The types of locations to limit the search to 1409 * @return The URL or null 1410 * @see #findURL(java.lang.String) 1411 * @see #findURL(java.lang.String, java.lang.String...) 1412 * @see #findURL(java.lang.String, jmri.util.FileUtil.Location, 1413 * java.lang.String...) 1414 */ 1415 public URL findURL(@Nonnull String path, Location locations) { 1416 return this.findURL(path, locations, new String[]{}); 1417 } 1418 1419 /** 1420 * Search for a file or JAR resource by name and return the 1421 * {@link java.net.URL} for that file. 1422 * <p> 1423 * Search order is: 1424 * <ol><li>For any provided searchPaths, iterate over the searchPaths by 1425 * prepending each searchPath to the path and following the following search 1426 * order: 1427 * <ol><li>As a {@link java.io.File} in the user preferences directory</li> 1428 * <li>As a File in the current working directory (usually, but not always 1429 * the JMRI distribution directory)</li> <li>As a File in the JMRI 1430 * distribution directory</li> <li>As a resource in jmri.jar</li></ol></li> 1431 * <li>If the file or resource has not been found in the searchPaths, search 1432 * in the four locations listed without prepending any path</li></ol> 1433 * <p> 1434 * The <code>locations</code> parameter limits the above logic by limiting 1435 * the location searched. 1436 * <ol><li>{@link Location#ALL} will not place any limits on the 1437 * search</li><li>{@link Location#NONE} effectively requires that 1438 * <code>path</code> be a portable 1439 * pathname</li><li>{@link Location#INSTALLED} limits the search to the 1440 * {@link FileUtil#PROGRAM} directory and JARs in the class 1441 * path</li><li>{@link Location#USER} limits the search to the 1442 * {@link FileUtil#PROFILE} directory</li></ol> 1443 * 1444 * @param path The relative path of the file or resource 1445 * @param locations The types of locations to limit the search to 1446 * @param searchPaths a list of paths to search for the path in 1447 * @return The URL or null 1448 * @see #findURL(java.lang.String) 1449 * @see #findURL(java.lang.String, jmri.util.FileUtil.Location) 1450 * @see #findURL(java.lang.String, java.lang.String...) 1451 */ 1452 public URL findURL(@Nonnull String path, @Nonnull Location locations, @Nonnull String... searchPaths) { 1453 URI file = this.findURI(path, locations, searchPaths); 1454 if (file != null) { 1455 try { 1456 return file.toURL(); 1457 } catch (MalformedURLException ex) { 1458 log.error("findURL MalformedURLException", ex); 1459 } 1460 } 1461 return null; 1462 } 1463 1464 /** 1465 * Return the {@link java.net.URI} for a given URL 1466 * 1467 * @param url the URL 1468 * @return a URI or null if the conversion would have caused a 1469 * {@link java.net.URISyntaxException} 1470 */ 1471 public URI urlToURI(@Nonnull URL url) { 1472 try { 1473 return url.toURI(); 1474 } catch (URISyntaxException ex) { 1475 log.error("Unable to get URI from URL", ex); 1476 return null; 1477 } 1478 } 1479 1480 /** 1481 * Return the {@link java.net.URL} for a given {@link java.io.File}. This 1482 * method catches a {@link java.net.MalformedURLException} and returns null 1483 * in its place, since we really do not expect a File object to ever give a 1484 * malformed URL. This method exists solely so implementing classes do not 1485 * need to catch that exception. 1486 * 1487 * @param file The File to convert. 1488 * @return a URL or null if the conversion would have caused a 1489 * MalformedURLException 1490 */ 1491 public URL fileToURL(@Nonnull File file) { 1492 try { 1493 return file.toURI().toURL(); 1494 } catch (MalformedURLException ex) { 1495 log.error("Unable to get URL from file", ex); 1496 return null; 1497 } 1498 } 1499 1500 /** 1501 * Get the JMRI distribution jar file. 1502 * 1503 * @return the JAR file containing the JMRI library or null if not running 1504 * from a JAR file 1505 */ 1506 public JarFile getJmriJarFile() { 1507 if (jarPath == null) { 1508 CodeSource sc = FileUtilSupport.class.getProtectionDomain().getCodeSource(); 1509 if (sc != null) { 1510 jarPath = sc.getLocation().toString(); 1511 if (jarPath.startsWith("jar:file:")) { 1512 // 9 = length of jar:file: 1513 jarPath = jarPath.substring(9, jarPath.lastIndexOf("!")); 1514 } else { 1515 log.info("Running from classes not in jar file."); 1516 jarPath = ""; // set to empty String to bypass search 1517 return null; 1518 } 1519 log.debug("jmri.jar path is {}", jarPath); 1520 } 1521 if (jarPath == null) { 1522 log.error("Unable to locate jmri.jar"); 1523 jarPath = ""; // set to empty String to bypass search 1524 return null; 1525 } 1526 } 1527 if (!jarPath.isEmpty()) { 1528 try { 1529 return new JarFile(jarPath); 1530 } catch (IOException ex) { 1531 log.error("Unable to open jmri.jar", ex); 1532 return null; 1533 } 1534 } 1535 return null; 1536 } 1537 1538 /** 1539 * Read a text file into a String. 1540 * 1541 * @param file The text file. 1542 * @return The contents of the file. 1543 * @throws java.io.IOException if the file cannot be read 1544 */ 1545 public String readFile(@Nonnull File file) throws IOException { 1546 return this.readURL(this.fileToURL(file)); 1547 } 1548 1549 /** 1550 * Read a text URL into a String. 1551 * 1552 * @param url The text URL. 1553 * @return The contents of the file. 1554 * @throws java.io.IOException if the URL cannot be read 1555 */ 1556 public String readURL(@Nonnull URL url) throws IOException { 1557 try { 1558 try (InputStreamReader in = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8); 1559 BufferedReader reader = new BufferedReader(in)) { 1560 return reader.lines().collect(Collectors.joining("\n")); // NOI18N 1561 } 1562 } catch (NullPointerException ex) { 1563 return null; 1564 } 1565 } 1566 1567 /** 1568 * Replaces most non-alphanumeric characters in name with an underscore. 1569 * 1570 * @param name The filename to be sanitized. 1571 * @return The sanitized filename. 1572 */ 1573 @Nonnull 1574 public String sanitizeFilename(@Nonnull String name) { 1575 name = name.trim().replaceAll(" ", "_").replaceAll("[.]+", "."); 1576 StringBuilder filename = new StringBuilder(); 1577 for (char c : name.toCharArray()) { 1578 if (c == '.' || Character.isJavaIdentifierPart(c)) { 1579 filename.append(c); 1580 } 1581 } 1582 return filename.toString(); 1583 } 1584 1585 /** 1586 * Create a directory if required. Any parent directories will also be 1587 * created. 1588 * 1589 * @param path directory to create 1590 */ 1591 public void createDirectory(@Nonnull String path) { 1592 this.createDirectory(new File(path)); 1593 } 1594 1595 /** 1596 * Create a directory if required. Any parent directories will also be 1597 * created. 1598 * 1599 * @param dir directory to create 1600 */ 1601 public void createDirectory(@Nonnull File dir) { 1602 if (!dir.exists()) { 1603 log.debug("Creating directory: {}", dir); 1604 if (!dir.mkdirs()) { 1605 log.error("Failed to create directory: {}", dir); 1606 } 1607 } 1608 } 1609 1610 /** 1611 * Recursively delete a path. It is recommended to use 1612 * {@link java.nio.file.Files#delete(java.nio.file.Path)} or 1613 * {@link java.nio.file.Files#deleteIfExists(java.nio.file.Path)} for files. 1614 * 1615 * @param path path to delete 1616 * @return true if path was deleted, false otherwise 1617 */ 1618 public boolean delete(@Nonnull File path) { 1619 if (path.isDirectory()) { 1620 File[] files = path.listFiles(); 1621 if (files != null) { 1622 for (File file : files) { 1623 this.delete(file); 1624 } 1625 } 1626 } 1627 return path.delete(); 1628 } 1629 1630 /** 1631 * Copy a file or directory. It is recommended to use 1632 * {@link java.nio.file.Files#copy(java.nio.file.Path, java.io.OutputStream)} 1633 * for files. 1634 * 1635 * @param source the file or directory to copy 1636 * @param dest must be the file or directory, not the containing directory 1637 * @throws java.io.IOException if file cannot be copied 1638 */ 1639 public void copy(@Nonnull File source, @Nonnull File dest) throws IOException { 1640 if (!source.exists()) { 1641 log.error("Attempting to copy non-existant file: {}", source); 1642 return; 1643 } 1644 if (!dest.exists()) { 1645 if (source.isDirectory()) { 1646 boolean ok = dest.mkdirs(); 1647 if (!ok) { 1648 throw new IOException("Could not use mkdirs to create destination directory"); 1649 } 1650 } else { 1651 boolean ok = dest.createNewFile(); 1652 if (!ok) { 1653 throw new IOException("Could not create destination file"); 1654 } 1655 } 1656 } 1657 Path srcPath = source.toPath(); 1658 Path dstPath = dest.toPath(); 1659 if (source.isDirectory()) { 1660 Files.walkFileTree(srcPath, new SimpleFileVisitor<Path>() { 1661 @Override 1662 public FileVisitResult preVisitDirectory(final Path dir, 1663 final BasicFileAttributes attrs) throws IOException { 1664 Files.createDirectories(dstPath.resolve(srcPath.relativize(dir))); 1665 return FileVisitResult.CONTINUE; 1666 } 1667 1668 @Override 1669 public FileVisitResult visitFile(final Path file, 1670 final BasicFileAttributes attrs) throws IOException { 1671 Files.copy(file, dstPath.resolve(srcPath.relativize(file)), StandardCopyOption.REPLACE_EXISTING); 1672 return FileVisitResult.CONTINUE; 1673 } 1674 }); 1675 } else { 1676 Files.copy(source.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING); 1677 } 1678 } 1679 1680 /** 1681 * Simple helper method to just append a text string to the end of the given 1682 * filename. The file will be created if it does not exist. 1683 * 1684 * @param file File to append text to 1685 * @param text Text to append 1686 * @throws java.io.IOException if file cannot be written to 1687 */ 1688 public void appendTextToFile(@Nonnull File file, @Nonnull String text) throws IOException { 1689 try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file, true), StandardCharsets.UTF_8))) { 1690 pw.println(text); 1691 } 1692 } 1693 1694 /** 1695 * Backup a file. The backup is in the same location as the original file, 1696 * has the extension <code>.bak</code> appended to the file name, and up to 1697 * four revisions are retained. The lowest numbered revision is the most 1698 * recent. 1699 * 1700 * @param file the file to backup 1701 * @throws java.io.IOException if a backup cannot be created 1702 */ 1703 public void backup(@Nonnull File file) throws IOException { 1704 this.rotate(file, 4, "bak"); 1705 } 1706 1707 /** 1708 * Rotate a file and its backups, retaining only a set number of backups. 1709 * 1710 * @param file the file to rotate 1711 * @param max maximum number of backups to retain 1712 * @param extension The extension to use for the rotations. If null or an 1713 * empty string, the rotation number is used as the 1714 * extension. 1715 * @throws java.io.IOException if a backup cannot be created 1716 * @throws IllegalArgumentException if max is less than one 1717 * @see #backup(java.io.File) 1718 */ 1719 public void rotate(@Nonnull File file, int max, @CheckForNull String extension) throws IOException { 1720 if (max < 1) { 1721 throw new IllegalArgumentException(); 1722 } 1723 String name = file.getName(); 1724 if (extension != null) { 1725 if (extension.length() > 0 && !extension.startsWith(".")) { 1726 extension = "." + extension; 1727 } 1728 } else { 1729 extension = ""; 1730 } 1731 File dir = file.getParentFile(); 1732 File source; 1733 int i = max; 1734 while (i > 1) { 1735 source = new File(dir, name + "." + (i - 1) + extension); 1736 if (source.exists()) { 1737 this.copy(source, new File(dir, name + "." + i + extension)); 1738 } 1739 i--; 1740 } 1741 this.copy(file, new File(dir, name + "." + i + extension)); 1742 } 1743 1744 /** 1745 * Get the default instance of a FileUtilSupport object. 1746 * <p> 1747 * Unlike most implementations of getDefault(), this does not return an 1748 * object held by {@link jmri.InstanceManager} due to the need for this 1749 * default instance to be available prior to the creation of an 1750 * InstanceManager. 1751 * 1752 * @return the default FileUtilSupport instance, creating it if necessary 1753 */ 1754 public static FileUtilSupport getDefault() { 1755 if (FileUtilSupport.defaultInstance == null) { 1756 FileUtilSupport.defaultInstance = new FileUtilSupport(); 1757 } 1758 return FileUtilSupport.defaultInstance; 1759 } 1760 1761 /** 1762 * Get the canonical path for a portable path. There are nine cases: 1763 * <ul> 1764 * <li>Starts with "resource:", treat the rest as a pathname relative to the 1765 * program directory (deprecated; see "program:" below)</li> 1766 * <li>Starts with "program:", treat the rest as a relative pathname below 1767 * the program directory</li> 1768 * <li>Starts with "preference:", treat the rest as a relative path below 1769 * the user's files directory</li> 1770 * <li>Starts with "settings:", treat the rest as a relative path below the 1771 * JMRI system preferences directory</li> 1772 * <li>Starts with "home:", treat the rest as a relative path below the 1773 * user.home directory</li> 1774 * <li>Starts with "file:", treat the rest as a relative path below the 1775 * resource directory in the preferences directory (deprecated; see 1776 * "preference:" above)</li> 1777 * <li>Starts with "profile:", treat the rest as a relative path below the 1778 * profile directory as specified in the 1779 * active{@link jmri.profile.Profile}</li> 1780 * <li>Starts with "scripts:", treat the rest as a relative path below the 1781 * scripts directory</li> 1782 * <li>Otherwise, treat the name as a relative path below the program 1783 * directory</li> 1784 * </ul> 1785 * In any case, absolute pathnames will work. 1786 * 1787 * @param path The name string, possibly starting with file:, home:, 1788 * profile:, program:, preference:, scripts:, settings, or 1789 * resource: 1790 * @return Canonical path to use, or null if one cannot be found. 1791 * @since 2.7.2 1792 */ 1793 private String pathFromPortablePath(@CheckForNull Profile profile, @Nonnull String path) { 1794 // As this method is called in Log4J setup, should not 1795 // contain standard logging statements. 1796 if (path.startsWith(PROGRAM)) { 1797 if (new File(path.substring(PROGRAM.length())).isAbsolute()) { 1798 path = path.substring(PROGRAM.length()); 1799 } else { 1800 path = path.replaceFirst(PROGRAM, Matcher.quoteReplacement(this.getProgramPath())); 1801 } 1802 } else if (path.startsWith(PREFERENCES)) { 1803 if (new File(path.substring(PREFERENCES.length())).isAbsolute()) { 1804 path = path.substring(PREFERENCES.length()); 1805 } else { 1806 path = path.replaceFirst(PREFERENCES, Matcher.quoteReplacement(this.getUserFilesPath(profile))); 1807 } 1808 } else if (path.startsWith(PROFILE)) { 1809 if (new File(path.substring(PROFILE.length())).isAbsolute()) { 1810 path = path.substring(PROFILE.length()); 1811 } else { 1812 path = path.replaceFirst(PROFILE, Matcher.quoteReplacement(this.getProfilePath(profile))); 1813 } 1814 } else if (path.startsWith(SCRIPTS)) { 1815 if (new File(path.substring(SCRIPTS.length())).isAbsolute()) { 1816 path = path.substring(SCRIPTS.length()); 1817 } else { 1818 path = path.replaceFirst(SCRIPTS, Matcher.quoteReplacement(this.getScriptsPath(profile))); 1819 } 1820 } else if (path.startsWith(SETTINGS)) { 1821 if (new File(path.substring(SETTINGS.length())).isAbsolute()) { 1822 path = path.substring(SETTINGS.length()); 1823 } else { 1824 path = path.replaceFirst(SETTINGS, Matcher.quoteReplacement(this.getPreferencesPath())); 1825 } 1826 } else if (path.startsWith(HOME)) { 1827 if (new File(path.substring(HOME.length())).isAbsolute()) { 1828 path = path.substring(HOME.length()); 1829 } else { 1830 path = path.replaceFirst(HOME, Matcher.quoteReplacement(this.getHomePath())); 1831 } 1832 } else if (!new File(path).isAbsolute()) { 1833 return null; 1834 } 1835 try { 1836 // if path cannot be converted into a canonical path, return null 1837 return new File(path.replace(SEPARATOR, File.separatorChar)).getCanonicalPath(); 1838 } catch (IOException ex) { 1839 System.err.println("Cannot convert " + path + " into a usable filename. " + ex.getMessage()); 1840 return null; 1841 } 1842 } 1843 1844}