001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.release.plugin.mojos; 018 019import java.io.File; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.OutputStream; 023import java.io.PrintWriter; 024import java.nio.file.Files; 025import java.util.ArrayList; 026import java.util.Collections; 027import java.util.HashSet; 028import java.util.List; 029import java.util.Set; 030 031import org.apache.commons.codec.digest.DigestUtils; 032import org.apache.commons.collections4.properties.SortedProperties; 033import org.apache.commons.lang3.StringUtils; 034import org.apache.commons.lang3.reflect.MethodUtils; 035import org.apache.commons.release.plugin.SharedFunctions; 036import org.apache.maven.artifact.Artifact; 037import org.apache.maven.plugin.AbstractMojo; 038import org.apache.maven.plugin.MojoExecutionException; 039import org.apache.maven.plugins.annotations.LifecyclePhase; 040import org.apache.maven.plugins.annotations.Mojo; 041import org.apache.maven.plugins.annotations.Parameter; 042import org.apache.maven.project.MavenProject; 043 044/** 045 * The purpose of this Maven mojo is to detach the artifacts generated by the maven-assembly-plugin, 046 * which for the Apache Commons Project do not get uploaded to Nexus, and put those artifacts 047 * in the dev distribution location for Apache projects. 048 * 049 * @since 1.0 050 */ 051@Mojo(name = "detach-distributions", 052 defaultPhase = LifecyclePhase.VERIFY, 053 threadSafe = true, 054 aggregator = true) 055public final class CommonsDistributionDetachmentMojo extends AbstractMojo { 056 057 /** 058 * A list of "artifact types" in the Maven vernacular, to 059 * be detached from the deployment. For the time being, we want 060 * all artifacts generated by the maven-assembly-plugin to be detached 061 * from the deployment, namely *-src.zip, *-src.tar.gz, *-bin.zip, 062 * *-bin.tar.gz, and the corresponding .asc PGP signatures. 063 */ 064 private static final Set<String> ARTIFACT_TYPES_TO_DETACH; 065 066 static { 067 final Set<String> hashSet = new HashSet<>(); 068 hashSet.add("zip"); 069 hashSet.add("tar.gz"); 070 hashSet.add("zip.asc"); 071 hashSet.add("tar.gz.asc"); 072 ARTIFACT_TYPES_TO_DETACH = Collections.unmodifiableSet(hashSet); 073 } 074 075 /** 076 * This list is supposed to hold the Maven references to the aforementioned artifacts so that we 077 * can upload them to SVN after they've been detached from the Maven deployment. 078 */ 079 private final List<Artifact> detachedArtifacts = new ArrayList<>(); 080 081 /** 082 * A {@link SortedProperties} mapping artifact file names to their SHA-512 hashes. 083 */ 084 private final SortedProperties artifactSha512s = new SortedProperties(); 085 086 /** 087 * The Maven project context injection so that we can access the project variables. 088 */ 089 @Parameter(defaultValue = "${project}", required = true) 090 private MavenProject project; 091 092 /** 093 * The working directory in <code>target</code> that we use as a sandbox for the plugin. 094 */ 095 @Parameter(defaultValue = "${project.build.directory}/commons-release-plugin", 096 property = "commons.outputDirectory") 097 private File workingDirectory; 098 099 /** 100 * The Subversion staging URL to which we upload all of our staged artifacts. 101 */ 102 @Parameter(defaultValue = "", property = "commons.distSvnStagingUrl") 103 private String distSvnStagingUrl; 104 105 /** 106 * A parameter to generally avoid running unless it is specifically turned on by the consuming module. 107 */ 108 @Parameter(defaultValue = "false", property = "commons.release.isDistModule") 109 private Boolean isDistModule; 110 111 /** 112 * Constructs a new instance. 113 */ 114 public CommonsDistributionDetachmentMojo() { 115 // empty 116 } 117 118 /** 119 * A helper method to copy the newly detached artifacts to <code>target/commons-release-plugin</code> 120 * so that the {@link CommonsDistributionStagingMojo} can find the artifacts later. 121 * 122 * @throws MojoExecutionException Thrown if an {@link IOException} occurs. 123 */ 124 private void copyRemovedArtifactsToWorkingDirectory() throws MojoExecutionException { 125 final String wdAbsolutePath = workingDirectory.getAbsolutePath(); 126 getLog().info( 127 "Copying " + detachedArtifacts.size() + " detached artifacts to working directory " + wdAbsolutePath); 128 for (final Artifact artifact: detachedArtifacts) { 129 final File artifactFile = artifact.getFile(); 130 final StringBuilder copiedArtifactAbsolutePath = new StringBuilder(wdAbsolutePath); 131 copiedArtifactAbsolutePath.append("/"); 132 copiedArtifactAbsolutePath.append(artifactFile.getName()); 133 final File copiedArtifact = new File(copiedArtifactAbsolutePath.toString()); 134 getLog().info("Copying: " + artifactFile.getName()); 135 SharedFunctions.copyFile(getLog(), artifactFile, copiedArtifact); 136 } 137 } 138 139 @Override 140 public void execute() throws MojoExecutionException { 141 if (!isDistModule) { 142 getLog().info("This module is marked as a non distribution or assembly module, and the plugin will not run."); 143 return; 144 } 145 if (StringUtils.isEmpty(distSvnStagingUrl)) { 146 getLog().warn("commons.distSvnStagingUrl is not set, the commons-release-plugin will not run."); 147 return; 148 } 149 getLog().info("Detaching Assemblies"); 150 for (final Artifact attachedArtifact : project.getAttachedArtifacts()) { 151 putAttachedArtifactInSha512Map(attachedArtifact); 152 if (ARTIFACT_TYPES_TO_DETACH.contains(attachedArtifact.getType())) { 153 detachedArtifacts.add(attachedArtifact); 154 } 155 } 156 if (detachedArtifacts.isEmpty()) { 157 getLog().info("Current project contains no distributions. Not executing."); 158 return; 159 } 160 // 161 // PROBLEM CODE for Maven >= 3.8.3 162 // https://issues.apache.org/jira/browse/MNG-7316 163 try { 164 // (1) Try the normal way 165 // Maven 3.8.3 throws an exception here because MavenProject.getAttachedArtifacts() 166 // returns an IMMUTABLE collection. 167 project.getAttachedArtifacts().removeAll(detachedArtifacts); 168 } catch (final UnsupportedOperationException e) { 169 // (2) HACK workaround for https://issues.apache.org/jira/browse/MNG-7316 170 final ArrayList<Artifact> arrayList = new ArrayList<>(project.getAttachedArtifacts()); 171 arrayList.removeAll(detachedArtifacts); 172 try { 173 // MavenProject#setAttachedArtifacts(List) is protected 174 MethodUtils.invokeMethod(project, true, "setAttachedArtifacts", arrayList); 175 } catch (final ReflectiveOperationException roe) { 176 throw new MojoExecutionException(roe); 177 } 178 } 179 if (!workingDirectory.exists()) { 180 SharedFunctions.initDirectory(getLog(), workingDirectory); 181 } 182 final Artifact mainArtifact = project.getArtifact(); 183 if (mainArtifact != null && mainArtifact.getFile() != null && !"pom".equals(mainArtifact.getType())) { 184 putFileInSha512Map(mainArtifact.getFile().getName(), mainArtifact.getFile()); 185 } 186 putFileInSha512Map(project.getArtifactId() + "-" + project.getVersion() + ".pom", project.getFile()); 187 writeAllArtifactsInSha512PropertiesFile(); 188 copyRemovedArtifactsToWorkingDirectory(); 189 getLog().info(""); 190 hashArtifacts(); 191 } 192 193 /** 194 * Gets the unique artifact key for storage in our sha512 map. For example, 195 * commons-test-1.4-src.tar.gz should have its name as the key. 196 * 197 * @param artifact The {@link Artifact} that we wish to generate a key for. 198 * @return The generated key 199 */ 200 private String getArtifactKey(final Artifact artifact) { 201 return artifact.getFile().getName(); 202 } 203 204 /** 205 * Gets the file path for the <code>sha512</code> checksum file for a given file. 206 * 207 * @param directory is the {@link File} for the directory in which to make the <code>.sha512</code> file. 208 * @param file The {@link File} whose name we should use to create the <code>.sha512</code> file. 209 * @return A {@link String} that is the absolute path to the <code>.sha512</code> file. 210 */ 211 private String getSha512FilePath(final File directory, final File file) { 212 final StringBuilder buffer = new StringBuilder(directory.getAbsolutePath()); 213 buffer.append("/"); 214 buffer.append(file.getName()); 215 buffer.append(".sha512"); 216 return buffer.toString(); 217 } 218 219 /** 220 * Creates SHA-512 checksum files for detached artifacts in the <code>target/commons-release-plugin</code> directory 221 * for upload by {@link CommonsDistributionStagingMojo}. 222 * 223 * @throws MojoExecutionException Thrown if an {@link IOException} occurs. 224 */ 225 private void hashArtifacts() throws MojoExecutionException { 226 for (final Artifact artifact : detachedArtifacts) { 227 if (!StringUtils.toRootLowerCase(artifact.getFile().getName()).contains("asc")) { 228 final String artifactKey = getArtifactKey(artifact); 229 try { 230 final String digest; 231 // SHA-512 232 digest = artifactSha512s.getProperty(artifactKey.toString()); 233 getLog().info(artifact.getFile().getName() + " sha512: " + digest); 234 try (PrintWriter printWriter = new PrintWriter( 235 getSha512FilePath(workingDirectory, artifact.getFile()))) { 236 printWriter.println(digest); 237 } 238 } catch (final IOException e) { 239 throw new MojoExecutionException("Could not sign file: " + artifact.getFile().getName(), e); 240 } 241 } 242 } 243 } 244 245 /** 246 * Computes the SHA-512 hash of an attached artifact and stores it in the map. 247 * 248 * @param artifact is a Maven {@link Artifact} taken from the project when the mojo starts. 249 * @throws MojoExecutionException Thrown if an {@link IOException} occurs when computing the SHA-512 hash of the 250 * artifact. 251 */ 252 private void putAttachedArtifactInSha512Map(final Artifact artifact) throws MojoExecutionException { 253 final String artifactKey = getArtifactKey(artifact); 254 if (!artifactKey.endsWith(".asc")) { // .asc files don't need hashes 255 putFileInSha512Map(artifactKey, artifact.getFile()); 256 } 257 } 258 259 /** 260 * Computes a file's SHA-512 hash and stores it under its release artifact name. 261 * 262 * @param artifactKey the release artifact name. 263 * @param file the file to hash. 264 * @throws MojoExecutionException if the file cannot be read. 265 */ 266 private void putFileInSha512Map(final String artifactKey, final File file) throws MojoExecutionException { 267 try (InputStream input = Files.newInputStream(file.toPath())) { 268 artifactSha512s.put(artifactKey, DigestUtils.sha512Hex(input)); 269 } catch (final IOException e) { 270 throw new MojoExecutionException("Could not compute SHA-512 for: " + file, e); 271 } 272 } 273 274 /** 275 * Writes the artifacts' SHA-512 hashes to ./target/commons-release-plugin/sha512.properties. 276 * 277 * @throws MojoExecutionException Thrown if an {@link IOException} occurs while writing the file. 278 */ 279 private void writeAllArtifactsInSha512PropertiesFile() throws MojoExecutionException { 280 final File propertiesFile = new File(workingDirectory, "sha512.properties"); 281 getLog().info("Writing " + propertiesFile); 282 try (OutputStream fileWriter = Files.newOutputStream(propertiesFile.toPath())) { 283 artifactSha512s.store(fileWriter, "Release SHA-512s"); 284 } catch (final IOException e) { 285 throw new MojoExecutionException("Failure to write SHA-512's", e); 286 } 287 } 288}