Clover icon

Coverage Report

  1. Project Clover database Thu Jan 15 2026 16:11:02 GMT
  2. Package javajs.async

File Assets.java

 

Coverage histogram

../../img/srcFileCovDistChart0.png
62% of files have more coverage

Code metrics

100
184
39
2
701
438
111
0.6
4.72
19.5
2.85

Classes

Class Line # Actions
Assets 71 175 105
0.00%
Assets.Asset 121 9 6
0.00%
 

Contributing tests

No tests hitting this source file were found.

Source view

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
63    * fullPath.
64    *
65    *
66    *
67    * @author hansonr
68    *
69    */
70   
 
71    public class Assets
72    {
73   
74    public static boolean isJS = /** @j2sNative true || */
75    false;
76   
77    public static JSUtilI jsutil;
78   
 
79  0 toggle static
80    {
81  0 try
82    {
83  0 if (isJS)
84    {
85  0 jsutil = ((JSUtilI) Class.forName("swingjs.JSUtil")
86    .getDeclaredConstructor().newInstance());
87    }
88   
89    } catch (Exception e)
90    {
91  0 System.err.println("Assets could not create swinjs.JSUtil instance");
92    }
93    }
94   
95    private Map<String, Map<String, ZipEntry>> htZipContents = new HashMap<>();
96   
97    private static boolean doCacheZipContents = true;
98   
99    private static Assets instance = new Assets();
100   
 
101  0 toggle private Assets()
102    {
103    }
104   
105    private Map<String, Asset> assetsByPath = new HashMap<>();
106   
107    private String[] sortedList = new String[0];
108   
109    /**
110    * If this object has been cached by SwingJS, add its bytes to the URL, URI,
111    * or File
112    *
113    * @param URLorURIorFile
114    * @return
115    */
 
116  0 toggle public static byte[] addJSCachedBytes(Object URLorURIorFile)
117    {
118  0 return (isJS ? jsutil.addJSCachedBytes(URLorURIorFile) : null);
119    }
120   
 
121    public static class Asset
122    {
123    String name;
124   
125    URI uri;
126   
127    String classPath;
128   
129    String zipPath;
130   
131    String[] classPaths;
132   
 
133  0 toggle public Asset(String name, String zipPath, String[] classPaths)
134    {
135  0 this.name = name;
136  0 this.zipPath = zipPath;
137  0 this.classPaths = classPaths;
138    }
139   
 
140  0 toggle public Asset(String name, String zipPath, String classPath)
141    {
142  0 this.name = name;
143  0 this.zipPath = zipPath;
144  0 uri = getAbsoluteURI(zipPath); // no spaces expected here.
145  0 this.classPath = classPath.endsWith("/") ? classPath
146    : classPath + "/";
147    }
148   
 
149  0 toggle public URL getURL(String fullPath) throws MalformedURLException
150    {
151  0 return (fullPath.indexOf(classPath) < 0 ? null
152    : new URL("jar", null, uri + "!/" + fullPath));// .replaceAll(" ",
153    // "%20")));
154    }
155   
 
156  0 toggle @Override
157    public String toString()
158    {
159  0 return "{" + "\"name\":" + "\"" + name + "\"," + "\"zipPath\":" + "\""
160    + zipPath + "\"," + "\"classPath\":" + "\"" + classPath + "\""
161    + "}";
162    }
163   
164    }
165   
 
166  0 toggle public static Assets getInstance()
167    {
168  0 return instance;
169    }
170   
171    /**
172    * The difference here is that URL will not insert the %20 for space that URI
173    * will.
174    *
175    * @param path
176    * @return
177    */
 
178  0 toggle @SuppressWarnings("deprecation")
179    public static URL getAbsoluteURL(String path)
180    {
181  0 URL url = null;
182  0 try
183    {
184  0 url = (path.indexOf(":/") < 0
185    ? new File(new File(path).getAbsolutePath()).toURL()
186    : new URL(path));
187  0 if (path.indexOf("!/") >= 0)
188  0 url = new URL("jar", null, url.toString());
189    } catch (MalformedURLException e)
190    {
191  0 e.printStackTrace();
192    }
193  0 return url;
194    }
195   
 
196  0 toggle public static URI getAbsoluteURI(String path)
197    {
198  0 URI uri = null;
199  0 try
200    {
201  0 uri = (path.indexOf(":/") < 0
202    ? new File(new File(path).getAbsolutePath()).toURI()
203    : new URI(path));
204    } catch (URISyntaxException e)
205    {
206  0 e.printStackTrace();
207    }
208  0 return uri;
209    }
210   
211    /**
212    * Allows passing a Java Asset or array of Assets or a JavaScript Object or
213    * Object array that contains name, zipPath, and classPath keys; in
214    * JavaScript, the keys can have multiple .
215    *
216    * @param o
217    */
 
218  0 toggle public static void add(Object o)
219    {
220  0 if (o == null)
221  0 return;
222  0 try
223    {
224  0 if (o instanceof Object[])
225    {
226  0 Object[] a = (Object[]) o;
227  0 for (int i = 0; i < a.length; i++)
228  0 add(a[i]);
229  0 return;
230    }
231    // In JavaScript this may not actually be an Asset, only a proxy for that.
232    // Just testing for keys. Only one of classPath and classPaths is allowed.
233  0 Asset a = (Asset) o;
234  0 if (a.name == null || a.zipPath == null
235    || a.classPath == null && a.classPaths == null
236    || a.classPath != null && a.classPaths != null)
237    {
238  0 throw new NullPointerException("Assets could not parse " + o);
239    }
240  0 if (a.classPaths == null)
241    {
242    // not possible in Java, but JavaScript may be passing an array of class
243    // paths
244  0 add(a.name, a.zipPath, a.classPath);
245    }
246    else
247    {
248  0 add(a.name, a.zipPath, a.classPaths);
249    }
250    } catch (Throwable t)
251    {
252  0 throw new IllegalArgumentException(t.getMessage());
253    }
254    }
255   
 
256  0 toggle public static void add(String name, String zipFile, String path)
257    {
258  0 add(name, zipFile, new String[] { path });
259    }
260   
261    private static HashSet<String> loadedAssets = new HashSet<>();
262   
 
263  0 toggle public static boolean hasLoaded(String name)
264    {
265  0 return loadedAssets.contains(name);
266    }
267   
 
268  0 toggle public static void reset()
269    {
270  0 getInstance().htZipContents.clear();
271  0 getInstance().assetsByPath.clear();
272  0 getInstance().sortedList = new String[0];
273    }
274   
 
275  0 toggle public static void add(String name, String zipFile, String[] paths)
276    {
277  0 getInstance()._add(name, zipFile, paths);
278    }
279   
 
280  0 toggle private void _add(String name, String zipFile, String[] paths)
281    {
282  0 if (hasLoaded(name))
283    {
284  0 System.err
285    .println("Assets warning: Asset " + name + " already exists");
286    }
287  0 loadedAssets.add(name);
288  0 for (int i = paths.length; --i >= 0;)
289    {
290  0 assetsByPath.put(paths[i], new Asset(name, zipFile, paths[i]));
291    }
292  0 resort();
293    }
294   
295    /**
296    * Gets the asset, preferably from a zip file asset, but not necessarily.
297    *
298    * @param assetPath
299    * @return
300    */
 
301  0 toggle public static byte[] getAssetBytes(String assetPath)
302    {
303  0 return getAssetBytes(assetPath, false);
304    }
305   
306    /**
307    * Gets the asset, preferably from a zip file asset, but not necessarily.
308    *
309    * @param assetPath
310    * @return
311    */
 
312  0 toggle public static String getAssetString(String assetPath)
313    {
314  0 return getAssetString(assetPath, false);
315    }
316   
317    /**
318    * Gets the asset, preferably from a zip file asset, but not necessarily.
319    *
320    * @param assetPath
321    * @return
322    */
 
323  0 toggle public static InputStream getAssetStream(String assetPath)
324    {
325  0 return getAssetStream(assetPath, false);
326    }
327   
328    /**
329    * Gets the asset from a zip file.
330    *
331    * @param assetPath
332    * @return
333    */
 
334  0 toggle public static byte[] getAssetBytesFromZip(String assetPath)
335    {
336  0 return getAssetBytes(assetPath, true);
337    }
338   
339    /**
340    * Gets the asset from a zip file.
341    *
342    * @param assetPath
343    * @return
344    */
 
345  0 toggle public static String getAssetStringFromZip(String assetPath)
346    {
347  0 return getAssetString(assetPath, true);
348    }
349   
350    /**
351    * Gets the asset from a zip file.
352    *
353    * @param assetPath
354    * @return
355    */
 
356  0 toggle public static InputStream getAssetStreamFromZip(String assetPath)
357    {
358  0 return getAssetStream(assetPath, true);
359    }
360   
361    /**
362    * Get the contents of a path from a zip file asset as byte[], optionally
363    * loading the resource directly using a class loader.
364    *
365    * @param path
366    * @param zipOnly
367    * @return
368    */
 
369  0 toggle private static byte[] getAssetBytes(String path, boolean zipOnly)
370    {
371  0 byte[] bytes = null;
372  0 try
373    {
374  0 URL url = getInstance()._getURLFromPath(path, true);
375  0 if (url == null && !zipOnly)
376    {
377  0 url = getAbsoluteURL(path);
378    // url = Assets.class.getResource(path);
379    }
380  0 if (url == null)
381  0 return null;
382  0 if (isJS)
383    {
384  0 bytes = jsutil.getURLBytes(url);
385  0 if (bytes == null)
386    {
387  0 url.openStream();
388  0 bytes = jsutil.getURLBytes(url);
389    }
390    }
391    else
392    {
393  0 bytes = getLimitedStreamBytes(url.openStream(), -1, null);
394    }
395    } catch (Throwable t)
396    {
397  0 t.printStackTrace();
398    }
399  0 return bytes;
400    }
401   
402    /**
403    * Get the contents of a path from a zip file asset as a String, optionally
404    * loading the resource directly using a class loader.
405    *
406    * @param path
407    * @param zipOnly
408    * @return
409    */
 
410  0 toggle private static String getAssetString(String path, boolean zipOnly)
411    {
412  0 byte[] bytes = getAssetBytes(path, zipOnly);
413  0 return (bytes == null ? null : new String(bytes));
414    }
415   
416    /**
417    * Get the contents of a path from a zip file asset as an InputStream,
418    * optionally loading the resource directly using a class loader.
419    *
420    * @param path
421    * @param zipOnly
422    * @return
423    */
 
424  0 toggle private static InputStream getAssetStream(String path, boolean zipOnly)
425    {
426  0 try
427    {
428  0 URL url = getInstance()._getURLFromPath(path, true);
429  0 if (url == null && !zipOnly)
430    {
431  0 url = Assets.class.getClassLoader().getResource(path);
432    }
433  0 if (url != null)
434  0 return url.openStream();
435    } catch (Throwable t)
436    {
437    }
438  0 return null;
439    }
440   
441    /**
442    * Determine the path to an asset. If not found in a zip file asset, return
443    * the absolute path to this resource.
444    *
445    * @param fullPath
446    * @return
447    */
 
448  0 toggle public static URL getURLFromPath(String fullPath)
449    {
450  0 return getInstance()._getURLFromPath(fullPath, false);
451    }
452   
453    /**
454    * Determine the path to an asset. If not found in a zip file asset,
455    * optionally return null or the absolute path to this resource.
456    *
457    * @param fullPath
458    * @param zipOnly
459    * @return the URL to this asset, or null if not found.
460    */
 
461  0 toggle public static URL getURLFromPath(String fullPath, boolean zipOnly)
462    {
463  0 return getInstance()._getURLFromPath(fullPath, zipOnly);
464    }
465   
 
466  0 toggle private URL _getURLFromPath(String fullPath, boolean zipOnly)
467    {
468  0 URL url = null;
469  0 try
470    {
471  0 if (fullPath.startsWith("/"))
472  0 fullPath = fullPath.substring(1);
473  0 for (int i = sortedList.length; --i >= 0;)
474    {
475  0 if (fullPath.startsWith(sortedList[i]))
476    {
477  0 url = assetsByPath.get(sortedList[i]).getURL(fullPath);
478  0 ZipEntry ze = findZipEntry(url);
479  0 if (ze == null)
480  0 break;
481  0 if (isJS)
482    {
483  0 jsutil.setURLBytes(url, jsutil.getZipBytes(ze));
484    }
485  0 return url;
486    }
487    }
488  0 if (!zipOnly)
489  0 return getAbsoluteURL(fullPath);
490    } catch (MalformedURLException e)
491    {
492    }
493  0 return null;
494    }
495   
 
496  0 toggle public static ZipEntry findZipEntry(URL url)
497    {
498  0 String[] parts = getJarURLParts(url.toString());
499  0 if (parts == null || parts[0] == null || parts[1].length() == 0)
500  0 return null;
501  0 return findZipEntry(parts[0], parts[1]);
502    }
503   
 
504  0 toggle public static ZipEntry findZipEntry(String zipFile, String fileName)
505    {
506  0 return getZipContents(zipFile).get(fileName);
507    }
508   
509    /**
510    * Gets the contents of a zip file.
511    *
512    * @param zipPath
513    * the path to the zip file
514    * @return a set of file names in alphabetical order
515    */
 
516  0 toggle public static Map<String, ZipEntry> getZipContents(String zipPath)
517    {
518  0 return getInstance()._getZipContents(zipPath);
519    }
520   
 
521  0 toggle private Map<String, ZipEntry> _getZipContents(String zipPath)
522    {
523  0 URL url = getURLWithCachedBytes(zipPath); // BH carry over bytes if we have
524    // them already
525  0 Map<String, ZipEntry> fileNames = htZipContents.get(url.toString());
526  0 if (fileNames != null)
527  0 return fileNames;
528  0 try
529    {
530    // Scan URL zip stream for files.
531  0 return readZipContents(url.openStream(), url);
532    } catch (Exception ex)
533    {
534  0 ex.printStackTrace();
535  0 return null;
536    }
537    }
538   
539    /**
540    * Deconstruct a jar URL into two parts, before and after "!/".
541    *
542    * @param source
543    * @return
544    */
 
545  0 toggle public static String[] getJarURLParts(String source)
546    {
547  0 int n = source.indexOf("!/");
548  0 if (n < 0)
549  0 return null;
550  0 String jarfile = source.substring(0, n).replace("jar:", "");
551  0 while (jarfile.startsWith("//"))
552  0 jarfile = jarfile.substring(1);
553  0 return new String[] { jarfile,
554  0 (n == source.length() - 2 ? null : source.substring(n + 2)) };
555    }
556   
557    /**
558    * Get the contents of any URL as a byte array. This method does not do any
559    * asset check. It just gets the url data as a byte array.
560    *
561    * @param url
562    * @return byte[]
563    *
564    * @author hansonr
565    */
 
566  0 toggle public static byte[] getURLContents(URL url)
567    {
568  0 if (url == null)
569  0 return null;
570  0 try
571    {
572  0 if (isJS)
573    {
574    // Java 9! return new String(url.openStream().readAllBytes());
575  0 return jsutil.readAllBytes(url.openStream());
576    }
577  0 return getLimitedStreamBytes(url.openStream(), -1, null);
578    } catch (IOException e)
579    {
580  0 e.printStackTrace();
581    }
582  0 return null;
583    }
584   
585    /**
586    *
587    * Convert a file path to a URL, retrieving any cached file data, as from DnD.
588    * Do not do any actual data transfer. This is a swingjs.JSUtil service.
589    *
590    * @param path
591    * @return
592    */
 
593  0 toggle private static URL getURLWithCachedBytes(String path)
594    {
595  0 URL url = getAbsoluteURL(path);
596  0 if (url != null)
597  0 addJSCachedBytes(url);
598  0 return url;
599    }
600   
 
601  0 toggle private Map<String, ZipEntry> readZipContents(InputStream is, URL url)
602    throws IOException
603    {
604  0 HashMap<String, ZipEntry> fileNames = new HashMap<String, ZipEntry>();
605  0 if (doCacheZipContents)
606  0 htZipContents.put(url.toString(), fileNames);
607  0 ZipInputStream input = new ZipInputStream(is);
608  0 ZipEntry zipEntry = null;
609  0 int n = 0;
610  0 while ((zipEntry = input.getNextEntry()) != null)
611    {
612  0 if (zipEntry.isDirectory() || zipEntry.getSize() == 0)
613  0 continue;
614  0 n++;
615  0 String fileName = zipEntry.getName();
616  0 fileNames.put(fileName, zipEntry); // Java has no use for the ZipEntry,
617    // but JavaScript can read it.
618    }
619  0 input.close();
620  0 System.out.println("Assets: " + n + " zip entries found in " + url); //$NON-NLS-1$
621  0 return fileNames;
622    }
623   
 
624  0 toggle private void resort()
625    {
626  0 sortedList = new String[assetsByPath.size()];
627  0 int i = 0;
628  0 for (String path : assetsByPath.keySet())
629    {
630  0 sortedList[i++] = path;
631    }
632  0 Arrays.sort(sortedList);
633    }
634   
635    /**
636    * Only needed for Java
637    *
638    * @param is
639    * @param n
640    * @param out
641    * @return
642    * @throws IOException
643    */
 
644  0 toggle private static byte[] getLimitedStreamBytes(InputStream is, long n,
645    OutputStream out) throws IOException
646    {
647   
648    // Note: You cannot use InputStream.available() to reliably read
649    // zip data from the web.
650   
651  0 boolean toOut = (out != null);
652  0 int buflen = (n > 0 && n < 1024 ? (int) n : 1024);
653  0 byte[] buf = new byte[buflen];
654  0 byte[] bytes = (out == null ? new byte[n < 0 ? 4096 : (int) n] : null);
655  0 int len = 0;
656  0 int totalLen = 0;
657  0 if (n < 0)
658  0 n = Integer.MAX_VALUE;
659  0 while (totalLen < n && (len = is.read(buf, 0, buflen)) > 0)
660    {
661  0 totalLen += len;
662  0 if (toOut)
663    {
664  0 out.write(buf, 0, len);
665    }
666    else
667    {
668  0 if (totalLen > bytes.length)
669  0 bytes = Arrays.copyOf(bytes, totalLen * 2);
670  0 System.arraycopy(buf, 0, bytes, totalLen - len, len);
671  0 if (n != Integer.MAX_VALUE && totalLen + buflen > bytes.length)
672  0 buflen = bytes.length - totalLen;
673    }
674    }
675  0 if (toOut)
676  0 return null;
677  0 if (totalLen == bytes.length)
678  0 return bytes;
679  0 buf = new byte[totalLen];
680  0 System.arraycopy(bytes, 0, buf, 0, totalLen);
681  0 return buf;
682    }
683   
684    /**
685    * Return all assets in the form that is appropriate for the Info.assets value
686    * in SwingJS.
687    *
688    */
 
689  0 toggle @Override
690    public String toString()
691    {
692  0 String s = "[";
693  0 for (int i = 0; i < sortedList.length; i++)
694    {
695  0 Asset a = assetsByPath.get(sortedList[i]);
696  0 s += (i == 0 ? "" : ",") + a;
697    }
698  0 return s + "]";
699    }
700   
701    }