001/* 002 * Copyright 2012-2016 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.tools; 018 019import java.io.BufferedInputStream; 020import java.io.ByteArrayInputStream; 021import java.io.File; 022import java.io.FileInputStream; 023import java.io.FileNotFoundException; 024import java.io.FileOutputStream; 025import java.io.FilterInputStream; 026import java.io.IOException; 027import java.io.InputStream; 028import java.io.OutputStream; 029import java.net.URL; 030import java.nio.file.Files; 031import java.nio.file.Path; 032import java.nio.file.attribute.PosixFilePermission; 033import java.util.Arrays; 034import java.util.Enumeration; 035import java.util.HashSet; 036import java.util.Set; 037import java.util.jar.JarEntry; 038import java.util.jar.JarFile; 039import java.util.jar.JarInputStream; 040import java.util.jar.JarOutputStream; 041import java.util.jar.Manifest; 042import java.util.zip.CRC32; 043import java.util.zip.ZipEntry; 044 045import org.springframework.lang.UsesJava7; 046 047/** 048 * Writes JAR content, ensuring valid directory entries are always create and duplicate 049 * items are ignored. 050 * 051 * @author Phillip Webb 052 * @author Andy Wilkinson 053 */ 054public class JarWriter implements LoaderClassesWriter { 055 056 private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar"; 057 058 private static final int BUFFER_SIZE = 32 * 1024; 059 060 private final JarOutputStream jarOutput; 061 062 private final Set<String> writtenEntries = new HashSet<String>(); 063 064 /** 065 * Create a new {@link JarWriter} instance. 066 * @param file the file to write 067 * @throws IOException if the file cannot be opened 068 * @throws FileNotFoundException if the file cannot be found 069 */ 070 public JarWriter(File file) throws FileNotFoundException, IOException { 071 this(file, null); 072 } 073 074 /** 075 * Create a new {@link JarWriter} instance. 076 * @param file the file to write 077 * @param launchScript an optional launch script to prepend to the front of the jar 078 * @throws IOException if the file cannot be opened 079 * @throws FileNotFoundException if the file cannot be found 080 */ 081 public JarWriter(File file, LaunchScript launchScript) 082 throws FileNotFoundException, IOException { 083 FileOutputStream fileOutputStream = new FileOutputStream(file); 084 if (launchScript != null) { 085 fileOutputStream.write(launchScript.toByteArray()); 086 setExecutableFilePermission(file); 087 } 088 this.jarOutput = new JarOutputStream(fileOutputStream); 089 } 090 091 @UsesJava7 092 private void setExecutableFilePermission(File file) { 093 try { 094 Path path = file.toPath(); 095 Set<PosixFilePermission> permissions = new HashSet<PosixFilePermission>( 096 Files.getPosixFilePermissions(path)); 097 permissions.add(PosixFilePermission.OWNER_EXECUTE); 098 Files.setPosixFilePermissions(path, permissions); 099 } 100 catch (Throwable ex) { 101 // Ignore and continue creating the jar 102 } 103 } 104 105 /** 106 * Write the specified manifest. 107 * @param manifest the manifest to write 108 * @throws IOException of the manifest cannot be written 109 */ 110 public void writeManifest(final Manifest manifest) throws IOException { 111 JarEntry entry = new JarEntry("META-INF/MANIFEST.MF"); 112 writeEntry(entry, new EntryWriter() { 113 @Override 114 public void write(OutputStream outputStream) throws IOException { 115 manifest.write(outputStream); 116 } 117 }); 118 } 119 120 /** 121 * Write all entries from the specified jar file. 122 * @param jarFile the source jar file 123 * @throws IOException if the entries cannot be written 124 */ 125 public void writeEntries(JarFile jarFile) throws IOException { 126 this.writeEntries(jarFile, new IdentityEntryTransformer()); 127 } 128 129 void writeEntries(JarFile jarFile, EntryTransformer entryTransformer) 130 throws IOException { 131 Enumeration<JarEntry> entries = jarFile.entries(); 132 while (entries.hasMoreElements()) { 133 JarEntry entry = entries.nextElement(); 134 ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream( 135 jarFile.getInputStream(entry)); 136 try { 137 if (inputStream.hasZipHeader() && entry.getMethod() != ZipEntry.STORED) { 138 new CrcAndSize(inputStream).setupStoredEntry(entry); 139 inputStream.close(); 140 inputStream = new ZipHeaderPeekInputStream( 141 jarFile.getInputStream(entry)); 142 } 143 EntryWriter entryWriter = new InputStreamEntryWriter(inputStream, true); 144 JarEntry transformedEntry = entryTransformer.transform(entry); 145 if (transformedEntry != null) { 146 writeEntry(transformedEntry, entryWriter); 147 } 148 } 149 finally { 150 inputStream.close(); 151 } 152 } 153 } 154 155 /** 156 * Writes an entry. The {@code inputStream} is closed once the entry has been written 157 * @param entryName The name of the entry 158 * @param inputStream The stream from which the entry's data can be read 159 * @throws IOException if the write fails 160 */ 161 @Override 162 public void writeEntry(String entryName, InputStream inputStream) throws IOException { 163 JarEntry entry = new JarEntry(entryName); 164 writeEntry(entry, new InputStreamEntryWriter(inputStream, true)); 165 } 166 167 /** 168 * Write a nested library. 169 * @param destination the destination of the library 170 * @param library the library 171 * @throws IOException if the write fails 172 */ 173 public void writeNestedLibrary(String destination, Library library) 174 throws IOException { 175 File file = library.getFile(); 176 JarEntry entry = new JarEntry(destination + library.getName()); 177 entry.setTime(getNestedLibraryTime(file)); 178 if (library.isUnpackRequired()) { 179 entry.setComment("UNPACK:" + FileUtils.sha1Hash(file)); 180 } 181 new CrcAndSize(file).setupStoredEntry(entry); 182 writeEntry(entry, new InputStreamEntryWriter(new FileInputStream(file), true)); 183 } 184 185 private long getNestedLibraryTime(File file) { 186 try { 187 JarFile jarFile = new JarFile(file); 188 try { 189 Enumeration<JarEntry> entries = jarFile.entries(); 190 while (entries.hasMoreElements()) { 191 JarEntry entry = entries.nextElement(); 192 if (!entry.isDirectory()) { 193 return entry.getTime(); 194 } 195 } 196 } 197 finally { 198 jarFile.close(); 199 } 200 } 201 catch (Exception ex) { 202 // Ignore and just use the source file timestamp 203 } 204 return file.lastModified(); 205 } 206 207 /** 208 * Write the required spring-boot-loader classes to the JAR. 209 * @throws IOException if the classes cannot be written 210 */ 211 @Override 212 public void writeLoaderClasses() throws IOException { 213 writeLoaderClasses(NESTED_LOADER_JAR); 214 } 215 216 /** 217 * Write the required spring-boot-loader classes to the JAR. 218 * @param loaderJarResourceName the name of the resource containing the loader classes 219 * to be written 220 * @throws IOException if the classes cannot be written 221 */ 222 @Override 223 public void writeLoaderClasses(String loaderJarResourceName) throws IOException { 224 URL loaderJar = getClass().getClassLoader().getResource(loaderJarResourceName); 225 JarInputStream inputStream = new JarInputStream( 226 new BufferedInputStream(loaderJar.openStream())); 227 JarEntry entry; 228 while ((entry = inputStream.getNextJarEntry()) != null) { 229 if (entry.getName().endsWith(".class")) { 230 writeEntry(entry, new InputStreamEntryWriter(inputStream, false)); 231 } 232 } 233 inputStream.close(); 234 } 235 236 /** 237 * Close the writer. 238 * @throws IOException if the file cannot be closed 239 */ 240 public void close() throws IOException { 241 this.jarOutput.close(); 242 } 243 244 /** 245 * Perform the actual write of a {@link JarEntry}. All other {@code write} method 246 * delegate to this one. 247 * @param entry the entry to write 248 * @param entryWriter the entry writer or {@code null} if there is no content 249 * @throws IOException in case of I/O errors 250 */ 251 private void writeEntry(JarEntry entry, EntryWriter entryWriter) throws IOException { 252 String parent = entry.getName(); 253 if (parent.endsWith("/")) { 254 parent = parent.substring(0, parent.length() - 1); 255 } 256 if (parent.lastIndexOf("/") != -1) { 257 parent = parent.substring(0, parent.lastIndexOf("/") + 1); 258 if (parent.length() > 0) { 259 writeEntry(new JarEntry(parent), null); 260 } 261 } 262 263 if (this.writtenEntries.add(entry.getName())) { 264 this.jarOutput.putNextEntry(entry); 265 if (entryWriter != null) { 266 entryWriter.write(this.jarOutput); 267 } 268 this.jarOutput.closeEntry(); 269 } 270 } 271 272 /** 273 * Interface used to write jar entry date. 274 */ 275 private interface EntryWriter { 276 277 /** 278 * Write entry data to the specified output stream. 279 * @param outputStream the destination for the data 280 * @throws IOException in case of I/O errors 281 */ 282 void write(OutputStream outputStream) throws IOException; 283 284 } 285 286 /** 287 * {@link EntryWriter} that writes content from an {@link InputStream}. 288 */ 289 private static class InputStreamEntryWriter implements EntryWriter { 290 291 private final InputStream inputStream; 292 293 private final boolean close; 294 295 InputStreamEntryWriter(InputStream inputStream, boolean close) { 296 this.inputStream = inputStream; 297 this.close = close; 298 } 299 300 @Override 301 public void write(OutputStream outputStream) throws IOException { 302 byte[] buffer = new byte[BUFFER_SIZE]; 303 int bytesRead; 304 while ((bytesRead = this.inputStream.read(buffer)) != -1) { 305 outputStream.write(buffer, 0, bytesRead); 306 } 307 outputStream.flush(); 308 if (this.close) { 309 this.inputStream.close(); 310 } 311 } 312 313 } 314 315 /** 316 * {@link InputStream} that can peek ahead at zip header bytes. 317 */ 318 private static class ZipHeaderPeekInputStream extends FilterInputStream { 319 320 private static final byte[] ZIP_HEADER = new byte[] { 0x50, 0x4b, 0x03, 0x04 }; 321 322 private final byte[] header; 323 324 private ByteArrayInputStream headerStream; 325 326 protected ZipHeaderPeekInputStream(InputStream in) throws IOException { 327 super(in); 328 this.header = new byte[4]; 329 int len = in.read(this.header); 330 this.headerStream = new ByteArrayInputStream(this.header, 0, len); 331 } 332 333 @Override 334 public int read() throws IOException { 335 int read = (this.headerStream == null ? -1 : this.headerStream.read()); 336 if (read != -1) { 337 this.headerStream = null; 338 return read; 339 } 340 return super.read(); 341 } 342 343 @Override 344 public int read(byte[] b) throws IOException { 345 return read(b, 0, b.length); 346 } 347 348 @Override 349 public int read(byte[] b, int off, int len) throws IOException { 350 int read = (this.headerStream == null ? -1 351 : this.headerStream.read(b, off, len)); 352 if (read != -1) { 353 this.headerStream = null; 354 return read; 355 } 356 return super.read(b, off, len); 357 } 358 359 public boolean hasZipHeader() { 360 return Arrays.equals(this.header, ZIP_HEADER); 361 } 362 363 } 364 365 /** 366 * Data holder for CRC and Size. 367 */ 368 private static class CrcAndSize { 369 370 private final CRC32 crc = new CRC32(); 371 372 private long size; 373 374 CrcAndSize(File file) throws IOException { 375 FileInputStream inputStream = new FileInputStream(file); 376 try { 377 load(inputStream); 378 } 379 finally { 380 inputStream.close(); 381 } 382 } 383 384 CrcAndSize(InputStream inputStream) throws IOException { 385 load(inputStream); 386 } 387 388 private void load(InputStream inputStream) throws IOException { 389 byte[] buffer = new byte[BUFFER_SIZE]; 390 int bytesRead; 391 while ((bytesRead = inputStream.read(buffer)) != -1) { 392 this.crc.update(buffer, 0, bytesRead); 393 this.size += bytesRead; 394 } 395 } 396 397 public void setupStoredEntry(JarEntry entry) { 398 entry.setSize(this.size); 399 entry.setCompressedSize(this.size); 400 entry.setCrc(this.crc.getValue()); 401 entry.setMethod(ZipEntry.STORED); 402 } 403 404 } 405 406 /** 407 * An {@code EntryTransformer} enables the transformation of {@link JarEntry jar 408 * entries} during the writing process. 409 */ 410 interface EntryTransformer { 411 412 JarEntry transform(JarEntry jarEntry); 413 414 } 415 416 /** 417 * An {@code EntryTransformer} that returns the entry unchanged. 418 */ 419 private static final class IdentityEntryTransformer implements EntryTransformer { 420 421 @Override 422 public JarEntry transform(JarEntry jarEntry) { 423 return jarEntry; 424 } 425 426 } 427 428}