4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.io.OutputStream;
7 import java.net.MalformedURLException;
9 import java.net.URISyntaxException;
11 import java.util.Arrays;
12 import java.util.HashMap;
13 import java.util.HashSet;
15 import java.util.zip.ZipEntry;
16 import java.util.zip.ZipInputStream;
18 import swingjs.api.JSUtilI;
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.
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"));
41 } catch (Exception e) {
42 OSPLog.warning("Error reading assets path. ");
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.
53 * Several static methods can be used to retrieve assets. Principal among those
57 * getAssetBytes(String fullPath)
58 * getAssetString(String fullPath)
59 * getAssetStream(String fullPath)
62 * If an asset is not found in a zip file, then it will be loaded from its fullPath.
72 public static boolean isJS = /** @j2sNative true || */
75 public static JSUtilI jsutil;
80 jsutil = ((JSUtilI) Class.forName("swingjs.JSUtil").newInstance());
83 } catch (Exception e) {
84 System.err.println("Assets could not create swinjs.JSUtil instance");
88 private Map<String, Map<String, ZipEntry>> htZipContents = new HashMap<>();
90 private static boolean doCacheZipContents = true;
92 private static Assets instance = new Assets();
97 private Map<String, Asset> assetsByPath = new HashMap<>();
99 private String[] sortedList = new String[0];
102 * If this object has been cached by SwingJS, add its bytes to the URL, URI, or
105 * @param URLorURIorFile
108 public static byte[] addJSCachedBytes(Object URLorURIorFile) {
109 return (isJS ? jsutil.addJSCachedBytes(URLorURIorFile) : null);
112 public static class Asset {
119 public Asset(String name, String zipPath, String[] classPaths) {
121 this.zipPath = zipPath;
122 this.classPaths = classPaths;
125 public Asset(String name, String zipPath, String classPath) {
127 this.zipPath = zipPath;
128 uri = getAbsoluteURI(zipPath); // no spaces expected here.
129 this.classPath = classPath.endsWith("/") ? classPath : classPath + "/";
132 public URL getURL(String fullPath) throws MalformedURLException {
133 return (fullPath.indexOf(classPath) < 0 ? null
134 : new URL("jar", null, uri + "!/" + fullPath));//.replaceAll(" ", "%20")));
138 public String toString() {
139 return "{" + "\"name\":" + "\"" + name + "\"," + "\"zipPath\":" + "\"" + zipPath + "\"," + "\"classPath\":"
140 + "\"" + classPath + "\"" + "}";
145 public static Assets getInstance() {
150 * The difference here is that URL will not insert the %20 for space that URI will.
155 @SuppressWarnings("deprecation")
156 public static URL getAbsoluteURL(String path) {
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) {
168 public static URI getAbsoluteURI(String path) {
171 uri = (path.indexOf(":/") < 0 ? new File(new File(path).getAbsolutePath()).toURI() : new URI(path));
172 } catch (URISyntaxException e) {
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 .
185 public static void add(Object o) {
189 if (o instanceof Object[]) {
190 Object[] a = (Object[]) o;
191 for (int i = 0; i < a.length; i++)
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.
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);
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);
206 add(a.name, a.zipPath, a.classPaths);
208 } catch (Throwable t) {
209 throw new IllegalArgumentException(t.getMessage());
213 public static void add(String name, String zipFile, String path) {
214 add(name, zipFile, new String[] { path });
217 private static HashSet<String> loadedAssets = new HashSet<>();
219 public static boolean hasLoaded(String name) {
220 return loadedAssets.contains(name);
223 public static void reset() {
224 getInstance().htZipContents.clear();
225 getInstance().assetsByPath.clear();
226 getInstance().sortedList = new String[0];
229 public static void add(String name, String zipFile, String[] paths) {
230 getInstance()._add(name, zipFile, paths);
233 private void _add(String name, String zipFile, String[] paths) {
234 if (hasLoaded(name)) {
235 System.err.println("Assets warning: Asset " + name + " already exists");
237 loadedAssets.add(name);
238 for (int i = paths.length; --i >= 0;) {
239 assetsByPath.put(paths[i], new Asset(name, zipFile, paths[i]));
246 * Gets the asset, preferably from a zip file asset, but not necessarily.
251 public static byte[] getAssetBytes(String assetPath) {
252 return getAssetBytes(assetPath, false);
256 * Gets the asset, preferably from a zip file asset, but not necessarily.
261 public static String getAssetString(String assetPath) {
262 return getAssetString(assetPath, false);
266 * Gets the asset, preferably from a zip file asset, but not necessarily.
271 public static InputStream getAssetStream(String assetPath) {
272 return getAssetStream(assetPath, false);
276 * Gets the asset from a zip file.
281 public static byte[] getAssetBytesFromZip(String assetPath) {
282 return getAssetBytes(assetPath, true);
286 * Gets the asset from a zip file.
291 public static String getAssetStringFromZip(String assetPath) {
292 return getAssetString(assetPath, true);
296 * Gets the asset from a zip file.
301 public static InputStream getAssetStreamFromZip(String assetPath) {
302 return getAssetStream(assetPath, true);
307 * Get the contents of a path from a zip file asset as byte[], optionally loading
308 * the resource directly using a class loader.
314 private static byte[] getAssetBytes(String path, boolean zipOnly) {
317 URL url = getInstance()._getURLFromPath(path, true);
318 if (url == null && !zipOnly) {
319 url = getAbsoluteURL(path);
320 //url = Assets.class.getResource(path);
325 bytes = jsutil.getURLBytes(url);
328 bytes = jsutil.getURLBytes(url);
331 bytes = getLimitedStreamBytes(url.openStream(), -1, null);
333 } catch (Throwable t) {
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.
347 private static String getAssetString(String path, boolean zipOnly) {
348 byte[] bytes = getAssetBytes(path, zipOnly);
349 return (bytes == null ? null : new String(bytes));
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.
360 private static InputStream getAssetStream(String path, boolean zipOnly) {
362 URL url = getInstance()._getURLFromPath(path, true);
363 if (url == null && !zipOnly) {
364 url = Assets.class.getClassLoader().getResource(path);
367 return url.openStream();
368 } catch (Throwable t) {
373 * Determine the path to an asset. If not found in a zip file asset, return the
374 * absolute path to this resource.
379 public static URL getURLFromPath(String fullPath) {
380 return getInstance()._getURLFromPath(fullPath, false);
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.
389 * @return the URL to this asset, or null if not found.
391 public static URL getURLFromPath(String fullPath, boolean zipOnly) {
392 return getInstance()._getURLFromPath(fullPath, zipOnly);
395 private URL _getURLFromPath(String fullPath, boolean zipOnly) {
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);
407 jsutil.setURLBytes(url, jsutil.getZipBytes(ze));
413 return getAbsoluteURL(fullPath);
414 } catch (MalformedURLException e) {
419 public static ZipEntry findZipEntry(URL url) {
420 String[] parts = getJarURLParts(url.toString());
421 if (parts == null || parts[0] == null || parts[1].length() == 0)
423 return findZipEntry(parts[0], parts[1]);
426 public static ZipEntry findZipEntry(String zipFile, String fileName) {
427 return getZipContents(zipFile).get(fileName);
431 * Gets the contents of a zip file.
433 * @param zipPath the path to the zip file
434 * @return a set of file names in alphabetical order
436 public static Map<String, ZipEntry> getZipContents(String zipPath) {
437 return getInstance()._getZipContents(zipPath);
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)
446 // Scan URL zip stream for files.
447 return readZipContents(url.openStream(), url);
448 } catch (Exception ex) {
449 ex.printStackTrace();
455 * Deconstruct a jar URL into two parts, before and after "!/".
460 public static String[] getJarURLParts(String source) {
461 int n = source.indexOf("!/");
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)) };
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.
478 public static byte[] getURLContents(URL url) {
483 // Java 9! return new String(url.openStream().readAllBytes());
484 return jsutil.readAllBytes(url.openStream());
486 return getLimitedStreamBytes(url.openStream(), -1, null);
487 } catch (IOException e) {
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.
501 private static URL getURLWithCachedBytes(String path) {
502 URL url = getAbsoluteURL(path);
504 addJSCachedBytes(url);
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;
515 while ((zipEntry = input.getNextEntry()) != null) {
516 if (zipEntry.isDirectory() || zipEntry.getSize() == 0)
519 String fileName = zipEntry.getName();
520 fileNames.put(fileName, zipEntry); // Java has no use for the ZipEntry, but JavaScript can read it.
523 System.out.println("Assets: " + n + " zip entries found in " + url); //$NON-NLS-1$
527 private void resort() {
528 sortedList = new String[assetsByPath.size()];
530 for (String path : assetsByPath.keySet()) {
531 sortedList[i++] = path;
533 Arrays.sort(sortedList);
538 * Only needed for Java
544 * @throws IOException
546 private static byte[] getLimitedStreamBytes(InputStream is, long n, OutputStream out) throws IOException {
548 // Note: You cannot use InputStream.available() to reliably read
549 // zip data from the web.
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);
558 n = Integer.MAX_VALUE;
559 while (totalLen < n && (len = is.read(buf, 0, buflen)) > 0) {
562 out.write(buf, 0, len);
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;
573 if (totalLen == bytes.length)
575 buf = new byte[totalLen];
576 System.arraycopy(bytes, 0, buf, 0, totalLen);
581 * Return all assets in the form that is appropriate for the Info.assets value in SwingJS.
585 public String toString() {
587 for (int i = 0; i < sortedList.length; i++) {
588 Asset a = assetsByPath.get(sortedList[i]);
589 s += (i == 0 ? "" : ",") + a;