package javajs.async; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import swingjs.api.JSUtilI; /** * The Assets class allows assets such as images and property files to be * combined into zip files rather than delivered individually. The Assets * instance is a singleton served by a set of static methods. In particular, the * three add(...) methods are used to create asset references, which include an * arbitrary name, a path to a zip file asset, and one or more class paths that * are covered by this zip file asset. * * For example: * * static { try { Assets.add(new Assets.Asset("osp", "osp-assets.zip", "org/opensourcephysics/resources")); Assets.add(new Assets.Asset("tracker", "tracker-assets.zip", "org/opensourcephysics/cabrillo/tracker/resources")); Assets.add(new Assets.Asset("physlets", "physlet-assets.zip", new String[] { "opticsimages", "images" })); // add the Info.assets last so that it can override these defaults if (OSPRuntime.isJS) { Assets.add(OSPRuntime.jsutil.getAppletInfo("assets")); } } catch (Exception e) { OSPLog.warning("Error reading assets path. "); } } * * * It is not clear that Java is well-served by this zip-file loading, but * certainly JavaScript is. What could be 100 downloads is just one, and SwingJS * (but not Java) can cache individual ZipEntry instances in order to unzip them * independently only when needed. This is potentially a huge savings. * * Several static methods can be used to retrieve assets. Principal among those * are: * * * getAssetBytes(String fullPath) * getAssetString(String fullPath) * getAssetStream(String fullPath) * * * If an asset is not found in a zip file, then it will be loaded from its fullPath. * * * * @author hansonr * */ public class Assets { public static boolean isJS = /** @j2sNative true || */ false; public static JSUtilI jsutil; static { try { if (isJS) { jsutil = ((JSUtilI) Class.forName("swingjs.JSUtil").newInstance()); } } catch (Exception e) { System.err.println("Assets could not create swinjs.JSUtil instance"); } } private Map> htZipContents = new HashMap<>(); private static boolean doCacheZipContents = true; private static Assets instance = new Assets(); private Assets() { } private Map assetsByPath = new HashMap<>(); private String[] sortedList = new String[0]; /** * If this object has been cached by SwingJS, add its bytes to the URL, URI, or * File * * @param URLorURIorFile * @return */ public static byte[] addJSCachedBytes(Object URLorURIorFile) { return (isJS ? jsutil.addJSCachedBytes(URLorURIorFile) : null); } public static class Asset { String name; URI uri; String classPath; String zipPath; String[] classPaths; public Asset(String name, String zipPath, String[] classPaths) { this.name = name; this.zipPath = zipPath; this.classPaths = classPaths; } public Asset(String name, String zipPath, String classPath) { this.name = name; this.zipPath = zipPath; uri = getAbsoluteURI(zipPath); // no spaces expected here. this.classPath = classPath.endsWith("/") ? classPath : classPath + "/"; } public URL getURL(String fullPath) throws MalformedURLException { return (fullPath.indexOf(classPath) < 0 ? null : new URL("jar", null, uri + "!/" + fullPath));//.replaceAll(" ", "%20"))); } @Override public String toString() { return "{" + "\"name\":" + "\"" + name + "\"," + "\"zipPath\":" + "\"" + zipPath + "\"," + "\"classPath\":" + "\"" + classPath + "\"" + "}"; } } public static Assets getInstance() { return instance; } /** * The difference here is that URL will not insert the %20 for space that URI will. * * @param path * @return */ @SuppressWarnings("deprecation") public static URL getAbsoluteURL(String path) { URL url = null; try { url = (path.indexOf(":/") < 0 ? new File(new File(path).getAbsolutePath()).toURL() : new URL(path)); if (path.indexOf("!/")>=0) url = new URL("jar", null, url.toString()); } catch (MalformedURLException e) { e.printStackTrace(); } return url; } public static URI getAbsoluteURI(String path) { URI uri = null; try { uri = (path.indexOf(":/") < 0 ? new File(new File(path).getAbsolutePath()).toURI() : new URI(path)); } catch (URISyntaxException e) { e.printStackTrace(); } return uri; } /** * Allows passing a Java Asset or array of Assets or a JavaScript Object or * Object array that contains name, zipPath, and classPath keys; in JavaScript, * the keys can have multiple . * * @param o */ public static void add(Object o) { if (o == null) return; try { if (o instanceof Object[]) { Object[] a = (Object[]) o; for (int i = 0; i < a.length; i++) add(a[i]); return; } // In JavaScript this may not actually be an Asset, only a proxy for that. // Just testing for keys. Only one of classPath and classPaths is allowed. Asset a = (Asset) o; if (a.name == null || a.zipPath == null || a.classPath == null && a.classPaths == null || a.classPath != null && a.classPaths != null) { throw new NullPointerException("Assets could not parse " + o); } if (a.classPaths == null) { // not possible in Java, but JavaScript may be passing an array of class paths add(a.name, a.zipPath, a.classPath); } else { add(a.name, a.zipPath, a.classPaths); } } catch (Throwable t) { throw new IllegalArgumentException(t.getMessage()); } } public static void add(String name, String zipFile, String path) { add(name, zipFile, new String[] { path }); } private static HashSet loadedAssets = new HashSet<>(); public static boolean hasLoaded(String name) { return loadedAssets.contains(name); } public static void reset() { getInstance().htZipContents.clear(); getInstance().assetsByPath.clear(); getInstance().sortedList = new String[0]; } public static void add(String name, String zipFile, String[] paths) { getInstance()._add(name, zipFile, paths); } private void _add(String name, String zipFile, String[] paths) { if (hasLoaded(name)) { System.err.println("Assets warning: Asset " + name + " already exists"); } loadedAssets.add(name); for (int i = paths.length; --i >= 0;) { assetsByPath.put(paths[i], new Asset(name, zipFile, paths[i])); } resort(); } /** * Gets the asset, preferably from a zip file asset, but not necessarily. * * @param assetPath * @return */ public static byte[] getAssetBytes(String assetPath) { return getAssetBytes(assetPath, false); } /** * Gets the asset, preferably from a zip file asset, but not necessarily. * * @param assetPath * @return */ public static String getAssetString(String assetPath) { return getAssetString(assetPath, false); } /** * Gets the asset, preferably from a zip file asset, but not necessarily. * * @param assetPath * @return */ public static InputStream getAssetStream(String assetPath) { return getAssetStream(assetPath, false); } /** * Gets the asset from a zip file. * * @param assetPath * @return */ public static byte[] getAssetBytesFromZip(String assetPath) { return getAssetBytes(assetPath, true); } /** * Gets the asset from a zip file. * * @param assetPath * @return */ public static String getAssetStringFromZip(String assetPath) { return getAssetString(assetPath, true); } /** * Gets the asset from a zip file. * * @param assetPath * @return */ public static InputStream getAssetStreamFromZip(String assetPath) { return getAssetStream(assetPath, true); } /** * Get the contents of a path from a zip file asset as byte[], optionally loading * the resource directly using a class loader. * * @param path * @param zipOnly * @return */ private static byte[] getAssetBytes(String path, boolean zipOnly) { byte[] bytes = null; try { URL url = getInstance()._getURLFromPath(path, true); if (url == null && !zipOnly) { url = getAbsoluteURL(path); //url = Assets.class.getResource(path); } if (url == null) return null; if (isJS) { bytes = jsutil.getURLBytes(url); if (bytes == null) { url.openStream(); bytes = jsutil.getURLBytes(url); } } else { bytes = getLimitedStreamBytes(url.openStream(), -1, null); } } catch (Throwable t) { t.printStackTrace(); } return bytes; } /** * Get the contents of a path from a zip file asset as a String, optionally * loading the resource directly using a class loader. * * @param path * @param zipOnly * @return */ private static String getAssetString(String path, boolean zipOnly) { byte[] bytes = getAssetBytes(path, zipOnly); return (bytes == null ? null : new String(bytes)); } /** * Get the contents of a path from a zip file asset as an InputStream, optionally * loading the resource directly using a class loader. * * @param path * @param zipOnly * @return */ private static InputStream getAssetStream(String path, boolean zipOnly) { try { URL url = getInstance()._getURLFromPath(path, true); if (url == null && !zipOnly) { url = Assets.class.getClassLoader().getResource(path); } if (url != null) return url.openStream(); } catch (Throwable t) { } return null; } /** * Determine the path to an asset. If not found in a zip file asset, return the * absolute path to this resource. * * @param fullPath * @return */ public static URL getURLFromPath(String fullPath) { return getInstance()._getURLFromPath(fullPath, false); } /** * Determine the path to an asset. If not found in a zip file asset, optionally * return null or the absolute path to this resource. * * @param fullPath * @param zipOnly * @return the URL to this asset, or null if not found. */ public static URL getURLFromPath(String fullPath, boolean zipOnly) { return getInstance()._getURLFromPath(fullPath, zipOnly); } private URL _getURLFromPath(String fullPath, boolean zipOnly) { URL url = null; try { if (fullPath.startsWith("/")) fullPath = fullPath.substring(1); for (int i = sortedList.length; --i >= 0;) { if (fullPath.startsWith(sortedList[i])) { url = assetsByPath.get(sortedList[i]).getURL(fullPath); ZipEntry ze = findZipEntry(url); if (ze == null) break; if (isJS) { jsutil.setURLBytes(url, jsutil.getZipBytes(ze)); } return url; } } if (!zipOnly) return getAbsoluteURL(fullPath); } catch (MalformedURLException e) { } return null; } public static ZipEntry findZipEntry(URL url) { String[] parts = getJarURLParts(url.toString()); if (parts == null || parts[0] == null || parts[1].length() == 0) return null; return findZipEntry(parts[0], parts[1]); } public static ZipEntry findZipEntry(String zipFile, String fileName) { return getZipContents(zipFile).get(fileName); } /** * Gets the contents of a zip file. * * @param zipPath the path to the zip file * @return a set of file names in alphabetical order */ public static Map getZipContents(String zipPath) { return getInstance()._getZipContents(zipPath); } private Map _getZipContents(String zipPath) { URL url = getURLWithCachedBytes(zipPath); // BH carry over bytes if we have them already Map fileNames = htZipContents.get(url.toString()); if (fileNames != null) return fileNames; try { // Scan URL zip stream for files. return readZipContents(url.openStream(), url); } catch (Exception ex) { ex.printStackTrace(); return null; } } /** * Deconstruct a jar URL into two parts, before and after "!/". * * @param source * @return */ public static String[] getJarURLParts(String source) { int n = source.indexOf("!/"); if (n < 0) return null; String jarfile = source.substring(0, n).replace("jar:", ""); while (jarfile.startsWith("//")) jarfile = jarfile.substring(1); return new String[] { jarfile, (n == source.length() - 2 ? null : source.substring(n + 2)) }; } /** * 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. * * @param url * @return byte[] * * @author hansonr */ public static byte[] getURLContents(URL url) { if (url == null) return null; try { if (isJS) { // Java 9! return new String(url.openStream().readAllBytes()); return jsutil.readAllBytes(url.openStream()); } return getLimitedStreamBytes(url.openStream(), -1, null); } catch (IOException e) { e.printStackTrace(); } return null; } /** * * Convert a file path to a URL, retrieving any cached file data, as from DnD. * Do not do any actual data transfer. This is a swingjs.JSUtil service. * * @param path * @return */ private static URL getURLWithCachedBytes(String path) { URL url = getAbsoluteURL(path); if (url != null) addJSCachedBytes(url); return url; } private Map readZipContents(InputStream is, URL url) throws IOException { HashMap fileNames = new HashMap(); if (doCacheZipContents) htZipContents.put(url.toString(), fileNames); ZipInputStream input = new ZipInputStream(is); ZipEntry zipEntry = null; int n = 0; while ((zipEntry = input.getNextEntry()) != null) { if (zipEntry.isDirectory() || zipEntry.getSize() == 0) continue; n++; String fileName = zipEntry.getName(); fileNames.put(fileName, zipEntry); // Java has no use for the ZipEntry, but JavaScript can read it. } input.close(); System.out.println("Assets: " + n + " zip entries found in " + url); //$NON-NLS-1$ return fileNames; } private void resort() { sortedList = new String[assetsByPath.size()]; int i = 0; for (String path : assetsByPath.keySet()) { sortedList[i++] = path; } Arrays.sort(sortedList); } /** * Only needed for Java * * @param is * @param n * @param out * @return * @throws IOException */ private static byte[] getLimitedStreamBytes(InputStream is, long n, OutputStream out) throws IOException { // Note: You cannot use InputStream.available() to reliably read // zip data from the web. boolean toOut = (out != null); int buflen = (n > 0 && n < 1024 ? (int) n : 1024); byte[] buf = new byte[buflen]; byte[] bytes = (out == null ? new byte[n < 0 ? 4096 : (int) n] : null); int len = 0; int totalLen = 0; if (n < 0) n = Integer.MAX_VALUE; while (totalLen < n && (len = is.read(buf, 0, buflen)) > 0) { totalLen += len; if (toOut) { out.write(buf, 0, len); } else { if (totalLen > bytes.length) bytes = Arrays.copyOf(bytes, totalLen * 2); System.arraycopy(buf, 0, bytes, totalLen - len, len); if (n != Integer.MAX_VALUE && totalLen + buflen > bytes.length) buflen = bytes.length - totalLen; } } if (toOut) return null; if (totalLen == bytes.length) return bytes; buf = new byte[totalLen]; System.arraycopy(bytes, 0, buf, 0, totalLen); return buf; } /** * Return all assets in the form that is appropriate for the Info.assets value in SwingJS. * */ @Override public String toString() { String s = "["; for (int i = 0; i < sortedList.length; i++) { Asset a = assetsByPath.get(sortedList[i]); s += (i == 0 ? "" : ",") + a; } return s + "]"; } }