001/* 002 * Copyright 2012-2018 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; 032import java.util.regex.Pattern; 033 034/** 035 * {@link URLStreamHandler} for Spring Boot loader {@link JarFile}s. 036 * 037 * @author Phillip Webb 038 * @author Andy Wilkinson 039 * @see JarFile#registerUrlProtocolHandler() 040 */ 041public class Handler extends URLStreamHandler { 042 043 // NOTE: in order to be found as a URL protocol handler, this class must be public, 044 // must be named Handler and must be in a package ending '.jar' 045 046 private static final String JAR_PROTOCOL = "jar:"; 047 048 private static final String FILE_PROTOCOL = "file:"; 049 050 private static final String SEPARATOR = "!/"; 051 052 private static final String CURRENT_DIR = "/./"; 053 054 private static final Pattern CURRENT_DIR_PATTERN = Pattern.compile(CURRENT_DIR); 055 056 private static final String PARENT_DIR = "/../"; 057 058 private static final String[] FALLBACK_HANDLERS = { 059 "sun.net.www.protocol.jar.Handler" }; 060 061 private static final Method OPEN_CONNECTION_METHOD; 062 063 static { 064 Method method = null; 065 try { 066 method = URLStreamHandler.class.getDeclaredMethod("openConnection", 067 URL.class); 068 } 069 catch (Exception ex) { 070 // Swallow and ignore 071 } 072 OPEN_CONNECTION_METHOD = method; 073 } 074 075 private static SoftReference<Map<File, JarFile>> rootFileCache; 076 077 static { 078 rootFileCache = new SoftReference<>(null); 079 } 080 081 private final JarFile jarFile; 082 083 private URLStreamHandler fallbackHandler; 084 085 public Handler() { 086 this(null); 087 } 088 089 public Handler(JarFile jarFile) { 090 this.jarFile = jarFile; 091 } 092 093 @Override 094 protected URLConnection openConnection(URL url) throws IOException { 095 if (this.jarFile != null && isUrlInJarFile(url, this.jarFile)) { 096 return JarURLConnection.get(url, this.jarFile); 097 } 098 try { 099 return JarURLConnection.get(url, getRootJarFileFromUrl(url)); 100 } 101 catch (Exception ex) { 102 return openFallbackConnection(url, ex); 103 } 104 } 105 106 private boolean isUrlInJarFile(URL url, JarFile jarFile) 107 throws MalformedURLException { 108 // Try the path first to save building a new url string each time 109 return url.getPath().startsWith(jarFile.getUrl().getPath()) 110 && url.toString().startsWith(jarFile.getUrlString()); 111 } 112 113 private URLConnection openFallbackConnection(URL url, Exception reason) 114 throws IOException { 115 try { 116 return openConnection(getFallbackHandler(), url); 117 } 118 catch (Exception ex) { 119 if (reason instanceof IOException) { 120 log(false, "Unable to open fallback handler", ex); 121 throw (IOException) reason; 122 } 123 log(true, "Unable to open fallback handler", ex); 124 if (reason instanceof RuntimeException) { 125 throw (RuntimeException) reason; 126 } 127 throw new IllegalStateException(reason); 128 } 129 } 130 131 private void log(boolean warning, String message, Exception cause) { 132 try { 133 Level level = warning ? Level.WARNING : Level.FINEST; 134 Logger.getLogger(getClass().getName()).log(level, message, cause); 135 } 136 catch (Exception ex) { 137 if (warning) { 138 System.err.println("WARNING: " + message); 139 } 140 } 141 } 142 143 private URLStreamHandler getFallbackHandler() { 144 if (this.fallbackHandler != null) { 145 return this.fallbackHandler; 146 } 147 for (String handlerClassName : FALLBACK_HANDLERS) { 148 try { 149 Class<?> handlerClass = Class.forName(handlerClassName); 150 this.fallbackHandler = (URLStreamHandler) handlerClass.newInstance(); 151 return this.fallbackHandler; 152 } 153 catch (Exception ex) { 154 // Ignore 155 } 156 } 157 throw new IllegalStateException("Unable to find fallback handler"); 158 } 159 160 private URLConnection openConnection(URLStreamHandler handler, URL url) 161 throws Exception { 162 if (OPEN_CONNECTION_METHOD == null) { 163 throw new IllegalStateException( 164 "Unable to invoke fallback open connection method"); 165 } 166 OPEN_CONNECTION_METHOD.setAccessible(true); 167 return (URLConnection) OPEN_CONNECTION_METHOD.invoke(handler, url); 168 } 169 170 @Override 171 protected void parseURL(URL context, String spec, int start, int limit) { 172 if (spec.regionMatches(true, 0, JAR_PROTOCOL, 0, JAR_PROTOCOL.length())) { 173 setFile(context, getFileFromSpec(spec.substring(start, limit))); 174 } 175 else { 176 setFile(context, getFileFromContext(context, spec.substring(start, limit))); 177 } 178 } 179 180 private String getFileFromSpec(String spec) { 181 int separatorIndex = spec.lastIndexOf("!/"); 182 if (separatorIndex == -1) { 183 throw new IllegalArgumentException("No !/ in spec '" + spec + "'"); 184 } 185 try { 186 new URL(spec.substring(0, separatorIndex)); 187 return spec; 188 } 189 catch (MalformedURLException ex) { 190 throw new IllegalArgumentException("Invalid spec URL '" + spec + "'", ex); 191 } 192 } 193 194 private String getFileFromContext(URL context, String spec) { 195 String file = context.getFile(); 196 if (spec.startsWith("/")) { 197 return trimToJarRoot(file) + SEPARATOR + spec.substring(1); 198 } 199 if (file.endsWith("/")) { 200 return file + spec; 201 } 202 int lastSlashIndex = file.lastIndexOf('/'); 203 if (lastSlashIndex == -1) { 204 throw new IllegalArgumentException( 205 "No / found in context URL's file '" + file + "'"); 206 } 207 return file.substring(0, lastSlashIndex + 1) + spec; 208 } 209 210 private String trimToJarRoot(String file) { 211 int lastSeparatorIndex = file.lastIndexOf(SEPARATOR); 212 if (lastSeparatorIndex == -1) { 213 throw new IllegalArgumentException( 214 "No !/ found in context URL's file '" + file + "'"); 215 } 216 return file.substring(0, lastSeparatorIndex); 217 } 218 219 private void setFile(URL context, String file) { 220 String path = normalize(file); 221 String query = null; 222 int queryIndex = path.lastIndexOf('?'); 223 if (queryIndex != -1) { 224 query = path.substring(queryIndex + 1); 225 path = path.substring(0, queryIndex); 226 } 227 setURL(context, JAR_PROTOCOL, null, -1, null, null, path, query, 228 context.getRef()); 229 } 230 231 private String normalize(String file) { 232 if (!file.contains(CURRENT_DIR) && !file.contains(PARENT_DIR)) { 233 return file; 234 } 235 int afterLastSeparatorIndex = file.lastIndexOf(SEPARATOR) + SEPARATOR.length(); 236 String afterSeparator = file.substring(afterLastSeparatorIndex); 237 afterSeparator = replaceParentDir(afterSeparator); 238 afterSeparator = replaceCurrentDir(afterSeparator); 239 return file.substring(0, afterLastSeparatorIndex) + afterSeparator; 240 } 241 242 private String replaceParentDir(String file) { 243 int parentDirIndex; 244 while ((parentDirIndex = file.indexOf(PARENT_DIR)) >= 0) { 245 int precedingSlashIndex = file.lastIndexOf('/', parentDirIndex - 1); 246 if (precedingSlashIndex >= 0) { 247 file = file.substring(0, precedingSlashIndex) 248 + file.substring(parentDirIndex + 3); 249 } 250 else { 251 file = file.substring(parentDirIndex + 4); 252 } 253 } 254 return file; 255 } 256 257 private String replaceCurrentDir(String file) { 258 return CURRENT_DIR_PATTERN.matcher(file).replaceAll("/"); 259 } 260 261 @Override 262 protected int hashCode(URL u) { 263 return hashCode(u.getProtocol(), u.getFile()); 264 } 265 266 private int hashCode(String protocol, String file) { 267 int result = (protocol != null) ? protocol.hashCode() : 0; 268 int separatorIndex = file.indexOf(SEPARATOR); 269 if (separatorIndex == -1) { 270 return result + file.hashCode(); 271 } 272 String source = file.substring(0, separatorIndex); 273 String entry = canonicalize(file.substring(separatorIndex + 2)); 274 try { 275 result += new URL(source).hashCode(); 276 } 277 catch (MalformedURLException ex) { 278 result += source.hashCode(); 279 } 280 result += entry.hashCode(); 281 return result; 282 } 283 284 @Override 285 protected boolean sameFile(URL u1, URL u2) { 286 if (!u1.getProtocol().equals("jar") || !u2.getProtocol().equals("jar")) { 287 return false; 288 } 289 int separator1 = u1.getFile().indexOf(SEPARATOR); 290 int separator2 = u2.getFile().indexOf(SEPARATOR); 291 if (separator1 == -1 || separator2 == -1) { 292 return super.sameFile(u1, u2); 293 } 294 String nested1 = u1.getFile().substring(separator1 + SEPARATOR.length()); 295 String nested2 = u2.getFile().substring(separator2 + SEPARATOR.length()); 296 if (!nested1.equals(nested2)) { 297 String canonical1 = canonicalize(nested1); 298 String canonical2 = canonicalize(nested2); 299 if (!canonical1.equals(canonical2)) { 300 return false; 301 } 302 } 303 String root1 = u1.getFile().substring(0, separator1); 304 String root2 = u2.getFile().substring(0, separator2); 305 try { 306 return super.sameFile(new URL(root1), new URL(root2)); 307 } 308 catch (MalformedURLException ex) { 309 // Continue 310 } 311 return super.sameFile(u1, u2); 312 } 313 314 private String canonicalize(String path) { 315 return path.replace(SEPARATOR, "/"); 316 } 317 318 public JarFile getRootJarFileFromUrl(URL url) throws IOException { 319 String spec = url.getFile(); 320 int separatorIndex = spec.indexOf(SEPARATOR); 321 if (separatorIndex == -1) { 322 throw new MalformedURLException("Jar URL does not contain !/ separator"); 323 } 324 String name = spec.substring(0, separatorIndex); 325 return getRootJarFile(name); 326 } 327 328 private JarFile getRootJarFile(String name) throws IOException { 329 try { 330 if (!name.startsWith(FILE_PROTOCOL)) { 331 throw new IllegalStateException("Not a file URL"); 332 } 333 String path = name.substring(FILE_PROTOCOL.length()); 334 File file = new File(URLDecoder.decode(path, "UTF-8")); 335 Map<File, JarFile> cache = rootFileCache.get(); 336 JarFile result = (cache != null) ? cache.get(file) : null; 337 if (result == null) { 338 result = new JarFile(file); 339 addToRootFileCache(file, result); 340 } 341 return result; 342 } 343 catch (Exception ex) { 344 throw new IOException("Unable to open root Jar file '" + name + "'", ex); 345 } 346 } 347 348 /** 349 * Add the given {@link JarFile} to the root file cache. 350 * @param sourceFile the source file to add 351 * @param jarFile the jar file. 352 */ 353 static void addToRootFileCache(File sourceFile, JarFile jarFile) { 354 Map<File, JarFile> cache = rootFileCache.get(); 355 if (cache == null) { 356 cache = new ConcurrentHashMap<>(); 357 rootFileCache = new SoftReference<>(cache); 358 } 359 cache.put(sourceFile, jarFile); 360 } 361 362 /** 363 * Set if a generic static exception can be thrown when a URL cannot be connected. 364 * This optimization is used during class loading to save creating lots of exceptions 365 * which are then swallowed. 366 * @param useFastConnectionExceptions if fast connection exceptions can be used. 367 */ 368 public static void setUseFastConnectionExceptions( 369 boolean useFastConnectionExceptions) { 370 JarURLConnection.setUseFastExceptions(useFastConnectionExceptions); 371 } 372 373}