X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjavajs%2Fasync%2FAssets.java;fp=src%2Fjavajs%2Fasync%2FAssets.java;h=05614cee3145ac7274200ddd2748c89d18eead4b;hb=4898f0ae429e0c61ddba72ca46be89b34bb4df8b;hp=0000000000000000000000000000000000000000;hpb=5a6ac5b535856903629234ad43a71319a91ebee5;p=jalview.git diff --git a/src/javajs/async/Assets.java b/src/javajs/async/Assets.java new file mode 100644 index 0000000..05614ce --- /dev/null +++ b/src/javajs/async/Assets.java @@ -0,0 +1,594 @@ +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 + "]"; + } + +}