Merge branch 'Jalview-JS/develop' into merge_js_develop
[jalview.git] / src / javajs / async / Assets.java
diff --git a/src/javajs/async/Assets.java b/src/javajs/async/Assets.java
new file mode 100644 (file)
index 0000000..05614ce
--- /dev/null
@@ -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:
+ * 
+ * <code>
+       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. ");
+               }
+
+       }
+ * </code>
+ * 
+ * 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:
+ * 
+ * <code>
+ * getAssetBytes(String fullPath)
+ * getAssetString(String fullPath)
+ * getAssetStream(String fullPath)
+ * </code>
+ * 
+ * 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<String, Map<String, ZipEntry>> htZipContents = new HashMap<>();
+
+       private static boolean doCacheZipContents = true;
+
+       private static Assets instance = new Assets();
+
+       private Assets() {
+       }
+
+       private Map<String, Asset> 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<String> 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<String, ZipEntry> getZipContents(String zipPath) {
+               return getInstance()._getZipContents(zipPath);
+       }
+
+       private Map<String, ZipEntry> _getZipContents(String zipPath) {
+               URL url = getURLWithCachedBytes(zipPath); // BH carry over bytes if we have them already
+               Map<String, ZipEntry> 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<String, ZipEntry> readZipContents(InputStream is, URL url) throws IOException {
+               HashMap<String, ZipEntry> fileNames = new HashMap<String, ZipEntry>();
+               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 + "]";
+       }
+
+}