001/* 002 * Copyright 2012-2017 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.loader.jar; 018 019import java.io.File; 020import java.io.IOException; 021import java.lang.ref.SoftReference; 022import java.lang.reflect.Method; 023import java.net.MalformedURLException; 024import java.net.URL; 025import java.net.URLConnection; 026import java.net.URLDecoder; 027import java.net.URLStreamHandler; 028import java.util.Map; 029import java.util.concurrent.ConcurrentHashMap; 030import java.util.logging.Level; 031import java.util.logging.Logger; 032 033/** 034 * {@link URLStreamHandler} for Spring Boot loader {@link JarFile}s. 035 * 036 * @author Phillip Webb 037 * @author Andy Wilkinson 038 * @see JarFile#registerUrlProtocolHandler() 039 */ 040public class Handler extends URLStreamHandler { 041 042 // NOTE: in order to be found as a URL protocol handler, this class must be public, 043 // must be named Handler and must be in a package ending '.jar' 044 045 private static final String JAR_PROTOCOL = "jar:"; 046 047 private static final String FILE_PROTOCOL = "file:"; 048 049 private static final String SEPARATOR = "!/"; 050 051 private static final String[] FALLBACK_HANDLERS = { 052 "sun.net.www.protocol.jar.Handler" }; 053 054 private static final Method OPEN_CONNECTION_METHOD; 055 056 static { 057 Method method = null; 058 try { 059 method = URLStreamHandler.class.getDeclaredMethod("openConnection", 060 URL.class); 061 } 062 catch (Exception ex) { 063 // Swallow and ignore 064 } 065 OPEN_CONNECTION_METHOD = method; 066 } 067 068 private static SoftReference<Map<File, JarFile>> rootFileCache; 069 070 static { 071 rootFileCache = new SoftReference<Map<File, JarFile>>(null); 072 } 073 074 private final JarFile jarFile; 075 076 private URLStreamHandler fallbackHandler; 077 078 public Handler() { 079 this(null); 080 } 081 082 public Handler(JarFile jarFile) { 083 this.jarFile = jarFile; 084 } 085 086 @Override 087 protected URLConnection openConnection(URL url) throws IOException { 088 if (this.jarFile != null) { 089 return JarURLConnection.get(url, this.jarFile); 090 } 091 try { 092 return JarURLConnection.get(url, getRootJarFileFromUrl(url)); 093 } 094 catch (Exception ex) { 095 return openFallbackConnection(url, ex); 096 } 097 } 098 099 private URLConnection openFallbackConnection(URL url, Exception reason) 100 throws IOException { 101 try { 102 return openConnection(getFallbackHandler(), url); 103 } 104 catch (Exception ex) { 105 if (reason instanceof IOException) { 106 log(false, "Unable to open fallback handler", ex); 107 throw (IOException) reason; 108 } 109 log(true, "Unable to open fallback handler", ex); 110 if (reason instanceof RuntimeException) { 111 throw (RuntimeException) reason; 112 } 113 throw new IllegalStateException(reason); 114 } 115 } 116 117 private void log(boolean warning, String message, Exception cause) { 118 try { 119 Logger.getLogger(getClass().getName()) 120 .log((warning ? Level.WARNING : Level.FINEST), message, cause); 121 } 122 catch (Exception ex) { 123 if (warning) { 124 System.err.println("WARNING: " + message); 125 } 126 } 127 } 128 129 private URLStreamHandler getFallbackHandler() { 130 if (this.fallbackHandler != null) { 131 return this.fallbackHandler; 132 } 133 for (String handlerClassName : FALLBACK_HANDLERS) { 134 try { 135 Class<?> handlerClass = Class.forName(handlerClassName); 136 this.fallbackHandler = (URLStreamHandler) handlerClass.newInstance(); 137 return this.fallbackHandler; 138 } 139 catch (Exception ex) { 140 // Ignore 141 } 142 } 143 throw new IllegalStateException("Unable to find fallback handler"); 144 } 145 146 private URLConnection openConnection(URLStreamHandler handler, URL url) 147 throws Exception { 148 if (OPEN_CONNECTION_METHOD == null) { 149 throw new IllegalStateException( 150 "Unable to invoke fallback open connection method"); 151 } 152 OPEN_CONNECTION_METHOD.setAccessible(true); 153 return (URLConnection) OPEN_CONNECTION_METHOD.invoke(handler, url); 154 } 155 156 @Override 157 protected void parseURL(URL context, String spec, int start, int limit) { 158 if (spec.toLowerCase().startsWith(JAR_PROTOCOL)) { 159 setFile(context, getFileFromSpec(spec.substring(start, limit))); 160 } 161 else { 162 setFile(context, getFileFromContext(context, spec.substring(start, limit))); 163 } 164 } 165 166 private String getFileFromSpec(String spec) { 167 int separatorIndex = spec.lastIndexOf("!/"); 168 if (separatorIndex == -1) { 169 throw new IllegalArgumentException("No !/ in spec '" + spec + "'"); 170 } 171 try { 172 new URL(spec.substring(0, separatorIndex)); 173 return spec; 174 } 175 catch (MalformedURLException ex) { 176 throw new IllegalArgumentException("Invalid spec URL '" + spec + "'", ex); 177 } 178 } 179 180 private String getFileFromContext(URL context, String spec) { 181 String file = context.getFile(); 182 if (spec.startsWith("/")) { 183 return trimToJarRoot(file) + SEPARATOR + spec.substring(1); 184 } 185 if (file.endsWith("/")) { 186 return file + spec; 187 } 188 int lastSlashIndex = file.lastIndexOf('/'); 189 if (lastSlashIndex == -1) { 190 throw new IllegalArgumentException( 191 "No / found in context URL's file '" + file + "'"); 192 } 193 return file.substring(0, lastSlashIndex + 1) + spec; 194 } 195 196 private String trimToJarRoot(String file) { 197 int lastSeparatorIndex = file.lastIndexOf(SEPARATOR); 198 if (lastSeparatorIndex == -1) { 199 throw new IllegalArgumentException( 200 "No !/ found in context URL's file '" + file + "'"); 201 } 202 return file.substring(0, lastSeparatorIndex); 203 } 204 205 private void setFile(URL context, String file) { 206 setURL(context, JAR_PROTOCOL, null, -1, null, null, normalize(file), null, null); 207 } 208 209 private String normalize(String file) { 210 int afterLastSeparatorIndex = file.lastIndexOf(SEPARATOR) + SEPARATOR.length(); 211 String afterSeparator = file.substring(afterLastSeparatorIndex); 212 afterSeparator = replaceParentDir(afterSeparator); 213 afterSeparator = replaceCurrentDir(afterSeparator); 214 return file.substring(0, afterLastSeparatorIndex) + afterSeparator; 215 } 216 217 private String replaceParentDir(String file) { 218 int parentDirIndex; 219 while ((parentDirIndex = file.indexOf("/../")) >= 0) { 220 int precedingSlashIndex = file.lastIndexOf('/', parentDirIndex - 1); 221 if (precedingSlashIndex >= 0) { 222 file = file.substring(0, precedingSlashIndex) 223 + file.substring(parentDirIndex + 3); 224 } 225 else { 226 file = file.substring(parentDirIndex + 4); 227 } 228 } 229 return file; 230 } 231 232 private String replaceCurrentDir(String file) { 233 return file.replace("/./", "/"); 234 } 235 236 @Override 237 protected int hashCode(URL u) { 238 return hashCode(u.getProtocol(), u.getFile()); 239 } 240 241 private int hashCode(String protocol, String file) { 242 int result = (protocol == null ? 0 : protocol.hashCode()); 243 int separatorIndex = file.indexOf(SEPARATOR); 244 if (separatorIndex == -1) { 245 return result + file.hashCode(); 246 } 247 String source = file.substring(0, separatorIndex); 248 String entry = canonicalize(file.substring(separatorIndex + 2)); 249 try { 250 result += new URL(source).hashCode(); 251 } 252 catch (MalformedURLException ex) { 253 result += source.hashCode(); 254 } 255 result += entry.hashCode(); 256 return result; 257 } 258 259 @Override 260 protected boolean sameFile(URL u1, URL u2) { 261 if (!u1.getProtocol().equals("jar") || !u2.getProtocol().equals("jar")) { 262 return false; 263 } 264 int separator1 = u1.getFile().indexOf(SEPARATOR); 265 int separator2 = u2.getFile().indexOf(SEPARATOR); 266 if (separator1 == -1 || separator2 == -1) { 267 return super.sameFile(u1, u2); 268 } 269 String nested1 = u1.getFile().substring(separator1 + SEPARATOR.length()); 270 String nested2 = u2.getFile().substring(separator2 + SEPARATOR.length()); 271 if (!nested1.equals(nested2)) { 272 String canonical1 = canonicalize(nested1); 273 String canonical2 = canonicalize(nested2); 274 if (!canonical1.equals(canonical2)) { 275 return false; 276 } 277 } 278 String root1 = u1.getFile().substring(0, separator1); 279 String root2 = u2.getFile().substring(0, separator2); 280 try { 281 return super.sameFile(new URL(root1), new URL(root2)); 282 } 283 catch (MalformedURLException ex) { 284 // Continue 285 } 286 return super.sameFile(u1, u2); 287 } 288 289 private String canonicalize(String path) { 290 return path.replace(SEPARATOR, "/"); 291 } 292 293 public JarFile getRootJarFileFromUrl(URL url) throws IOException { 294 String spec = url.getFile(); 295 int separatorIndex = spec.indexOf(SEPARATOR); 296 if (separatorIndex == -1) { 297 throw new MalformedURLException("Jar URL does not contain !/ separator"); 298 } 299 String name = spec.substring(0, separatorIndex); 300 return getRootJarFile(name); 301 } 302 303 private JarFile getRootJarFile(String name) throws IOException { 304 try { 305 if (!name.startsWith(FILE_PROTOCOL)) { 306 throw new IllegalStateException("Not a file URL"); 307 } 308 String path = name.substring(FILE_PROTOCOL.length()); 309 File file = new File(URLDecoder.decode(path, "UTF-8")); 310 Map<File, JarFile> cache = rootFileCache.get(); 311 JarFile result = (cache == null ? null : cache.get(file)); 312 if (result == null) { 313 result = new JarFile(file); 314 addToRootFileCache(file, result); 315 } 316 return result; 317 } 318 catch (Exception ex) { 319 throw new IOException("Unable to open root Jar file '" + name + "'", ex); 320 } 321 } 322 323 /** 324 * Add the given {@link JarFile} to the root file cache. 325 * @param sourceFile the source file to add 326 * @param jarFile the jar file. 327 */ 328 static void addToRootFileCache(File sourceFile, JarFile jarFile) { 329 Map<File, JarFile> cache = rootFileCache.get(); 330 if (cache == null) { 331 cache = new ConcurrentHashMap<File, JarFile>(); 332 rootFileCache = new SoftReference<Map<File, JarFile>>(cache); 333 } 334 cache.put(sourceFile, jarFile); 335 } 336 337 /** 338 * Set if a generic static exception can be thrown when a URL cannot be connected. 339 * This optimization is used during class loading to save creating lots of exceptions 340 * which are then swallowed. 341 * @param useFastConnectionExceptions if fast connection exceptions can be used. 342 */ 343 public static void setUseFastConnectionExceptions( 344 boolean useFastConnectionExceptions) { 345 JarURLConnection.setUseFastExceptions(useFastConnectionExceptions); 346 } 347 348}