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 * http://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 putting 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 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} of {@link Artifact} → {@link String} containing the sha512 signatures 083 * for the individual artifacts, where the {@link Artifact} is represented as: 084 * <code>groupId:artifactId:version:type=sha512</code>. 085 */ 086 private final SortedProperties artifactSha512s = new SortedProperties(); 087 088 /** 089 * The maven project context injection so that we can get a hold of the variables at hand. 090 */ 091 @Parameter(defaultValue = "${project}", required = true) 092 private MavenProject project; 093 094 /** 095 * The working directory in <code>target</code> that we use as a sandbox for the plugin. 096 */ 097 @Parameter(defaultValue = "${project.build.directory}/commons-release-plugin", 098 property = "commons.outputDirectory") 099 private File workingDirectory; 100 101 /** 102 * The subversion staging url to which we upload all of our staged artifacts. 103 */ 104 @Parameter(defaultValue = "", property = "commons.distSvnStagingUrl") 105 private String distSvnStagingUrl; 106 107 /** 108 * A parameter to generally avoid running unless it is specifically turned on by the consuming module. 109 */ 110 @Parameter(defaultValue = "false", property = "commons.release.isDistModule") 111 private Boolean isDistModule; 112 113 /** 114 * Constructs a new instance. 115 */ 116 public CommonsDistributionDetachmentMojo() { 117 // empty 118 } 119 120 /** 121 * A helper method to copy the newly detached artifacts to <code>target/commons-release-plugin</code> 122 * so that the {@link CommonsDistributionStagingMojo} can find the artifacts later. 123 * 124 * @throws MojoExecutionException if some form of an {@link IOException} occurs, we want it 125 * properly wrapped so that Maven can handle it. 126 */ 127 private void copyRemovedArtifactsToWorkingDirectory() throws MojoExecutionException { 128 final String wdAbsolutePath = workingDirectory.getAbsolutePath(); 129 getLog().info( 130 "Copying " + detachedArtifacts.size() + " detached artifacts to working directory " + wdAbsolutePath); 131 for (final Artifact artifact: detachedArtifacts) { 132 final File artifactFile = artifact.getFile(); 133 final StringBuilder copiedArtifactAbsolutePath = new StringBuilder(wdAbsolutePath); 134 copiedArtifactAbsolutePath.append("/"); 135 copiedArtifactAbsolutePath.append(artifactFile.getName()); 136 final File copiedArtifact = new File(copiedArtifactAbsolutePath.toString()); 137 getLog().info("Copying: " + artifactFile.getName()); 138 SharedFunctions.copyFile(getLog(), artifactFile, copiedArtifact); 139 } 140 } 141 142 @Override 143 public void execute() throws MojoExecutionException { 144 if (!isDistModule) { 145 getLog().info("This module is marked as a non distribution or assembly module, and the plugin will not run."); 146 return; 147 } 148 if (StringUtils.isEmpty(distSvnStagingUrl)) { 149 getLog().warn("commons.distSvnStagingUrl is not set, the commons-release-plugin will not run."); 150 return; 151 } 152 getLog().info("Detaching Assemblies"); 153 for (final Artifact attachedArtifact : project.getAttachedArtifacts()) { 154 putAttachedArtifactInSha512Map(attachedArtifact); 155 if (ARTIFACT_TYPES_TO_DETACH.contains(attachedArtifact.getType())) { 156 detachedArtifacts.add(attachedArtifact); 157 } 158 } 159 if (detachedArtifacts.isEmpty()) { 160 getLog().info("Current project contains no distributions. Not executing."); 161 return; 162 } 163 // 164 // PROBLEM CODE for Maven >= 3.8.3 165 // https://issues.apache.org/jira/browse/MNG-7316 166 try { 167 // (1) Try the normal way 168 // Maven 3.8.3 throws an exception here because MavenProject.getAttachedArtifacts() 169 // returns an IMMUTABLE collection. 170 project.getAttachedArtifacts().removeAll(detachedArtifacts); 171 } catch (final UnsupportedOperationException e) { 172 // (2) HACK workaround for https://issues.apache.org/jira/browse/MNG-7316 173 final ArrayList<Artifact> arrayList = new ArrayList<>(project.getAttachedArtifacts()); 174 arrayList.removeAll(detachedArtifacts); 175 try { 176 // MavenProject#setAttachedArtifacts(List) is protected 177 MethodUtils.invokeMethod(project, true, "setAttachedArtifacts", arrayList); 178 } catch (final ReflectiveOperationException roe) { 179 throw new MojoExecutionException(roe); 180 } 181 } 182 if (!workingDirectory.exists()) { 183 SharedFunctions.initDirectory(getLog(), workingDirectory); 184 } 185 writeAllArtifactsInSha512PropertiesFile(); 186 copyRemovedArtifactsToWorkingDirectory(); 187 getLog().info(""); 188 hashArtifacts(); 189 } 190 191 /** 192 * Generates the unique artifact key for storage in our sha512 map. For example, 193 * commons-test-1.4-src.tar.gz should have its name as the key. 194 * 195 * @param artifact the {@link Artifact} that we wish to generate a key for. 196 * @return the generated key 197 */ 198 private String getArtifactKey(final Artifact artifact) { 199 return artifact.getFile().getName(); 200 } 201 202 /** 203 * A helper method to create a file path for the <code>sha512</code> signature file from a given file. 204 * 205 * @param directory is the {@link File} for the directory in which to make the <code>.sha512</code> file. 206 * @param file the {@link File} whose name we should use to create the <code>.sha512</code> file. 207 * @return a {@link String} that is the absolute path to the <code>.sha512</code> file. 208 */ 209 private String getSha512FilePath(final File directory, final File file) { 210 final StringBuilder buffer = new StringBuilder(directory.getAbsolutePath()); 211 buffer.append("/"); 212 buffer.append(file.getName()); 213 buffer.append(".sha512"); 214 return buffer.toString(); 215 } 216 217 /** 218 * A helper method that creates sha512 signature files for our detached artifacts in the 219 * <code>target/commons-release-plugin</code> directory for the purpose of being uploaded by 220 * the {@link CommonsDistributionStagingMojo}. 221 * 222 * @throws MojoExecutionException if some form of an {@link IOException} occurs, we want it 223 * properly wrapped so that Maven can handle it. 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 * Takes an attached artifact and puts the signature in the map. 247 * @param artifact is a Maven {@link Artifact} taken from the project at start time of mojo. 248 * @throws MojoExecutionException if an {@link IOException} occurs when getting the sha512 of the 249 * artifact. 250 */ 251 private void putAttachedArtifactInSha512Map(final Artifact artifact) throws MojoExecutionException { 252 try { 253 final String artifactKey = getArtifactKey(artifact); 254 if (!artifactKey.endsWith(".asc")) { // .asc files don't need hashes 255 try (InputStream fis = Files.newInputStream(artifact.getFile().toPath())) { 256 artifactSha512s.put(artifactKey, DigestUtils.sha512Hex(fis)); 257 } 258 } 259 } catch (final IOException e) { 260 throw new MojoExecutionException( 261 "Could not find artifact signature for: " 262 + artifact.getArtifactId() 263 + "-" 264 + artifact.getClassifier() 265 + "-" 266 + artifact.getVersion() 267 + " type: " 268 + artifact.getType(), 269 e); 270 } 271 } 272 273 /** 274 * Writes to ./target/commons-release-plugin/sha512.properties the artifact sha512's. 275 * 276 * @throws MojoExecutionException if we can't write the file due to an {@link IOException}. 277 */ 278 private void writeAllArtifactsInSha512PropertiesFile() throws MojoExecutionException { 279 final File propertiesFile = new File(workingDirectory, "sha512.properties"); 280 getLog().info("Writing " + propertiesFile); 281 try (OutputStream fileWriter = Files.newOutputStream(propertiesFile.toPath())) { 282 artifactSha512s.store(fileWriter, "Release SHA-512s"); 283 } catch (final IOException e) { 284 throw new MojoExecutionException("Failure to write SHA-512's", e); 285 } 286 } 287}