/* * SonarQube * Copyright (C) 2009-2017 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.scanner.report; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Throwables; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Writer; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.LinkedHashMap; import java.util.Map; import javax.annotation.Nullable; import okhttp3.HttpUrl; import org.apache.commons.io.FileUtils; import org.picocontainer.Startable; import org.sonar.api.CoreProperties; import org.sonar.api.batch.ScannerSide; import org.sonar.api.batch.bootstrap.ProjectDefinition; import org.sonar.api.config.Settings; import org.sonar.api.platform.Server; import org.sonar.api.utils.MessageException; import org.sonar.api.utils.TempFolder; import org.sonar.api.utils.ZipUtils; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.scanner.analysis.DefaultAnalysisMode; import org.sonar.scanner.bootstrap.ScannerWsClient; import org.sonar.scanner.protocol.output.ScannerReportWriter; import org.sonar.scanner.scan.ImmutableProjectReactor; import org.sonarqube.ws.MediaTypes; import org.sonarqube.ws.WsCe; import org.sonarqube.ws.client.HttpException; import org.sonarqube.ws.client.PostRequest; import org.sonarqube.ws.client.WsResponse; import static org.sonar.core.util.FileUtils.deleteQuietly; @ScannerSide public class ReportPublisher implements Startable { private static final Logger LOG = Loggers.get(ReportPublisher.class); public static final String KEEP_REPORT_PROP_KEY = "sonar.batch.keepReport"; public static final String VERBOSE_KEY = "sonar.verbose"; public static final String METADATA_DUMP_FILENAME = "report-task.txt"; private final Settings settings; private final ScannerWsClient wsClient; private final AnalysisContextReportPublisher contextPublisher; private final ImmutableProjectReactor projectReactor; private final DefaultAnalysisMode analysisMode; private final TempFolder temp; private final ReportPublisherStep[] publishers; private final Server server; private File reportDir; private ScannerReportWriter writer; public ReportPublisher(Settings settings, ScannerWsClient wsClient, Server server, AnalysisContextReportPublisher contextPublisher, ImmutableProjectReactor projectReactor, DefaultAnalysisMode analysisMode, TempFolder temp, ReportPublisherStep[] publishers) { this.settings = settings; this.wsClient = wsClient; this.server = server; this.contextPublisher = contextPublisher; this.projectReactor = projectReactor; this.analysisMode = analysisMode; this.temp = temp; this.publishers = publishers; } @Override public void start() { reportDir = new File(projectReactor.getRoot().getWorkDir(), "batch-report"); writer = new ScannerReportWriter(reportDir); contextPublisher.init(writer); if (!analysisMode.isIssues() && !analysisMode.isMediumTest()) { String publicUrl = server.getPublicRootUrl(); if (HttpUrl.parse(publicUrl) == null) { throw MessageException.of("Failed to parse public URL set in SonarQube server: " + publicUrl); } } } @Override public void stop() { if (!shouldKeepReport()) { deleteQuietly(reportDir); } } public File getReportDir() { return reportDir; } public ScannerReportWriter getWriter() { return writer; } public void execute() { // If this is a issues mode analysis then we should not upload reports String taskId = null; if (!analysisMode.isIssues()) { File report = generateReportFile(); if (shouldKeepReport()) { LOG.info("Analysis report generated in " + reportDir); } if (!analysisMode.isMediumTest()) { taskId = upload(report); } } logSuccess(taskId); } private boolean shouldKeepReport() { return settings.getBoolean(KEEP_REPORT_PROP_KEY) || settings.getBoolean(VERBOSE_KEY); } private File generateReportFile() { try { long startTime = System.currentTimeMillis(); for (ReportPublisherStep publisher : publishers) { publisher.publish(writer); } long stopTime = System.currentTimeMillis(); LOG.info("Analysis report generated in {}ms, dir size={}", stopTime - startTime, FileUtils.byteCountToDisplaySize(FileUtils.sizeOfDirectory(reportDir))); startTime = System.currentTimeMillis(); File reportZip = temp.newFile("batch-report", ".zip"); ZipUtils.zipDir(reportDir, reportZip); stopTime = System.currentTimeMillis(); LOG.info("Analysis reports compressed in {}ms, zip size={}", stopTime - startTime, FileUtils.byteCountToDisplaySize(FileUtils.sizeOf(reportZip))); return reportZip; } catch (IOException e) { throw new IllegalStateException("Unable to prepare analysis report", e); } } /** * Uploads the report file to server and returns the generated task id */ @VisibleForTesting String upload(File report) { LOG.debug("Upload report"); long startTime = System.currentTimeMillis(); ProjectDefinition projectDefinition = projectReactor.getRoot(); PostRequest.Part filePart = new PostRequest.Part(MediaTypes.ZIP, report); PostRequest post = new PostRequest("api/ce/submit") .setMediaType(MediaTypes.PROTOBUF) .setParam("organization", settings.getString(CoreProperties.PROJECT_ORGANIZATION_PROPERTY)) .setParam("projectKey", projectDefinition.getKey()) .setParam("projectName", projectDefinition.getOriginalName()) .setParam("projectBranch", projectDefinition.getBranch()) .setPart("report", filePart); WsResponse response; try { response = wsClient.call(post).failIfNotSuccessful(); } catch (HttpException e) { throw MessageException.of(String.format("Failed to upload report - %d: %s", e.code(), ScannerWsClient.tryParseAsJsonError(e.content()))); } try (InputStream protobuf = response.contentStream()) { return WsCe.SubmitResponse.parser().parseFrom(protobuf).getTaskId(); } catch (Exception e) { throw Throwables.propagate(e); } finally { long stopTime = System.currentTimeMillis(); LOG.info("Analysis report uploaded in " + (stopTime - startTime) + "ms"); } } @VisibleForTesting void logSuccess(@Nullable String taskId) { if (taskId == null) { LOG.info("ANALYSIS SUCCESSFUL"); } else { String publicUrl = server.getPublicRootUrl(); HttpUrl httpUrl = HttpUrl.parse(publicUrl); Map<String, String> metadata = new LinkedHashMap<>(); String effectiveKey = projectReactor.getRoot().getKeyWithBranch(); if (settings.hasKey(CoreProperties.PROJECT_ORGANIZATION_PROPERTY)) { metadata.put("organization", settings.getString(CoreProperties.PROJECT_ORGANIZATION_PROPERTY)); } metadata.put("projectKey", effectiveKey); metadata.put("serverUrl", publicUrl); metadata.put("serverVersion", server.getVersion()); URL dashboardUrl = httpUrl.newBuilder() .addPathSegment("dashboard").addPathSegment("index").addPathSegment(effectiveKey) .build() .url(); metadata.put("dashboardUrl", dashboardUrl.toExternalForm()); URL taskUrl = HttpUrl.parse(publicUrl).newBuilder() .addPathSegment("api").addPathSegment("ce").addPathSegment("task") .addQueryParameter("id", taskId) .build() .url(); metadata.put("ceTaskId", taskId); metadata.put("ceTaskUrl", taskUrl.toExternalForm()); LOG.info("ANALYSIS SUCCESSFUL, you can browse {}", dashboardUrl); LOG.info("Note that you will be able to access the updated dashboard once the server has processed the submitted analysis report"); LOG.info("More about the report processing at {}", taskUrl); dumpMetadata(metadata); } } private void dumpMetadata(Map<String, String> metadata) { Path file = projectReactor.getRoot().getWorkDir().toPath().resolve(METADATA_DUMP_FILENAME); try (Writer output = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { for (Map.Entry<String, String> entry : metadata.entrySet()) { output.write(entry.getKey()); output.write("="); output.write(entry.getValue()); output.write("\n"); } LOG.debug("Report metadata written to {}", file); } catch (IOException e) { throw new IllegalStateException("Unable to dump " + file, e); } } }