3 import java.awt.Toolkit;
5 import java.io.IOException;
6 import java.io.InputStream;
7 import java.io.OutputStream;
8 import java.net.MalformedURLException;
10 import java.net.URISyntaxException;
12 import java.util.Arrays;
13 import java.util.HashMap;
14 import java.util.HashSet;
16 import java.util.zip.ZipEntry;
17 import java.util.zip.ZipInputStream;
19 import swingjs.api.JSUtilI;
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.
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"));
42 } catch (Exception e) {
43 OSPLog.warning("Error reading assets path. ");
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.
54 * Several static methods can be used to retrieve assets. Principal among those
58 * getAssetBytes(String fullPath)
59 * getAssetString(String fullPath)
60 * getAssetStream(String fullPath)
63 * If an asset is not found in a zip file, then it will be loaded from its fullPath.
73 public static boolean isJS = /** @j2sNative true || */
76 public static JSUtilI jsutil;
81 jsutil = ((JSUtilI) Class.forName("swingjs.JSUtil").newInstance());
84 } catch (Exception e) {
85 System.err.println("Assets could not create swinjs.JSUtil instance");
89 private Map<String, Map<String, ZipEntry>> htZipContents = new HashMap<>();
91 private static boolean doCacheZipContents = true;
93 private static Assets instance = new Assets();
98 private Map<String, Asset> assetsByPath = new HashMap<>();
100 private String[] sortedList = new String[0];
103 * If this object has been cached by SwingJS, add its bytes to the URL, URI, or
106 * @param URLorURIorFile
109 public static byte[] addJSCachedBytes(Object URLorURIorFile) {
110 return (isJS ? jsutil.addJSCachedBytes(URLorURIorFile) : null);
113 public static class Asset {
120 public Asset(String name, String zipPath, String[] classPaths) {
122 this.zipPath = zipPath;
123 this.classPaths = classPaths;
126 public Asset(String name, String zipPath, String classPath) {
128 this.zipPath = zipPath;
129 uri = getAbsoluteURI(zipPath); // no spaces expected here.
130 this.classPath = classPath.endsWith("/") ? classPath : classPath + "/";
133 public URL getURL(String fullPath) throws MalformedURLException {
134 return (fullPath.indexOf(classPath) < 0 ? null
135 : new URL("jar", null, uri + "!/" + fullPath));//.replaceAll(" ", "%20")));
139 public String toString() {
140 return "{" + "\"name\":" + "\"" + name + "\"," + "\"zipPath\":" + "\"" + zipPath + "\"," + "\"classPath\":"
141 + "\"" + classPath + "\"" + "}";
146 public static Assets getInstance() {
151 * The difference here is that URL will not insert the %20 for space that URI will.
156 @SuppressWarnings("deprecation")
157 public static URL getAbsoluteURL(String path) {
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) {
169 public static URI getAbsoluteURI(String path) {
172 uri = (path.indexOf(":/") < 0 ? new File(new File(path).getAbsolutePath()).toURI() : new URI(path));
173 } catch (URISyntaxException e) {
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 .
186 public static void add(Object o) {
190 if (o instanceof Object[]) {
191 Object[] a = (Object[]) o;
192 for (int i = 0; i < a.length; i++)
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.
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);
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);
207 add(a.name, a.zipPath, a.classPaths);
209 } catch (Throwable t) {
210 throw new IllegalArgumentException(t.getMessage());
214 public static void add(String name, String zipFile, String path) {
215 add(name, zipFile, new String[] { path });
218 private static HashSet<String> loadedAssets = new HashSet<>();
220 public static boolean hasLoaded(String name) {
221 return loadedAssets.contains(name);
224 public static void reset() {
225 getInstance().htZipContents.clear();
226 getInstance().assetsByPath.clear();
227 getInstance().sortedList = new String[0];
230 public static void add(String name, String zipFile, String[] paths) {
231 getInstance()._add(name, zipFile, paths);
234 private void _add(String name, String zipFile, String[] paths) {
235 if (hasLoaded(name)) {
236 System.err.println("Assets warning: Asset " + name + " already exists");
238 loadedAssets.add(name);
239 for (int i = paths.length; --i >= 0;) {
240 assetsByPath.put(paths[i], new Asset(name, zipFile, paths[i]));
247 * Gets the asset, preferably from a zip file asset, but not necessarily.
252 public static byte[] getAssetBytes(String assetPath) {
253 return getAssetBytes(assetPath, false);
257 * Gets the asset, preferably from a zip file asset, but not necessarily.
262 public static String getAssetString(String assetPath) {
263 return getAssetString(assetPath, false);
267 * Gets the asset, preferably from a zip file asset, but not necessarily.
272 public static InputStream getAssetStream(String assetPath) {
273 return getAssetStream(assetPath, false);
277 * Gets the asset from a zip file.
282 public static byte[] getAssetBytesFromZip(String assetPath) {
283 return getAssetBytes(assetPath, true);
287 * Gets the asset from a zip file.
292 public static String getAssetStringFromZip(String assetPath) {
293 return getAssetString(assetPath, true);
297 * Gets the asset from a zip file.
302 public static InputStream getAssetStreamFromZip(String assetPath) {
303 return getAssetStream(assetPath, true);
308 * Get the contents of a path from a zip file asset as byte[], optionally loading
309 * the resource directly using a class loader.
315 private static byte[] getAssetBytes(String path, boolean zipOnly) {
318 URL url = getInstance()._getURLFromPath(path, true);
319 if (url == null && !zipOnly) {
320 url = getAbsoluteURL(path);
321 //url = Assets.class.getResource(path);
326 bytes = jsutil.getURLBytes(url);
329 bytes = jsutil.getURLBytes(url);
332 bytes = getLimitedStreamBytes(url.openStream(), -1, null);
334 } catch (Throwable t) {
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.
348 private static String getAssetString(String path, boolean zipOnly) {
349 byte[] bytes = getAssetBytes(path, zipOnly);
350 return (bytes == null ? null : new String(bytes));
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.
361 private static InputStream getAssetStream(String path, boolean zipOnly) {
363 URL url = getInstance()._getURLFromPath(path, true);
364 if (url == null && !zipOnly) {
365 url = Assets.class.getClassLoader().getResource(path);
368 return url.openStream();
369 } catch (Throwable t) {
374 * Determine the path to an asset. If not found in a zip file asset, return the
375 * absolute path to this resource.
380 public static URL getURLFromPath(String fullPath) {
381 return getInstance()._getURLFromPath(fullPath, false);
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.
390 * @return the URL to this asset, or null if not found.
392 public static URL getURLFromPath(String fullPath, boolean zipOnly) {
393 return getInstance()._getURLFromPath(fullPath, zipOnly);
396 private URL _getURLFromPath(String fullPath, boolean zipOnly) {
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);
408 jsutil.setURLBytes(url, jsutil.getZipBytes(ze));
414 return getAbsoluteURL(fullPath);
415 } catch (MalformedURLException e) {
420 public static ZipEntry findZipEntry(URL url) {
421 String[] parts = getJarURLParts(url.toString());
422 if (parts == null || parts[0] == null || parts[1].length() == 0)
424 return findZipEntry(parts[0], parts[1]);
427 public static ZipEntry findZipEntry(String zipFile, String fileName) {
428 return getZipContents(zipFile).get(fileName);
432 * Gets the contents of a zip file.
434 * @param zipPath the path to the zip file
435 * @return a set of file names in alphabetical order
437 public static Map<String, ZipEntry> getZipContents(String zipPath) {
438 return getInstance()._getZipContents(zipPath);
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)
447 // Scan URL zip stream for files.
448 return readZipContents(url.openStream(), url);
449 } catch (Exception ex) {
450 ex.printStackTrace();
456 * Deconstruct a jar URL into two parts, before and after "!/".
461 public static String[] getJarURLParts(String source) {
462 int n = source.indexOf("!/");
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)) };
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.
479 public static byte[] getURLContents(URL url) {
484 // Java 9! return new String(url.openStream().readAllBytes());
485 return jsutil.readAllBytes(url.openStream());
487 return getLimitedStreamBytes(url.openStream(), -1, null);
488 } catch (IOException e) {
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.
502 private static URL getURLWithCachedBytes(String path) {
503 URL url = getAbsoluteURL(path);
505 addJSCachedBytes(url);
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;
516 while ((zipEntry = input.getNextEntry()) != null) {
517 if (zipEntry.isDirectory() || zipEntry.getSize() == 0)
520 String fileName = zipEntry.getName();
521 fileNames.put(fileName, zipEntry); // Java has no use for the ZipEntry, but JavaScript can read it.
524 System.out.println("Assets: " + n + " zip entries found in " + url); //$NON-NLS-1$
528 private void resort() {
529 sortedList = new String[assetsByPath.size()];
531 for (String path : assetsByPath.keySet()) {
532 sortedList[i++] = path;
534 Arrays.sort(sortedList);
539 * Only needed for Java
545 * @throws IOException
547 private static byte[] getLimitedStreamBytes(InputStream is, long n, OutputStream out) throws IOException {
549 // Note: You cannot use InputStream.available() to reliably read
550 // zip data from the web.
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);
559 n = Integer.MAX_VALUE;
560 while (totalLen < n && (len = is.read(buf, 0, buflen)) > 0) {
563 out.write(buf, 0, len);
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;
574 if (totalLen == bytes.length)
576 buf = new byte[totalLen];
577 System.arraycopy(bytes, 0, buf, 0, totalLen);
582 * Return all assets in the form that is appropriate for the Info.assets value in SwingJS.
586 public String toString() {
588 for (int i = 0; i < sortedList.length; i++) {
589 Asset a = assetsByPath.get(sortedList[i]);
590 s += (i == 0 ? "" : ",") + a;