Browse Source

SONAR-8857 add parameter "organization" to api/qualityprofiles/restore"

If unset, then the default organization is used.
tags/6.4-RC1
Simon Brandhof 7 years ago
parent
commit
f65a697f05

+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java View File

@@ -140,7 +140,7 @@ import org.sonar.server.projecttag.ws.ProjectTagsWsModule;
import org.sonar.server.property.InternalPropertiesImpl;
import org.sonar.server.property.ws.PropertiesWs;
import org.sonar.server.qualitygate.QualityGateModule;
import org.sonar.server.qualityprofile.QProfileBackuper;
import org.sonar.server.qualityprofile.QProfileBackuperImpl;
import org.sonar.server.qualityprofile.QProfileComparison;
import org.sonar.server.qualityprofile.QProfileCopier;
import org.sonar.server.qualityprofile.QProfileExporters;
@@ -275,7 +275,7 @@ public class PlatformLevel4 extends PlatformLevel {
RuleActivatorContextFactory.class,
QProfileFactory.class,
QProfileCopier.class,
QProfileBackuper.class,
QProfileBackuperImpl.class,
QProfileReset.class,
QProfilesWsModule.class,


+ 9
- 184
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileBackuper.java View File

@@ -19,199 +19,24 @@
*/
package org.sonar.server.qualityprofile;

import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.io.Reader;
import java.io.Writer;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.CompareToBuilder;
import org.codehaus.staxmate.SMInputFactory;
import org.codehaus.staxmate.in.SMHierarchicCursor;
import org.codehaus.staxmate.in.SMInputCursor;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.text.XmlWriter;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.qualityprofile.ActiveRuleDto;
import org.sonar.db.qualityprofile.ActiveRuleParamDto;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.qualityprofile.QualityProfileDto;

@ServerSide
public class QProfileBackuper {

private static final Joiner RULE_KEY_JOINER = Joiner.on(", ").skipNulls();

private final QProfileReset reset;
private final DbClient db;

public QProfileBackuper(QProfileReset reset, DbClient db) {
this.reset = reset;
this.db = db;
}

public void backup(DbSession dbSession, QualityProfileDto profileDto, Writer writer) {
List<ActiveRuleDto> activeRules = db.activeRuleDao().selectByProfileKey(dbSession, profileDto.getKey());
activeRules.sort(BackupActiveRuleComparator.INSTANCE);
writeXml(dbSession, writer, profileDto, activeRules.iterator());
}
/**
* Backup and restore a Quality profile.
*/
public interface QProfileBackuper {

private void writeXml(DbSession dbSession, Writer writer, QualityProfileDto profile, Iterator<ActiveRuleDto> activeRules) {
XmlWriter xml = XmlWriter.of(writer).declaration();
xml.begin("profile");
xml.prop("name", profile.getName());
xml.prop("language", profile.getLanguage());
xml.begin("rules");
while (activeRules.hasNext()) {
ActiveRuleDto activeRule = activeRules.next();
xml.begin("rule");
xml.prop("repositoryKey", activeRule.getKey().ruleKey().repository());
xml.prop("key", activeRule.getKey().ruleKey().rule());
xml.prop("priority", activeRule.getSeverityString());
xml.begin("parameters");
for (ActiveRuleParamDto param : db.activeRuleDao().selectParamsByActiveRuleId(dbSession, activeRule.getId())) {
xml
.begin("parameter")
.prop("key", param.getKey())
.prop("value", param.getValue())
.end();
}
xml.end("parameters");
xml.end("rule");
}
xml.end("rules").end("profile").close();
}
void backup(DbSession dbSession, QualityProfileDto profile, Writer backupWriter);

/**
* @param reader the XML backup
* @param toProfileName the target profile. If <code>null</code>, then use the
* lang and name declared in the backup
* Restore a profile backup in the specified organization. The parameter {@code overriddenProfileName}
* is the name of the profile to be used. If null then name is loaded from backup.
*/
public BulkChangeResult restore(DbSession dbSession, Reader reader, @Nullable QProfileName toProfileName) {
try {
String profileLang = null;
String profileName = null;
List<RuleActivation> ruleActivations = Lists.newArrayList();
SMInputFactory inputFactory = initStax();
SMHierarchicCursor rootC = inputFactory.rootElementCursor(reader);
rootC.advance(); // <profile>
if (!rootC.getLocalName().equals("profile")) {
throw new IllegalArgumentException("Backup XML is not valid. Root element must be <profile>.");
}
SMInputCursor cursor = rootC.childElementCursor();
while (cursor.getNext() != null) {
String nodeName = cursor.getLocalName();
if (StringUtils.equals("name", nodeName)) {
profileName = StringUtils.trim(cursor.collectDescendantText(false));

} else if (StringUtils.equals("language", nodeName)) {
profileLang = StringUtils.trim(cursor.collectDescendantText(false));

} else if (StringUtils.equals("rules", nodeName)) {
SMInputCursor rulesCursor = cursor.childElementCursor("rule");
ruleActivations = parseRuleActivations(rulesCursor);
}
}

QProfileName target = (QProfileName) ObjectUtils.defaultIfNull(toProfileName, new QProfileName(profileLang, profileName));
return reset.reset(dbSession, target, ruleActivations);
} catch (XMLStreamException e) {
throw new IllegalStateException("Fail to restore Quality profile backup", e);
}
}

private List<RuleActivation> parseRuleActivations(SMInputCursor rulesCursor) throws XMLStreamException {
List<RuleActivation> activations = Lists.newArrayList();
Set<RuleKey> activatedKeys = Sets.newHashSet();
List<RuleKey> duplicatedKeys = Lists.newArrayList();
while (rulesCursor.getNext() != null) {
SMInputCursor ruleCursor = rulesCursor.childElementCursor();
String repositoryKey = null;
String key = null;
String severity = null;
Map<String, String> parameters = Maps.newHashMap();
while (ruleCursor.getNext() != null) {
String nodeName = ruleCursor.getLocalName();
if (StringUtils.equals("repositoryKey", nodeName)) {
repositoryKey = StringUtils.trim(ruleCursor.collectDescendantText(false));

} else if (StringUtils.equals("key", nodeName)) {
key = StringUtils.trim(ruleCursor.collectDescendantText(false));

} else if (StringUtils.equals("priority", nodeName)) {
severity = StringUtils.trim(ruleCursor.collectDescendantText(false));

} else if (StringUtils.equals("parameters", nodeName)) {
SMInputCursor propsCursor = ruleCursor.childElementCursor("parameter");
readParameters(propsCursor, parameters);
}
}
RuleKey ruleKey = RuleKey.of(repositoryKey, key);
if (activatedKeys.contains(ruleKey)) {
duplicatedKeys.add(ruleKey);
}
activatedKeys.add(ruleKey);
RuleActivation activation = new RuleActivation(ruleKey);
activation.setSeverity(severity);
activation.setParameters(parameters);
activations.add(activation);
}
if (!duplicatedKeys.isEmpty()) {
throw new IllegalArgumentException("The quality profile cannot be restored as it contains duplicates for the following rules: " +
RULE_KEY_JOINER.join(duplicatedKeys));
}
return activations;
}

private void readParameters(SMInputCursor propsCursor, Map<String, String> parameters) throws XMLStreamException {
while (propsCursor.getNext() != null) {
SMInputCursor propCursor = propsCursor.childElementCursor();
String key = null;
String value = null;
while (propCursor.getNext() != null) {
String nodeName = propCursor.getLocalName();
if (StringUtils.equals("key", nodeName)) {
key = StringUtils.trim(propCursor.collectDescendantText(false));
} else if (StringUtils.equals("value", nodeName)) {
value = StringUtils.trim(propCursor.collectDescendantText(false));
}
}
if (key != null) {
parameters.put(key, value);
}
}
}

private static SMInputFactory initStax() {
XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE);
// just so it won't try to load DTD in if there's DOCTYPE
xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
return new SMInputFactory(xmlFactory);
}

private enum BackupActiveRuleComparator implements Comparator<ActiveRuleDto> {
INSTANCE;
BulkChangeResult restore(DbSession dbSession, Reader backup, OrganizationDto organization, @Nullable String overriddenProfileName);

@Override
public int compare(ActiveRuleDto o1, ActiveRuleDto o2) {
return new CompareToBuilder()
.append(o1.getKey().ruleKey().repository(), o2.getKey().ruleKey().repository())
.append(o1.getKey().ruleKey().rule(), o2.getKey().ruleKey().rule())
.toComparison();
}
}
}

+ 215
- 0
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileBackuperImpl.java View File

@@ -0,0 +1,215 @@
/*
* 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.server.qualityprofile;

import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.io.Reader;
import java.io.Writer;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.CompareToBuilder;
import org.codehaus.staxmate.SMInputFactory;
import org.codehaus.staxmate.in.SMHierarchicCursor;
import org.codehaus.staxmate.in.SMInputCursor;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.text.XmlWriter;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.qualityprofile.ActiveRuleDto;
import org.sonar.db.qualityprofile.ActiveRuleParamDto;
import org.sonar.db.qualityprofile.QualityProfileDto;

@ServerSide
public class QProfileBackuperImpl implements QProfileBackuper {

private static final Joiner RULE_KEY_JOINER = Joiner.on(", ").skipNulls();

private final QProfileReset reset;
private final DbClient db;

public QProfileBackuperImpl(QProfileReset reset, DbClient db) {
this.reset = reset;
this.db = db;
}

@Override
public void backup(DbSession dbSession, QualityProfileDto profileDto, Writer writer) {
List<ActiveRuleDto> activeRules = db.activeRuleDao().selectByProfileKey(dbSession, profileDto.getKey());
activeRules.sort(BackupActiveRuleComparator.INSTANCE);
writeXml(dbSession, writer, profileDto, activeRules.iterator());
}

private void writeXml(DbSession dbSession, Writer writer, QualityProfileDto profile, Iterator<ActiveRuleDto> activeRules) {
XmlWriter xml = XmlWriter.of(writer).declaration();
xml.begin("profile");
xml.prop("name", profile.getName());
xml.prop("language", profile.getLanguage());
xml.begin("rules");
while (activeRules.hasNext()) {
ActiveRuleDto activeRule = activeRules.next();
xml.begin("rule");
xml.prop("repositoryKey", activeRule.getKey().ruleKey().repository());
xml.prop("key", activeRule.getKey().ruleKey().rule());
xml.prop("priority", activeRule.getSeverityString());
xml.begin("parameters");
for (ActiveRuleParamDto param : db.activeRuleDao().selectParamsByActiveRuleId(dbSession, activeRule.getId())) {
xml
.begin("parameter")
.prop("key", param.getKey())
.prop("value", param.getValue())
.end();
}
xml.end("parameters");
xml.end("rule");
}
xml.end("rules").end("profile").close();
}

@Override
public BulkChangeResult restore(DbSession dbSession, Reader backup, OrganizationDto organization, @Nullable String overriddenProfileName) {
try {
String profileLang = null;
String profileName = null;
List<RuleActivation> ruleActivations = Lists.newArrayList();
SMInputFactory inputFactory = initStax();
SMHierarchicCursor rootC = inputFactory.rootElementCursor(backup);
rootC.advance(); // <profile>
if (!"profile".equals(rootC.getLocalName())) {
throw new IllegalArgumentException("Backup XML is not valid. Root element must be <profile>.");
}
SMInputCursor cursor = rootC.childElementCursor();
while (cursor.getNext() != null) {
String nodeName = cursor.getLocalName();
if (StringUtils.equals("name", nodeName)) {
profileName = StringUtils.trim(cursor.collectDescendantText(false));

} else if (StringUtils.equals("language", nodeName)) {
profileLang = StringUtils.trim(cursor.collectDescendantText(false));

} else if (StringUtils.equals("rules", nodeName)) {
SMInputCursor rulesCursor = cursor.childElementCursor("rule");
ruleActivations = parseRuleActivations(rulesCursor);
}
}

QProfileName target = new QProfileName(profileLang, MoreObjects.firstNonNull(overriddenProfileName, profileName));
return reset.reset(dbSession, organization, target, ruleActivations);
} catch (XMLStreamException e) {
throw new IllegalStateException("Fail to restore Quality profile backup", e);
}
}

private List<RuleActivation> parseRuleActivations(SMInputCursor rulesCursor) throws XMLStreamException {
List<RuleActivation> activations = Lists.newArrayList();
Set<RuleKey> activatedKeys = Sets.newHashSet();
List<RuleKey> duplicatedKeys = Lists.newArrayList();
while (rulesCursor.getNext() != null) {
SMInputCursor ruleCursor = rulesCursor.childElementCursor();
String repositoryKey = null;
String key = null;
String severity = null;
Map<String, String> parameters = Maps.newHashMap();
while (ruleCursor.getNext() != null) {
String nodeName = ruleCursor.getLocalName();
if (StringUtils.equals("repositoryKey", nodeName)) {
repositoryKey = StringUtils.trim(ruleCursor.collectDescendantText(false));

} else if (StringUtils.equals("key", nodeName)) {
key = StringUtils.trim(ruleCursor.collectDescendantText(false));

} else if (StringUtils.equals("priority", nodeName)) {
severity = StringUtils.trim(ruleCursor.collectDescendantText(false));

} else if (StringUtils.equals("parameters", nodeName)) {
SMInputCursor propsCursor = ruleCursor.childElementCursor("parameter");
readParameters(propsCursor, parameters);
}
}
RuleKey ruleKey = RuleKey.of(repositoryKey, key);
if (activatedKeys.contains(ruleKey)) {
duplicatedKeys.add(ruleKey);
}
activatedKeys.add(ruleKey);
RuleActivation activation = new RuleActivation(ruleKey);
activation.setSeverity(severity);
activation.setParameters(parameters);
activations.add(activation);
}
if (!duplicatedKeys.isEmpty()) {
throw new IllegalArgumentException("The quality profile cannot be restored as it contains duplicates for the following rules: " +
RULE_KEY_JOINER.join(duplicatedKeys));
}
return activations;
}

private static void readParameters(SMInputCursor propsCursor, Map<String, String> parameters) throws XMLStreamException {
while (propsCursor.getNext() != null) {
SMInputCursor propCursor = propsCursor.childElementCursor();
String key = null;
String value = null;
while (propCursor.getNext() != null) {
String nodeName = propCursor.getLocalName();
if (StringUtils.equals("key", nodeName)) {
key = StringUtils.trim(propCursor.collectDescendantText(false));
} else if (StringUtils.equals("value", nodeName)) {
value = StringUtils.trim(propCursor.collectDescendantText(false));
}
}
if (key != null) {
parameters.put(key, value);
}
}
}

private static SMInputFactory initStax() {
XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE);
// just so it won't try to load DTD in if there's DOCTYPE
xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
return new SMInputFactory(xmlFactory);
}

private enum BackupActiveRuleComparator implements Comparator<ActiveRuleDto> {
INSTANCE;

@Override
public int compare(ActiveRuleDto o1, ActiveRuleDto o2) {
return new CompareToBuilder()
.append(o1.getKey().ruleKey().repository(), o2.getKey().ruleKey().repository())
.append(o1.getKey().ruleKey().rule(), o2.getKey().ruleKey().rule())
.toComparison();
}
}
}

+ 12
- 9
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileCopier.java View File

@@ -31,6 +31,7 @@ import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.TempFolder;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.qualityprofile.QualityProfileDto;
import org.sonar.server.qualityprofile.ws.QProfileWsSupport;

@@ -53,27 +54,29 @@ public class QProfileCopier {
this.qProfileWsSupport = qProfileWsSupport;
}

public QualityProfileDto copyToName(DbSession dbSession, String fromProfileKey, String toName) {
QualityProfileDto from = db.qualityProfileDao().selectOrFailByKey(dbSession, fromProfileKey);
QualityProfileDto to = prepareTarget(dbSession, from, toName);
public QualityProfileDto copyToName(DbSession dbSession, String fromKey, String toName) {
QualityProfileDto from = db.qualityProfileDao().selectOrFailByKey(dbSession, fromKey);
OrganizationDto organization = db.organizationDao().selectByUuid(dbSession, from.getOrganizationUuid())
.orElseThrow(() -> new IllegalStateException("Organization with UUID [" + from.getOrganizationUuid() + "] does not exist"));
QualityProfileDto to = prepareTarget(dbSession, organization, from, toName);
File backupFile = temp.newFile();
try {
backup(dbSession, from, backupFile);
restore(dbSession, backupFile, QProfileName.createFor(to.getLanguage(), to.getName()));
restore(dbSession, backupFile, organization, to.getName());
return to;
} finally {
org.sonar.core.util.FileUtils.deleteQuietly(backupFile);
}
}

private QualityProfileDto prepareTarget(DbSession dbSession, QualityProfileDto from, String toName) {
private QualityProfileDto prepareTarget(DbSession dbSession, OrganizationDto organization, QualityProfileDto from, String toName) {
QProfileName toProfileName = new QProfileName(from.getLanguage(), toName);
verify(from, toProfileName);
QualityProfileDto toProfile = db.qualityProfileDao().selectByNameAndLanguage(toProfileName.getName(), toProfileName.getLanguage(), dbSession);
QualityProfileDto toProfile = db.qualityProfileDao().selectByNameAndLanguage(organization, toProfileName.getName(), toProfileName.getLanguage(), dbSession);
if (toProfile == null) {
// Do not delegate creation to QProfileBackuper because we need to keep
// the parent-child association, if exists.
toProfile = factory.create(dbSession, qProfileWsSupport.getDefaultOrganization(dbSession), toProfileName);
toProfile = factory.create(dbSession, organization, toProfileName);
toProfile.setParentKee(from.getParentKee());
db.qualityProfileDao().update(dbSession, toProfile);
dbSession.commit();
@@ -101,9 +104,9 @@ public class QProfileCopier {
}
}

private void restore(DbSession dbSession, File backupFile, QProfileName profileName) {
private void restore(DbSession dbSession, File backupFile, OrganizationDto org, String profileName) {
try (Reader reader = new InputStreamReader(FileUtils.openInputStream(backupFile), UTF_8)) {
backuper.restore(dbSession, reader, profileName);
backuper.restore(dbSession, reader, org, profileName);
} catch (IOException e) {
throw new IllegalStateException("Fail to create temporary backup file: " + backupFile, e);
}

+ 3
- 2
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileReset.java View File

@@ -38,6 +38,7 @@ import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.ValidationMessages;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.qualityprofile.ActiveRuleDto;
import org.sonar.db.qualityprofile.ActiveRuleKey;
import org.sonar.db.qualityprofile.QualityProfileDto;
@@ -106,8 +107,8 @@ public class QProfileReset {
/**
* Reset the profile, which is created if it does not exist
*/
BulkChangeResult reset(DbSession dbSession, QProfileName profileName, Collection<RuleActivation> activations) {
QualityProfileDto profile = factory.getOrCreate(dbSession, qProfileWsSupport.getDefaultOrganization(dbSession), profileName);
BulkChangeResult reset(DbSession dbSession, OrganizationDto organization, QProfileName profileName, Collection<RuleActivation> activations) {
QualityProfileDto profile = factory.getOrCreate(dbSession, organization, profileName);
return doReset(dbSession, profile, activations);
}


+ 3
- 1
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/OldRestoreAction.java View File

@@ -31,6 +31,7 @@ import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.text.JsonWriter;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.qualityprofile.QualityProfileDto;
import org.sonar.server.qualityprofile.BulkChangeResult;
import org.sonar.server.qualityprofile.QProfileBackuper;
@@ -85,7 +86,8 @@ public class OldRestoreAction implements WsAction {
try (DbSession dbSession = dbClient.openSession(false)) {
checkArgument(backup != null, "A backup file must be provided");
reader = new InputStreamReader(backup, StandardCharsets.UTF_8);
BulkChangeResult result = backuper.restore(dbSession, reader, null);
OrganizationDto defaultOrg = qProfileWsSupport.getOrganizationByKey(dbSession, null);
BulkChangeResult result = backuper.restore(dbSession, reader, defaultOrg,null);
writeResponse(response.newJsonWriter(), result);
} finally {
IOUtils.closeQuietly(reader);

+ 16
- 3
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/RestoreAction.java View File

@@ -30,12 +30,16 @@ import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.text.JsonWriter;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.permission.OrganizationPermission;
import org.sonar.db.qualityprofile.QualityProfileDto;
import org.sonar.server.qualityprofile.BulkChangeResult;
import org.sonar.server.qualityprofile.QProfileBackuper;
import org.sonar.server.user.UserSession;

import static com.google.common.base.Preconditions.checkArgument;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_ORGANIZATION;

public class RestoreAction implements QProfileWsAction {

@@ -44,12 +48,14 @@ public class RestoreAction implements QProfileWsAction {
private final DbClient dbClient;
private final QProfileBackuper backuper;
private final Languages languages;
private final UserSession userSession;
private final QProfileWsSupport wsSupport;

public RestoreAction(DbClient dbClient, QProfileBackuper backuper, Languages languages, QProfileWsSupport wsSupport) {
public RestoreAction(DbClient dbClient, QProfileBackuper backuper, Languages languages, UserSession userSession, QProfileWsSupport wsSupport) {
this.dbClient = dbClient;
this.backuper = backuper;
this.languages = languages;
this.userSession = userSession;
this.wsSupport = wsSupport;
}

@@ -67,19 +73,26 @@ public class RestoreAction implements QProfileWsAction {
.setDescription("A profile backup file in XML format, as generated by api/qualityprofiles/backup " +
"or the former api/profiles/backup.")
.setRequired(true);

QProfileWsSupport.createOrganizationParam(action).setSince("6.4");
}

@Override
public void handle(Request request, Response response) throws Exception {
wsSupport.checkQProfileAdminPermission();
userSession.checkLoggedIn();

InputStream backup = request.paramAsInputStream(PARAM_BACKUP);
String organizationKey = request.param(PARAM_ORGANIZATION);
InputStreamReader reader = null;

try (DbSession dbSession = dbClient.openSession(false)) {
checkArgument(backup != null, "A backup file must be provided");
reader = new InputStreamReader(backup, UTF_8);
BulkChangeResult result = backuper.restore(dbSession, reader, null);

OrganizationDto organization = wsSupport.getOrganizationByKey(dbSession, organizationKey);
userSession.checkPermission(OrganizationPermission.ADMINISTER_QUALITY_PROFILES, organization);

BulkChangeResult result = backuper.restore(dbSession, reader, organization, null);
writeResponse(response.newJsonWriter(), result);
} finally {
IOUtils.closeQuietly(reader);

+ 10
- 10
server/sonar-server/src/test/java/org/sonar/server/qualityprofile/QProfileBackuperMediumTest.java View File

@@ -146,7 +146,7 @@ public class QProfileBackuperMediumTest {
// Backup file declares profile P1 on xoo
tester.get(QProfileBackuper.class).restore(dbSession, new StringReader(
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/restore.xml"), StandardCharsets.UTF_8)),
null);
organization, null);

// Check in db
QualityProfileDto profile = db.qualityProfileDao().selectByNameAndLanguage("P1", "xoo", dbSession);
@@ -187,7 +187,7 @@ public class QProfileBackuperMediumTest {
// restore backup, which activates only x1
// -> update x1 and deactivate x2
tester.get(QProfileBackuper.class).restore(dbSession, new StringReader(
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/restore.xml"), StandardCharsets.UTF_8)), null);
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/restore.xml"), StandardCharsets.UTF_8)), organization, null);

// Check in db
List<ActiveRuleDto> activeRules = db.activeRuleDao().selectByProfileKey(dbSession, XOO_P1_KEY);
@@ -225,7 +225,7 @@ public class QProfileBackuperMediumTest {

// restore backup of child profile -> overrides x1
tester.get(QProfileBackuper.class).restore(dbSession, new StringReader(
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/restore-child.xml"), StandardCharsets.UTF_8)), null);
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/restore-child.xml"), StandardCharsets.UTF_8)), organization, null);

// parent profile is unchanged
List<ActiveRuleDto> activeRules = db.activeRuleDao().selectByProfileKey(dbSession, XOO_P1_KEY);
@@ -273,7 +273,7 @@ public class QProfileBackuperMediumTest {

// restore backup of parent profile -> update x1 and propagates to child
tester.get(QProfileBackuper.class).restore(dbSession, new StringReader(
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/restore-parent.xml"), StandardCharsets.UTF_8)), null);
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/restore-parent.xml"), StandardCharsets.UTF_8)), organization, null);

// parent profile is updated
List<ActiveRuleDto> activeRules = db.activeRuleDao().selectByProfileKey(dbSession, XOO_P1_KEY);
@@ -322,7 +322,7 @@ public class QProfileBackuperMediumTest {

// backup of child profile contains x2 but not x1
tester.get(QProfileBackuper.class).restore(dbSession, new StringReader(
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/keep_other_inherited_rules.xml"), StandardCharsets.UTF_8)), XOO_P2_NAME);
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/keep_other_inherited_rules.xml"), StandardCharsets.UTF_8)), organization, XOO_P2_NAME.getName());

// x1 and x2
assertThat(db.activeRuleDao().selectByProfileKey(dbSession, XOO_P2_KEY)).hasSize(2);
@@ -332,7 +332,7 @@ public class QProfileBackuperMediumTest {
public void fail_to_restore_if_not_xml_backup() throws Exception {
try {
tester.get(QProfileBackuper.class).restore(dbSession, new StringReader(
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/not-xml-backup.txt"), StandardCharsets.UTF_8)), null);
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/not-xml-backup.txt"), StandardCharsets.UTF_8)), organization, null);
fail();
} catch (IllegalStateException e) {
assertThat(e).hasMessage("Fail to restore Quality profile backup");
@@ -344,7 +344,7 @@ public class QProfileBackuperMediumTest {
public void fail_to_restore_if_bad_xml_format() throws Exception {
try {
tester.get(QProfileBackuper.class).restore(dbSession, new StringReader(
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/bad-xml-backup.xml"), StandardCharsets.UTF_8)), null);
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/bad-xml-backup.xml"), StandardCharsets.UTF_8)), organization, null);
fail();
} catch (IllegalArgumentException e) {
assertThat(e).hasMessage("Backup XML is not valid. Root element must be <profile>.");
@@ -355,7 +355,7 @@ public class QProfileBackuperMediumTest {
public void fail_to_restore_if_duplicate_rule() throws Exception {
try {
tester.get(QProfileBackuper.class).restore(dbSession, new StringReader(
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/duplicates-xml-backup.xml"), StandardCharsets.UTF_8)), null);
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/duplicates-xml-backup.xml"), StandardCharsets.UTF_8)), organization, null);
fail();
} catch (IllegalArgumentException e) {
assertThat(e).hasMessage("The quality profile cannot be restored as it contains duplicates for the following rules: xoo:x1, xoo:x2");
@@ -366,7 +366,7 @@ public class QProfileBackuperMediumTest {
public void restore_and_override_profile_name() throws Exception {
tester.get(QProfileBackuper.class).restore(dbSession, new StringReader(
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/restore.xml"), StandardCharsets.UTF_8)),
XOO_P3_NAME);
organization, XOO_P3_NAME.getName());

List<ActiveRuleDto> activeRules = db.activeRuleDao().selectByProfileKey(dbSession, XOO_P1_KEY);
assertThat(activeRules).hasSize(0);
@@ -380,7 +380,7 @@ public class QProfileBackuperMediumTest {
public void restore_profile_with_zero_rules() throws Exception {
tester.get(QProfileBackuper.class).restore(dbSession, new StringReader(
Resources.toString(getClass().getResource("QProfileBackuperMediumTest/empty.xml"), StandardCharsets.UTF_8)),
null);
organization, null);

dbSession.clearCache();
assertThat(db.activeRuleDao().selectAll(dbSession)).hasSize(0);

+ 10
- 2
server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/BackupActionTest.java View File

@@ -33,6 +33,7 @@ import org.sonar.server.language.LanguageTesting;
import org.sonar.server.organization.DefaultOrganizationProvider;
import org.sonar.server.organization.TestDefaultOrganizationProvider;
import org.sonar.server.qualityprofile.QProfileBackuper;
import org.sonar.server.qualityprofile.QProfileBackuperImpl;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.TestResponse;
import org.sonar.server.ws.WsActionTester;
@@ -50,7 +51,7 @@ public class BackupActionTest {
@Rule
public UserSessionRule userSession = UserSessionRule.standalone();

private QProfileBackuper backuper = new QProfileBackuper(null, db.getDbClient());
private QProfileBackuper backuper = new QProfileBackuperImpl(null, db.getDbClient());
private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
private QProfileWsSupport wsSupport = new QProfileWsSupport(db.getDbClient(), userSession, defaultOrganizationProvider);
private Languages languages = LanguageTesting.newLanguages(A_LANGUAGE);
@@ -149,6 +150,13 @@ public class BackupActionTest {
.execute();
}

@Test
public void throws_IAE_if_profile_reference_is_not_set() throws Exception {
expectedException.expect(IllegalArgumentException.class);

tester.newRequest().execute();
}

private static String xmlForProfileWithoutRules(QualityProfileDto profile) {
return "<?xml version='1.0' encoding='UTF-8'?>" +
"<profile>" +
@@ -160,7 +168,7 @@ public class BackupActionTest {

private static QualityProfileDto newProfile(OrganizationDto org) {
return QualityProfileTesting.newQualityProfileDto()
.setLanguage("xoo")
.setLanguage(A_LANGUAGE)
.setOrganizationUuid(org.getUuid());
}
}

+ 115
- 26
server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/RestoreActionTest.java View File

@@ -19,7 +19,11 @@
*/
package org.sonar.server.qualityprofile.ws;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import javax.annotation.Nullable;
import org.apache.commons.io.IOUtils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
@@ -30,23 +34,20 @@ import org.sonar.db.DbTester;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.qualityprofile.QualityProfileDto;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.exceptions.UnauthorizedException;
import org.sonar.server.language.LanguageTesting;
import org.sonar.server.organization.DefaultOrganizationProvider;
import org.sonar.server.organization.TestDefaultOrganizationProvider;
import org.sonar.server.qualityprofile.BulkChangeResult;
import org.sonar.server.qualityprofile.QProfileBackuper;
import org.sonar.server.qualityprofile.QProfileName;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.TestResponse;
import org.sonar.server.ws.WsActionTester;
import org.sonar.test.JsonAssert;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.sonar.db.permission.OrganizationPermission.ADMINISTER_QUALITY_PROFILES;

public class RestoreActionTest {
@@ -60,11 +61,11 @@ public class RestoreActionTest {
@Rule
public UserSessionRule userSession = UserSessionRule.standalone();

private QProfileBackuper backuper = mock(QProfileBackuper.class);
private TestBackuper backuper = new TestBackuper();
private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
private QProfileWsSupport wsSupport = new QProfileWsSupport(db.getDbClient(), userSession, defaultOrganizationProvider);
private Languages languages = LanguageTesting.newLanguages(A_LANGUAGE);
private WsActionTester tester = new WsActionTester(new RestoreAction(db.getDbClient(), backuper, languages, wsSupport));
private WsActionTester tester = new WsActionTester(new RestoreAction(db.getDbClient(), backuper, languages, userSession, wsSupport));

@Test
public void test_definition() {
@@ -76,24 +77,58 @@ public class RestoreActionTest {
assertThat(definition.description()).isNotEmpty();

// parameters
assertThat(definition.params()).hasSize(1);
assertThat(definition.param("backup").isRequired()).isTrue();
assertThat(definition.params()).hasSize(2);
WebService.Param backupParam = definition.param("backup");
assertThat(backupParam.isRequired()).isTrue();
assertThat(backupParam.since()).isNull();
WebService.Param orgParam = definition.param("organization");
assertThat(orgParam.isRequired()).isFalse();
assertThat(orgParam.since()).isEqualTo("6.4");
}

@Test
public void restore_the_uploaded_backup_on_default_organization() throws Exception {
QualityProfileDto profile = QualityProfileDto.createFor("P1")
.setDefault(false).setLanguage("xoo").setName("Sonar way");
BulkChangeResult restoreResult = new BulkChangeResult(profile);
when(backuper.restore(any(DbSession.class), any(Reader.class), any(QProfileName.class))).thenReturn(restoreResult);

public void profile_is_restored_on_default_organization_with_the_name_provided_in_backup() throws Exception {
logInAsQProfileAdministrator(db.getDefaultOrganization());
TestResponse response = restore("<backup/>");

JsonAssert.assertJson(response.getInput()).isSimilarTo(getClass().getResource("RestoreActionTest/restore_profile.json"));
verify(backuper).restore(any(DbSession.class), any(Reader.class), any(QProfileName.class));
TestResponse response = restore("<backup/>", null);

assertThat(backuper.restoredOrganization.getUuid()).isEqualTo(db.getDefaultOrganization().getUuid());
assertThat(backuper.restoredBackup).isEqualTo("<backup/>");
assertThat(backuper.restoredProfile.getName()).isEqualTo("the-name-in-backup");
JsonAssert.assertJson(response.getInput()).isSimilarTo("{" +
" \"profile\": {" +
" \"name\": \"the-name-in-backup\"," +
" \"language\": \"xoo\"," +
" \"languageName\": \"Xoo\"," +
" \"isDefault\": false," +
" \"isInherited\": false" +
" }," +
" \"ruleSuccesses\": 0," +
" \"ruleFailures\": 0" +
"}");
}

@Test
public void profile_is_restored_on_specified_organization_with_the_name_provided_in_backup() throws Exception {
OrganizationDto org = db.organizations().insert();
logInAsQProfileAdministrator(org);
TestResponse response = restore("<backup/>", org.getKey());

assertThat(backuper.restoredOrganization.getUuid()).isEqualTo(org.getUuid());
assertThat(backuper.restoredBackup).isEqualTo("<backup/>");
assertThat(backuper.restoredProfile.getName()).isEqualTo("the-name-in-backup");
JsonAssert.assertJson(response.getInput()).isSimilarTo("{" +
" \"profile\": {" +
" \"name\": \"the-name-in-backup\"," +
" \"language\": \"xoo\"," +
" \"languageName\": \"Xoo\"," +
" \"isDefault\": false," +
" \"isInherited\": false" +
" }," +
" \"ruleSuccesses\": 0," +
" \"ruleFailures\": 0" +
"}");

}
@Test
public void throw_IAE_if_backup_is_missing() throws Exception {
logInAsQProfileAdministrator(db.getDefaultOrganization());
@@ -107,13 +142,34 @@ public class RestoreActionTest {
}

@Test
public void throw_ForbiddenException_if_not_profile_administrator() throws Exception {
public void throw_ForbiddenException_if_not_profile_administrator_of_default_organization() throws Exception {
userSession.logIn();

expectedException.expect(ForbiddenException.class);
expectedException.expectMessage("Insufficient privileges");

restore("<backup/>");
restore("<backup/>", null);
}

@Test
public void throw_ForbiddenException_if_not_profile_administrator_of_specified_organization() throws Exception {
OrganizationDto org = db.organizations().insert();
logInAsQProfileAdministrator(db.getDefaultOrganization());

expectedException.expect(ForbiddenException.class);
expectedException.expectMessage("Insufficient privileges");

restore("<backup/>", org.getKey());
}

@Test
public void throw_NotFoundException_if_specified_organization_does_not_exist() throws Exception {
userSession.logIn();

expectedException.expect(NotFoundException.class);
expectedException.expectMessage("No organization with key 'missing'");

restore("<backup/>", "missing");
}

@Test
@@ -123,7 +179,7 @@ public class RestoreActionTest {
expectedException.expect(UnauthorizedException.class);
expectedException.expectMessage("Authentication is required");

restore("<backup/>");
restore("<backup/>", null);
}

private void logInAsQProfileAdministrator(OrganizationDto org) {
@@ -132,10 +188,43 @@ public class RestoreActionTest {
.addPermission(ADMINISTER_QUALITY_PROFILES, org);
}

private TestResponse restore(String backupContent) {
return tester.newRequest()
private TestResponse restore(String backupContent, @Nullable String organizationKey) {
TestRequest request = tester.newRequest()
.setMethod("POST")
.setParam("backup", backupContent)
.execute();
.setParam("backup", backupContent);
if (organizationKey != null) {
request.setParam("organization", organizationKey);
}
return request.execute();
}

private static class TestBackuper implements QProfileBackuper {

private String restoredBackup;
private OrganizationDto restoredOrganization;
private QualityProfileDto restoredProfile;

@Override
public void backup(DbSession dbSession, QualityProfileDto profile, Writer backupWriter) {
throw new UnsupportedOperationException();
}

@Override
public BulkChangeResult restore(DbSession dbSession, Reader backup, OrganizationDto organization, @Nullable String overriddenProfileName) {
if (restoredProfile != null) {
throw new IllegalStateException("Already restored");
}
try {
this.restoredBackup = IOUtils.toString(backup);
} catch (IOException e) {
throw new IllegalStateException(e);
}
this.restoredOrganization = organization;
restoredProfile = QualityProfileDto.createFor("P1")
.setDefault(false)
.setLanguage("xoo")
.setName(overriddenProfileName != null ? overriddenProfileName : "the-name-in-backup");
return new BulkChangeResult(restoredProfile);
}
}
}

Loading…
Cancel
Save