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