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