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.Locale; 030import java.util.Set; 031 032import org.apache.commons.codec.digest.DigestUtils; 033import org.apache.commons.collections4.properties.SortedProperties; 034import org.apache.commons.lang3.StringUtils; 035import org.apache.commons.lang3.reflect.MethodUtils; 036import org.apache.commons.release.plugin.SharedFunctions; 037import org.apache.maven.artifact.Artifact; 038import org.apache.maven.plugin.AbstractMojo; 039import org.apache.maven.plugin.MojoExecutionException; 040import org.apache.maven.plugins.annotations.LifecyclePhase; 041import org.apache.maven.plugins.annotations.Mojo; 042import org.apache.maven.plugins.annotations.Parameter; 043import org.apache.maven.project.MavenProject; 044 045/** 046 * The purpose of this Maven mojo is to detach the artifacts generated by the maven-assembly-plugin, 047 * which for the Apache Commons Project do not get uploaded to Nexus, and putting those artifacts 048 * in the dev distribution location for Apache projects. 049 * 050 * @since 1.0 051 */ 052@Mojo(name = "detach-distributions", 053 defaultPhase = LifecyclePhase.VERIFY, 054 threadSafe = true, 055 aggregator = true) 056public class CommonsDistributionDetachmentMojo extends AbstractMojo { 057 058 /** 059 * A list of "artifact types" in the Maven vernacular, to 060 * be detached from the deployment. For the time being we want 061 * all artifacts generated by the maven-assembly-plugin to be detached 062 * from the deployment, namely *-src.zip, *-src.tar.gz, *-bin.zip, 063 * *-bin.tar.gz, and the corresponding .asc pgp signatures. 064 */ 065 private static final Set<String> ARTIFACT_TYPES_TO_DETACH; 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 @Override 114 public void execute() throws MojoExecutionException { 115 if (!isDistModule) { 116 getLog().info("This module is marked as a non distribution or assembly module, and the plugin will not run."); 117 return; 118 } 119 if (StringUtils.isEmpty(distSvnStagingUrl)) { 120 getLog().warn("commons.distSvnStagingUrl is not set, the commons-release-plugin will not run."); 121 return; 122 } 123 getLog().info("Detaching Assemblies"); 124 for (final Artifact attachedArtifact : project.getAttachedArtifacts()) { 125 putAttachedArtifactInSha512Map(attachedArtifact); 126 if (ARTIFACT_TYPES_TO_DETACH.contains(attachedArtifact.getType())) { 127 detachedArtifacts.add(attachedArtifact); 128 } 129 } 130 if (detachedArtifacts.isEmpty()) { 131 getLog().info("Current project contains no distributions. Not executing."); 132 return; 133 } 134 // 135 // PROBLEM CODE for Maven >= 3.8.3 136 // https://issues.apache.org/jira/browse/MNG-7316 137 try { 138 // (1) Try the normal way 139 // Maven 3.8.3 throws an exception here because MavenProject.getAttachedArtifacts() 140 // returns an IMMUTABLE collection. 141 project.getAttachedArtifacts().removeAll(detachedArtifacts); 142 } catch (final UnsupportedOperationException e) { 143 // (2) HACK workaround for https://issues.apache.org/jira/browse/MNG-7316 144 final ArrayList<Artifact> arrayList = new ArrayList<>(project.getAttachedArtifacts()); 145 arrayList.removeAll(detachedArtifacts); 146 try { 147 // MavenProject#setAttachedArtifacts(List) is protected 148 MethodUtils.invokeMethod(project, true, "setAttachedArtifacts", arrayList); 149 } catch (final ReflectiveOperationException roe) { 150 throw new MojoExecutionException(roe); 151 } 152 } 153 if (!workingDirectory.exists()) { 154 SharedFunctions.initDirectory(getLog(), workingDirectory); 155 } 156 writeAllArtifactsInSha512PropertiesFile(); 157 copyRemovedArtifactsToWorkingDirectory(); 158 getLog().info(""); 159 hashArtifacts(); 160 } 161 162 /** 163 * Takes an attached artifact and puts the signature in the map. 164 * @param artifact is a Maven {@link Artifact} taken from the project at start time of mojo. 165 * @throws MojoExecutionException if an {@link IOException} occurs when getting the sha512 of the 166 * artifact. 167 */ 168 private void putAttachedArtifactInSha512Map(final Artifact artifact) throws MojoExecutionException { 169 try { 170 final String artifactKey = getArtifactKey(artifact); 171 if (!artifactKey.endsWith(".asc")) { // .asc files don't need hashes 172 try (InputStream fis = Files.newInputStream(artifact.getFile().toPath())) { 173 artifactSha512s.put(artifactKey, DigestUtils.sha512Hex(fis)); 174 } 175 } 176 } catch (final IOException e) { 177 throw new MojoExecutionException( 178 "Could not find artifact signature for: " 179 + artifact.getArtifactId() 180 + "-" 181 + artifact.getClassifier() 182 + "-" 183 + artifact.getVersion() 184 + " type: " 185 + artifact.getType(), 186 e); 187 } 188 } 189 190 /** 191 * Writes to ./target/commons-release-plugin/sha512.properties the artifact sha512's. 192 * 193 * @throws MojoExecutionException if we can't write the file due to an {@link IOException}. 194 */ 195 private void writeAllArtifactsInSha512PropertiesFile() throws MojoExecutionException { 196 final File propertiesFile = new File(workingDirectory, "sha512.properties"); 197 getLog().info("Writing " + propertiesFile); 198 try (OutputStream fileWriter = Files.newOutputStream(propertiesFile.toPath())) { 199 artifactSha512s.store(fileWriter, "Release SHA-512s"); 200 } catch (final IOException e) { 201 throw new MojoExecutionException("Failure to write SHA-512's", e); 202 } 203 } 204 205 /** 206 * A helper method to copy the newly detached artifacts to <code>target/commons-release-plugin</code> 207 * so that the {@link CommonsDistributionStagingMojo} can find the artifacts later. 208 * 209 * @throws MojoExecutionException if some form of an {@link IOException} occurs, we want it 210 * properly wrapped so that Maven can handle it. 211 */ 212 private void copyRemovedArtifactsToWorkingDirectory() throws MojoExecutionException { 213 final String wdAbsolutePath = workingDirectory.getAbsolutePath(); 214 getLog().info( 215 "Copying " + detachedArtifacts.size() + " detached artifacts to working directory " + wdAbsolutePath); 216 for (final Artifact artifact: detachedArtifacts) { 217 final File artifactFile = artifact.getFile(); 218 final StringBuilder copiedArtifactAbsolutePath = new StringBuilder(wdAbsolutePath); 219 copiedArtifactAbsolutePath.append("/"); 220 copiedArtifactAbsolutePath.append(artifactFile.getName()); 221 final File copiedArtifact = new File(copiedArtifactAbsolutePath.toString()); 222 getLog().info("Copying: " + artifactFile.getName()); 223 SharedFunctions.copyFile(getLog(), artifactFile, copiedArtifact); 224 } 225 } 226 227 /** 228 * A helper method that creates sha512 signature files for our detached artifacts in the 229 * <code>target/commons-release-plugin</code> directory for the purpose of being uploaded by 230 * the {@link CommonsDistributionStagingMojo}. 231 * 232 * @throws MojoExecutionException if some form of an {@link IOException} occurs, we want it 233 * properly wrapped so that Maven can handle it. 234 */ 235 private void hashArtifacts() throws MojoExecutionException { 236 for (final Artifact artifact : detachedArtifacts) { 237 if (!artifact.getFile().getName().toLowerCase(Locale.ROOT).contains("asc")) { 238 final String artifactKey = getArtifactKey(artifact); 239 try { 240 final String digest; 241 // SHA-512 242 digest = artifactSha512s.getProperty(artifactKey.toString()); 243 getLog().info(artifact.getFile().getName() + " sha512: " + digest); 244 try (PrintWriter printWriter = new PrintWriter( 245 getSha512FilePath(workingDirectory, artifact.getFile()))) { 246 printWriter.println(digest); 247 } 248 } catch (final IOException e) { 249 throw new MojoExecutionException("Could not sign file: " + artifact.getFile().getName(), e); 250 } 251 } 252 } 253 } 254 255 /** 256 * A helper method to create a file path for the <code>sha512</code> signature file from a given file. 257 * 258 * @param directory is the {@link File} for the directory in which to make the <code>.sha512</code> file. 259 * @param file the {@link File} whose name we should use to create the <code>.sha512</code> file. 260 * @return a {@link String} that is the absolute path to the <code>.sha512</code> file. 261 */ 262 private String getSha512FilePath(final File directory, final File file) { 263 final StringBuilder buffer = new StringBuilder(directory.getAbsolutePath()); 264 buffer.append("/"); 265 buffer.append(file.getName()); 266 buffer.append(".sha512"); 267 return buffer.toString(); 268 } 269 270 /** 271 * Generates the unique artifact key for storage in our sha512 map. For example, 272 * commons-test-1.4-src.tar.gz should have its name as the key. 273 * 274 * @param artifact the {@link Artifact} that we wish to generate a key for. 275 * @return the generated key 276 */ 277 private String getArtifactKey(final Artifact artifact) { 278 return artifact.getFile().getName(); 279 } 280}