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.io.InputStream; 022import java.lang.ref.SoftReference; 023import java.net.MalformedURLException; 024import java.net.URL; 025import java.net.URLStreamHandler; 026import java.net.URLStreamHandlerFactory; 027import java.util.Enumeration; 028import java.util.Iterator; 029import java.util.function.Supplier; 030import java.util.jar.JarInputStream; 031import java.util.jar.Manifest; 032import java.util.zip.ZipEntry; 033 034import org.springframework.boot.loader.data.RandomAccessData; 035import org.springframework.boot.loader.data.RandomAccessDataFile; 036 037/** 038 * Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but 039 * offers the following additional functionality. 040 * <ul> 041 * <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based 042 * on any directory entry.</li> 043 * <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for 044 * embedded JAR files (as long as their entry is not compressed).</li> 045 * </ul> 046 * 047 * @author Phillip Webb 048 * @author Andy Wilkinson 049 */ 050public class JarFile extends java.util.jar.JarFile { 051 052 private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; 053 054 private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; 055 056 private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader"; 057 058 private static final AsciiBytes META_INF = new AsciiBytes("META-INF/"); 059 060 private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF"); 061 062 private final RandomAccessDataFile rootFile; 063 064 private final String pathFromRoot; 065 066 private final RandomAccessData data; 067 068 private final JarFileType type; 069 070 private URL url; 071 072 private String urlString; 073 074 private JarFileEntries entries; 075 076 private Supplier<Manifest> manifestSupplier; 077 078 private SoftReference<Manifest> manifest; 079 080 private boolean signed; 081 082 /** 083 * Create a new {@link JarFile} backed by the specified file. 084 * @param file the root jar file 085 * @throws IOException if the file cannot be read 086 */ 087 public JarFile(File file) throws IOException { 088 this(new RandomAccessDataFile(file)); 089 } 090 091 /** 092 * Create a new {@link JarFile} backed by the specified file. 093 * @param file the root jar file 094 * @throws IOException if the file cannot be read 095 */ 096 JarFile(RandomAccessDataFile file) throws IOException { 097 this(file, "", file, JarFileType.DIRECT); 098 } 099 100 /** 101 * Private constructor used to create a new {@link JarFile} either directly or from a 102 * nested entry. 103 * @param rootFile the root jar file 104 * @param pathFromRoot the name of this file 105 * @param data the underlying data 106 * @param type the type of the jar file 107 * @throws IOException if the file cannot be read 108 */ 109 private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, 110 RandomAccessData data, JarFileType type) throws IOException { 111 this(rootFile, pathFromRoot, data, null, type, null); 112 } 113 114 private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, 115 RandomAccessData data, JarEntryFilter filter, JarFileType type, 116 Supplier<Manifest> manifestSupplier) throws IOException { 117 super(rootFile.getFile()); 118 this.rootFile = rootFile; 119 this.pathFromRoot = pathFromRoot; 120 CentralDirectoryParser parser = new CentralDirectoryParser(); 121 this.entries = parser.addVisitor(new JarFileEntries(this, filter)); 122 parser.addVisitor(centralDirectoryVisitor()); 123 this.data = parser.parse(data, filter == null); 124 this.type = type; 125 this.manifestSupplier = (manifestSupplier != null) ? manifestSupplier : () -> { 126 try (InputStream inputStream = getInputStream(MANIFEST_NAME)) { 127 if (inputStream == null) { 128 return null; 129 } 130 return new Manifest(inputStream); 131 } 132 catch (IOException ex) { 133 throw new RuntimeException(ex); 134 } 135 }; 136 } 137 138 private CentralDirectoryVisitor centralDirectoryVisitor() { 139 return new CentralDirectoryVisitor() { 140 141 @Override 142 public void visitStart(CentralDirectoryEndRecord endRecord, 143 RandomAccessData centralDirectoryData) { 144 } 145 146 @Override 147 public void visitFileHeader(CentralDirectoryFileHeader fileHeader, 148 int dataOffset) { 149 AsciiBytes name = fileHeader.getName(); 150 if (name.startsWith(META_INF) 151 && name.endsWith(SIGNATURE_FILE_EXTENSION)) { 152 JarFile.this.signed = true; 153 } 154 } 155 156 @Override 157 public void visitEnd() { 158 } 159 160 }; 161 } 162 163 protected final RandomAccessDataFile getRootJarFile() { 164 return this.rootFile; 165 } 166 167 RandomAccessData getData() { 168 return this.data; 169 } 170 171 @Override 172 public Manifest getManifest() throws IOException { 173 Manifest manifest = (this.manifest != null) ? this.manifest.get() : null; 174 if (manifest == null) { 175 try { 176 manifest = this.manifestSupplier.get(); 177 } 178 catch (RuntimeException ex) { 179 throw new IOException(ex); 180 } 181 this.manifest = new SoftReference<>(manifest); 182 } 183 return manifest; 184 } 185 186 @Override 187 public Enumeration<java.util.jar.JarEntry> entries() { 188 final Iterator<JarEntry> iterator = this.entries.iterator(); 189 return new Enumeration<java.util.jar.JarEntry>() { 190 191 @Override 192 public boolean hasMoreElements() { 193 return iterator.hasNext(); 194 } 195 196 @Override 197 public java.util.jar.JarEntry nextElement() { 198 return iterator.next(); 199 } 200 201 }; 202 } 203 204 public JarEntry getJarEntry(CharSequence name) { 205 return this.entries.getEntry(name); 206 } 207 208 @Override 209 public JarEntry getJarEntry(String name) { 210 return (JarEntry) getEntry(name); 211 } 212 213 public boolean containsEntry(String name) { 214 return this.entries.containsEntry(name); 215 } 216 217 @Override 218 public ZipEntry getEntry(String name) { 219 return this.entries.getEntry(name); 220 } 221 222 @Override 223 public synchronized InputStream getInputStream(ZipEntry entry) throws IOException { 224 if (entry instanceof JarEntry) { 225 return this.entries.getInputStream((JarEntry) entry); 226 } 227 return getInputStream((entry != null) ? entry.getName() : null); 228 } 229 230 InputStream getInputStream(String name) throws IOException { 231 return this.entries.getInputStream(name); 232 } 233 234 /** 235 * Return a nested {@link JarFile} loaded from the specified entry. 236 * @param entry the zip entry 237 * @return a {@link JarFile} for the entry 238 * @throws IOException if the nested jar file cannot be read 239 */ 240 public synchronized JarFile getNestedJarFile(ZipEntry entry) throws IOException { 241 return getNestedJarFile((JarEntry) entry); 242 } 243 244 /** 245 * Return a nested {@link JarFile} loaded from the specified entry. 246 * @param entry the zip entry 247 * @return a {@link JarFile} for the entry 248 * @throws IOException if the nested jar file cannot be read 249 */ 250 public synchronized JarFile getNestedJarFile(JarEntry entry) throws IOException { 251 try { 252 return createJarFileFromEntry(entry); 253 } 254 catch (Exception ex) { 255 throw new IOException( 256 "Unable to open nested jar file '" + entry.getName() + "'", ex); 257 } 258 } 259 260 private JarFile createJarFileFromEntry(JarEntry entry) throws IOException { 261 if (entry.isDirectory()) { 262 return createJarFileFromDirectoryEntry(entry); 263 } 264 return createJarFileFromFileEntry(entry); 265 } 266 267 private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException { 268 AsciiBytes name = entry.getAsciiBytesName(); 269 JarEntryFilter filter = (candidate) -> { 270 if (candidate.startsWith(name) && !candidate.equals(name)) { 271 return candidate.substring(name.length()); 272 } 273 return null; 274 }; 275 return new JarFile(this.rootFile, 276 this.pathFromRoot + "!/" 277 + entry.getName().substring(0, name.length() - 1), 278 this.data, filter, JarFileType.NESTED_DIRECTORY, this.manifestSupplier); 279 } 280 281 private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException { 282 if (entry.getMethod() != ZipEntry.STORED) { 283 throw new IllegalStateException("Unable to open nested entry '" 284 + entry.getName() + "'. It has been compressed and nested " 285 + "jar files must be stored without compression. Please check the " 286 + "mechanism used to create your executable jar file"); 287 } 288 RandomAccessData entryData = this.entries.getEntryData(entry.getName()); 289 return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(), 290 entryData, JarFileType.NESTED_JAR); 291 } 292 293 @Override 294 public int size() { 295 return this.entries.getSize(); 296 } 297 298 @Override 299 public void close() throws IOException { 300 super.close(); 301 if (this.type == JarFileType.DIRECT) { 302 this.rootFile.close(); 303 } 304 } 305 306 String getUrlString() throws MalformedURLException { 307 if (this.urlString == null) { 308 this.urlString = getUrl().toString(); 309 } 310 return this.urlString; 311 } 312 313 /** 314 * Return a URL that can be used to access this JAR file. NOTE: the specified URL 315 * cannot be serialized and or cloned. 316 * @return the URL 317 * @throws MalformedURLException if the URL is malformed 318 */ 319 public URL getUrl() throws MalformedURLException { 320 if (this.url == null) { 321 Handler handler = new Handler(this); 322 String file = this.rootFile.getFile().toURI() + this.pathFromRoot + "!/"; 323 file = file.replace("file:////", "file://"); // Fix UNC paths 324 this.url = new URL("jar", "", -1, file, handler); 325 } 326 return this.url; 327 } 328 329 @Override 330 public String toString() { 331 return getName(); 332 } 333 334 @Override 335 public String getName() { 336 return this.rootFile.getFile() + this.pathFromRoot; 337 } 338 339 boolean isSigned() { 340 return this.signed; 341 } 342 343 void setupEntryCertificates(JarEntry entry) { 344 // Fallback to JarInputStream to obtain certificates, not fast but hopefully not 345 // happening that often. 346 try { 347 try (JarInputStream inputStream = new JarInputStream( 348 getData().getInputStream())) { 349 java.util.jar.JarEntry certEntry = inputStream.getNextJarEntry(); 350 while (certEntry != null) { 351 inputStream.closeEntry(); 352 if (entry.getName().equals(certEntry.getName())) { 353 setCertificates(entry, certEntry); 354 } 355 setCertificates(getJarEntry(certEntry.getName()), certEntry); 356 certEntry = inputStream.getNextJarEntry(); 357 } 358 } 359 } 360 catch (IOException ex) { 361 throw new IllegalStateException(ex); 362 } 363 } 364 365 private void setCertificates(JarEntry entry, java.util.jar.JarEntry certEntry) { 366 if (entry != null) { 367 entry.setCertificates(certEntry); 368 } 369 } 370 371 public void clearCache() { 372 this.entries.clearCache(); 373 } 374 375 protected String getPathFromRoot() { 376 return this.pathFromRoot; 377 } 378 379 JarFileType getType() { 380 return this.type; 381 } 382 383 /** 384 * Register a {@literal 'java.protocol.handler.pkgs'} property so that a 385 * {@link URLStreamHandler} will be located to deal with jar URLs. 386 */ 387 public static void registerUrlProtocolHandler() { 388 String handlers = System.getProperty(PROTOCOL_HANDLER, ""); 389 System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE 390 : handlers + "|" + HANDLERS_PACKAGE)); 391 resetCachedUrlHandlers(); 392 } 393 394 /** 395 * Reset any cached handlers just in case a jar protocol has already been used. We 396 * reset the handler by trying to set a null {@link URLStreamHandlerFactory} which 397 * should have no effect other than clearing the handlers cache. 398 */ 399 private static void resetCachedUrlHandlers() { 400 try { 401 URL.setURLStreamHandlerFactory(null); 402 } 403 catch (Error ex) { 404 // Ignore 405 } 406 } 407 408 /** 409 * The type of a {@link JarFile}. 410 */ 411 enum JarFileType { 412 413 DIRECT, NESTED_DIRECTORY, NESTED_JAR 414 415 } 416 417}