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