From 51e596c8fff5742e3292d002c3fbc02e9575cc65 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lievremont Date: Tue, 14 Apr 2015 16:53:27 +0200 Subject: [PATCH] SONAR-6399 WS to export a quality profile for external tools --- .../server/platform/ServerComponents.java | 2 + .../qualityprofile/QProfileBackuper.java | 2 + .../qualityprofile/ws/LanguageParamUtils.java | 9 + .../ws/QProfileBackupAction.java | 2 +- .../ws/QProfileExportAction.java | 136 ++++++++++++ .../qualityprofile/ws/example-exporters.json | 8 + .../ws/QProfileExportActionTest.java | 198 ++++++++++++++++++ 7 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/QProfileExportAction.java create mode 100644 server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/example-exporters.json create mode 100644 server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/QProfileExportActionTest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java index adea270c55a..d3fdaff68bb 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java @@ -595,6 +595,8 @@ class ServerComponents { pico.addSingleton(QProfileChangeParentAction.class); pico.addSingleton(QProfileChangelogAction.class); pico.addSingleton(QProfileCompareAction.class); + pico.addSingleton(QProfileExportAction.class); + pico.addSingleton(QProfileExportersAction.class); pico.addSingleton(QProfilesWs.class); pico.addSingleton(ProfilesWs.class); pico.addSingleton(RuleActivationActions.class); diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileBackuper.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileBackuper.java index 87e0ab6929a..a5a2146e9e3 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileBackuper.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileBackuper.java @@ -50,6 +50,8 @@ import java.util.Set; public class QProfileBackuper implements ServerComponent { + public static final String MEDIA_TYPE_XML = "text/xml"; + private final QProfileReset reset; private final DbClient db; private final IndexClient index; diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/LanguageParamUtils.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/LanguageParamUtils.java index e4f04ce4a99..fe7d71ff613 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/LanguageParamUtils.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/LanguageParamUtils.java @@ -33,6 +33,15 @@ class LanguageParamUtils { // Utility class } + static String getExampleValue(Languages languages) { + Language[] languageArray = languages.all(); + if (languageArray.length > 0) { + return languageArray[0].getKey(); + } else { + return ""; + } + } + static Collection getLanguageKeys(Languages languages) { return Collections2.transform(Arrays.asList(languages.all()), new NonNullInputFunction() { @Override diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/QProfileBackupAction.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/QProfileBackupAction.java index f7d6557a1c5..0c0aadf68cc 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/QProfileBackupAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/QProfileBackupAction.java @@ -65,7 +65,7 @@ public class QProfileBackupAction implements BaseQProfileWsAction { @Override public void handle(Request request, Response response) throws Exception { Stream stream = response.stream(); - stream.setMediaType("text/xml"); + stream.setMediaType(QProfileBackuper.MEDIA_TYPE_XML); OutputStreamWriter writer = new OutputStreamWriter(stream.output(), Charsets.UTF_8); DbSession session = dbClient.openSession(false); try { diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/QProfileExportAction.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/QProfileExportAction.java new file mode 100644 index 00000000000..7174e369b38 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/QProfileExportAction.java @@ -0,0 +1,136 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.qualityprofile.ws; + +import com.google.common.collect.Lists; +import org.apache.commons.io.IOUtils; +import org.sonar.api.profiles.ProfileExporter; +import org.sonar.api.resources.Language; +import org.sonar.api.resources.Languages; +import org.sonar.api.server.ws.*; +import org.sonar.api.server.ws.Response.Stream; +import org.sonar.api.server.ws.WebService.NewAction; +import org.sonar.core.persistence.DbSession; +import org.sonar.core.qualityprofile.db.QualityProfileDto; +import org.sonar.server.db.DbClient; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.qualityprofile.QProfileBackuper; +import org.sonar.server.qualityprofile.QProfileExporters; +import org.sonar.server.qualityprofile.QProfileFactory; + +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.List; + +public class QProfileExportAction implements BaseQProfileWsAction { + + private static final String PARAM_PROFILE_NAME = "name"; + private static final String PARAM_LANGUAGE = "language"; + private static final String PARAM_FORMAT = "format"; + + private final DbClient dbClient; + + private final QProfileFactory profileFactory; + + private final QProfileBackuper backuper; + + private final QProfileExporters exporters; + + private final Languages languages; + + public QProfileExportAction(DbClient dbClient, QProfileFactory profileFactory, QProfileBackuper backuper, QProfileExporters exporters, Languages languages) { + this.dbClient = dbClient; + this.profileFactory = profileFactory; + this.backuper = backuper; + this.exporters = exporters; + this.languages = languages; + } + + @Override + public void define(WebService.NewController controller) { + NewAction create = controller.createAction("export") + .setSince("5.2") + .setDescription("Export a quality profile.") + .setHandler(this); + + create.createParam(PARAM_PROFILE_NAME) + .setDescription("The name of the quality profile to export. If left empty, will export the default profile for the language.") + .setExampleValue("My Sonar way"); + + create.createParam(PARAM_LANGUAGE) + .setDescription("The language for the quality profile.") + .setExampleValue(LanguageParamUtils.getExampleValue(languages)) + .setPossibleValues(LanguageParamUtils.getLanguageKeys(languages)) + .setRequired(true); + + List exporterKeys = Lists.newArrayList(); + for (Language lang: languages.all()) { + for(ProfileExporter exporter: exporters.exportersForLanguage(lang.getKey())) { + exporterKeys.add(exporter.getKey()); + } + } + if (!exporterKeys.isEmpty()) { + create.createParam(PARAM_FORMAT) + .setDescription("Output format. If left empty, the same format as api/qualityprofiles/backup is used.") + .setPossibleValues(exporterKeys); + } + } + + @Override + public void handle(Request request, Response response) throws Exception { + String name = request.param(PARAM_PROFILE_NAME); + String language = request.mandatoryParam(PARAM_LANGUAGE); + String format = null; + if (!exporters.exportersForLanguage(language).isEmpty()) { + format = request.param(PARAM_FORMAT); + } + + DbSession dbSession = dbClient.openSession(false); + Stream stream = response.stream(); + OutputStream output = stream.output(); + Writer writer = new OutputStreamWriter(output); + + try { + QualityProfileDto profile = null; + if (name == null) { + profile = profileFactory.getDefault(dbSession, language); + } else { + profile = profileFactory.getByNameAndLanguage(dbSession, name, language); + } + if (profile == null) { + throw new NotFoundException(String.format("Could not find profile with name '%s' for language '%s'", name, language)); + } + + String profileKey = profile.getKey(); + if (format == null) { + stream.setMediaType(QProfileBackuper.MEDIA_TYPE_XML); + backuper.backup(profileKey, writer); + } else { + stream.setMediaType(exporters.mimeType(format)); + exporters.export(profileKey, format, writer); + } + } finally { + IOUtils.closeQuietly(writer); + IOUtils.closeQuietly(output); + dbSession.close(); + } + } +} diff --git a/server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/example-exporters.json b/server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/example-exporters.json new file mode 100644 index 00000000000..86d3e20568d --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/example-exporters.json @@ -0,0 +1,8 @@ +{ + "exporters": [ + {"key": "pmd", "name": "PMD", "languages": ["java"]}, + {"key": "checkstyle", "name": "Checkstyle", "languages": ["java"]}, + {"key": "js-lint", "name": "JS Lint", "languages": ["js"]}, + {"key": "android-lint", "name": "Android Lint", "languages": ["xml", "java"]} + ] +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/QProfileExportActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/QProfileExportActionTest.java new file mode 100644 index 00000000000..6033875c4dd --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/QProfileExportActionTest.java @@ -0,0 +1,198 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.server.qualityprofile.ws; + +import com.google.common.collect.Sets; +import org.apache.commons.lang.StringUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.mockito.Matchers; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.sonar.api.profiles.ProfileExporter; +import org.sonar.api.profiles.RulesProfile; +import org.sonar.api.server.ws.WebService.Action; +import org.sonar.api.utils.System2; +import org.sonar.core.persistence.DbSession; +import org.sonar.core.persistence.DbTester; +import org.sonar.core.qualityprofile.db.QualityProfileDao; +import org.sonar.core.qualityprofile.db.QualityProfileDto; +import org.sonar.server.db.DbClient; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.language.LanguageTesting; +import org.sonar.server.qualityprofile.*; +import org.sonar.server.qualityprofile.index.ActiveRuleIndex; +import org.sonar.server.search.IndexClient; +import org.sonar.server.ws.WsTester; +import org.sonar.server.ws.WsTester.Result; + +import java.io.IOException; +import java.io.Writer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class QProfileExportActionTest { + + @ClassRule + public static final DbTester db = new DbTester(); + + WsTester wsTester; + + QualityProfileDao qualityProfileDao; + + DbClient dbClient; + + DbSession session; + + QProfileBackuper backuper; + + QProfileExporters exporters; + + @Before + public void before() throws Exception { + qualityProfileDao = new QualityProfileDao(db.myBatis(), mock(System2.class)); + dbClient = new DbClient(db.database(), db.myBatis(), qualityProfileDao); + session = dbClient.openSession(false); + backuper = mock(QProfileBackuper.class); + + db.truncateTables(); + + ProfileExporter exporter1 = newExporter("polop"); + ProfileExporter exporter2 = newExporter("palap"); + + IndexClient indexClient = mock(IndexClient.class); + ActiveRuleIndex activeRuleIndex = mock(ActiveRuleIndex.class); + when(activeRuleIndex.findByProfile(Matchers.anyString())).thenReturn(Sets.newHashSet().iterator()); + + when(indexClient.get(ActiveRuleIndex.class)).thenReturn(activeRuleIndex); + exporters = new QProfileExporters(new QProfileLoader(dbClient, indexClient), null, null, new ProfileExporter[] {exporter1, exporter2}, null); + wsTester = new WsTester(new QProfilesWs(mock(RuleActivationActions.class), + mock(BulkRuleActivationActions.class), + mock(ProjectAssociationActions.class), + new QProfileExportAction(dbClient, new QProfileFactory(dbClient), backuper, exporters, LanguageTesting.newLanguages("xoo")))); + } + + @After + public void after() throws Exception { + session.close(); + } + + private ProfileExporter newExporter(final String key) { + return new ProfileExporter(key, StringUtils.capitalize(key)) { + @Override + public String getMimeType() { + return "text/plain+" + key; + } + + @Override + public void exportProfile(RulesProfile profile, Writer writer) { + try { + writer.write(String.format("Profile %s exported by %s", profile.getName(), key)); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + }; + } + + @Test + public void export_without_format() throws Exception { + QualityProfileDto profile = QProfileTesting.newXooP1(); + qualityProfileDao.insert(session, profile); + session.commit(); + + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + invocation.getArgumentAt(1, Writer.class).write("As exported by SQ !"); + return null; + } + }).when(backuper).backup(Matchers.eq(profile.getKey()), Matchers.any(Writer.class)); + + Result result = wsTester.newGetRequest("api/qualityprofiles", "export").setParam("language", profile.getLanguage()).setParam("name", profile.getName()).execute(); + + assertThat(result.outputAsString()).isEqualTo("As exported by SQ !"); + } + + @Test + public void export_with_format() throws Exception { + QualityProfileDto profile = QProfileTesting.newXooP1(); + qualityProfileDao.insert(session, profile); + session.commit(); + + Result result = wsTester.newGetRequest("api/qualityprofiles", "export") + .setParam("language", profile.getLanguage()).setParam("name", profile.getName()).setParam("format", "polop").execute(); + + assertThat(result.outputAsString()).isEqualTo("Profile " + profile.getName() + " exported by polop"); + } + + @Test + public void export_default_profile() throws Exception { + QualityProfileDto profile1 = QProfileTesting.newXooP1(); + QualityProfileDto profile2 = QProfileTesting.newXooP2().setDefault(true); + qualityProfileDao.insert(session, profile1, profile2); + session.commit(); + + Result result = wsTester.newGetRequest("api/qualityprofiles", "export") + .setParam("language", "xoo").setParam("format", "polop").execute(); + + assertThat(result.outputAsString()).isEqualTo("Profile " + profile2.getName() + " exported by polop"); + } + + @Test(expected = NotFoundException.class) + public void fail_on_unknown_profile() throws Exception { + wsTester.newGetRequest("api/qualityprofiles", "export") + .setParam("language", "xoo").setParam("format", "polop").execute(); + } + + @Test(expected = IllegalArgumentException.class) + public void fail_on_unknown_exporter() throws Exception { + QualityProfileDto profile = QProfileTesting.newXooP1(); + qualityProfileDao.insert(session, profile); + session.commit(); + + wsTester.newGetRequest("api/qualityprofiles", "export") + .setParam("language", "xoo").setParam("format", "unknown").execute(); + } + + @Test + public void do_not_fail_when_no_exporters() throws Exception { + exporters = new QProfileExporters(null, null, null, new ProfileExporter[0], null); + wsTester = new WsTester(new QProfilesWs(mock(RuleActivationActions.class), + mock(BulkRuleActivationActions.class), + mock(ProjectAssociationActions.class), + new QProfileExportAction(dbClient, new QProfileFactory(dbClient), backuper, exporters, LanguageTesting.newLanguages("xoo")))); + + Action export = wsTester.controller("api/qualityprofiles").action("export"); + assertThat(export.params()).hasSize(2); + + QualityProfileDto profile = QProfileTesting.newXooP1(); + qualityProfileDao.insert(session, profile); + session.commit(); + + wsTester.newGetRequest("api/qualityprofiles", "export").setParam("language", "xoo").setParam("name", profile.getName()).execute(); + + } +} -- 2.39.5