diff options
author | Sven Gothel <[email protected]> | 2012-03-17 21:15:49 +0100 |
---|---|---|
committer | Sven Gothel <[email protected]> | 2012-03-17 21:15:49 +0100 |
commit | 235f8b1cbff8ed13071d5c19c0be492c0b25cb78 (patch) | |
tree | 659845e16bd69372bc7ddc3a42b3aa7130d18df5 /src/java/com/jogamp/common/net | |
parent | 0cfc7847c58b51c9a26b50d905b592d1fc4c8578 (diff) |
Add 'asset' URLConnection; IOUtil uses URLConnection / incr. effeciency; Android ClassLoaderUtil cleanup;
- Add 'asset' URLConnection
- Please read API doc 'PiggybackURLConnection' and 'AssetURLConnection'
- Solves generic resource handling where platform locations may differ,
ie ClassLoader lookup on Android in the 'assets/' subfolder.
- New Android 'AssetDexClassLoader' uses 'assets/' folder for findResource(..)
- aapt.signed (our APK ant task)
- uses 'assets/' folder
- adds the 'assetsdir' attribute allowing to copy other assets into the APK
- IOUtil uses URLConnection / incr. effeciency
- using URLConnection on all getResource(..) since URL
is connected anyways for validation and URLConnection can be used by caller right away
- String getRelativeOf(URL, String) -> URL getRelativeOf(URL, String)
- preserves scheme, authority, etc
- simple parentOf handling, more efficient
- reusing new 'asset' protocol impl.
- Android ClassLoaderUtil cleanup;
- Use createClassLoader(..) impl for build-in static jogamp and user APKs,
which removes code redundancy
Tests: New code path, especially 'assets' are covered by new unit tests, no regressions on Linux.
Diffstat (limited to 'src/java/com/jogamp/common/net')
7 files changed, 553 insertions, 0 deletions
diff --git a/src/java/com/jogamp/common/net/AssetURLConnection.java b/src/java/com/jogamp/common/net/AssetURLConnection.java new file mode 100644 index 0000000..f2a5a01 --- /dev/null +++ b/src/java/com/jogamp/common/net/AssetURLConnection.java @@ -0,0 +1,98 @@ +package com.jogamp.common.net; + +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URL; + +/** + * See base class {@link PiggybackURLConnection} for motivation. + * + * <p> + * <i>asset</i> resource location protocol connection. + * </p> + * + * <p> + * See {@link AssetURLContext#resolve(String)} how resources are being resolved. + * </p> + * + * <h3>Example:</h3> + * + * Assuming the plain <i>asset entry</i> <b><code>test/lala.txt</code></b> is being resolved by + * a class <code>test.LaLaTest</code>, ie. using the <i>asset aware</i> ClassLoader, + * one would use the following <i>asset</i> aware filesystem layout: + * + * <pre> + * test/LaLaTest.class + * assets/test/lala.txt + * </pre> + * + * The above maybe on a plain filesystem, or within a JAR or an APK file, + * e.g. <code>jogamp.test.apk</code>. + * + * The above would result in the following possible URLs + * reflecting the plain and resolved state of the <i>asset URL</i>: + * <pre> + * 0 Entry test/lala.txt + * 1 Plain asset:test/lala.txt + * 2 Resolved asset:jar:file:/data/app/jogamp.test.apk!/assets/test/lala.txt + * </pre> + * + * <p> + * The sub protocol URL of the resolved <i>asset</i> + * <pre> + * 3 Sub-URL jar:file:/data/app/jogamp.test.apk!/assets/test/lala.txt + * </pre> + * can be retrieved using {@link #getSubProtocol()}. + * </p> + * + * In all above cases, the <i>asset entry</i> is <b><code>test/lala.txt</code></b>, + * which can be retrieved via {@link #getEntryName()}. + * + * <p> + * <h3>General Implementation Notes:</h3> + * An <i>asset</i> URL is resolved using {@link AssetURLContext#getClassLoader()}.{@link ClassLoader#getResource(String) getResource(String)}, + * hence the only requirement for an implementation is to have an <i>asset</i> aware ClassLoader + * as described in {@link AssetURLContext#getClassLoader()}. + * </p> + * <p> + * <h3>Warning:</h3> + * Since the <i>asset</i> protocol is currently not being implemented + * on all platform with an appropriate ClassLoader, a user shall not create the <i>asset</i> URL manually.<br> + * </p> + * + * <h3>Android Implementation Notes:</h3> + * <p> + * The Android ClassLoader {@link jogamp.android.launcher.AssetDexClassLoader} + * resolves the resource as an <i>asset</i> URL in it's {@link ClassLoader#findResource(String)} implementation.</p> + * <p> + * Currently we attach our <i>asset</i> {@link java.net.URLStreamHandlerFactory} + * to allow {@link java.net.URL} to handle <i>asset</i> URLs via our <i>asset</i> {@link java.net.URLStreamHandler} implementation. + * </p> + */ +public class AssetURLConnection extends PiggybackURLConnection<AssetURLContext> { + + public AssetURLConnection(URL url, AssetURLContext implHelper) { + super(url, implHelper); + } + + @Override + public String getEntryName() throws IOException { + if(!connected) { + throw new IOException("not connected"); + } + + final String urlPath ; + if(subConn instanceof JarURLConnection) { + urlPath = ((JarURLConnection)subConn).getEntryName(); + } else { + urlPath = subConn.getURL().getPath(); + } + + if(urlPath.startsWith(AssetURLContext.assets_folder)) { + return urlPath.substring(AssetURLContext.assets_folder.length()); + } else { + return urlPath; + } + } + +} diff --git a/src/java/com/jogamp/common/net/AssetURLContext.java b/src/java/com/jogamp/common/net/AssetURLContext.java new file mode 100644 index 0000000..00c7df7 --- /dev/null +++ b/src/java/com/jogamp/common/net/AssetURLContext.java @@ -0,0 +1,212 @@ +package com.jogamp.common.net; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +import jogamp.common.Debug; + +import com.jogamp.common.os.AndroidVersion; +import com.jogamp.common.util.IOUtil; + +/** + * See {@link PiggybackURLConnection} for description and examples. + */ +public abstract class AssetURLContext implements PiggybackURLContext { + private static final boolean DEBUG = Debug.isPropertyDefined("jogamp.debug.IOUtil", true); + + /** The <i>asset URL</i> protocol name <code>asset</code> */ + public static final String asset_protocol = "asset"; + + /** The <i>asset URL</i> protocol prefix <code>asset:</code> */ + public static final String asset_protocol_prefix = "asset:"; + + /** + * The <i>optional</i> <i>asset</i> folder name with ending slash <code>assets/</code>. + * <p> + * Note that the <i>asset</i> folder is not used on all platforms using the <i>asset</i> protocol + * and you should not rely on it, use {@link AssetURLConnection#getEntryName()}. + * </p> + **/ + public static final String assets_folder = "assets/"; + + public static AssetURLContext create(final ClassLoader cl) { + return new AssetURLContext() { + @Override + public ClassLoader getClassLoader() { + return cl; + } + }; + } + + public static AssetURLStreamHandler createHandler(final ClassLoader cl) { + return new AssetURLStreamHandler(create(cl)); + } + + /** + * Create an <i>asset</i> URL, suitable even w/o the registered <i>asset</i> URLStreamHandler. + * <p> + * This is equivalent with: + * <pre> + * return new URL(null, path.startsWith("asset:") ? path : "asset:" + path, new AssetURLStreamHandler(cl)); + * </pre> + * </p> + * @param path resource path, with or w/o <code>asset:</code> prefix + * @param cl the ClassLoader used to resolve the location, see {@link #getClassLoader()}. + * @return + * @throws MalformedURLException + */ + public static URL createURL(String path, ClassLoader cl) throws MalformedURLException { + return new URL(null, path.startsWith(asset_protocol_prefix) ? path : asset_protocol_prefix + path, createHandler(cl)); + } + + /** + * Create an <i>asset</i> URL, suitable only with the registered <i>asset</i> URLStreamHandler. + * <p> + * This is equivalent with: + * <pre> + * return new URL(path.startsWith("asset:") ? path : "asset:" + path); + * </pre> + * </p> + * @param path resource path, with or w/o <code>asset:</code> prefix + * @return + * @throws MalformedURLException + */ + public static URL createURL(String path) throws MalformedURLException { + return new URL(path.startsWith(asset_protocol_prefix) ? path : asset_protocol_prefix + path); + } + + /** + * Returns the <i>asset</i> handler previously set via {@link #registerHandler(ClassLoader)}, + * or null if none was set. + */ + public static URLStreamHandler getRegisteredHandler() { + final GenericURLStreamHandlerFactory f = GenericURLStreamHandlerFactory.register(); + return ( null != f ) ? f.getHandler(asset_protocol) : null; + } + + /** + * Registers the generic URLStreamHandlerFactory via {@link GenericURLStreamHandlerFactory#register()} + * and if successful sets the <i>asset</i> <code>handler</code> for the given ClassLoader <code>cl</code>. + * + * @return true if successful, otherwise false + */ + public static boolean registerHandler(ClassLoader cl) { + final GenericURLStreamHandlerFactory f = GenericURLStreamHandlerFactory.register(); + if( null != f ) { + f.setHandler(asset_protocol, createHandler(cl)); + return true; + } else { + return false; + } + } + + /** + * Returns an <i>asset</i> aware ClassLoader. + * <p> + * The ClassLoader is required to find the <i>asset</i> resource + * via it's <code>URL findResource(String)</code> implementation. + * </p> + * <p> + * It's <code>URL findResource(String)</code> implementation shall return either + * an <i>asset</i> URL <code>asset:sub-protocol</code> or just the sub-protocol URL. + * </p> + * <p> + * For example, on Android, we <i>redirect</i> all <code>path</code> request to <i>assets/</i><code>path</code>. + * </p> + */ + public abstract ClassLoader getClassLoader(); + + @Override + public String getImplementedProtocol() { + return asset_protocol; + } + + /** + * {@inheritDoc} + * <p> + * This implementation attempts to resolve <code>path</code> in the following order: + * <ol> + * <li> as a valid URL: <code>new URL(path)</code>, use sub-protocol if <i>asset</i> URL</li> + * <li> via ClassLoader: {@link #getClassLoader()}.{@link ClassLoader#getResource(String) getResource(path)}, use sub-protocol if <i>asset</i> URL </li> + * <li> as a File: <code>new File(path).toURI().toURL()</code> + * </ol> + * </p> + * <p> + * In case of using the ClassLoader (2) <b>and</b> if running on Android, + * the {@link #assets_folder} is being prepended to <code>path</code> if missing. + * </p> + **/ + @Override + public URLConnection resolve(String path) throws IOException { + return resolve(path, getClassLoader()); + } + + public static URLConnection resolve(String path, ClassLoader cl) throws IOException { + URL url = null; + URLConnection conn = null; + int type = -1; + + if(DEBUG) { + System.err.println("AssetURLContext.resolve: <"+path+">"); + } + + try { + // lookup as valid sub-protocol + url = new URL(path); + conn = open(url); + type = null != conn ? 1 : -1; + } catch(MalformedURLException e1) { if(DEBUG) { System.err.println("ERR: "+e1.getMessage()); } } + + if(null == conn && null != cl) { + // lookup via ClassLoader .. cleanup leading '/' + String cpath = path; + while(cpath.startsWith("/")) { + cpath = cpath.substring(1); + } + if(AndroidVersion.isAvailable) { + cpath = cpath.startsWith(assets_folder) ? cpath : assets_folder + cpath; + } + url = cl.getResource(cpath); + conn = open(url); + type = null != conn ? 2 : -1; + } + + if(null == conn) { + // lookup as File + try { + File file = new File(path); + if(file.exists()) { + url = IOUtil.toURLSimple(file); + conn = open(url); + type = null != conn ? 3 : -1; + } + } catch (Throwable e) { if(DEBUG) { System.err.println("ERR: "+e.getMessage()); } } + } + + if(DEBUG) { + System.err.println("AssetURLContext.resolve: type "+type+": url <"+url+">, conn <"+conn+">, connURL <"+(null!=conn?conn.getURL():null)+">"); + } + if(null == conn) { + throw new FileNotFoundException("Could not look-up: "+path+" as URL, w/ ClassLoader or as File"); + } + return conn; + } + + private static URLConnection open(URL url) { + if(null==url) { + return null; + } + try { + final URLConnection c = url.openConnection(); + c.connect(); // redundant + return c; + } catch (IOException ioe) { if(DEBUG) { System.err.println("ERR: "+ioe.getMessage()); } } + return null; + } + +} diff --git a/src/java/com/jogamp/common/net/AssetURLStreamHandler.java b/src/java/com/jogamp/common/net/AssetURLStreamHandler.java new file mode 100644 index 0000000..8d95b2d --- /dev/null +++ b/src/java/com/jogamp/common/net/AssetURLStreamHandler.java @@ -0,0 +1,37 @@ +package com.jogamp.common.net; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +import com.jogamp.common.net.AssetURLConnection; + +/** + * {@link URLStreamHandler} to handle the asset protocol. + * + * <p> + * This is the <i>asset</i> URLStreamHandler variation + * for manual use. + * </p> + * <p> + * It requires passing a valid {@link AssetURLContext} + * for construction, hence it's not suitable for the pkg factory model. + * </p> + */ +public class AssetURLStreamHandler extends URLStreamHandler { + AssetURLContext ctx; + + public AssetURLStreamHandler(AssetURLContext ctx) { + this.ctx = ctx; + } + + @Override + protected URLConnection openConnection(URL u) throws IOException { + final AssetURLConnection c = new AssetURLConnection(u, ctx); + c.connect(); + return c; + } + + +} diff --git a/src/java/com/jogamp/common/net/GenericURLStreamHandlerFactory.java b/src/java/com/jogamp/common/net/GenericURLStreamHandlerFactory.java new file mode 100644 index 0000000..79d7f71 --- /dev/null +++ b/src/java/com/jogamp/common/net/GenericURLStreamHandlerFactory.java @@ -0,0 +1,66 @@ +package com.jogamp.common.net; + +import java.net.URL; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.HashMap; +import java.util.Map; + +public class GenericURLStreamHandlerFactory implements URLStreamHandlerFactory { + private static GenericURLStreamHandlerFactory factory = null; + + private final Map<String, URLStreamHandler> protocolHandlers; + + private GenericURLStreamHandlerFactory() { + protocolHandlers = new HashMap<String, URLStreamHandler>(); + } + + /** + * Sets the <code>handler</code> for <code>protocol</code>. + * + * @return the previous set <code>handler</code>, or null if none was set. + */ + public synchronized final URLStreamHandler setHandler(String protocol, URLStreamHandler handler) { + return protocolHandlers.put(protocol, handler); + } + + /** + * Returns the <code>protocol</code> handler previously set via {@link #setHandler(String, URLStreamHandler)}, + * or null if none was set. + */ + public synchronized final URLStreamHandler getHandler(String protocol) { + return protocolHandlers.get(protocol); + } + + @Override + public synchronized final URLStreamHandler createURLStreamHandler(String protocol) { + return getHandler(protocol); + } + + /** + * Returns the singleton instance of the registered GenericURLStreamHandlerFactory + * or null if registration was not successful. + * <p> + * Registration is only performed once. + * </p> + */ + public synchronized static GenericURLStreamHandlerFactory register() { + if(null == factory) { + factory = AccessController.doPrivileged(new PrivilegedAction<GenericURLStreamHandlerFactory>() { + public GenericURLStreamHandlerFactory run() { + boolean ok = false; + GenericURLStreamHandlerFactory f = new GenericURLStreamHandlerFactory(); + try { + URL.setURLStreamHandlerFactory(f); + ok = true; + } catch (Throwable e) { + System.err.println("GenericURLStreamHandlerFactory: Setting URLStreamHandlerFactory failed: "+e.getMessage()); + } + return ok ? f : null; + } } ); + } + return factory; + } +} diff --git a/src/java/com/jogamp/common/net/PiggybackURLConnection.java b/src/java/com/jogamp/common/net/PiggybackURLConnection.java new file mode 100644 index 0000000..39f1637 --- /dev/null +++ b/src/java/com/jogamp/common/net/PiggybackURLConnection.java @@ -0,0 +1,84 @@ +package com.jogamp.common.net; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +/** + * Generic resource location protocol connection, + * using another sub-protocol as the vehicle for a piggyback protocol. + * <p> + * The details of the sub-protocol can be queried using {@link #getSubProtocol()}. + * </p> + * <p> + * See example in {@link AssetURLConnection}. + * </p> + */ +public abstract class PiggybackURLConnection<I extends PiggybackURLContext> extends URLConnection { + protected URL subUrl; + protected URLConnection subConn; + protected I context; + + /** + * @param url the specific URL for this instance + * @param context the piggyback context, defining state independent code and constants + */ + protected PiggybackURLConnection(URL url, I context) { + super(url); + this.context = context; + } + + /** + * <p> + * Resolves the URL via {@link PiggybackURLContext#resolve(String)}, + * see {@link AssetURLContext#resolve(String)} for an example. + * </p> + * + * {@inheritDoc} + */ + @Override + public synchronized void connect() throws IOException { + if(!connected) { + subConn = context.resolve(url.getPath()); + subUrl = subConn.getURL(); + connected = true; + } + } + + @Override + public InputStream getInputStream() throws IOException { + if(!connected) { + throw new IOException("not connected"); + } + return subConn.getInputStream(); + } + + /** + * Returns the <i>entry name</i> of the asset. + * <pre> + * Plain asset:test/lala.txt + * Resolved asset:jar:file:/data/app/jogamp.test.apk!/assets/test/lala.txt + * Result test/lala.txt + * </pre> + * @throws IOException is not connected + **/ + public abstract String getEntryName() throws IOException; + + /** + * Returns the resolved <i>sub protocol</i> of the asset or null, ie: + * <pre> + * Plain asset:test/lala.txt + * Resolved asset:jar:file:/data/app/jogamp.test.apk!/assets/test/lala.txt + * Result jar:file:/data/app/jogamp.test.apk!/assets/test/lala.txt + * </pre> + * + * @throws IOException is not connected + */ + public URL getSubProtocol() throws IOException { + if(!connected) { + throw new IOException("not connected"); + } + return subUrl; + } +} diff --git a/src/java/com/jogamp/common/net/PiggybackURLContext.java b/src/java/com/jogamp/common/net/PiggybackURLContext.java new file mode 100644 index 0000000..2cb48b5 --- /dev/null +++ b/src/java/com/jogamp/common/net/PiggybackURLContext.java @@ -0,0 +1,18 @@ +package com.jogamp.common.net; + +import java.io.IOException; +import java.net.URLConnection; + +/** + * See {@link PiggybackURLConnection} for description and examples. + */ +public interface PiggybackURLContext { + + /** Returns the specific protocol, constant for this implementation. */ + public String getImplementedProtocol(); + + /** + * Resolving path to a URL sub protocol and return it's open URLConnection + **/ + public URLConnection resolve(String path) throws IOException; +} diff --git a/src/java/com/jogamp/common/net/asset/Handler.java b/src/java/com/jogamp/common/net/asset/Handler.java new file mode 100644 index 0000000..1063797 --- /dev/null +++ b/src/java/com/jogamp/common/net/asset/Handler.java @@ -0,0 +1,38 @@ +package com.jogamp.common.net.asset; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +import com.jogamp.common.net.AssetURLConnection; +import com.jogamp.common.net.AssetURLContext; + +/** + * {@link URLStreamHandler} to handle the asset protocol. + * + * <p> + * This is the <i>asset</i> URLStreamHandler variation + * using this class ClassLoader for the pkg factory model. + * </p> + */ +public class Handler extends URLStreamHandler { + static final AssetURLContext localCL = new AssetURLContext() { + @Override + public ClassLoader getClassLoader() { + return Handler.class.getClassLoader(); + } + }; + + public Handler() { + super(); + } + + @Override + protected URLConnection openConnection(URL u) throws IOException { + final AssetURLConnection c = new AssetURLConnection(u, localCL); + c.connect(); + return c; + } + +} |