package com.github.atdi.gboot.loader.jar; import java.io.File; import java.io.IOException; import java.lang.ref.SoftReference; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.URLDecoder; import java.net.URLStreamHandler; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; /** * {@link java.net.URLStreamHandler} for Spring Boot loader {@link GBootJarFile}s. * * @see GBootJarFile#registerUrlProtocolHandler() */ public class Handler extends URLStreamHandler { private static final Logger logger = Logger.getLogger(Handler.class.getName()); // NOTE: in order to be found as a URL protocol hander, this class must be public, // must be named Handler and must be in a package ending '.jar' private static final String FILE_PROTOCOL = "file:"; private static final String SEPARATOR = "!/"; private static final String[] FALLBACK_HANDLERS = { "sun.net.www.protocol.jar.Handler" }; private static final Method OPEN_CONNECTION_METHOD; static { Method method = null; try { method = URLStreamHandler.class .getDeclaredMethod("openConnection", URL.class); } catch (Exception ex) { logger.log(Level.SEVERE, "Error while trying to load openConnection method", ex); } OPEN_CONNECTION_METHOD = method; } private static SoftReference<Map<File, GBootJarFile>> rootFileCache; static { rootFileCache = new SoftReference<Map<File, GBootJarFile>>(null); } private final GBootJarFile jarFile; private URLStreamHandler fallbackHandler; public Handler() { this(null); } public Handler(GBootJarFile jarFile) { this.jarFile = jarFile; } @Override protected URLConnection openConnection(URL url) throws IOException { if (this.jarFile != null) { return new GBootJarURLConnection(url, this.jarFile); } try { return new GBootJarURLConnection(url, getRootJarFileFromUrl(url)); } catch (Exception ex) { return openFallbackConnection(url, ex); } } private URLConnection openFallbackConnection(URL url, Exception reason) throws IOException { try { return openConnection(getFallbackHandler(), url); } catch (Exception ex) { if (reason instanceof IOException) { logger.log(Level.FINEST, "Unable to open fallback handler", ex); throw (IOException) reason; } logger.log(Level.WARNING, "Unable to open fallback handler", ex); if (reason instanceof RuntimeException) { throw (RuntimeException) reason; } throw new IllegalStateException(reason); } } private URLStreamHandler getFallbackHandler() { if (this.fallbackHandler != null) { return this.fallbackHandler; } for (String handlerClassName : FALLBACK_HANDLERS) { try { Class<?> handlerClass = Class.forName(handlerClassName); this.fallbackHandler = (URLStreamHandler) handlerClass.newInstance(); return this.fallbackHandler; } catch (Exception ex) { logger.log(Level.SEVERE, "Error trying to get fallback class handler", ex); } } throw new IllegalStateException("Unable to find fallback handler"); } private URLConnection openConnection(URLStreamHandler handler, URL url) throws Exception { if (OPEN_CONNECTION_METHOD == null) { throw new IllegalStateException( "Unable to invoke fallback open connection method"); } OPEN_CONNECTION_METHOD.setAccessible(true); return (URLConnection) OPEN_CONNECTION_METHOD.invoke(handler, url); } public GBootJarFile getRootJarFileFromUrl(URL url) throws IOException { String spec = url.getFile(); int separatorIndex = spec.indexOf(SEPARATOR); if (separatorIndex == -1) { throw new MalformedURLException("Jar URL does not contain !/ separator"); } String name = spec.substring(0, separatorIndex); return getRootJarFile(name); } private GBootJarFile getRootJarFile(String name) throws IOException { try { if (!name.startsWith(FILE_PROTOCOL)) { throw new IllegalStateException("Not a file URL"); } String path = name.substring(FILE_PROTOCOL.length()); File file = new File(URLDecoder.decode(path, "UTF-8")); Map<File, GBootJarFile> cache = rootFileCache.get(); GBootJarFile jarFile = (cache == null ? null : cache.get(file)); if (jarFile == null) { jarFile = new GBootJarFile(file); addToRootFileCache(file, jarFile); } return jarFile; } catch (Exception ex) { throw new IOException("Unable to open root Jar file '" + name + "'", ex); } } /** * Add the given {@link GBootJarFile} to the root file cache. * @param sourceFile the source file to add * @param jarFile the jar file. */ static void addToRootFileCache(File sourceFile, GBootJarFile jarFile) { Map<File, GBootJarFile> cache = rootFileCache.get(); if (cache == null) { cache = new ConcurrentHashMap<File, GBootJarFile>(); rootFileCache = new SoftReference<Map<File, GBootJarFile>>(cache); } cache.put(sourceFile, jarFile); } /** * Set if a generic static exception can be thrown when a URL cannot be connected. * This optimization is used during class loading to save creating lots of exceptions * which are then swallowed. * @param useFastConnectionExceptions if fast connection exceptions can be used. */ public static void setUseFastConnectionExceptions(boolean useFastConnectionExceptions) { GBootJarURLConnection.setUseFastExceptions(useFastConnectionExceptions); } }