/*
* Copyright (C) 2011 JFrog Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jfrog.build.extractor.maven;
import com.google.common.collect.Sets;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.logging.Logger;
import org.jfrog.build.api.Artifact;
import org.jfrog.build.api.Build;
import org.jfrog.build.api.BuildInfoConfigProperties;
import org.jfrog.build.api.Module;
import org.jfrog.build.api.util.FileChecksumCalculator;
import org.jfrog.build.client.DeployDetails;
import org.jfrog.build.extractor.BuildInfoExtractorUtils;
import org.jfrog.build.extractor.clientConfiguration.ArtifactoryClientConfiguration;
import org.jfrog.build.extractor.clientConfiguration.IncludeExcludePatterns;
import org.jfrog.build.extractor.clientConfiguration.PatternMatcher;
import org.jfrog.build.extractor.clientConfiguration.client.ArtifactoryBuildInfoClient;
import org.jfrog.build.extractor.retention.Utils;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author Noam Y. Tenne
*/
@Component(role = BuildDeploymentHelper.class)
public class BuildDeploymentHelper {
private final JsonMergeHelper buildInfoMergeHelper = new JsonMergeHelper( "id", "name" );
private final JsonMergeHelper deployablesMergeHelper = new JsonMergeHelper( "artifactPath" );
@Requirement
private Logger logger;
@Requirement
private BuildInfoClientBuilder buildInfoClientBuilder;
public void deploy( Build build,
ArtifactoryClientConfiguration clientConf,
Map<String, DeployDetails> deployableArtifactBuilders,
boolean wereThereTestFailures,
File basedir ) {
Set<DeployDetails> deployableArtifacts = prepareDeployableArtifacts(build, deployableArtifactBuilders);
logger.debug("Build Info Recorder: " + clientConf.publisher.isPublishBuildInfo());
File aggregateDirectory;
File buildInfoAggregated = null;
File buildInfoFile = null;
if (clientConf.publisher.isPublishBuildInfo() || clientConf.publisher.getAggregateArtifacts() != null) {
buildInfoFile = saveBuildInfoToFile(build, clientConf, basedir);
}
if (clientConf.publisher.getAggregateArtifacts() != null) {
aggregateDirectory = new File( clientConf.publisher.getAggregateArtifacts());
buildInfoAggregated = new File( aggregateDirectory, "build-info.json" );
boolean isCopyAggregatedArtifacts = clientConf.publisher.isCopyAggregatedArtifacts();
boolean isPublishAggregatedArtifacts = clientConf.publisher.isPublishAggregatedArtifacts();
deployableArtifacts = aggregateArtifacts( aggregateDirectory, buildInfoFile, buildInfoAggregated, deployableArtifacts,
isCopyAggregatedArtifacts, isPublishAggregatedArtifacts );
if (!isPublishAggregatedArtifacts) {
return;
}
}
if (!StringUtils.isEmpty(clientConf.info.getGeneratedBuildInfoFilePath())) {
try {
BuildInfoExtractorUtils.saveBuildInfoToFile(build, new File(clientConf.info.getGeneratedBuildInfoFilePath()));
} catch (Exception e) {
logger.error("Failed writing build info to file: " , e);
throw new RuntimeException("Failed writing build info to file", e);
}
}
if (clientConf.publisher.isPublishBuildInfo() || clientConf.publisher.isPublishArtifacts()) {
ArtifactoryBuildInfoClient client = buildInfoClientBuilder.resolveProperties(clientConf);
boolean isDeployArtifacts = clientConf.publisher.isPublishArtifacts() &&
( deployableArtifacts != null ) &&
( ! deployableArtifacts.isEmpty()) &&
( clientConf.publisher.isEvenUnstable() || ( ! wereThereTestFailures ));
boolean isSendBuildInfo = clientConf.publisher.isPublishBuildInfo() &&
( clientConf.publisher.isEvenUnstable() || ( ! wereThereTestFailures ));
try {
if ( isDeployArtifacts ) {
deployArtifacts(clientConf.publisher, deployableArtifacts, client);
}
if ( isSendBuildInfo ) {
logger.info("Artifactory Build Info Recorder: Deploying build info ...");
try {
if ( buildInfoAggregated != null ) {
String buildInfoJson = client.buildInfoToJsonString( build );
String buildInfoAggregatedJson = FileUtils.readFileToString( buildInfoAggregated, "UTF-8" );
String buildInfoMerged = buildInfoMergeHelper.mergeJsons( buildInfoAggregatedJson, buildInfoJson );
client.sendBuildInfo( buildInfoMerged );
}
else {
Utils.sendBuildAndBuildRetention(client, build, clientConf);
}
} catch ( Exception e ) {
throw new RuntimeException(e);
}
}
} finally {
client.close();
}
}
}
private File saveBuildInfoToFile(Build build, ArtifactoryClientConfiguration clientConf, File basedir) {
String outputFile = clientConf.getExportFile();
File buildInfoFile = StringUtils.isBlank(outputFile) ? new File(basedir, "target/build-info.json" ) :
new File(outputFile);
logger.debug("Build Info Recorder: " + BuildInfoConfigProperties.EXPORT_FILE + " = " + outputFile);
logger.info("Artifactory Build Info Recorder: Saving Build Info to '" + buildInfoFile + "'" );
try {
BuildInfoExtractorUtils.saveBuildInfoToFile(build, buildInfoFile.getCanonicalFile());
} catch (IOException e) {
throw new RuntimeException("Error occurred while persisting Build Info to '" + buildInfoFile + "'", e);
}
return buildInfoFile;
}
@SuppressWarnings({ "TypeMayBeWeakened" , "SuppressionAnnotation" })
private Set<DeployDetails> aggregateArtifacts ( File aggregateDirectory,
File buildInfoSource,
File buildInfoDestination,
Set<DeployDetails> deployables,
boolean isCopyAggregatedArtifacts,
boolean isPublishAggregatedArtifacts ){
try {
File deployablesDestination = new File( aggregateDirectory, "deployables.json" );
List<Map<String,?>> mergedDeployables = null;
if ( buildInfoDestination.isFile()) {
Map<String,Object> buildInfoSourceMap = buildInfoMergeHelper.jsonToObject( buildInfoSource, Map.class );
Map<String,Object> buildInfoDestinationMap = buildInfoMergeHelper.jsonToObject( buildInfoDestination, Map.class );
int durationMillis = ( Integer ) buildInfoSourceMap.get( "durationMillis" ) +
( Integer ) buildInfoDestinationMap.get( "durationMillis" );
buildInfoSourceMap.put( "started", buildInfoDestinationMap.get( "started" ));
buildInfoSourceMap.put( "durationMillis", durationMillis );
buildInfoMergeHelper.mergeAndWrite( buildInfoSourceMap, buildInfoDestinationMap, buildInfoDestination );
}
else {
FileUtils.copyFile( buildInfoSource, buildInfoDestination );
}
if ( deployablesDestination.isFile()) {
List<Map<String,?>> currentDeployables = deployablesMergeHelper.jsonToObject ( deployablesMergeHelper.objectToJson( deployables ), List.class );
List<Map<String,?>> previousDeployables = deployablesMergeHelper.jsonToObject ( deployablesDestination, List.class );
mergedDeployables = deployablesMergeHelper.mergeAndWrite( currentDeployables, previousDeployables, deployablesDestination );
}
else {
FileUtils.write( deployablesDestination, deployablesMergeHelper.objectToJson( deployables ), "UTF-8" );
}
if ( isCopyAggregatedArtifacts ) {
for ( DeployDetails details : deployables ) {
/**
* We could check MD5 checksum of destination file (if it exists) and save on copy operation but since most *.jar
* files contain a timestamp in pom.properties (thanks, Maven) - checksum would only match for POM files.
*/
File aggregatedFile = aggregatedFile( aggregateDirectory, details.getFile());
FileUtils.copyFile( details.getFile(), aggregatedFile );
}
}
return ( isPublishAggregatedArtifacts && ( mergedDeployables != null )) ?
convertDeployables( aggregateDirectory, mergedDeployables, isCopyAggregatedArtifacts ) :
deployables;
}
catch ( IOException e ){
throw new RuntimeException( "Failed to aggregate artifacts and Build Info in [" + aggregateDirectory + "]",
e );
}
}
@SuppressWarnings({ "FeatureEnvy" , "SuppressionAnnotation" })
private Set<DeployDetails> convertDeployables ( File aggregateDirectory, Iterable<Map<String, ?>> deployables, boolean isCopyAggregatedArtifacts )
throws IOException
{
Set<DeployDetails> result = new HashSet<DeployDetails>();
for ( Map<String,?> map : deployables ) {
File file = new File(( String ) map.get( "file" ));
if ( isCopyAggregatedArtifacts ){ file = aggregatedFile( aggregateDirectory, file ); }
DeployDetails.Builder builder = new DeployDetails.Builder().
targetRepository(( String ) map.get( "targetRepository" )).
artifactPath(( String ) map.get( "artifactPath" )).
file( file ).
sha1(( String ) map.get( "sha1" )).
md5(( String ) map.get( "md5" )).
addProperties(( Map<String, String> ) map.get( "properties" ));
result.add( builder.build());
}
return result;
}
private File aggregatedFile( File aggregateDirectory, File file ) throws IOException
{
String workspacePath = aggregateDirectory.getParentFile().getCanonicalPath().replace( '\\', '/' );
String artifactPath = file.getCanonicalPath().replace( '\\', '/' );
String artifactRelativePath = artifactPath.startsWith( workspacePath ) ?
/**
* "/Users/evgenyg/.hudson/jobs/teamcity-artifactory-plugin/workspace/agent/target/teamcity-artifactory-plugin-agent-2.1.x-SNAPSHOT.jar" =>
* "agent/target/teamcity-artifactory-plugin-agent-2.1.x-SNAPSHOT.jar"
*/
artifactPath.substring( workspacePath.length() + 1 ) :
/**
* Artifact is outside workspace, wonder if it works on Windows
*/
artifactPath;
return new File( aggregateDirectory, artifactRelativePath );
}
private Set<DeployDetails> prepareDeployableArtifacts(Build build,
Map<String, DeployDetails> deployableArtifactBuilders) {
Set<DeployDetails> deployableArtifacts = Sets.newLinkedHashSet();
List<Module> modules = build.getModules();
for (Module module : modules) {
List<Artifact> artifacts = module.getArtifacts();
if(artifacts!=null){
for (Artifact artifact : artifacts) {
String artifactId = BuildInfoExtractorUtils.getArtifactId(module.getId(), artifact.getName());
DeployDetails deployable = deployableArtifactBuilders.get(artifactId);
if (deployable != null) {
File file = deployable.getFile();
setArtifactChecksums(file, artifact);
deployableArtifacts.add(new DeployDetails.Builder().artifactPath(deployable.getArtifactPath()).
file(file).md5(artifact.getMd5()).sha1(artifact.getSha1()).
addProperties(deployable.getProperties()).
targetRepository(deployable.getTargetRepository()).build());
}
}
}
}
return deployableArtifacts;
}
private void deployArtifacts(ArtifactoryClientConfiguration.PublisherHandler publishConf,
Set<DeployDetails> deployableArtifacts,
ArtifactoryBuildInfoClient client) {
IncludeExcludePatterns includeExcludePatterns = getArtifactDeploymentPatterns(publishConf);
for (DeployDetails artifact : deployableArtifacts) {
String artifactPath = artifact.getArtifactPath();
if (PatternMatcher.pathConflicts(artifactPath, includeExcludePatterns)) {
logger.info("Artifactory Build Info Recorder: Skipping the deployment of '" +
artifactPath + "' due to the defined include-exclude patterns.");
continue;
}
try {
client.deployArtifact(artifact);
} catch (IOException e) {
throw new RuntimeException("Error occurred while publishing artifact to Artifactory: " +
artifact.getFile() +
".\n Skipping deployment of remaining artifacts (if any) and build info.", e);
}
}
}
private void setArtifactChecksums(File artifactFile, org.jfrog.build.api.Artifact artifact) {
if ((artifactFile != null) && (artifactFile.isFile())) {
try {
Map<String, String> checksums = FileChecksumCalculator.calculateChecksums(artifactFile, "md5", "sha1");
artifact.setMd5(checksums.get("md5"));
artifact.setSha1(checksums.get("sha1"));
} catch (Exception e) {
logger.error("Could not set checksum values on '" + artifact.getName() + "': " + e.getMessage(), e);
}
}
}
private IncludeExcludePatterns getArtifactDeploymentPatterns(
ArtifactoryClientConfiguration.PublisherHandler publishConf) {
return new IncludeExcludePatterns(publishConf.getIncludePatterns(), publishConf.getExcludePatterns());
}
}