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.maven; 018 019import java.io.File; 020import java.io.IOException; 021import java.util.ArrayList; 022import java.util.List; 023import java.util.Properties; 024import java.util.Set; 025import java.util.regex.Pattern; 026 027import org.apache.maven.artifact.Artifact; 028import org.apache.maven.model.Dependency; 029import org.apache.maven.plugin.MojoExecutionException; 030import org.apache.maven.plugin.MojoFailureException; 031import org.apache.maven.plugins.annotations.Component; 032import org.apache.maven.plugins.annotations.LifecyclePhase; 033import org.apache.maven.plugins.annotations.Mojo; 034import org.apache.maven.plugins.annotations.Parameter; 035import org.apache.maven.plugins.annotations.ResolutionScope; 036import org.apache.maven.project.MavenProject; 037import org.apache.maven.project.MavenProjectHelper; 038import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter; 039import org.apache.maven.shared.artifact.filter.collection.ScopeFilter; 040 041import org.springframework.boot.loader.tools.DefaultLaunchScript; 042import org.springframework.boot.loader.tools.LaunchScript; 043import org.springframework.boot.loader.tools.Layout; 044import org.springframework.boot.loader.tools.LayoutFactory; 045import org.springframework.boot.loader.tools.Layouts.Expanded; 046import org.springframework.boot.loader.tools.Layouts.Jar; 047import org.springframework.boot.loader.tools.Layouts.None; 048import org.springframework.boot.loader.tools.Layouts.War; 049import org.springframework.boot.loader.tools.Libraries; 050import org.springframework.boot.loader.tools.Repackager; 051import org.springframework.boot.loader.tools.Repackager.MainClassTimeoutWarningListener; 052 053/** 054 * Repackages existing JAR and WAR archives so that they can be executed from the command 055 * line using {@literal java -jar}. With <code>layout=NONE</code> can also be used simply 056 * to package a JAR with nested dependencies (and no main class, so not executable). 057 * 058 * @author Phillip Webb 059 * @author Dave Syer 060 * @author Stephane Nicoll 061 * @author Björn Lindström 062 */ 063@Mojo(name = "repackage", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME) 064public class RepackageMojo extends AbstractDependencyFilterMojo { 065 066 private static final Pattern WHITE_SPACE_PATTERN = Pattern.compile("\\s+"); 067 068 /** 069 * The Maven project. 070 * @since 1.0 071 */ 072 @Parameter(defaultValue = "${project}", readonly = true, required = true) 073 private MavenProject project; 074 075 /** 076 * Maven project helper utils. 077 * @since 1.0 078 */ 079 @Component 080 private MavenProjectHelper projectHelper; 081 082 /** 083 * Directory containing the generated archive. 084 * @since 1.0 085 */ 086 @Parameter(defaultValue = "${project.build.directory}", required = true) 087 private File outputDirectory; 088 089 /** 090 * Name of the generated archive. 091 * @since 1.0 092 */ 093 @Parameter(defaultValue = "${project.build.finalName}", readonly = true) 094 private String finalName; 095 096 /** 097 * Skip the execution. 098 * @since 1.2 099 */ 100 @Parameter(property = "spring-boot.repackage.skip", defaultValue = "false") 101 private boolean skip; 102 103 /** 104 * Classifier to add to the repackaged archive. If not given, the main artifact will 105 * be replaced by the repackaged archive. If given, the classifier will also be used 106 * to determine the source archive to repackage: if an artifact with that classifier 107 * already exists, it will be used as source and replaced. If no such artifact exists, 108 * the main artifact will be used as source and the repackaged archive will be 109 * attached as a supplemental artifact with that classifier. Attaching the artifact 110 * allows to deploy it alongside to the original one, see <a href= 111 * "http://maven.apache.org/plugins/maven-deploy-plugin/examples/deploying-with-classifiers.html" 112 * > the maven documentation for more details</a>. 113 * @since 1.0 114 */ 115 @Parameter 116 private String classifier; 117 118 /** 119 * Attach the repackaged archive to be installed and deployed. 120 * @since 1.4 121 */ 122 @Parameter(defaultValue = "true") 123 private boolean attach = true; 124 125 /** 126 * The name of the main class. If not specified the first compiled class found that 127 * contains a 'main' method will be used. 128 * @since 1.0 129 */ 130 @Parameter 131 private String mainClass; 132 133 /** 134 * The type of archive (which corresponds to how the dependencies are laid out inside 135 * it). Possible values are JAR, WAR, ZIP, DIR, NONE. Defaults to a guess based on the 136 * archive type. 137 * @since 1.0 138 */ 139 @Parameter 140 private LayoutType layout; 141 142 /** 143 * The layout factory that will be used to create the executable archive if no 144 * explicit layout is set. Alternative layouts implementations can be provided by 3rd 145 * parties. 146 * @since 1.5 147 */ 148 @Parameter 149 private LayoutFactory layoutFactory; 150 151 /** 152 * A list of the libraries that must be unpacked from fat jars in order to run. 153 * Specify each library as a {@code <dependency>} with a {@code <groupId>} and a 154 * {@code <artifactId>} and they will be unpacked at runtime. 155 * @since 1.1 156 */ 157 @Parameter 158 private List<Dependency> requiresUnpack; 159 160 /** 161 * Make a fully executable jar for *nix machines by prepending a launch script to the 162 * jar. 163 * <p> 164 * Currently, some tools do not accept this format so you may not always be able to 165 * use this technique. For example, {@code jar -xf} may silently fail to extract a jar 166 * or war that has been made fully-executable. It is recommended that you only enable 167 * this option if you intend to execute it directly, rather than running it with 168 * {@code java -jar} or deploying it to a servlet container. 169 * @since 1.3 170 */ 171 @Parameter(defaultValue = "false") 172 private boolean executable; 173 174 /** 175 * The embedded launch script to prepend to the front of the jar if it is fully 176 * executable. If not specified the 'Spring Boot' default script will be used. 177 * @since 1.3 178 */ 179 @Parameter 180 private File embeddedLaunchScript; 181 182 /** 183 * Properties that should be expanded in the embedded launch script. 184 * @since 1.3 185 */ 186 @Parameter 187 private Properties embeddedLaunchScriptProperties; 188 189 /** 190 * Exclude Spring Boot devtools from the repackaged archive. 191 * @since 1.3 192 */ 193 @Parameter(defaultValue = "true") 194 private boolean excludeDevtools = true; 195 196 /** 197 * Include system scoped dependencies. 198 * @since 1.4 199 */ 200 @Parameter(defaultValue = "false") 201 public boolean includeSystemScope; 202 203 @Override 204 public void execute() throws MojoExecutionException, MojoFailureException { 205 if (this.project.getPackaging().equals("pom")) { 206 getLog().debug("repackage goal could not be applied to pom project."); 207 return; 208 } 209 if (this.skip) { 210 getLog().debug("skipping repackaging as per configuration."); 211 return; 212 } 213 repackage(); 214 } 215 216 private void repackage() throws MojoExecutionException { 217 Artifact source = getSourceArtifact(); 218 File target = getTargetFile(); 219 Repackager repackager = getRepackager(source.getFile()); 220 Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), 221 getFilters(getAdditionalFilters())); 222 Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, 223 getLog()); 224 try { 225 LaunchScript launchScript = getLaunchScript(); 226 repackager.repackage(target, libraries, launchScript); 227 } 228 catch (IOException ex) { 229 throw new MojoExecutionException(ex.getMessage(), ex); 230 } 231 updateArtifact(source, target, repackager.getBackupFile()); 232 } 233 234 /** 235 * Return the source {@link Artifact} to repackage. If a classifier is specified and 236 * an artifact with that classifier exists, it is used. Otherwise, the main artifact 237 * is used. 238 * @return the source artifact to repackage 239 */ 240 private Artifact getSourceArtifact() { 241 Artifact sourceArtifact = getArtifact(this.classifier); 242 return (sourceArtifact != null) ? sourceArtifact : this.project.getArtifact(); 243 } 244 245 private Artifact getArtifact(String classifier) { 246 if (classifier != null) { 247 for (Artifact attachedArtifact : this.project.getAttachedArtifacts()) { 248 if (classifier.equals(attachedArtifact.getClassifier()) 249 && attachedArtifact.getFile() != null 250 && attachedArtifact.getFile().isFile()) { 251 return attachedArtifact; 252 } 253 } 254 } 255 return null; 256 } 257 258 private File getTargetFile() { 259 String classifier = (this.classifier != null) ? this.classifier.trim() : ""; 260 if (!classifier.isEmpty() && !classifier.startsWith("-")) { 261 classifier = "-" + classifier; 262 } 263 if (!this.outputDirectory.exists()) { 264 this.outputDirectory.mkdirs(); 265 } 266 return new File(this.outputDirectory, this.finalName + classifier + "." 267 + this.project.getArtifact().getArtifactHandler().getExtension()); 268 } 269 270 private Repackager getRepackager(File source) { 271 Repackager repackager = new Repackager(source, this.layoutFactory); 272 repackager.addMainClassTimeoutWarningListener( 273 new LoggingMainClassTimeoutWarningListener()); 274 repackager.setMainClass(this.mainClass); 275 if (this.layout != null) { 276 getLog().info("Layout: " + this.layout); 277 repackager.setLayout(this.layout.layout()); 278 } 279 return repackager; 280 } 281 282 private ArtifactsFilter[] getAdditionalFilters() { 283 List<ArtifactsFilter> filters = new ArrayList<>(); 284 if (this.excludeDevtools) { 285 Exclude exclude = new Exclude(); 286 exclude.setGroupId("org.springframework.boot"); 287 exclude.setArtifactId("spring-boot-devtools"); 288 ExcludeFilter filter = new ExcludeFilter(exclude); 289 filters.add(filter); 290 } 291 if (!this.includeSystemScope) { 292 filters.add(new ScopeFilter(null, Artifact.SCOPE_SYSTEM)); 293 } 294 return filters.toArray(new ArtifactsFilter[0]); 295 } 296 297 private LaunchScript getLaunchScript() throws IOException { 298 if (this.executable || this.embeddedLaunchScript != null) { 299 return new DefaultLaunchScript(this.embeddedLaunchScript, 300 buildLaunchScriptProperties()); 301 } 302 return null; 303 } 304 305 private Properties buildLaunchScriptProperties() { 306 Properties properties = new Properties(); 307 if (this.embeddedLaunchScriptProperties != null) { 308 properties.putAll(this.embeddedLaunchScriptProperties); 309 } 310 putIfMissing(properties, "initInfoProvides", this.project.getArtifactId()); 311 putIfMissing(properties, "initInfoShortDescription", this.project.getName(), 312 this.project.getArtifactId()); 313 putIfMissing(properties, "initInfoDescription", 314 removeLineBreaks(this.project.getDescription()), this.project.getName(), 315 this.project.getArtifactId()); 316 return properties; 317 } 318 319 private String removeLineBreaks(String description) { 320 return (description != null) 321 ? WHITE_SPACE_PATTERN.matcher(description).replaceAll(" ") : null; 322 } 323 324 private void putIfMissing(Properties properties, String key, 325 String... valueCandidates) { 326 if (!properties.containsKey(key)) { 327 for (String candidate : valueCandidates) { 328 if (candidate != null && !candidate.isEmpty()) { 329 properties.put(key, candidate); 330 return; 331 } 332 } 333 } 334 } 335 336 private void updateArtifact(Artifact source, File target, File original) { 337 if (this.attach) { 338 attachArtifact(source, target); 339 } 340 else if (source.getFile().equals(target) && original.exists()) { 341 String artifactId = (this.classifier != null) 342 ? "artifact with classifier " + this.classifier : "main artifact"; 343 getLog().info(String.format("Updating %s %s to %s", artifactId, 344 source.getFile(), original)); 345 source.setFile(original); 346 } 347 else if (this.classifier != null) { 348 getLog().info("Creating repackaged archive " + target + " with classifier " 349 + this.classifier); 350 } 351 } 352 353 private void attachArtifact(Artifact source, File target) { 354 if (this.classifier != null && !source.getFile().equals(target)) { 355 getLog().info("Attaching repackaged archive " + target + " with classifier " 356 + this.classifier); 357 this.projectHelper.attachArtifact(this.project, this.project.getPackaging(), 358 this.classifier, target); 359 } 360 else { 361 String artifactId = (this.classifier != null) 362 ? "artifact with classifier " + this.classifier : "main artifact"; 363 getLog().info("Replacing " + artifactId + " with repackaged archive"); 364 source.setFile(target); 365 } 366 } 367 368 private class LoggingMainClassTimeoutWarningListener 369 implements MainClassTimeoutWarningListener { 370 371 @Override 372 public void handleTimeoutWarning(long duration, String mainMethod) { 373 getLog().warn("Searching for the main-class is taking some time, " 374 + "consider using the mainClass configuration " + "parameter"); 375 } 376 377 } 378 379 /** 380 * Archive layout types. 381 */ 382 public enum LayoutType { 383 384 /** 385 * Jar Layout. 386 */ 387 JAR(new Jar()), 388 389 /** 390 * War Layout. 391 */ 392 WAR(new War()), 393 394 /** 395 * Zip Layout. 396 */ 397 ZIP(new Expanded()), 398 399 /** 400 * Dir Layout. 401 */ 402 DIR(new Expanded()), 403 404 /** 405 * No Layout. 406 */ 407 NONE(new None()); 408 409 private final Layout layout; 410 411 LayoutType(Layout layout) { 412 this.layout = layout; 413 } 414 415 public Layout layout() { 416 return this.layout; 417 } 418 419 } 420 421}