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