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.tools; 018 019import java.io.File; 020import java.io.FileInputStream; 021import java.io.IOException; 022import java.io.InputStream; 023import java.util.ArrayList; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Set; 027import java.util.concurrent.TimeUnit; 028import java.util.jar.JarEntry; 029import java.util.jar.JarFile; 030import java.util.jar.Manifest; 031 032import org.springframework.boot.loader.tools.JarWriter.EntryTransformer; 033import org.springframework.core.io.support.SpringFactoriesLoader; 034import org.springframework.lang.UsesJava8; 035import org.springframework.util.Assert; 036import org.springframework.util.StringUtils; 037 038/** 039 * Utility class that can be used to repackage an archive so that it can be executed using 040 * '{@literal java -jar}'. 041 * 042 * @author Phillip Webb 043 * @author Andy Wilkinson 044 * @author Stephane Nicoll 045 */ 046public class Repackager { 047 048 private static final String MAIN_CLASS_ATTRIBUTE = "Main-Class"; 049 050 private static final String START_CLASS_ATTRIBUTE = "Start-Class"; 051 052 private static final String BOOT_VERSION_ATTRIBUTE = "Spring-Boot-Version"; 053 054 private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib"; 055 056 private static final String BOOT_CLASSES_ATTRIBUTE = "Spring-Boot-Classes"; 057 058 private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 }; 059 060 private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10); 061 062 private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication"; 063 064 private List<MainClassTimeoutWarningListener> mainClassTimeoutListeners = new ArrayList<MainClassTimeoutWarningListener>(); 065 066 private String mainClass; 067 068 private boolean backupSource = true; 069 070 private final File source; 071 072 private Layout layout; 073 074 private LayoutFactory layoutFactory; 075 076 public Repackager(File source) { 077 this(source, null); 078 } 079 080 public Repackager(File source, LayoutFactory layoutFactory) { 081 if (source == null) { 082 throw new IllegalArgumentException("Source file must be provided"); 083 } 084 if (!source.exists() || !source.isFile()) { 085 throw new IllegalArgumentException("Source must refer to an existing file, " 086 + "got " + source.getAbsolutePath()); 087 } 088 this.source = source.getAbsoluteFile(); 089 this.layoutFactory = layoutFactory; 090 } 091 092 /** 093 * Add a listener that will be triggered to display a warning if searching for the 094 * main class takes too long. 095 * @param listener the listener to add 096 */ 097 public void addMainClassTimeoutWarningListener( 098 MainClassTimeoutWarningListener listener) { 099 this.mainClassTimeoutListeners.add(listener); 100 } 101 102 /** 103 * Sets the main class that should be run. If not specified the value from the 104 * MANIFEST will be used, or if no manifest entry is found the archive will be 105 * searched for a suitable class. 106 * @param mainClass the main class name 107 */ 108 public void setMainClass(String mainClass) { 109 this.mainClass = mainClass; 110 } 111 112 /** 113 * Sets if source files should be backed up when they would be overwritten. 114 * @param backupSource if source files should be backed up 115 */ 116 public void setBackupSource(boolean backupSource) { 117 this.backupSource = backupSource; 118 } 119 120 /** 121 * Sets the layout to use for the jar. Defaults to {@link Layouts#forFile(File)}. 122 * @param layout the layout 123 */ 124 public void setLayout(Layout layout) { 125 if (layout == null) { 126 throw new IllegalArgumentException("Layout must not be null"); 127 } 128 this.layout = layout; 129 } 130 131 /** 132 * Sets the layout factory for the jar. The factory can be used when no specific 133 * layout is specified. 134 * @param layoutFactory the layout factory to set 135 */ 136 public void setLayoutFactory(LayoutFactory layoutFactory) { 137 this.layoutFactory = layoutFactory; 138 } 139 140 /** 141 * Repackage the source file so that it can be run using '{@literal java -jar}'. 142 * @param libraries the libraries required to run the archive 143 * @throws IOException if the file cannot be repackaged 144 */ 145 public void repackage(Libraries libraries) throws IOException { 146 repackage(this.source, libraries); 147 } 148 149 /** 150 * Repackage to the given destination so that it can be launched using ' 151 * {@literal java -jar}'. 152 * @param destination the destination file (may be the same as the source) 153 * @param libraries the libraries required to run the archive 154 * @throws IOException if the file cannot be repackaged 155 */ 156 public void repackage(File destination, Libraries libraries) throws IOException { 157 repackage(destination, libraries, null); 158 } 159 160 /** 161 * Repackage to the given destination so that it can be launched using ' 162 * {@literal java -jar}'. 163 * @param destination the destination file (may be the same as the source) 164 * @param libraries the libraries required to run the archive 165 * @param launchScript an optional launch script prepended to the front of the jar 166 * @throws IOException if the file cannot be repackaged 167 * @since 1.3.0 168 */ 169 public void repackage(File destination, Libraries libraries, 170 LaunchScript launchScript) throws IOException { 171 if (destination == null || destination.isDirectory()) { 172 throw new IllegalArgumentException("Invalid destination"); 173 } 174 if (libraries == null) { 175 throw new IllegalArgumentException("Libraries must not be null"); 176 } 177 if (this.layout == null) { 178 this.layout = getLayoutFactory().getLayout(this.source); 179 } 180 if (alreadyRepackaged()) { 181 return; 182 } 183 destination = destination.getAbsoluteFile(); 184 File workingSource = this.source; 185 if (this.source.equals(destination)) { 186 workingSource = getBackupFile(); 187 workingSource.delete(); 188 renameFile(this.source, workingSource); 189 } 190 destination.delete(); 191 try { 192 JarFile jarFileSource = new JarFile(workingSource); 193 try { 194 repackage(jarFileSource, destination, libraries, launchScript); 195 } 196 finally { 197 jarFileSource.close(); 198 } 199 } 200 finally { 201 if (!this.backupSource && !this.source.equals(workingSource)) { 202 deleteFile(workingSource); 203 } 204 } 205 } 206 207 private LayoutFactory getLayoutFactory() { 208 if (this.layoutFactory != null) { 209 return this.layoutFactory; 210 } 211 List<LayoutFactory> factories = SpringFactoriesLoader 212 .loadFactories(LayoutFactory.class, null); 213 if (factories.isEmpty()) { 214 return new DefaultLayoutFactory(); 215 } 216 Assert.state(factories.size() == 1, "No unique LayoutFactory found"); 217 return factories.get(0); 218 } 219 220 /** 221 * Return the {@link File} to use to backup the original source. 222 * @return the file to use to backup the original source 223 */ 224 public final File getBackupFile() { 225 return new File(this.source.getParentFile(), this.source.getName() + ".original"); 226 } 227 228 private boolean alreadyRepackaged() throws IOException { 229 JarFile jarFile = new JarFile(this.source); 230 try { 231 Manifest manifest = jarFile.getManifest(); 232 return (manifest != null && manifest.getMainAttributes() 233 .getValue(BOOT_VERSION_ATTRIBUTE) != null); 234 } 235 finally { 236 jarFile.close(); 237 } 238 } 239 240 private void repackage(JarFile sourceJar, File destination, Libraries libraries, 241 LaunchScript launchScript) throws IOException { 242 JarWriter writer = new JarWriter(destination, launchScript); 243 try { 244 final List<Library> unpackLibraries = new ArrayList<Library>(); 245 final List<Library> standardLibraries = new ArrayList<Library>(); 246 libraries.doWithLibraries(new LibraryCallback() { 247 248 @Override 249 public void library(Library library) throws IOException { 250 File file = library.getFile(); 251 if (isZip(file)) { 252 if (library.isUnpackRequired()) { 253 unpackLibraries.add(library); 254 } 255 else { 256 standardLibraries.add(library); 257 } 258 } 259 } 260 261 }); 262 repackage(sourceJar, writer, unpackLibraries, standardLibraries); 263 } 264 finally { 265 try { 266 writer.close(); 267 } 268 catch (Exception ex) { 269 // Ignore 270 } 271 } 272 } 273 274 private void repackage(JarFile sourceJar, JarWriter writer, 275 final List<Library> unpackLibraries, final List<Library> standardLibraries) 276 throws IOException { 277 writer.writeManifest(buildManifest(sourceJar)); 278 Set<String> seen = new HashSet<String>(); 279 writeNestedLibraries(unpackLibraries, seen, writer); 280 if (this.layout instanceof RepackagingLayout) { 281 writer.writeEntries(sourceJar, new RenamingEntryTransformer( 282 ((RepackagingLayout) this.layout).getRepackagedClassesLocation())); 283 } 284 else { 285 writer.writeEntries(sourceJar); 286 } 287 writeNestedLibraries(standardLibraries, seen, writer); 288 writeLoaderClasses(writer); 289 } 290 291 private void writeNestedLibraries(List<Library> libraries, Set<String> alreadySeen, 292 JarWriter writer) throws IOException { 293 for (Library library : libraries) { 294 String destination = Repackager.this.layout 295 .getLibraryDestination(library.getName(), library.getScope()); 296 if (destination != null) { 297 if (!alreadySeen.add(destination + library.getName())) { 298 throw new IllegalStateException( 299 "Duplicate library " + library.getName()); 300 } 301 writer.writeNestedLibrary(destination, library); 302 } 303 } 304 } 305 306 private void writeLoaderClasses(JarWriter writer) throws IOException { 307 if (this.layout instanceof CustomLoaderLayout) { 308 ((CustomLoaderLayout) this.layout).writeLoadedClasses(writer); 309 } 310 else if (this.layout.isExecutable()) { 311 writer.writeLoaderClasses(); 312 } 313 } 314 315 private boolean isZip(File file) { 316 try { 317 FileInputStream fileInputStream = new FileInputStream(file); 318 try { 319 return isZip(fileInputStream); 320 } 321 finally { 322 fileInputStream.close(); 323 } 324 } 325 catch (IOException ex) { 326 return false; 327 } 328 } 329 330 private boolean isZip(InputStream inputStream) throws IOException { 331 for (int i = 0; i < ZIP_FILE_HEADER.length; i++) { 332 if (inputStream.read() != ZIP_FILE_HEADER[i]) { 333 return false; 334 } 335 } 336 return true; 337 } 338 339 private Manifest buildManifest(JarFile source) throws IOException { 340 Manifest manifest = source.getManifest(); 341 if (manifest == null) { 342 manifest = new Manifest(); 343 manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); 344 } 345 manifest = new Manifest(manifest); 346 String startClass = this.mainClass; 347 if (startClass == null) { 348 startClass = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE); 349 } 350 if (startClass == null) { 351 startClass = findMainMethodWithTimeoutWarning(source); 352 } 353 String launcherClassName = this.layout.getLauncherClassName(); 354 if (launcherClassName != null) { 355 manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, 356 launcherClassName); 357 if (startClass == null) { 358 throw new IllegalStateException("Unable to find main class"); 359 } 360 manifest.getMainAttributes().putValue(START_CLASS_ATTRIBUTE, startClass); 361 } 362 else if (startClass != null) { 363 manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, startClass); 364 } 365 String bootVersion = getClass().getPackage().getImplementationVersion(); 366 manifest.getMainAttributes().putValue(BOOT_VERSION_ATTRIBUTE, bootVersion); 367 manifest.getMainAttributes().putValue(BOOT_CLASSES_ATTRIBUTE, 368 (this.layout instanceof RepackagingLayout) 369 ? ((RepackagingLayout) this.layout).getRepackagedClassesLocation() 370 : this.layout.getClassesLocation()); 371 String lib = this.layout.getLibraryDestination("", LibraryScope.COMPILE); 372 if (StringUtils.hasLength(lib)) { 373 manifest.getMainAttributes().putValue(BOOT_LIB_ATTRIBUTE, lib); 374 } 375 return manifest; 376 } 377 378 private String findMainMethodWithTimeoutWarning(JarFile source) throws IOException { 379 long startTime = System.currentTimeMillis(); 380 String mainMethod = findMainMethod(source); 381 long duration = System.currentTimeMillis() - startTime; 382 if (duration > FIND_WARNING_TIMEOUT) { 383 for (MainClassTimeoutWarningListener listener : this.mainClassTimeoutListeners) { 384 listener.handleTimeoutWarning(duration, mainMethod); 385 } 386 } 387 return mainMethod; 388 } 389 390 protected String findMainMethod(JarFile source) throws IOException { 391 return MainClassFinder.findSingleMainClass(source, 392 this.layout.getClassesLocation(), SPRING_BOOT_APPLICATION_CLASS_NAME); 393 } 394 395 private void renameFile(File file, File dest) { 396 if (!file.renameTo(dest)) { 397 throw new IllegalStateException( 398 "Unable to rename '" + file + "' to '" + dest + "'"); 399 } 400 } 401 402 private void deleteFile(File file) { 403 if (!file.delete()) { 404 throw new IllegalStateException("Unable to delete '" + file + "'"); 405 } 406 } 407 408 /** 409 * Callback interface used to present a warning when finding the main class takes too 410 * long. 411 */ 412 public interface MainClassTimeoutWarningListener { 413 414 /** 415 * Handle a timeout warning. 416 * @param duration the amount of time it took to find the main method 417 * @param mainMethod the main method that was actually found 418 */ 419 void handleTimeoutWarning(long duration, String mainMethod); 420 421 } 422 423 /** 424 * An {@code EntryTransformer} that renames entries by applying a prefix. 425 */ 426 private static final class RenamingEntryTransformer implements EntryTransformer { 427 428 private final String namePrefix; 429 430 private RenamingEntryTransformer(String namePrefix) { 431 this.namePrefix = namePrefix; 432 } 433 434 @Override 435 public JarEntry transform(JarEntry entry) { 436 if (entry.getName().equals("META-INF/INDEX.LIST")) { 437 return null; 438 } 439 if ((entry.getName().startsWith("META-INF/") 440 && !entry.getName().equals("META-INF/aop.xml")) 441 || entry.getName().startsWith("BOOT-INF/")) { 442 return entry; 443 } 444 JarEntry renamedEntry = new JarEntry(this.namePrefix + entry.getName()); 445 renamedEntry.setTime(entry.getTime()); 446 renamedEntry.setSize(entry.getSize()); 447 renamedEntry.setMethod(entry.getMethod()); 448 if (entry.getComment() != null) { 449 renamedEntry.setComment(entry.getComment()); 450 } 451 renamedEntry.setCompressedSize(entry.getCompressedSize()); 452 renamedEntry.setCrc(entry.getCrc()); 453 setCreationTimeIfPossible(entry, renamedEntry); 454 if (entry.getExtra() != null) { 455 renamedEntry.setExtra(entry.getExtra()); 456 } 457 setLastAccessTimeIfPossible(entry, renamedEntry); 458 setLastModifiedTimeIfPossible(entry, renamedEntry); 459 return renamedEntry; 460 } 461 462 @UsesJava8 463 private void setCreationTimeIfPossible(JarEntry source, JarEntry target) { 464 try { 465 if (source.getCreationTime() != null) { 466 target.setCreationTime(source.getCreationTime()); 467 } 468 } 469 catch (NoSuchMethodError ex) { 470 // Not running on Java 8. Continue. 471 } 472 } 473 474 @UsesJava8 475 private void setLastAccessTimeIfPossible(JarEntry source, JarEntry target) { 476 try { 477 if (source.getLastAccessTime() != null) { 478 target.setLastAccessTime(source.getLastAccessTime()); 479 } 480 } 481 catch (NoSuchMethodError ex) { 482 // Not running on Java 8. Continue. 483 } 484 } 485 486 @UsesJava8 487 private void setLastModifiedTimeIfPossible(JarEntry source, JarEntry target) { 488 try { 489 if (source.getLastModifiedTime() != null) { 490 target.setLastModifiedTime(source.getLastModifiedTime()); 491 } 492 } 493 catch (NoSuchMethodError ex) { 494 // Not running on Java 8. Continue. 495 } 496 } 497 498 } 499 500}