001/* Utilities used to manipulate files 002 003 Copyright (c) 2004-2017 The Regents of the University of California. 004 All rights reserved. 005 Permission is hereby granted, without written agreement and without 006 license or royalty fees, to use, copy, modify, and distribute this 007 software and its documentation for any purpose, provided that the above 008 copyright notice and the following two paragraphs appear in all copies 009 of this software. 010 011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 015 SUCH DAMAGE. 016 017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 022 ENHANCEMENTS, OR MODIFICATIONS. 023 024 PT_COPYRIGHT_VERSION_2 025 COPYRIGHTENDKEY 026 027 */ 028package ptolemy.util; 029 030import java.io.BufferedInputStream; 031import java.io.BufferedOutputStream; 032import java.io.BufferedReader; 033import java.io.ByteArrayOutputStream; 034import java.io.File; 035import java.io.FileNotFoundException; 036import java.io.FileOutputStream; 037import java.io.FileWriter; 038import java.io.IOException; 039import java.io.InputStream; 040import java.io.InputStreamReader; 041import java.io.OutputStream; 042import java.io.PrintWriter; 043import java.io.Writer; 044import java.net.HttpURLConnection; 045import java.net.JarURLConnection; 046import java.net.URI; 047import java.net.URL; 048import java.net.URLConnection; 049import java.nio.charset.Charset; 050import java.nio.file.Files; 051import java.nio.file.Path; 052import java.util.Arrays; 053import java.util.Enumeration; 054import java.util.List; 055import java.util.jar.JarEntry; 056import java.util.jar.JarFile; 057import java.util.zip.ZipEntry; 058 059// Avoid importing any packages from ptolemy.* here so that we 060// can ship Ptplot. 061/////////////////////////////////////////////////////////////////// 062//// FileUtilities 063 064/** 065 A collection of utilities for manipulating files 066 These utilities do not depend on any other ptolemy.* packages. 067 068 @author Christopher Brooks 069 @version $Id$ 070 @since Ptolemy II 4.0 071 @Pt.ProposedRating Green (cxh) 072 @Pt.AcceptedRating Green (cxh) 073 */ 074public class FileUtilities { 075 /** Instances of this class cannot be created. 076 */ 077 private FileUtilities() { 078 } 079 080 /////////////////////////////////////////////////////////////////// 081 //// public methods //// 082 083 /** Copy sourceURL to destinationFile without doing any byte conversion. 084 * @param sourceURL The source URL 085 * @param destinationFile The destination File. 086 * @return true if the file was copied, false if the file was not 087 * copied because the sourceURL and the destinationFile refer to the 088 * same file. 089 * @exception IOException If the source file does not exist. 090 */ 091 public static boolean binaryCopyURLToFile(URL sourceURL, 092 File destinationFile) throws IOException { 093 URL destinationURL = destinationFile.getCanonicalFile().toURI().toURL(); 094 095 if (sourceURL.sameFile(destinationURL)) { 096 return false; 097 } 098 099 // If sourceURL is of the form file:./foo, then we need to try again. 100 File sourceFile = new File(sourceURL.getFile()); 101 102 // If the sourceURL is not a jar URL, then check to see if we 103 // have the same file. 104 // FIXME: should we check for !/ and !\ everywhere? 105 if (sourceFile.getPath().indexOf("!/") == -1 106 && sourceFile.getPath().indexOf("!\\") == -1) { 107 try { 108 if (sourceFile.getCanonicalFile().toURI().toURL() 109 .sameFile(destinationURL)) { 110 return false; 111 } 112 } catch (IOException ex) { 113 // JNLP Jar urls sometimes throw an exception here. 114 // IOException constructor does not take a cause 115 IOException ioException = new IOException( 116 "Cannot find canonical file name of '" + sourceFile 117 + "'"); 118 ioException.initCause(ex); 119 throw ioException; 120 } 121 } 122 123 URLConnection sourceURLConnection = null; 124 InputStream sourceURLInputStream = null; 125 try { 126 sourceURLConnection = sourceURL.openConnection(); 127 if (sourceURLConnection == null) { 128 throw new IOException( 129 "Failed to open a connection on " + sourceURL); 130 } 131 sourceURLInputStream = sourceURLConnection.getInputStream(); 132 if (sourceURLInputStream == null) { 133 throw new IOException( 134 "Failed to open a stream on " + sourceURLConnection); 135 } 136 137 if (!(sourceURLConnection instanceof JarURLConnection)) { 138 _binaryCopyStream(sourceURLInputStream, destinationFile); 139 } else { 140 JarURLConnection jarURLConnection = (JarURLConnection) sourceURLConnection; 141 JarEntry jarEntry = jarURLConnection.getJarEntry(); 142 if (jarEntry != null && !jarEntry.isDirectory()) { 143 // Simply copying a file. 144 _binaryCopyStream(sourceURLInputStream, destinationFile); 145 } else { 146 // It is a directory if jarEntry == null, a Jar file. 147 _binaryCopyDirectory(jarURLConnection, destinationFile); 148 } 149 } 150 } finally { 151 if (sourceURLConnection != null) { 152 // Work around 153 // "JarUrlConnection.getInputStream().close() throws 154 // NPE when entry is a directory" 155 // https://bugs.openjdk.java.net/browse/JDK-8080094 156 if (sourceURLConnection instanceof JarURLConnection) { 157 JarURLConnection jar = (JarURLConnection) sourceURLConnection; 158 if (jar.getUseCaches()) { 159 jar.getJarFile().close(); 160 } 161 } else { 162 if (sourceURLInputStream != null) { 163 sourceURLInputStream.close(); 164 } 165 } 166 } 167 } 168 169 return true; 170 } 171 172 /** Read a sourceURL without doing any byte conversion. 173 * @param sourceURL The source URL 174 * @return The array of bytes read from the URL. 175 * @exception IOException If the source URL does not exist. 176 */ 177 public static byte[] binaryReadURLToByteArray(URL sourceURL) 178 throws IOException { 179 return _binaryReadStream(sourceURL.openStream()); 180 } 181 182 /** Create a link. 183 * @param newLink the link to be created 184 * @param temporary the path to the temporary location where the directory to be replaced by the link should be placed. 185 * @param target the target of the link to be created. 186 * @exception IOException If there are problems creating the link 187 * @return A status message 188 */ 189 public static String createLink(Path newLink, Path temporary, Path target) 190 throws IOException { 191 // 192 // Path currentRelativePath = Paths.get("."); 193 // throw new IOException(newLink + " does not exist? That directory should be " 194 // + " in the jar file so that we can move it aside. " 195 // + "The current relative path is " 196 // + currentRelativePath.toAbsolutePath()); 197 // } 198 199 String results = ""; 200 if (Files.isSymbolicLink(newLink)) { 201 if (Files.isSameFile(target, Files.readSymbolicLink(newLink))) { 202 return "FileUtilities.java: createLink(): " + target + " and " 203 + Files.readSymbolicLink(newLink) + " are the same."; 204 } 205 results = "FileUtilities.java: createLink(): " + target + " and " 206 + Files.readSymbolicLink(newLink) + " were not the same."; 207 } else { 208 results = "FileUtilities.java: createLink(): " + newLink 209 + " is not a link."; 210 } 211 212 boolean moveBack = false; 213 if (Files.isReadable(newLink)) { 214 try { 215 // Save the directory that will be replaced by the link. 216 // System.out.println("Moving " + newLink + " to " + temporary); 217 Files.move(newLink, temporary); 218 } catch (Throwable throwable) { 219 IOException exception = new IOException( 220 "Could not move " + newLink + " to " + temporary); 221 exception.initCause(throwable); 222 throw exception; 223 } 224 moveBack = true; 225 } 226 227 try { 228 Files.createSymbolicLink(newLink, target); 229 } catch (IOException ex) { 230 String message = "Failed to create symbolic link from " + newLink 231 + " to " + target + ": " + ex; 232 if (moveBack) { 233 try { 234 // System.out.println("Moving " + temporary + " to " + newLink); 235 Files.move(temporary, newLink); 236 237 } catch (Throwable throwable) { 238 message += " In addition, could not move " + temporary 239 + " back to " + newLink + ": " + throwable; 240 } 241 } 242 IOException exception = new IOException(message); 243 exception.initCause(ex); 244 throw exception; 245 } catch (UnsupportedOperationException ex2) { 246 try { 247 // System.out.println("Creating link from " + newLink + " to " + temporary); 248 Files.createLink(newLink, target); 249 } catch (Throwable ex3) { 250 String message = "Failed to create a hard link from " + newLink 251 + " to " + target + ": " + ex3; 252 253 if (moveBack) { 254 try { 255 // System.out.println("Moving " + temporary + " to " + newLink); 256 Files.move(temporary, newLink); 257 } catch (Throwable throwable) { 258 message += " In addition, could not move " + temporary 259 + " back to " + newLink + ": " + throwable; 260 } 261 } 262 263 IOException exception = new IOException(message); 264 exception.initCause(ex3); 265 throw exception; 266 } 267 } 268 269 if (moveBack) { 270 try { 271 // System.out.println("Deleting " + temporary); 272 FileUtilities.deleteDirectory(temporary.toFile()); 273 } catch (Throwable throwable) { 274 IOException exception = new IOException( 275 "Failed to delete " + temporary); 276 exception.initCause(throwable); 277 throw exception; 278 } 279 } 280 return results + " Link successfully created."; 281 } 282 283 /** Delete a directory. 284 * @param directory the File naming the directory. 285 * @return true if the toplevel directory was deleted or does not 286 * exist. 287 */ 288 static public boolean deleteDirectory(File directory) { 289 boolean deletedAllFiles = true; 290 if (!directory.exists()) { 291 return true; 292 } else { 293 if (Files.isSymbolicLink(directory.toPath())) { 294 if (!directory.delete()) { 295 deletedAllFiles = false; 296 } 297 } else { 298 File[] files = directory.listFiles(); 299 if (files != null) { 300 for (int i = 0; i < files.length; i++) { 301 if (files[i].isDirectory() 302 && !Files.isSymbolicLink(files[i].toPath())) { 303 deleteDirectory(files[i]); 304 } else { 305 if (!files[i].delete()) { 306 deletedAllFiles = false; 307 } 308 } 309 } 310 } 311 } 312 } 313 return directory.delete() && deletedAllFiles; 314 } 315 316 /** 317 * Delete a directory and all of its content. 318 * @param filepath The path for the directory or file to be deleted. 319 * @return false if one or more files or directories cannot be deleted. 320 */ 321 public static boolean deleteDirectory(String filepath) { 322 return FileUtilities.deleteDirectory(new File(filepath)); 323 } 324 325 /** Extract a jar file into a directory. This is a trivial 326 * implementation of the <code>jar -xf</code> command. 327 * @param jarFileName The name of the jar file to extract 328 * @param directoryName The name of the directory. If this argument 329 * is null, then the files are extracted in the current directory. 330 * @exception IOException If the jar file cannot be opened, or 331 * if there are problems extracting the contents of the jar file 332 */ 333 public static void extractJarFile(String jarFileName, String directoryName) 334 throws IOException { 335 JarFile jarFile = null; 336 try { 337 jarFile = new JarFile(jarFileName); 338 Enumeration entries = jarFile.entries(); 339 while (entries.hasMoreElements()) { 340 JarEntry jarEntry = (JarEntry) entries.nextElement(); 341 File destinationFile = new File(directoryName, 342 jarEntry.getName()); 343 if (jarEntry.isDirectory()) { 344 if (!destinationFile.isDirectory() 345 && !destinationFile.mkdirs()) { 346 throw new IOException("Warning, failed to create " 347 + "directory for \"" + destinationFile + "\"."); 348 } 349 } else { 350 InputStream jarInputStream = null; 351 try { 352 jarInputStream = jarFile.getInputStream(jarEntry); 353 _binaryCopyStream(jarInputStream, destinationFile); 354 } finally { 355 if (jarInputStream != null) { 356 jarInputStream.close(); 357 } 358 } 359 } 360 } 361 } finally { 362 if (jarFile != null) { 363 jarFile.close(); 364 } 365 } 366 } 367 368 /** If necessary, unjar the entire jar file that contains a target 369 * file. 370 * 371 * @param targetFileName If the file exists relative to the 372 * directoryName, then do nothing. Otherwise, look for the 373 * targetFile in the classpath and unjar the jar file in which it 374 * is found in the directory named by the <i>directoryName</i> 375 * parameter. 376 * @param directoryName The name of the directory in which to 377 * search for the file named by the <i>targetFileName</i> 378 * parameter and in which the jar file is possibly unjared. 379 * @exception IOException If there is problem finding the target 380 * file or extracting the jar file. 381 */ 382 public static void extractJarFileIfNecessary(String targetFileName, 383 String directoryName) throws IOException { 384 File targetFile = new File( 385 directoryName + File.separator + targetFileName); 386 if (targetFile.exists()) { 387 return; 388 } else { 389 URL targetFileURL = FileUtilities 390 .nameToURL("$CLASSPATH/" + targetFileName, null, null); 391 if (targetFileURL == null) { 392 throw new FileNotFoundException("Could not find " 393 + targetFileName + " as a file or in the CLASSPATH."); 394 } 395 396 String targetFileURLName = targetFileURL.toString(); 397 // Remove the jar:file: and everything after !/ 398 String jarFileName = targetFileURLName.substring(9, 399 targetFileURLName.indexOf("!/")); 400 FileUtilities.extractJarFile(jarFileName, directoryName); 401 targetFile = new File( 402 directoryName + File.separator + targetFileName); 403 if (!targetFile.exists()) { 404 throw new FileNotFoundException("Could not find " 405 + targetFileName + " after extracting " + jarFileName); 406 } 407 } 408 } 409 410 /** Given a URL, if it starts with http, the follow up to 10 redirects. 411 * 412 * <p>If the URL is null or does not start with "http", then return the 413 * URL.</p> 414 * 415 * @param url The URL to be followed. 416 * @return The new URL if any. 417 * @exception IOException If there is a problem opening the URL or 418 * if there are more than 10 redirects. 419 */ 420 public static URL followRedirects(URL url) throws IOException { 421 422 if (url == null || !url.getProtocol().startsWith("http")) { 423 return url; 424 } 425 URL temporaryURL = url; 426 int count; 427 for (count = 0; count < 10; count++) { 428 HttpURLConnection connection = (HttpURLConnection) temporaryURL 429 .openConnection(); 430 connection.setConnectTimeout(15000); 431 connection.setReadTimeout(15000); 432 connection.setInstanceFollowRedirects(false); 433 434 switch (connection.getResponseCode()) { 435 case HttpURLConnection.HTTP_MOVED_PERM: 436 case HttpURLConnection.HTTP_MOVED_TEMP: 437 String location = connection.getHeaderField("Location"); 438 // Handle relative URLs. 439 temporaryURL = new URL(temporaryURL, location); 440 continue; 441 } 442 443 connection.disconnect(); 444 return temporaryURL; 445 } 446 throw new IOException("Failed to resolve " + url 447 + " after 10 attempts. The last url was " + temporaryURL); 448 449 } 450 451 /** Return the string contents of the file at the specified location. 452 * @param path The location. 453 * @return The contents as a string, assuming the default encoding of 454 * this JVM (probably utf-8). 455 * @exception IOException If the file cannot be read. 456 */ 457 public static String getFileAsString(String path) throws IOException { 458 // Use nameToURL so that we look in the classpath for jar files 459 // that might contain the resource. 460 URL url = FileUtilities.nameToURL(path, null, null); 461 byte[] encoded = FileUtilities.binaryReadURLToByteArray(url); 462 return new String(encoded, Charset.defaultCharset()); 463 } 464 465 /** Return true if the command can be found in the directories 466 * listed in the directories contained in the PATH environment 467 * variable. 468 * @param command The command for which to search. 469 * @return True if the command can be found in $PATH 470 */ 471 public static boolean inPath(String command) { 472 String path = System.getenv("PATH"); 473 List<String> directories = Arrays 474 .asList(path.split(File.pathSeparator)); 475 for (String directory : directories) { 476 File file = new File(directory, command); 477 if (file.exists() && file.canExecute()) { 478 return true; 479 } 480 } 481 return false; 482 } 483 484 /** Extract the contents of a jar file. 485 * @param args An array of arguments. The first argument 486 * names the jar file to be extracted. The first argument 487 * is required. The second argument names the directory in 488 * which to extract the files from the jar file. The second 489 * argument is optional. 490 */ 491 public static void main(String[] args) { 492 if (args.length < 1 || args.length > 2) { 493 System.err.println("Usage: java -classpath $PTII " 494 + "ptolemy.util.FileUtilities jarFile [directory]\n" 495 + "where jarFile is the name of the jar file\n" 496 + "and directory is the optional directory in which to " 497 + "extract."); 498 StringUtilities.exit(2); 499 } 500 String jarFileName = args[0]; 501 String directoryName = null; 502 if (args.length >= 2) { 503 directoryName = args[1]; 504 } 505 try { 506 extractJarFile(jarFileName, directoryName); 507 } catch (Throwable throwable) { 508 System.err.println("Failed to extract \"" + jarFileName + "\""); 509 throwable.printStackTrace(); 510 StringUtilities.exit(3); 511 } 512 } 513 514 /** Given a file name or URL, construct a java.io.File object that 515 * refers to the file name or URL. This method 516 * first attempts to directly use the file name to construct the 517 * File. If the resulting File is a relative pathname, then 518 * it is resolved relative to the specified base URI, if 519 * there is one. If there is no such base URI, then it simply 520 * returns the relative File object. See the java.io.File 521 * documentation for a details about relative and absolute pathnames. 522 * 523 * <p>If the name begins with 524 * "xxxxxxCLASSPATHxxxxxx" or "$CLASSPATH" then search for the 525 * file relative to the classpath. 526 * 527 * <p>Note that "xxxxxxCLASSPATHxxxxxx" is the value of the 528 * globally defined constant $CLASSPATH available in the Ptolemy 529 * II expression language. 530 * 531 * <p>If the name begins with $CLASSPATH or "xxxxxxCLASSPATHxxxxxx" 532 * but that name cannot be found in the classpath, the value 533 * of the ptolemy.ptII.dir property is substituted in. 534 * <p> 535 * The file need not exist for this method to succeed. Thus, 536 * this method can be used to determine whether a file with a given 537 * name exists, prior to calling openForWriting(), for example. 538 * 539 * <p>This method is similar to 540 * {@link #nameToURL(String, URI, ClassLoader)} 541 * except that in this method, the file or URL must be readable. 542 * Usually, this method is use for write a file and 543 * {@link #nameToURL(String, URI, ClassLoader)} is used for reading. 544 * 545 * @param name The file name or URL. 546 * @param base The base for relative URLs. 547 * @return A File, or null if the filename argument is null or 548 * an empty string. 549 * @see #nameToURL(String, URI, ClassLoader) 550 */ 551 public static File nameToFile(String name, URI base) { 552 if (name == null || name.trim().equals("")) { 553 return null; 554 } 555 556 if (name.startsWith(_CLASSPATH_VALUE) 557 || name.startsWith("$CLASSPATH")) { 558 URL result = null; 559 try { 560 result = _searchClassPath(name, null); 561 } catch (IOException ex) { 562 // Ignore. In nameToFile(), it is ok if we don't find the variable 563 } 564 if (result != null) { 565 return new File(result.getPath()); 566 } else { 567 String ptII = StringUtilities.getProperty("ptolemy.ptII.dir"); 568 if (ptII != null && ptII.length() > 0) { 569 return new File(ptII, _trimClassPath(name)); 570 } 571 } 572 } 573 574 File file = new File(name); 575 576 if (!file.isAbsolute()) { 577 // Try to resolve the base directory. 578 if (base != null) { 579 // Need to replace \ with /, otherwise resolve would fail even 580 // if invoked in a windows OS. -- tfeng (02/27/2009) 581 URI newURI = base.resolve(StringUtilities 582 .substitute(name, " ", "%20").replace('\\', '/')); 583 584 //file = new File(newURI); 585 String urlString = newURI.getPath(); 586 file = new File( 587 StringUtilities.substitute(urlString, "%20", " ")); 588 } 589 } 590 return file; 591 } 592 593 /** Given a file or URL name, return as a URL. If the file name 594 * is relative, then it is interpreted as being relative to the 595 * specified base directory. If the name begins with 596 * "xxxxxxCLASSPATHxxxxxx" or "$CLASSPATH" then search for the 597 * file relative to the classpath. 598 * 599 * <p>Note that "xxxxxxCLASSPATHxxxxxx" is the value of the 600 * globally defined constant $CLASSPATH available in the Ptolemy 601 * II expression language. 602 * II expression language. 603 * 604 * <p>If no file is found, then throw an exception. 605 * 606 * <p>This method is similar to {@link #nameToFile(String, URI)} 607 * except that in this method, the file or URL must be readable. 608 * Usually, this method is use for reading a file and 609 * is used for writing {@link #nameToFile(String, URI)}. 610 * 611 * @param name The name of a file or URL. 612 * @param baseDirectory The base directory for relative file names, 613 * or null to specify none. 614 * @param classLoader The class loader to use to locate system 615 * resources, or null to use the system class loader that was used 616 * to load this class. 617 * @return A URL, or null if the name is null or the empty string. 618 * @exception IOException If the file cannot be read, or 619 * if the file cannot be represented as a URL (e.g. System.in), or 620 * the name specification cannot be parsed. 621 * @see #nameToFile(String, URI) 622 */ 623 public static URL nameToURL(String name, URI baseDirectory, 624 ClassLoader classLoader) throws IOException { 625 if (name == null || name.trim().equals("")) { 626 return null; 627 } 628 629 if (name.startsWith(_CLASSPATH_VALUE) 630 || name.startsWith("$CLASSPATH")) { 631 if (name.contains("#")) { 632 name = name.substring(0, name.indexOf("#")); 633 } 634 635 URL result = _searchClassPath(name, classLoader); 636 if (result == null) { 637 throw new IOException("Cannot find file '" 638 + _trimClassPath(name) + "' in classpath"); 639 } 640 641 return result; 642 } 643 644 File file = new File(name); 645 646 // Be careful here, we need to be sure that we are reading 647 // relative to baseDirectory if baseDirectory is not null. 648 649 // The security tests rely on baseDirectory, to replicate: 650 // (cd $PTII/ptII/ptolemy/actor/lib/security/test; rm rm foo.keystore auto/foo.keystore; make) 651 652 if (file.isAbsolute() || (file.canRead() && baseDirectory == null)) { 653 // If the URL has a "fragment" (also called a reference), which is 654 // a pointer into the file, we have to strip that off before we 655 // get the file, and the reinsert it before returning the URL. 656 String fragment = null; 657 if (!file.canRead()) { 658 659 // FIXME: Need to strip off the fragment part 660 // (the "reference") of the name (after the #), 661 // if there is one, and add it in again by calling set() 662 // on the URL at the end. 663 String[] splitName = name.split("#"); 664 if (splitName.length > 1) { 665 name = splitName[0]; 666 fragment = splitName[1]; 667 } 668 669 // FIXME: This is a hack. 670 // Expanding the configuration with Ptolemy II installed 671 // in a directory with spaces in the name fails on 672 // JAIImageReader because PtolemyII.jpg is passed in 673 // to this method as C:\Program%20Files\Ptolemy\... 674 file = new File(StringUtilities.substitute(name, "%20", " ")); 675 676 URL possibleJarURL = null; 677 678 if (!file.canRead()) { 679 // ModelReference and FilePortParameters sometimes 680 // have paths that have !/ in them. 681 possibleJarURL = ClassUtilities.jarURLEntryResource(name); 682 683 if (possibleJarURL != null) { 684 file = new File(possibleJarURL.getFile()); 685 } 686 } 687 688 if (!file.canRead()) { 689 throw new IOException("Cannot read file '" + name + "' or '" 690 + StringUtilities.substitute(name, "%20", " ") + "'" 691 + (possibleJarURL == null ? "" 692 : " or '" + possibleJarURL.getFile() + "")); 693 } 694 } 695 696 URL result = file.toURI().toURL(); 697 if (fragment != null) { 698 result = new URL(result.toString() + "#" + fragment); 699 } 700 return result; 701 } else { 702 // Try relative to the base directory. 703 if (baseDirectory != null) { 704 // Try to resolve the URI. 705 URI newURI; 706 707 try { 708 newURI = baseDirectory.resolve(name); 709 } catch (Exception ex) { 710 // FIXME: Another hack 711 // This time, if we try to open some of the JAI 712 // demos that have actors that have defaults FileParameters 713 // like "$PTII/doc/img/PtolemyII.jpg", then resolve() 714 // bombs. 715 String name2 = StringUtilities.substitute(name, "%20", " "); 716 try { 717 newURI = baseDirectory.resolve(name2); 718 name = name2; 719 } catch (Exception ex2) { 720 IOException io = new IOException( 721 "Problem with URI format in '" + name + "'. " 722 + "and '" + name2 + "' " 723 + "This can happen if the file name " 724 + "is not absolute " 725 + "and is not present relative to the " 726 + "directory in which the specified model " 727 + "was read (which was '" 728 + baseDirectory + "')"); 729 io.initCause(ex2); 730 throw io; 731 } 732 } 733 734 String urlString = newURI.toString(); 735 736 try { 737 // Adding another '/' for remote execution. 738 if (newURI.getScheme() != null 739 && newURI.getAuthority() == null) { 740 // Change from Efrat: 741 // "I made these change to allow remote 742 // execution of a workflow from within a web 743 // service." 744 745 // "The first modification was due to a URI 746 // authentication exception when trying to 747 // create a file object from a URI on the 748 // remote side. The second modification was 749 // due to the file protocol requirements to 750 // use 3 slashes, 'file:///' on the remote 751 // side, although it would be probably be a 752 // good idea to also make sure first that the 753 // url string actually represents the file 754 // protocol." 755 urlString = urlString.substring(0, 6) + "//" 756 + urlString.substring(6); 757 758 //} else { 759 // urlString = urlString.substring(0, 6) + "/" 760 // + urlString.substring(6); 761 } 762 // Unfortunately, between Java 1.5 and 1.6, 763 // The URL constructor changed. 764 // In 1.5, new URL("file:////foo").toString() 765 // returns "file://foo" 766 // In 1.6, new URL("file:////foo").toString() 767 // return "file:////foo". 768 // See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6561321 769 return new URL(urlString); 770 } catch (Exception ex3) { 771 try { 772 // Under Webstart, opening 773 // hoc/demo/ModelReference/ModelReference.xml 774 // requires this because the URL is relative. 775 return new URL(baseDirectory.toURL(), urlString); 776 } catch (Exception ex4) { 777 778 try { 779 // Under Webstart, ptalon, EightChannelFFT 780 // requires this. 781 return new URL(baseDirectory.toURL(), 782 newURI.toString()); 783 } catch (Exception ex5) { 784 // Ignore 785 } 786 787 IOException io = new IOException( 788 "Problem with URI format in '" + urlString 789 + "'. " + "This can happen if the '" 790 + urlString + "' is not absolute" 791 + " and is not present relative to the directory" 792 + " in which the specified model was read" 793 + " (which was '" + baseDirectory 794 + "')"); 795 io.initCause(ex3); 796 throw io; 797 } 798 } 799 } 800 801 // As a last resort, try an absolute URL. 802 803 URL url = new URL(name); 804 805 // If we call new URL("http", null, /foo); 806 // then we get "http:/foo", which should be "http://foo" 807 // This change suggested by Dan Higgins and Kevin Kruland 808 // See kepler/src/util/URLToLocalFile.java 809 try { 810 String fixedURLAsString = url.toString() 811 .replaceFirst("(https?:)//?", "$1//"); 812 url = new URL(fixedURLAsString); 813 } catch (Exception e) { 814 // Ignore 815 url = new URL(name); 816 } 817 return url; 818 } 819 } 820 821 /** Open the specified file for reading. If the specified name is 822 * "System.in", then a reader from standard in is returned. If 823 * the name begins with "$CLASSPATH" or "xxxxxxCLASSPATHxxxxxx", 824 * then the name is passed to {@link #nameToURL(String, URI, ClassLoader)} 825 * If the file name is not absolute, the it is assumed to be relative to 826 * the specified base URI. 827 * @see #nameToURL(String, URI, ClassLoader) 828 * @param name File name. 829 * @param base The base URI for relative references. 830 * @param classLoader The class loader to use to locate system 831 * resources, or null to use the system class loader that was used 832 * to load this class. 833 * @return If the name is null or the empty string, 834 * then null is returned, otherwise a buffered reader is returned. 835 836 * @exception IOException If the file cannot be opened. 837 */ 838 public static BufferedReader openForReading(String name, URI base, 839 ClassLoader classLoader) throws IOException { 840 if (name == null || name.trim().equals("")) { 841 return null; 842 } 843 844 if (name.trim().equals("System.in")) { 845 if (STD_IN == null) { 846 STD_IN = new BufferedReader(new InputStreamReader(System.in)); 847 } 848 849 return STD_IN; 850 } 851 852 // Not standard input. Try URL mechanism. 853 URL url = nameToURL(name, base, classLoader); 854 855 if (url == null) { 856 throw new IOException("Could not convert \"" + name 857 + "\" with base \"" + base + "\" to a URL."); 858 } 859 860 InputStreamReader inputStreamReader = null; 861 try { 862 inputStreamReader = new InputStreamReader(url.openStream()); 863 } catch (IOException ex) { 864 // Try it as a jar url. 865 // WebStart ptalon MapReduce needs this. 866 try { 867 URL possibleJarURL = ClassUtilities 868 .jarURLEntryResource(url.toString()); 869 if (possibleJarURL != null) { 870 inputStreamReader = new InputStreamReader( 871 possibleJarURL.openStream()); 872 } 873 // If possibleJarURL is null, this throws an exception, 874 // which we ignore and report the first exception (ex) 875 if (inputStreamReader == null) { 876 throw new NullPointerException("Could not open " + url); 877 } else { 878 return new BufferedReader(inputStreamReader); 879 } 880 } catch (Throwable throwable2) { 881 try { 882 if (inputStreamReader != null) { 883 inputStreamReader.close(); 884 } 885 } catch (IOException ex3) { 886 // Ignore 887 } 888 IOException ioException = new IOException( 889 "Failed to open \"" + url + "\"."); 890 ioException.initCause(ex); 891 throw ioException; 892 } 893 } 894 895 return new BufferedReader(inputStreamReader); 896 } 897 898 /** Open the specified file for writing or appending. If the 899 * specified name is "System.out", then a writer to standard out 900 * is returned; otherwise, pass the name and base to {@link 901 * #nameToFile(String, URI)} and create a file writer. If the 902 * file does not exist, then create it. If the file name is not 903 * absolute, the it is assumed to be relative to the specified 904 * base directory. If permitted, this method will return a 905 * Writer that will simply overwrite the contents of the file. It 906 * is up to the user of this method to check whether this is OK 907 * (by first calling {@link #nameToFile(String, URI)} and calling 908 * exists() on the returned value). 909 * 910 * @param name File name. 911 * @param base The base URI for relative references. 912 * @param append If true, then append to the file rather than 913 * overwriting. 914 * @return If the name is null or the empty string, 915 * then null is returned, otherwise a writer is returned. 916 * @exception IOException If the file cannot be opened 917 * or created. 918 */ 919 public static Writer openForWriting(String name, URI base, boolean append) 920 throws IOException { 921 if (name == null || name.trim().equals("")) { 922 return null; 923 } 924 925 if (name.trim().equals("System.out")) { 926 if (STD_OUT == null) { 927 STD_OUT = new PrintWriter(System.out); 928 } 929 930 return STD_OUT; 931 } 932 933 File file = nameToFile(name, base); 934 return new FileWriter(file, append); 935 } 936 937 /** Given a URL, open a stream. 938 * 939 * <p>If the URL starts with "http", then follow up to 10 redirects 940 * and return the the final HttpURLConnection.</p> 941 * 942 * <p>If the URL does not start with "http", then call 943 * URL.openStream().</p> 944 * 945 * @param url The URL to be opened. 946 * @return The input stream 947 * @exception IOException If there is a problem opening the URL or 948 * if there are more than 10 redirects. 949 */ 950 public static InputStream openStreamFollowingRedirects(URL url) 951 throws IOException { 952 return openStreamFollowingRedirectsReturningBoth(url).stream(); 953 } 954 955 /** A class that contains an InputStream and a URL 956 * so that we don't have to follow redirects twice. 957 */ 958 public static class StreamAndURL { 959 /** Create an object containing an InputStream 960 * and a URL. 961 * @param stream The stream. 962 * @param url The url. 963 */ 964 public StreamAndURL(InputStream stream, URL url) { 965 _stream = stream; 966 _url = url; 967 } 968 969 /** Return the stream. 970 * @return The stream. 971 */ 972 public InputStream stream() { 973 return _stream; 974 } 975 976 /** Return the url. 977 * @return The url. 978 */ 979 public URL url() { 980 return _url; 981 } 982 983 private InputStream _stream; 984 private URL _url; 985 } 986 987 /** Given a URL, open a stream and return an object containing 988 * both the inputStream and the possibly redirected URL. 989 * 990 * <p>If the URL starts with "http", then follow up to 10 redirects 991 * and return the the final HttpURLConnection.</p> 992 * 993 * <p>If the URL does not start with "http", then call 994 * URL.openStream().</p> 995 * 996 * @param url The URL to be opened. 997 * @return The input stream 998 * @exception IOException If there is a problem opening the URL or 999 * if there are more than 10 redirects. 1000 */ 1001 public static StreamAndURL openStreamFollowingRedirectsReturningBoth( 1002 URL url) throws IOException { 1003 1004 if (!url.getProtocol().startsWith("http")) { 1005 return new StreamAndURL(url.openStream(), url); 1006 } 1007 1008 // followRedirects() also calls openConnection() and then closes 1009 // the connection with disconnect(). We could duplicate the code 1010 // here, but it seems safer to avoid the duplication. 1011 URL redirectedURL = FileUtilities.followRedirects(url); 1012 return new StreamAndURL(redirectedURL.openStream(), redirectedURL); 1013 } 1014 1015 /** Utility method to read a string from an input stream. 1016 * @param stream The stream. 1017 * @return The string. 1018 * @exception IOException If the stream cannot be read. 1019 */ 1020 public static String readFromInputStream(InputStream stream) 1021 throws IOException { 1022 StringBuffer response = new StringBuffer(); 1023 BufferedReader reader = null; 1024 try { 1025 String line = ""; 1026 // Avoid Coverity Scan: "Dubious method used (FB.DM_DEFAULT_ENCODING)" 1027 reader = new BufferedReader(new InputStreamReader(stream, 1028 java.nio.charset.Charset.defaultCharset())); 1029 1030 String lineBreak = System.getProperty("line.separator"); 1031 while ((line = reader.readLine()) != null) { 1032 response.append(line); 1033 if (!line.endsWith(lineBreak)) { 1034 response.append(lineBreak); 1035 } 1036 } 1037 } finally { 1038 if (reader != null) { 1039 reader.close(); 1040 } 1041 } 1042 return response.toString(); 1043 } 1044 1045 /////////////////////////////////////////////////////////////////// 1046 //// public members //// 1047 1048 /** Standard in as a reader, which will be non-null 1049 * only after a call to openForReading("System.in"). 1050 */ 1051 public static BufferedReader STD_IN = null; 1052 1053 /** Standard out as a writer, which will be non-null 1054 * only after a call to openForWriting("System.out"). 1055 */ 1056 public static PrintWriter STD_OUT = null; 1057 1058 /////////////////////////////////////////////////////////////////// 1059 //// private methods //// 1060 1061 /** Copy a directory in a jar file to a physical directory. 1062 * @param jarURLConnection the connection to the jar file 1063 * @param destinationDirectory The destination directory, which must already exist. 1064 * @exception IOException If there are problems reading, writing or closing. 1065 */ 1066 private static void _binaryCopyDirectory(JarURLConnection jarURLConnection, 1067 File destinationDirectory) throws IOException { 1068 // Get the path of the resource in the jar file 1069 String entryBaseName = jarURLConnection.getEntryName(); 1070 JarFile jarFile = jarURLConnection.getJarFile(); 1071 Enumeration<? extends ZipEntry> jarEntries = jarFile.entries(); 1072 while (jarEntries.hasMoreElements()) { 1073 ZipEntry zipEntry = jarEntries.nextElement(); 1074 String name = zipEntry.getName(); 1075 if (!name.startsWith(entryBaseName)) { 1076 continue; 1077 } 1078 1079 String entryFileName = name.substring(entryBaseName.length()); 1080 File fileOrDirectory = new File(destinationDirectory, 1081 entryFileName); 1082 if (zipEntry.isDirectory()) { 1083 if (!fileOrDirectory.mkdir()) { 1084 throw new IOException( 1085 "Could not create \"" + fileOrDirectory + "\""); 1086 } 1087 } else { 1088 InputStream inputStream = null; 1089 OutputStream outputStream = null; 1090 try { 1091 inputStream = jarFile.getInputStream(zipEntry); 1092 outputStream = new BufferedOutputStream( 1093 new FileOutputStream(fileOrDirectory)); 1094 byte buffer[] = new byte[4096]; 1095 int readCount; 1096 while ((readCount = inputStream.read(buffer)) > 0) { 1097 outputStream.write(buffer, 0, readCount); 1098 } 1099 } finally { 1100 try { 1101 if (outputStream != null) { 1102 outputStream.close(); 1103 } 1104 } finally { 1105 if (inputStream != null) { 1106 inputStream.close(); 1107 } 1108 } 1109 } 1110 } 1111 } 1112 } 1113 1114 /** Copy files safely. If there are problems, the streams are 1115 * close appropriately. 1116 * @param inputStream The input stream. 1117 * @param destinationFile The destination File. 1118 * @exception IOException If the input stream cannot be created 1119 * or read, or * if there is a problem writing to the destination 1120 * file. 1121 */ 1122 private static void _binaryCopyStream(InputStream inputStream, 1123 File destinationFile) throws IOException { 1124 // Copy the source file. 1125 BufferedInputStream input = null; 1126 1127 try { 1128 input = new BufferedInputStream(inputStream); 1129 1130 if (input == null) { 1131 throw new IOException( 1132 "Could not create a BufferedInputStream from \"" 1133 + inputStream 1134 + "\". This can happen if the input " 1135 + "is a JarURL entry that refers to a directory " 1136 + "in the jar file."); 1137 } 1138 1139 BufferedOutputStream output = null; 1140 1141 try { 1142 File parent = destinationFile.getParentFile(); 1143 if (parent != null && !parent.exists()) { 1144 if (!parent.mkdirs()) { 1145 throw new IOException("Failed to create directories " 1146 + "for \"" + parent + "\"."); 1147 } 1148 } 1149 1150 output = new BufferedOutputStream( 1151 new FileOutputStream(destinationFile)); 1152 1153 int c; 1154 1155 try { 1156 while ((c = input.read()) != -1) { 1157 output.write(c); 1158 } 1159 } catch (NullPointerException ex) { 1160 NullPointerException npe = new NullPointerException( 1161 "While reading from \"" + input 1162 + "\" and writing to \"" + output 1163 + "\", a NullPointerException occurred. " 1164 + "This can happen when attempting to read " 1165 + "from a JarURL entry that points to a directory."); 1166 npe.initCause(ex); 1167 throw npe; 1168 } 1169 } finally { 1170 if (output != null) { 1171 try { 1172 output.close(); 1173 } catch (Throwable throwable) { 1174 throw new RuntimeException(throwable); 1175 } 1176 } 1177 } 1178 } finally { 1179 if (input != null) { 1180 try { 1181 input.close(); 1182 } catch (NullPointerException npe) { 1183 // Ignore, see 1184 // Work around 1185 // "JarUrlConnection.getInputStream().close() throws 1186 // NPE when entry is a directory" 1187 // https://bugs.openjdk.java.net/browse/JDK-8080094 1188 } catch (Throwable throwable) { 1189 throw new RuntimeException(throwable); 1190 } 1191 } 1192 } 1193 } 1194 1195 /** Read a stream safely. If there are problems, the streams are 1196 * close appropriately. 1197 * @param inputStream The input stream. 1198 * @exception IOException If the input stream cannot be read. 1199 */ 1200 private static byte[] _binaryReadStream(InputStream inputStream) 1201 throws IOException { 1202 // Copy the source file. 1203 BufferedInputStream input = null; 1204 1205 ByteArrayOutputStream output = null; 1206 1207 try { 1208 input = new BufferedInputStream(inputStream); 1209 1210 try { 1211 output = new ByteArrayOutputStream(); 1212 // Read the stream in 8k chunks 1213 final int BUFFERSIZE = 8192; 1214 byte[] buffer = new byte[BUFFERSIZE]; 1215 int bytesRead = 0; 1216 while ((bytesRead = input.read(buffer, 0, BUFFERSIZE)) != -1) { 1217 output.write(buffer, 0, bytesRead); 1218 } 1219 } finally { 1220 if (output != null) { 1221 try { 1222 // ByteArrayOutputStream.close() has no 1223 // effect, but we try it anyway for good form. 1224 output.close(); 1225 } catch (Throwable throwable) { 1226 throw new RuntimeException(throwable); 1227 } 1228 } 1229 } 1230 } finally { 1231 if (input != null) { 1232 try { 1233 input.close(); 1234 } catch (Throwable throwable) { 1235 throw new RuntimeException(throwable); 1236 } 1237 } 1238 } 1239 if (output != null) { 1240 return output.toByteArray(); 1241 } 1242 return null; 1243 } 1244 1245 /** Search the classpath. 1246 * @param name The name to be searched 1247 * @param classLoader The class loader to use to locate system 1248 * resources, or null to use the system class loader that was used 1249 * to load this class. 1250 * @return null if name does not start with "$CLASSPATH" 1251 * or _CLASSPATH_VALUE or if name cannot be found. 1252 */ 1253 private static URL _searchClassPath(String name, ClassLoader classLoader) 1254 throws IOException { 1255 1256 URL result = null; 1257 1258 // If the name begins with "$CLASSPATH", or 1259 // "xxxxxxCLASSPATHxxxxxx",then attempt to open the file 1260 // relative to the classpath. 1261 // NOTE: Use the dummy variable constant set up in the constructor. 1262 if (name.startsWith(_CLASSPATH_VALUE) 1263 || name.startsWith("$CLASSPATH")) { 1264 // Try relative to classpath. 1265 String trimmedName = _trimClassPath(name); 1266 1267 if (classLoader == null) { 1268 String referenceClassName = "ptolemy.util.FileUtilities"; 1269 1270 try { 1271 // WebStart: We might be in the Swing Event thread, so 1272 // Thread.currentThread().getContextClassLoader() 1273 // .getResource(entry) probably will not work so we 1274 // use a marker class. 1275 Class referenceClass = Class.forName(referenceClassName); 1276 classLoader = referenceClass.getClassLoader(); 1277 } catch (Exception ex) { 1278 // IOException constructor does not take a cause 1279 IOException ioException = new IOException( 1280 "Cannot look up class \"" + referenceClassName 1281 + "\" or get its ClassLoader."); 1282 ioException.initCause(ex); 1283 throw ioException; 1284 } 1285 } 1286 1287 // Use Thread.currentThread()... for Web Start. 1288 result = classLoader.getResource(trimmedName); 1289 } 1290 return result; 1291 } 1292 1293 /** Remove the value of _CLASSPATH_VALUE or "$CLASSPATH". 1294 */ 1295 private static String _trimClassPath(String name) { 1296 String classpathKey; 1297 1298 if (name.startsWith(_CLASSPATH_VALUE)) { 1299 classpathKey = _CLASSPATH_VALUE; 1300 } else { 1301 classpathKey = "$CLASSPATH"; 1302 } 1303 1304 return name.substring(classpathKey.length() + 1); 1305 } 1306 1307 /////////////////////////////////////////////////////////////////// 1308 //// private members //// 1309 1310 /** Tag value used by this class and registered as a parser 1311 * constant for the identifier "CLASSPATH" to indicate searching 1312 * in the classpath. This is a hack, but it deals with the fact 1313 * that Java is not symmetric in how it deals with getting files 1314 * from the classpath (using getResource) and getting files from 1315 * the file system. 1316 */ 1317 private static String _CLASSPATH_VALUE = "xxxxxxCLASSPATHxxxxxx"; 1318}