If unset, then the default organization is used.tags/6.4-RC1
@@ -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, | |||
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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); | |||
} | |||
@@ -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); |
@@ -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); |
@@ -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); |
@@ -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()); | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |