001/* Utilities for JNLP aka Web Start
002
003 Copyright (c) 2002-2016 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.actor.gui;
029
030import java.io.File;
031import java.io.FileNotFoundException;
032import java.io.IOException;
033import java.net.URI;
034import java.net.URISyntaxException;
035import java.net.URL;
036import java.nio.file.Files;
037import java.nio.file.Path;
038import java.util.ArrayList;
039import java.util.Arrays;
040import java.util.HashMap;
041import java.util.Map;
042
043import ptolemy.util.ClassUtilities;
044import ptolemy.util.FileUtilities;
045import ptolemy.util.StringUtilities;
046
047///////////////////////////////////////////////////////////////////
048//// JNLPUtilities
049
050/** This class contains utilities for use with JNLP, aka Web Start.
051
052 <p>For more information about Web Start, see
053 <a href="http://www.oracle.com/technetwork/java/javase/tech/index-jsp-136112.html" target="_top"><code>http://www.oracle.com/technetwork/java/javase/tech/index-jsp-136112.html</code></a>
054 or <code>$PTII/doc/webStartHelp</code>
055
056 @author Christopher Hylands
057 @version $Id$
058 @since Ptolemy II 2.0
059 @Pt.ProposedRating Red (cxh)
060 @Pt.AcceptedRating Red (cxh)
061 @see Configuration
062 */
063public class JNLPUtilities {
064    /** Instances of this class cannot be created.
065     */
066    private JNLPUtilities() {
067    }
068
069    ///////////////////////////////////////////////////////////////////
070    ////                         public methods                    ////
071
072    /** Canonicalize a jar URL.  If the possibleJarURL argument is a
073     *  jar URL (that is, it starts with 'jar:'), then convert any
074     *  space characters to %20.  If the possibleJarURL argument is
075     *  not a jar URL, then return the possibleJarURL argument.
076     *  @param possibleJarURL  A URL that may or may not be a jar URL
077     *  @return either the original possibleJarURL or a canonicalized
078     *  jar URL
079     *  @exception java.net.MalformedURLException If new URL() throws it.
080     */
081    public static URL canonicalizeJarURL(URL possibleJarURL)
082            throws java.net.MalformedURLException {
083        // This method is needed so that under Web Start we are always
084        // referring to files like intro.htm with the same URL.
085        // The reason is that the Web Start under Windows is likely
086        // to be in c:/Documents and Settings/username
087        // so we want to always refer to the files with the same URL
088        // so as to avoid duplicate windows
089        if (possibleJarURL.toExternalForm().startsWith("jar:")) {
090            String possibleJarURLPath = StringUtilities
091                    .substitute(possibleJarURL.toExternalForm(), " ", "%20");
092            if (possibleJarURLPath.contains("..")) {
093                // A jar URL with a relative path.  about:checkCompleteDemos will generate these.
094                String[] path = possibleJarURLPath.split("/");
095                ArrayList<String> paths = new ArrayList(Arrays.asList(path));
096
097                for (int j = 0; j < paths.size(); j++) {
098                    // System.out.println(paths.size() + " paths.get(" + j + "): "+ paths.get(j) + " paths: " + paths);
099                    if (paths.get(j).equals("..")) {
100                        if (j > 0) {
101                            //System.out.println(j-1 + " Removing: " + paths.get(j-1));
102                            paths.remove(j - 1);
103                        }
104                        //System.out.println(j-1 + "Removing: " + paths.get(j-1));
105                        paths.remove(j - 1);
106                        j = j - 2;
107                    }
108                }
109                StringBuffer newPath = new StringBuffer();
110                for (String pathElement : paths) {
111                    newPath.append(pathElement + "/");
112                }
113                possibleJarURLPath = newPath.toString().substring(0,
114                        newPath.length() - 1);
115                //System.out.println("JNLPUtilities: possibleJarURLPath: " + possibleJarURLPath);
116                try {
117                    URL jarURL = ClassUtilities
118                            .jarURLEntryResource(possibleJarURLPath);
119                    //System.out.println("JNLPUtilities: jarURL: " + jarURL);
120                    return jarURL;
121                } catch (IOException ex) {
122                    throw new java.net.MalformedURLException(ex.toString());
123                }
124            }
125
126            // FIXME: Could it be that we only want to convert spaces before
127            // the '!/' string?
128            URL jarURL = new URL(possibleJarURLPath);
129            //System.out.println("JNLPUtilities: 2 jarURL: " + jarURL);
130            // FIXME: should we check to see if the jarURL exists here?
131            //            if (jarURL == null) {
132            //                try {
133            //                    return ClassUtilities
134            //                            .jarURLEntryResource(possibleJarURLPath);
135            //                } catch (IOException ex) {
136            //                    throw new java.net.MalformedURLException(ex.toString());
137            //                }
138            //            }
139            return jarURL;
140        }
141
142        return possibleJarURL;
143    }
144
145    /** Get the resource, if it is in a jar URL, then
146     *  copy the resource to a temporary file first.
147     *
148     *  If the file is copied to a temporary location, then
149     *  it is deleted when the process exits.
150     *
151     *  This method is used when jar URLs are not
152     *  able to be read in by a function call.
153     *
154     *  If the spec refers to a URL that is a directory,
155     *  then the possibly shortened spec is returned
156     *  with a trailing /.  No temporary directory
157     *  is created.
158     *
159     *  @param spec The string to be found as a resource.
160     *  @return The File.
161     *  @exception IOException If the jar URL cannot be saved as a temporary file.
162     */
163    public static File getResourceSaveJarURLAsTempFile(String spec)
164            throws IOException {
165        // System.out.println("JNLPUtilities.g.r.s.j.u.a.t.f(): start spec: " + spec);
166        // If the spec is not a jar URL, then check in file system.
167        // This method is used by CapeCode to find .js file resources with require().
168        int jarSeparatorIndex = spec.indexOf("!" + File.separator);
169        File results = null;
170        if (jarSeparatorIndex == -1) {
171            results = new File(spec);
172            if (results.exists()) {
173                // System.out.println("JNLPUtilities.g.r.s.j.u.a.t.f(): start spec: " + spec + " 0 return: " + results);
174                return results;
175            }
176        } else {
177            // Strip off the text leading up to !/ or !\
178            spec = spec.substring(jarSeparatorIndex + 2);
179        }
180
181        // If the resources is not found at all, return null.
182        URL url = ClassUtilities.getResource(spec);
183        if (url == null) {
184            // System.out.println("JNLPUtilities.g.r.s.j.u.a.t.f(): start spec: " + spec + " 0.5");
185            // Windows, getResource() on spec with backslashes causes problems.
186            String spec2 = spec.replace("\\", "/");
187            url = ClassUtilities.getResource(spec2);
188            if (url == null) {
189                // If we are trying to read something with a path like ./decode.js, then check the _lastSpec
190                if (spec.startsWith("." + File.separator)
191                        && _lastSpec != null) {
192                    String parentLastSpec = _lastSpec.substring(0,
193                            _lastSpec.lastIndexOf(File.separator) + 1);
194                    return getResourceSaveJarURLAsTempFile(
195                            parentLastSpec + spec.substring(2));
196                }
197                return null;
198            }
199        }
200
201        results = null;
202
203        // For jar urls, copy the file to a temporary
204        // location that is removed when the process exits.
205        if (url.toExternalForm().startsWith("jar:")) {
206            // If we have already seen the url, then return
207            // what was returned last time
208            try {
209                // We use a map of URIs because FindBugs reports:
210                // "Dm: Maps and sets of URLs can be performance hogs (DMI_COLLECTION_OF_URLS)"
211                // See http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html
212                if (_jarURITemporaryFiles != null
213                        && _jarURITemporaryFiles.containsKey(url.toURI())) {
214                    results = _jarURITemporaryFiles.get(url.toURI());
215                    _lastSpec = spec;
216                    // System.out.println("JNLPUtilities.g.r.s.j.u.a.t.f(): start spec: " + spec + " 1 return: " + results);
217                    return results;
218                }
219            } catch (URISyntaxException ex) {
220                IOException ioException = new IOException(
221                        "Failed to look up " + url + " in the cache.");
222                ioException.initCause(ex);
223                throw ioException;
224            }
225            String prefix = "";
226            String suffix = "";
227            int lastIndexOfSlash = spec.lastIndexOf(File.separator);
228            int lastIndexOfDot = spec.lastIndexOf(".");
229            if (lastIndexOfSlash == -1) {
230                if (lastIndexOfDot == -1) {
231                    prefix = spec;
232                } else {
233                    prefix = spec.substring(0, lastIndexOfDot);
234                    suffix = "." + spec.substring(lastIndexOfDot + 1);
235                }
236            } else {
237                if (lastIndexOfDot == -1) {
238                    prefix = spec.substring(lastIndexOfSlash + 1);
239                } else {
240                    prefix = spec.substring(lastIndexOfSlash + 1,
241                            lastIndexOfDot);
242                    suffix = "." + spec.substring(lastIndexOfDot + 1);
243                }
244            }
245            try {
246                String temporaryFileName = saveJarURLAsTempFile(url.toString(),
247                        prefix, suffix, null /*directory*/);
248                results = new File(temporaryFileName);
249                // System.out.println("JNLPUtilities.g.r.s.j.u.a.t.f(): start spec: " + spec + " 1.5 reslts: " + results + " exists: " + results.exists());
250            } catch (IOException ex) {
251                // If the spec exists with a trailing / or \ , then just
252                // return that so that we can detect that it is a
253                // directory.  FIXME: the directory is not actually
254                // created here, which could be confusing.
255                if (spec.length() > 0 && spec
256                        .charAt(spec.length() - 1) != File.separatorChar) {
257                    URL urlDirectory = ClassUtilities
258                            .getResource(spec + File.separator);
259                    if (urlDirectory != null) {
260                        results = new File(spec + File.separator);
261                    }
262                } else {
263                    results = null;
264                }
265            }
266            if (_jarURITemporaryFiles == null) {
267                _jarURITemporaryFiles = new HashMap<URI, File>();
268            }
269            try {
270                _jarURITemporaryFiles.put(url.toURI(), results);
271            } catch (URISyntaxException ex) {
272                IOException ioException = new IOException(
273                        "Failed to add " + url + " in the cache.");
274                ioException.initCause(ex);
275                throw ioException;
276            }
277            _lastSpec = spec;
278            // System.out.println("JNLPUtilities.g.r.s.j.u.a.t.f(): start spec: " + spec + " 2 return: " + results);
279            return results;
280        } else {
281            // If the resource is not a jar URL, try
282            // creating a file.
283            try {
284                results = new File(url.toURI());
285            } catch (URISyntaxException e) {
286                results = new File(url.getPath());
287            } catch (IllegalArgumentException e) {
288                results = new File(url.getPath());
289            }
290            // System.out.println("JNLPUtilities.g.r.s.j.u.a.t.f(): start spec: " + spec + " 3 return: " + results);
291            return results;
292        }
293    }
294
295    /** Return true if we are running under WebStart.
296     *  @return True if we are running under WebStart.
297     */
298    public static boolean isRunningUnderWebStart() {
299        try {
300            // NOTE: getProperty() will probably fail in applets, which
301            // is why this is in a try block.
302            String javaWebStart = System.getProperty("javawebstart.version");
303
304            if (javaWebStart != null) {
305                return true;
306            }
307        } catch (SecurityException security) {
308            // Ignored
309        }
310
311        return false;
312    }
313
314    /** Given a jar url of the format jar:{url}!/{entry}, return
315     *  the resource, if any of the {entry}.
316     *  If the string does not contain <code>!/</code>, then return
317     *  null.  Web Start uses jar URL, and there are some cases where
318     *  if we have a jar URL, then we may need to strip off the
319     *  <code>jar:<i>url</i>!/</code> part so that we can search for
320     *  the {entry} as a resource.
321     *
322     *  @param spec The string containing the jar url.
323     *  @exception IOException If it cannot convert the specification to
324     *   a URL.
325     *  @return the resource if any.
326     *  @deprecated Use ptolemy.util.ClassUtilities#jarURLEntryResource(String)
327     *  @see ptolemy.util.ClassUtilities#jarURLEntryResource(String)
328     *  @see java.net.JarURLConnection
329     */
330    @Deprecated
331    public static URL jarURLEntryResource(String spec) throws IOException {
332        return ClassUtilities.jarURLEntryResource(spec);
333    }
334
335    /** Given a jar URL, read in the resource and save it as a file.
336     *  The file is created using the prefix and suffix in the
337     *  directory referred to by the directory argument.  If the
338     *  directory argument is null, then it is saved in the platform
339     *  dependent temporary directory.
340     *  The file is deleted upon exit.
341     *  @see java.io.File#createTempFile(java.lang.String, java.lang.String, java.io.File)
342     *  @param jarURLName The name of the jar URL to read.  jar URLS start
343     *  with "jar:" and have a "!/" in them.
344     *  @param prefix The prefix used to generate the name, it must be
345     *  at least three characters long.
346     *  @param suffix The suffix to use to generate the name.  If the
347     *  suffix is null, then the suffix of the jarURLName is used.  If
348     *  the jarURLName does not contain a ".", then ".tmp" will be used
349     *  @param directory The directory where the temporary file is
350     *  created.  If directory is null then the platform dependent
351     *  temporary directory is used.
352     *  @return the name of the temporary file that was created
353     *  @exception IOException If there is a problem saving the jar URL.
354     */
355    public static String saveJarURLAsTempFile(String jarURLName, String prefix,
356            String suffix, File directory) throws IOException {
357        URL jarURL = _lookupJarURL(jarURLName);
358        jarURLName = jarURL.toString();
359
360        // File.createTempFile() does the bulk of the work for us,
361        // we just check to see if suffix is null, and if it is,
362        // get the suffix from the jarURLName.
363        if (suffix == null) {
364            // If the jarURLName does not contain a ".", then we pass
365            // suffix = null to File.createTempFile(), which defaults
366            // to ".tmp"
367            if (jarURLName.lastIndexOf('.') != -1) {
368                suffix = jarURLName.substring(jarURLName.lastIndexOf('.'));
369            }
370        }
371
372        File temporaryFile = null;
373        try {
374
375            temporaryFile = File.createTempFile(prefix, suffix, directory);
376        } catch (Exception ex) {
377            throw new IOException("While trying to save a jar URL \""
378                    + jarURLName
379                    + "\", failed to create temporary file with prefix: \""
380                    + prefix + "\", suffix: \"" + suffix + "\", directory: \""
381                    + directory);
382        }
383        temporaryFile.deleteOnExit();
384
385        try {
386            // The resource pointed to might be a pdf file, which
387            // is binary, so we are careful to read it byte by
388            // byte and not do any conversions of the bytes.
389            FileUtilities.binaryCopyURLToFile(jarURL, temporaryFile);
390        } catch (Throwable throwable) {
391            // Hmm, jarURL could be referring to a directory.
392            if (temporaryFile.delete()) {
393                throw new IOException("Copying \"" + jarURL + "\" to \""
394                        + temporaryFile + "\" failed: " + throwable
395                        + "  Then deleting \"" + temporaryFile + "\" failed?");
396            }
397            Path directoryPath = directory.toPath();
398            Path temporaryDirectory = Files.createTempDirectory(directoryPath,
399                    prefix);
400            temporaryFile = temporaryDirectory.toFile();
401            temporaryFile.deleteOnExit();
402            FileUtilities.binaryCopyURLToFile(jarURL, temporaryFile);
403        }
404        return temporaryFile.toString();
405    }
406
407    /** Given a jar URL, read in the resource and save it as a file in
408     *  a similar directory in the classpath if possible.  In this
409     *  context, by similar directory, we mean the directory where
410     *  the file would found if it was not in the jar url.
411     *  For example, if the jar url is
412     *  jar:file:/ptII/doc/design.jar!/doc/design/design.pdf
413     *  then this method will read design.pdf from design.jar
414     *  and save it as /ptII/doc/design.pdf.
415     *
416     *  @param jarURLName The name of the jar URL to read.  jar URLS start
417     *  with "jar:" and have a "!/" in them.
418     *  @return the name of the file that was created or
419     *  null if the file cannot be created
420     *  @exception IOException If there is a problem saving the jar URL.
421     */
422    public static String saveJarURLInClassPath(String jarURLName)
423            throws IOException {
424        URL jarURL = _lookupJarURL(jarURLName);
425        jarURLName = jarURL.toString();
426
427        int jarSeparatorIndex = jarURLName.indexOf("!/");
428
429        if (jarSeparatorIndex == -1) {
430            // Could be that we found a copy of the file in the classpath.
431            return jarURLName;
432        }
433
434        // If the entry directory matches the jarURL directory, then
435        // write out the file in the proper location.
436        String jarURLFileName = jarURLName.substring(0, jarSeparatorIndex);
437        String entryFileName = jarURLName.substring(jarSeparatorIndex + 2);
438
439        // We assume / is the file separator here because URLs
440        // _BY_DEFINITION_ have / as a separator and not the Microsoft
441        // non-conforming hack of using a backslash.
442        String jarURLParentFileName = jarURLFileName.substring(0,
443                jarURLFileName.lastIndexOf("/"));
444
445        String parentEntryFileName = entryFileName.substring(0,
446                entryFileName.lastIndexOf("/"));
447
448        if (jarURLParentFileName.endsWith(parentEntryFileName)
449                && jarURLParentFileName.startsWith("jar:file:/")) {
450            // The top level directory, probably $PTII
451            String jarURLTop = jarURLParentFileName.substring(9,
452                    jarURLParentFileName.length()
453                            - parentEntryFileName.length());
454
455            File temporaryFile = new File(jarURLTop, entryFileName);
456
457            // If the file exists, we assume that it is the right one.
458            // FIXME: we could do more here, like check for file sizes.
459            if (!temporaryFile.exists()) {
460                FileUtilities.binaryCopyURLToFile(jarURL, temporaryFile);
461            }
462
463            return temporaryFile.toString();
464        }
465
466        return null;
467    }
468
469    ///////////////////////////////////////////////////////////////////
470    ////                         private methods                   ////
471    // Lookup a jarURLName as a resource.
472    private static URL _lookupJarURL(String jarURLName) throws IOException {
473        // We call jarURLEntryResource here so that we get a URL
474        // that has the right jar file associated with the right entry.
475        URL jarURL = jarURLEntryResource(jarURLName);
476
477        if (jarURL == null) {
478            jarURL = ClassUtilities.getResource(jarURLName);
479        }
480
481        if (jarURL == null) {
482            throw new FileNotFoundException(
483                    "Could not find '" + jarURLName + "'");
484        }
485
486        return jarURL;
487    }
488
489    ///////////////////////////////////////////////////////////////////
490    ////                         private variables                 ////
491
492    /** The map of URIs to Files used by
493     * getResourceSaveJarURIAsTempFile().
494     */
495    private static Map<URI, File> _jarURITemporaryFiles;
496
497    private static String _lastSpec = null;
498}