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}