Merge branch 'Jalview-JS/develop.JAL-3446.ctrlDown' into
[jalview.git] / src / javajs / async / Assets.java
1 package javajs.async;
2
3 import java.awt.Toolkit;
4 import java.io.File;
5 import java.io.IOException;
6 import java.io.InputStream;
7 import java.io.OutputStream;
8 import java.net.MalformedURLException;
9 import java.net.URI;
10 import java.net.URISyntaxException;
11 import java.net.URL;
12 import java.util.Arrays;
13 import java.util.HashMap;
14 import java.util.HashSet;
15 import java.util.Map;
16 import java.util.zip.ZipEntry;
17 import java.util.zip.ZipInputStream;
18
19 import swingjs.api.JSUtilI;
20
21 /**
22  * The Assets class allows assets such as images and property files to be
23  * combined into zip files rather than delivered individually. The Assets
24  * instance is a singleton served by a set of static methods. In particular, the
25  * three add(...) methods are used to create asset references, which include an
26  * arbitrary name, a path to a zip file asset, and one or more class paths that
27  * are covered by this zip file asset.
28  * 
29  * For example:
30  * 
31  * <code>
32         static {
33                 try {
34                         Assets.add(new Assets.Asset("osp", "osp-assets.zip", "org/opensourcephysics/resources"));
35                         Assets.add(new Assets.Asset("tracker", "tracker-assets.zip",
36                                         "org/opensourcephysics/cabrillo/tracker/resources"));
37                         Assets.add(new Assets.Asset("physlets", "physlet-assets.zip", new String[] { "opticsimages", "images" }));
38                         // add the Info.assets last so that it can override these defaults
39                         if (OSPRuntime.isJS) {
40                                 Assets.add(OSPRuntime.jsutil.getAppletInfo("assets"));
41                         }
42                 } catch (Exception e) {
43                         OSPLog.warning("Error reading assets path. ");
44                 }
45
46         }
47  * </code>
48  * 
49  * It is not clear that Java is well-served by this zip-file loading, but
50  * certainly JavaScript is. What could be 100 downloads is just one, and SwingJS
51  * (but not Java) can cache individual ZipEntry instances in order to unzip them
52  * independently only when needed. This is potentially a huge savings.
53  * 
54  * Several static methods can be used to retrieve assets. Principal among those
55  * are:
56  * 
57  * <code>
58  * getAssetBytes(String fullPath)
59  * getAssetString(String fullPath)
60  * getAssetStream(String fullPath)
61  * </code>
62  * 
63  * If an asset is not found in a zip file, then it will be loaded from its fullPath. 
64  * 
65  * 
66  * 
67  * @author hansonr
68  *
69  */
70
71 public class Assets {
72
73         public static boolean isJS = /** @j2sNative true || */
74                         false;
75
76         public static JSUtilI jsutil;
77
78         static {
79                 try {
80                         if (isJS) {
81                                 jsutil = ((JSUtilI) Class.forName("swingjs.JSUtil").newInstance());
82                         }
83
84                 } catch (Exception e) {
85                         System.err.println("Assets could not create swinjs.JSUtil instance");
86                 }
87         }
88
89         private Map<String, Map<String, ZipEntry>> htZipContents = new HashMap<>();
90
91         private static boolean doCacheZipContents = true;
92
93         private static Assets instance = new Assets();
94
95         private Assets() {
96         }
97
98         private Map<String, Asset> assetsByPath = new HashMap<>();
99
100         private String[] sortedList = new String[0];
101
102         /**
103          * If this object has been cached by SwingJS, add its bytes to the URL, URI, or
104          * File
105          * 
106          * @param URLorURIorFile
107          * @return
108          */
109         public static byte[] addJSCachedBytes(Object URLorURIorFile) {
110                 return (isJS ? jsutil.addJSCachedBytes(URLorURIorFile) : null);
111         }
112
113         public static class Asset {
114                 String name;
115                 URI uri;
116                 String classPath;
117                 String zipPath;
118                 String[] classPaths;
119
120                 public Asset(String name, String zipPath, String[] classPaths) {
121                         this.name = name;
122                         this.zipPath = zipPath;
123                         this.classPaths = classPaths;
124                 }
125
126                 public Asset(String name, String zipPath, String classPath) {
127                         this.name = name;
128                         this.zipPath = zipPath;
129                         uri = getAbsoluteURI(zipPath); // no spaces expected here.
130                         this.classPath = classPath.endsWith("/") ? classPath : classPath + "/";
131                 }
132
133                 public URL getURL(String fullPath) throws MalformedURLException {
134                         return (fullPath.indexOf(classPath) < 0 ? null
135                                         : new URL("jar", null, uri + "!/" + fullPath));//.replaceAll(" ", "%20")));
136                 }
137
138                 @Override
139                 public String toString() {
140                         return "{" + "\"name\":" + "\"" + name + "\"," + "\"zipPath\":" + "\"" + zipPath + "\"," + "\"classPath\":"
141                                         + "\"" + classPath + "\"" + "}";
142                 }
143
144         }
145
146         public static Assets getInstance() {
147                 return instance;
148         }
149
150         /**
151          * The difference here is that URL will not insert the %20 for space that URI will.
152          * 
153          * @param path
154          * @return
155          */
156         @SuppressWarnings("deprecation")
157         public static URL getAbsoluteURL(String path) {
158                 URL url = null;
159                 try {
160                         url = (path.indexOf(":/") < 0 ? new File(new File(path).getAbsolutePath()).toURL() : new URL(path));
161                         if (path.indexOf("!/")>=0)
162                                 url = new URL("jar", null, url.toString());
163                 } catch (MalformedURLException e) {
164                         e.printStackTrace();
165                 }
166                 return url;
167         }
168
169         public static URI getAbsoluteURI(String path) {
170                 URI uri = null;
171                 try {
172                         uri = (path.indexOf(":/") < 0 ? new File(new File(path).getAbsolutePath()).toURI() : new URI(path));
173                 } catch (URISyntaxException e) {
174                         e.printStackTrace();
175                 }
176                 return uri;
177         }
178
179         /**
180          * Allows passing a Java Asset or array of Assets or a JavaScript Object or
181          * Object array that contains name, zipPath, and classPath keys; in JavaScript,
182          * the keys can have multiple .
183          * 
184          * @param o
185          */
186         public static void add(Object o) {
187                 if (o == null)
188                         return;
189                 try {
190                         if (o instanceof Object[]) {
191                                 Object[] a = (Object[]) o;
192                                 for (int i = 0; i < a.length; i++)
193                                         add(a[i]);
194                                 return;
195                         }
196                         // In JavaScript this may not actually be an Asset, only a proxy for that.
197                         // Just testing for keys. Only one of classPath and classPaths is allowed.
198                         Asset a = (Asset) o;
199                         if (a.name == null || a.zipPath == null || a.classPath == null && a.classPaths == null
200                                         || a.classPath != null && a.classPaths != null) {
201                                 throw new NullPointerException("Assets could not parse " + o);
202                         }
203                         if (a.classPaths == null) {
204                                 // not possible in Java, but JavaScript may be passing an array of class paths
205                                 add(a.name, a.zipPath, a.classPath);
206                         } else {
207                                 add(a.name, a.zipPath, a.classPaths);
208                         }
209                 } catch (Throwable t) {
210                         throw new IllegalArgumentException(t.getMessage());
211                 }
212         }
213
214         public static void add(String name, String zipFile, String path) {
215                 add(name, zipFile, new String[] { path });
216         }
217
218         private static HashSet<String> loadedAssets = new HashSet<>();
219         
220         public static boolean hasLoaded(String name) {
221                 return loadedAssets.contains(name);
222         }
223         
224         public static void reset() {
225                 getInstance().htZipContents.clear();
226                 getInstance().assetsByPath.clear();
227                 getInstance().sortedList = new String[0];
228         }
229
230         public static void add(String name, String zipFile, String[] paths) {
231                 getInstance()._add(name, zipFile, paths);
232         }
233
234         private void _add(String name, String zipFile, String[] paths) {
235                 if (hasLoaded(name)) {
236                         System.err.println("Assets warning: Asset " + name + " already exists");
237                 }
238                 loadedAssets.add(name);
239                 for (int i = paths.length; --i >= 0;) {
240                         assetsByPath.put(paths[i], new Asset(name, zipFile, paths[i]));
241                 }
242                 resort();
243         }
244         
245
246         /**
247          * Gets the asset, preferably from a zip file asset, but not necessarily.
248          * 
249          * @param assetPath
250          * @return
251          */
252         public static byte[] getAssetBytes(String assetPath) {
253                 return getAssetBytes(assetPath, false);
254         }
255
256         /**
257          * Gets the asset, preferably from a zip file asset, but not necessarily.
258          * 
259          * @param assetPath
260          * @return
261          */
262         public static String getAssetString(String assetPath) {
263                 return getAssetString(assetPath, false);
264         }
265
266         /**
267          * Gets the asset, preferably from a zip file asset, but not necessarily.
268          * 
269          * @param assetPath
270          * @return
271          */
272         public static InputStream getAssetStream(String assetPath) {
273                 return getAssetStream(assetPath, false);
274         }
275
276         /**
277          * Gets the asset from a zip file.
278          * 
279          * @param assetPath
280          * @return
281          */
282         public static byte[] getAssetBytesFromZip(String assetPath) {
283                 return getAssetBytes(assetPath, true);
284         }
285
286         /**
287          * Gets the asset from a zip file.
288          * 
289          * @param assetPath
290          * @return
291          */
292         public static String getAssetStringFromZip(String assetPath) {
293                 return getAssetString(assetPath, true);
294         }
295
296         /**
297          * Gets the asset from a zip file.
298          * 
299          * @param assetPath
300          * @return
301          */
302         public static InputStream getAssetStreamFromZip(String assetPath) {
303                 return getAssetStream(assetPath, true);
304         }
305
306
307         /**
308          * Get the contents of a path from a zip file asset as byte[], optionally loading
309          * the resource directly using a class loader.
310          * 
311          * @param path
312          * @param zipOnly
313          * @return
314          */
315         private static byte[] getAssetBytes(String path, boolean zipOnly) {
316                 byte[] bytes = null;
317                 try {
318                         URL url = getInstance()._getURLFromPath(path, true);
319                         if (url == null && !zipOnly) {
320                                 url = getAbsoluteURL(path);
321                                 //url = Assets.class.getResource(path);
322                         }
323                         if (url == null)
324                                 return null;
325                         if (isJS) {
326                                 bytes = jsutil.getURLBytes(url);
327                                 if (bytes == null) {
328                                         url.openStream();
329                                         bytes = jsutil.getURLBytes(url);
330                                 }
331                         } else {
332                                 bytes = getLimitedStreamBytes(url.openStream(), -1, null);
333                         }
334                 } catch (Throwable t) {
335                         t.printStackTrace();
336                 }
337                 return bytes;
338         }
339
340         /**
341          * Get the contents of a path from a zip file asset as a String, optionally
342          * loading the resource directly using a class loader.
343          * 
344          * @param path
345          * @param zipOnly
346          * @return
347          */
348         private static String getAssetString(String path, boolean zipOnly) {
349                 byte[] bytes = getAssetBytes(path, zipOnly);
350                 return (bytes == null ? null : new String(bytes));
351         }
352
353         /**
354          * Get the contents of a path from a zip file asset as an InputStream, optionally
355          * loading the resource directly using a class loader.
356          * 
357          * @param path
358          * @param zipOnly
359          * @return
360          */
361         private static InputStream getAssetStream(String path, boolean zipOnly) {
362                 try {
363                         URL url = getInstance()._getURLFromPath(path, true);
364                         if (url == null && !zipOnly) {
365                                 url = Assets.class.getClassLoader().getResource(path);
366                         }
367                         if (url != null)
368                                 return url.openStream();
369                 } catch (Throwable t) {
370                 }
371                 return null;
372         }
373         /**
374          * Determine the path to an asset. If not found in a zip file asset, return the
375          * absolute path to this resource.
376          * 
377          * @param fullPath
378          * @return
379          */
380         public static URL getURLFromPath(String fullPath) {
381                 return getInstance()._getURLFromPath(fullPath, false);
382         }
383
384         /**
385          * Determine the path to an asset. If not found in a zip file asset, optionally
386          * return null or the absolute path to this resource.
387          * 
388          * @param fullPath
389          * @param zipOnly
390          * @return the URL to this asset, or null if not found.
391          */
392         public static URL getURLFromPath(String fullPath, boolean zipOnly) {
393                 return getInstance()._getURLFromPath(fullPath, zipOnly);
394         }
395
396         private URL _getURLFromPath(String fullPath, boolean zipOnly) {
397                 URL url = null;
398                 try {
399                         if (fullPath.startsWith("/"))
400                                 fullPath = fullPath.substring(1);
401                         for (int i = sortedList.length; --i >= 0;) {
402                                 if (fullPath.startsWith(sortedList[i])) {
403                                         url = assetsByPath.get(sortedList[i]).getURL(fullPath);
404                                         ZipEntry ze = findZipEntry(url);
405                                         if (ze == null)
406                                                 break;
407                                         if (isJS) {
408                                                 jsutil.setURLBytes(url, jsutil.getZipBytes(ze));
409                                         }
410                                         return url;
411                                 }
412                         }
413                         if (!zipOnly)
414                                 return getAbsoluteURL(fullPath);
415                 } catch (MalformedURLException e) {
416                 }
417                 return null;
418         }
419
420         public static ZipEntry findZipEntry(URL url) {
421                 String[] parts = getJarURLParts(url.toString());
422                 if (parts == null || parts[0] == null || parts[1].length() == 0)
423                         return null;
424                 return findZipEntry(parts[0], parts[1]);
425         }
426
427         public static ZipEntry findZipEntry(String zipFile, String fileName) {
428                 return getZipContents(zipFile).get(fileName);
429         }
430
431         /**
432          * Gets the contents of a zip file.
433          * 
434          * @param zipPath the path to the zip file
435          * @return a set of file names in alphabetical order
436          */
437         public static Map<String, ZipEntry> getZipContents(String zipPath) {
438                 return getInstance()._getZipContents(zipPath);
439         }
440
441         private Map<String, ZipEntry> _getZipContents(String zipPath) {
442                 URL url = getURLWithCachedBytes(zipPath); // BH carry over bytes if we have them already
443                 Map<String, ZipEntry> fileNames = htZipContents.get(url.toString());
444                 if (fileNames != null)
445                         return fileNames;
446                 try {
447                         // Scan URL zip stream for files.
448                         return readZipContents(url.openStream(), url);
449                 } catch (Exception ex) {
450                         ex.printStackTrace();
451                         return null;
452                 }
453         }
454
455         /**
456          * Deconstruct a jar URL into two parts, before and after "!/".
457          * 
458          * @param source
459          * @return
460          */
461         public static String[] getJarURLParts(String source) {
462                 int n = source.indexOf("!/");
463                 if (n < 0)
464                         return null;
465                 String jarfile = source.substring(0, n).replace("jar:", "");
466                 while (jarfile.startsWith("//"))
467                         jarfile = jarfile.substring(1);
468                 return new String[] { jarfile, (n == source.length() - 2 ? null : source.substring(n + 2)) };
469         }
470
471         /**
472          * Get the contents of any URL as a byte array. This method does not do any asset check. It just gets the url data as a byte array.
473          * 
474          * @param url
475          * @return byte[]
476          * 
477          * @author hansonr
478          */
479         public static byte[] getURLContents(URL url) {
480                 if (url == null)
481                         return null;
482                 try {
483                         if (isJS) {
484                                 // Java 9! return new String(url.openStream().readAllBytes());
485                                 return jsutil.readAllBytes(url.openStream());
486                         }
487                         return getLimitedStreamBytes(url.openStream(), -1, null);
488                 } catch (IOException e) {
489                         e.printStackTrace();
490                 }
491                 return null;
492         }
493
494         /**
495          * 
496          * Convert a file path to a URL, retrieving any cached file data, as from DnD.
497          * Do not do any actual data transfer. This is a swingjs.JSUtil service.
498          * 
499          * @param path
500          * @return
501          */
502         private static URL getURLWithCachedBytes(String path) {
503                 URL url = getAbsoluteURL(path);
504                 if (url != null)
505                         addJSCachedBytes(url);
506                 return url;
507         }
508
509         private Map<String, ZipEntry> readZipContents(InputStream is, URL url) throws IOException {
510                 HashMap<String, ZipEntry> fileNames = new HashMap<String, ZipEntry>();
511                 if (doCacheZipContents)
512                         htZipContents.put(url.toString(), fileNames);
513                 ZipInputStream input = new ZipInputStream(is);
514                 ZipEntry zipEntry = null;
515                 int n = 0;
516                 while ((zipEntry = input.getNextEntry()) != null) {
517                         if (zipEntry.isDirectory() || zipEntry.getSize() == 0)
518                                 continue;
519                         n++;
520                         String fileName = zipEntry.getName();
521                         fileNames.put(fileName, zipEntry); // Java has no use for the ZipEntry, but JavaScript can read it.
522                 }
523                 input.close();
524                 System.out.println("Assets: " + n + " zip entries found in " + url); //$NON-NLS-1$
525                 return fileNames;
526         }
527
528         private void resort() {
529                 sortedList = new String[assetsByPath.size()];
530                 int i = 0;
531                 for (String path : assetsByPath.keySet()) {
532                         sortedList[i++] = path;
533                 }
534                 Arrays.sort(sortedList);
535         }
536
537
538         /**
539          * Only needed for Java
540          * 
541          * @param is
542          * @param n
543          * @param out
544          * @return
545          * @throws IOException
546          */
547         private static byte[] getLimitedStreamBytes(InputStream is, long n, OutputStream out) throws IOException {
548
549                 // Note: You cannot use InputStream.available() to reliably read
550                 // zip data from the web.
551
552                 boolean toOut = (out != null);
553                 int buflen = (n > 0 && n < 1024 ? (int) n : 1024);
554                 byte[] buf = new byte[buflen];
555                 byte[] bytes = (out == null ? new byte[n < 0 ? 4096 : (int) n] : null);
556                 int len = 0;
557                 int totalLen = 0;
558                 if (n < 0)
559                         n = Integer.MAX_VALUE;
560                 while (totalLen < n && (len = is.read(buf, 0, buflen)) > 0) {
561                         totalLen += len;
562                         if (toOut) {
563                                 out.write(buf, 0, len);
564                         } else {
565                                 if (totalLen > bytes.length)
566                                         bytes = Arrays.copyOf(bytes, totalLen * 2);
567                                 System.arraycopy(buf, 0, bytes, totalLen - len, len);
568                                 if (n != Integer.MAX_VALUE && totalLen + buflen > bytes.length)
569                                         buflen = bytes.length - totalLen;
570                         }
571                 }
572                 if (toOut)
573                         return null;
574                 if (totalLen == bytes.length)
575                         return bytes;
576                 buf = new byte[totalLen];
577                 System.arraycopy(bytes, 0, buf, 0, totalLen);
578                 return buf;
579         }
580
581         /**
582          * Return all assets in the form that is appropriate for the Info.assets value in SwingJS.
583          * 
584          */
585         @Override
586         public String toString() {
587                 String s = "[";
588                 for (int i = 0; i < sortedList.length; i++) {
589                         Asset a = assetsByPath.get(sortedList[i]);
590                         s += (i == 0 ? "" : ",") + a;
591                 }
592                 return s + "]";
593         }
594
595 }