@@ -91,6 +91,7 @@ public final class IssueDto implements Serializable { | |||
// joins | |||
private String ruleKey; | |||
private String ruleRepo; | |||
private boolean isExternal; | |||
private String language; | |||
private String componentKey; | |||
private String moduleUuid; | |||
@@ -119,6 +120,7 @@ public final class IssueDto implements Serializable { | |||
.setAssignee(issue.assignee()) | |||
.setRuleId(ruleId) | |||
.setRuleKey(issue.ruleKey().repository(), issue.ruleKey().rule()) | |||
.setExternal(issue.isFromExternalRuleEngine()) | |||
.setTags(issue.tags()) | |||
.setComponentUuid(issue.componentUuid()) | |||
.setComponentKey(issue.componentKey()) | |||
@@ -166,6 +168,7 @@ public final class IssueDto implements Serializable { | |||
.setIssueAttributes(KeyValueFormat.format(issue.attributes())) | |||
.setAuthorLogin(issue.authorLogin()) | |||
.setRuleKey(issue.ruleKey().repository(), issue.ruleKey().rule()) | |||
.setExternal(issue.isFromExternalRuleEngine()) | |||
.setTags(issue.tags()) | |||
.setComponentUuid(issue.componentUuid()) | |||
.setComponentKey(issue.componentKey()) | |||
@@ -185,6 +188,7 @@ public final class IssueDto implements Serializable { | |||
public static IssueDto createFor(Project project, RuleDto rule) { | |||
return new IssueDto() | |||
.setProjectUuid(project.getUuid()) | |||
.setExternal(rule.isExternal()) | |||
.setRuleId(rule.getId()) | |||
.setKee(Uuids.create()); | |||
} | |||
@@ -455,6 +459,7 @@ public final class IssueDto implements Serializable { | |||
this.ruleKey = rule.getRuleKey(); | |||
this.ruleRepo = rule.getRepositoryKey(); | |||
this.language = rule.getLanguage(); | |||
this.isExternal = rule.isExternal(); | |||
return this; | |||
} | |||
@@ -480,6 +485,15 @@ public final class IssueDto implements Serializable { | |||
return this; | |||
} | |||
public boolean isExternal() { | |||
return isExternal; | |||
} | |||
public IssueDto setExternal(boolean external) { | |||
isExternal = external; | |||
return this; | |||
} | |||
public String getComponentKey() { | |||
return componentKey; | |||
} | |||
@@ -719,6 +733,7 @@ public final class IssueDto implements Serializable { | |||
issue.setUpdateDate(longToDate(issueUpdateDate)); | |||
issue.setSelectedAt(selectedAt); | |||
issue.setLocations(parseLocations()); | |||
issue.setFromExternalRuleEngine(isExternal); | |||
return issue; | |||
} | |||
} |
@@ -27,6 +27,7 @@ | |||
i.issue_close_date as issueCloseTime, | |||
i.created_at as createdAt, | |||
i.updated_at as updatedAt, | |||
r.is_external as "isExternal", | |||
r.plugin_rule_key as ruleKey, | |||
r.plugin_name as ruleRepo, | |||
r.language as language, | |||
@@ -84,6 +85,7 @@ | |||
i.issue_close_date as "issueCloseDate", | |||
i.issue_creation_date as "issueCreationDate", | |||
i.issue_update_date as "issueUpdateDate", | |||
r.is_external as "isExternal", | |||
r.plugin_name as "pluginName", | |||
r.plugin_rule_key as "pluginRuleKey", | |||
r.language, |
@@ -100,6 +100,7 @@ public class IssueDaoTest { | |||
assertThat(issue.getProjectKey()).isEqualTo(PROJECT_KEY); | |||
assertThat(issue.getLocations()).isNull(); | |||
assertThat(issue.parseLocations()).isNull(); | |||
assertThat(issue.isExternal()).isTrue(); | |||
} | |||
@Test | |||
@@ -304,7 +305,7 @@ public class IssueDaoTest { | |||
} | |||
private void prepareTables() { | |||
db.rules().insertRule(RULE); | |||
db.rules().insertRule(RULE.setIsExternal(true)); | |||
OrganizationDto organizationDto = db.organizations().insert(); | |||
ComponentDto projectDto = db.components().insertPrivateProject(organizationDto, (t) -> t.setUuid(PROJECT_UUID).setDbKey(PROJECT_KEY)); | |||
db.components().insertComponent(newFileDto(projectDto).setUuid(FILE_UUID).setDbKey(FILE_KEY)); |
@@ -116,7 +116,7 @@ public class IssueDtoTest { | |||
public void set_rule() { | |||
IssueDto dto = new IssueDto() | |||
.setKee("100") | |||
.setRule(new RuleDefinitionDto().setId(1).setRuleKey("AvoidCycle").setRepositoryKey("squid")) | |||
.setRule(new RuleDefinitionDto().setId(1).setRuleKey("AvoidCycle").setRepositoryKey("squid").setIsExternal(true)) | |||
.setLanguage("xoo"); | |||
assertThat(dto.getRuleId()).isEqualTo(1); | |||
@@ -124,6 +124,7 @@ public class IssueDtoTest { | |||
assertThat(dto.getRule()).isEqualTo("AvoidCycle"); | |||
assertThat(dto.getRuleKey().toString()).isEqualTo("squid:AvoidCycle"); | |||
assertThat(dto.getLanguage()).isEqualTo("xoo"); | |||
assertThat(dto.isExternal()).isEqualTo(true); | |||
} | |||
@Test |
@@ -41,12 +41,14 @@ import static java.util.Objects.requireNonNull; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; | |||
import static org.apache.commons.lang.math.RandomUtils.nextInt; | |||
import static org.sonar.api.rule.RuleKey.EXTERNAL_RULE_REPO_PREFIX; | |||
/** | |||
* Utility class for tests involving rules | |||
*/ | |||
public class RuleTesting { | |||
public static final RuleKey EXTERNAL_XOO = RuleKey.of(EXTERNAL_RULE_REPO_PREFIX + "xoo", "x1"); | |||
public static final RuleKey XOO_X1 = RuleKey.of("xoo", "x1"); | |||
public static final RuleKey XOO_X2 = RuleKey.of("xoo", "x2"); | |||
public static final RuleKey XOO_X3 = RuleKey.of("xoo", "x3"); |
@@ -55,6 +55,7 @@ public abstract class CommonRule { | |||
issue.setSeverity(activeRule.get().getSeverity()); | |||
issue.setLine(null); | |||
issue.setChecksum(""); | |||
issue.setFromExternalRuleEngine(false); | |||
} | |||
} | |||
return issue; |
@@ -21,9 +21,12 @@ package org.sonar.server.issue; | |||
import java.util.Collection; | |||
import java.util.Map; | |||
import com.google.common.base.Preconditions; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.core.util.stream.MoreCollectors; | |||
import org.sonar.server.exceptions.BadRequestException; | |||
import org.sonar.server.issue.workflow.Transition; | |||
import org.sonar.server.user.UserSession; | |||
@@ -62,6 +65,9 @@ public class TransitionAction extends Action { | |||
} | |||
private boolean canExecuteTransition(DefaultIssue issue, String transitionKey) { | |||
checkArgument(!issue.isFromExternalRuleEngine(), "No transition allowed on issue from externally define rule"); | |||
return transitionService.listTransitions(issue) | |||
.stream() | |||
.map(Transition::key) |
@@ -162,8 +162,9 @@ public class SearchAction implements IssuesWsAction { | |||
new Change("6.3", "response field 'email' is renamed 'avatar'"), | |||
new Change("5.5", "response fields 'reporter' and 'actionPlan' are removed (drop of action plan and manual issue features)"), | |||
new Change("5.5", "parameters 'reporters', 'actionPlans' and 'planned' are dropped and therefore ignored (drop of action plan and manual issue features)"), | |||
new Change("5.5", "response field 'debt' is renamed 'effort'")) | |||
.setResponseExample(getClass().getResource("search-example.json")); | |||
new Change("5.5", "response field 'debt' is renamed 'effort'"), | |||
new Change("7.2", "response 'issue' now indicates if issue has been created by an externally defined rule engine and his name through the optional 'externalRuleEngine' field") | |||
).setResponseExample(getClass().getResource("search-example.json")); | |||
action.addPagingParams(100, MAX_LIMIT); | |||
action.createParam(Param.FACETS) |
@@ -30,6 +30,7 @@ import java.util.Set; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.resources.Language; | |||
import org.sonar.api.resources.Languages; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.api.utils.DateUtils; | |||
import org.sonar.api.utils.Duration; | |||
import org.sonar.api.utils.Durations; | |||
@@ -59,8 +60,10 @@ import org.sonarqube.ws.Issues.SearchWsResponse; | |||
import org.sonarqube.ws.Issues.Transitions; | |||
import org.sonarqube.ws.Issues.Users; | |||
import static com.google.common.base.Preconditions.checkState; | |||
import static com.google.common.base.Strings.emptyToNull; | |||
import static com.google.common.base.Strings.nullToEmpty; | |||
import static org.sonar.api.rule.RuleKey.EXTERNAL_RULE_REPO_PREFIX; | |||
import static org.sonar.core.util.Protobuf.setNullable; | |||
public class SearchResponseFormat { | |||
@@ -171,6 +174,9 @@ public class SearchResponseFormat { | |||
} | |||
} | |||
issueBuilder.setRule(dto.getRuleKey().toString()); | |||
if (dto.isExternal()) { | |||
issueBuilder.setExternalRuleEngine(engineNameFrom(dto.getRuleKey())); | |||
} | |||
issueBuilder.setSeverity(Common.Severity.valueOf(dto.getSeverity())); | |||
setNullable(emptyToNull(dto.getAssignee()), issueBuilder::setAssignee); | |||
setNullable(emptyToNull(dto.getResolution()), issueBuilder::setResolution); | |||
@@ -192,6 +198,11 @@ public class SearchResponseFormat { | |||
setNullable(dto.getIssueCloseDate(), issueBuilder::setCloseDate, DateUtils::formatDateTime); | |||
} | |||
private String engineNameFrom(RuleKey ruleKey) { | |||
checkState(ruleKey.repository().startsWith(EXTERNAL_RULE_REPO_PREFIX)); | |||
return ruleKey.repository().replace(EXTERNAL_RULE_REPO_PREFIX, ""); | |||
} | |||
private static void completeIssueLocations(IssueDto dto, Issue.Builder issueBuilder, SearchResponseData data) { | |||
DbIssues.Locations locations = dto.parseLocations(); | |||
if (locations == null) { |
@@ -30,6 +30,7 @@ import org.sonar.server.computation.task.projectanalysis.issue.Rule; | |||
import org.sonar.server.computation.task.projectanalysis.issue.RuleImpl; | |||
import org.sonar.server.rule.index.RuleIndexer; | |||
import static org.sonar.api.rule.RuleStatus.READY; | |||
import static org.sonar.db.rule.RuleDto.Scope.ALL; | |||
public class ExternalRuleCreator { | |||
@@ -59,6 +60,7 @@ public class ExternalRuleCreator { | |||
.setScope(ALL) | |||
.setStatus(RuleStatus.READY) | |||
.setSeverity(external.getSeverity()) | |||
.setStatus(READY) | |||
.setCreatedAt(system2.now()) | |||
.setUpdatedAt(system2.now())); | |||
@@ -213,7 +213,7 @@ public class IssueAssignerTest { | |||
"moduleUuid=<null>,moduleUuidPath=<null>,projectUuid=<null>,projectKey=<null>,ruleKey=<null>,language=<null>,severity=<null>," + | |||
"manualSeverity=false,message=<null>,line=2,gap=<null>,effort=<null>,status=<null>,resolution=<null>," + | |||
"assignee=<null>,checksum=<null>,attributes=<null>,authorLogin=<null>,comments=<null>,tags=<null>," + | |||
"locations=<null>,creationDate=<null>,updateDate=<null>,closeDate=<null>,currentChange=<null>,changes=<null>,isNew=true,isCopied=false," + | |||
"locations=<null>,isFromExternalRuleEngine=false,creationDate=<null>,updateDate=<null>,closeDate=<null>,currentChange=<null>,changes=<null>,isNew=true,isCopied=false," + | |||
"beingClosed=false,onDisabledRule=false,isChanged=false,sendNotifications=false,selectedAt=<null>]"); | |||
} | |||
@@ -21,7 +21,9 @@ package org.sonar.server.issue; | |||
import com.google.common.collect.ImmutableMap; | |||
import com.google.common.collect.Lists; | |||
import java.util.Date; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
@@ -42,6 +44,7 @@ import static java.util.Collections.emptyList; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.api.issue.Issue.STATUS_CLOSED; | |||
import static org.sonar.api.web.UserRole.ISSUE_ADMIN; | |||
import static org.sonar.db.component.ComponentTesting.newFileDto; | |||
import static org.sonar.db.issue.IssueTesting.newDto; | |||
@@ -86,11 +89,11 @@ public class TransitionActionTest { | |||
@Test | |||
public void does_not_execute_if_transition_is_not_available() { | |||
loginAndAddProjectPermission("john", ISSUE_ADMIN); | |||
issue.setStatus(Issue.STATUS_CLOSED); | |||
issue.setStatus(STATUS_CLOSED); | |||
action.execute(ImmutableMap.of("transition", "reopen"), context); | |||
assertThat(issue.status()).isEqualTo(Issue.STATUS_CLOSED); | |||
assertThat(issue.status()).isEqualTo(STATUS_CLOSED); | |||
} | |||
@Test | |||
@@ -106,6 +109,21 @@ public class TransitionActionTest { | |||
action.verify(ImmutableMap.of("unknwown", "reopen"), Lists.newArrayList(), userSession); | |||
} | |||
@Test | |||
public void do_not_allow_transitions_for_issues_from_external_rule_engine() { | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("No transition allowed on issue from externally define rule"); | |||
loginAndAddProjectPermission("john", ISSUE_ADMIN); | |||
context.issue() | |||
.setFromExternalRuleEngine(true) | |||
.setStatus(STATUS_CLOSED); | |||
action.execute(ImmutableMap.of("transition", "close"), context); | |||
} | |||
@Test | |||
public void should_support_all_issues() { | |||
assertThat(action.supports(new DefaultIssue().setResolution(null))).isTrue(); |
@@ -35,13 +35,18 @@ import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.component.ComponentDbTester; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.issue.IssueChangeDto; | |||
import org.sonar.db.issue.IssueDbTester; | |||
import org.sonar.db.issue.IssueDto; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.db.rule.RuleDbTester; | |||
import org.sonar.db.rule.RuleDefinitionDto; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.es.EsTester; | |||
import org.sonar.server.exceptions.BadRequestException; | |||
import org.sonar.server.exceptions.UnauthorizedException; | |||
import org.sonar.server.issue.Action; | |||
import org.sonar.server.issue.IssueFieldsSetter; | |||
@@ -86,6 +91,7 @@ import static org.sonar.core.util.Protobuf.setNullable; | |||
import static org.sonar.db.component.ComponentTesting.newFileDto; | |||
import static org.sonar.db.issue.IssueChangeDto.TYPE_COMMENT; | |||
import static org.sonar.db.issue.IssueTesting.newDto; | |||
import static org.sonar.db.rule.RuleTesting.newRule; | |||
import static org.sonar.db.rule.RuleTesting.newRuleDto; | |||
public class BulkChangeActionTest { | |||
@@ -114,6 +120,9 @@ public class BulkChangeActionTest { | |||
private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); | |||
private List<Action> actions = new ArrayList<>(); | |||
private RuleDbTester ruleDbTester = new RuleDbTester(db); | |||
private IssueDbTester issueDbTester = new IssueDbTester(db); | |||
private RuleDto rule; | |||
private OrganizationDto organization; | |||
private ComponentDto project; | |||
@@ -500,6 +509,39 @@ public class BulkChangeActionTest { | |||
.build()); | |||
} | |||
@Test | |||
public void fail_when_requesting_transition_on_issue_from_external_rules_engine(){ | |||
setUserProjectPermissions(USER, ISSUE_ADMIN); | |||
UserDto userToAssign = db.users().insertUser("arthur"); | |||
db.organizations().addMember(organization, user); | |||
db.organizations().addMember(organization, userToAssign); | |||
RuleDefinitionDto rule = ruleDbTester.insert(newRule() | |||
.setIsExternal(true)); | |||
IssueDto issue1 = issueDbTester.insertIssue(newIssue() | |||
.setStatus(STATUS_OPEN) | |||
.setResolution(null) | |||
.setRuleId(rule.getId()) | |||
.setRuleKey(rule.getRuleKey(), rule.getRepositoryKey()) | |||
.setAssignee(user.getLogin()) | |||
.setType(BUG) | |||
.setSeverity(MINOR)); | |||
IssueDto issue2 = issueDbTester.insertIssue(newIssue() | |||
.setStatus(STATUS_OPEN) | |||
.setResolution(null) | |||
.setRuleId(rule.getId()) | |||
.setRuleKey(rule.getRuleKey(), rule.getRepositoryKey()) | |||
.setAssignee(user.getLogin()) | |||
.setType(BUG) | |||
.setSeverity(MAJOR)); | |||
BulkChangeWsResponse confirm = call(builder().setIssues(asList(issue1.getKey(), issue2.getKey())).setDoTransition("confirm").build()); | |||
assertThat(confirm.getFailures()).isEqualTo(2); | |||
} | |||
@Test | |||
public void fail_when_number_of_issues_is_more_than_500() { | |||
userSession.logIn("john"); | |||
@@ -591,6 +633,11 @@ public class BulkChangeActionTest { | |||
return newDto(rule, file, project).setStatus(STATUS_CLOSED).setResolution(RESOLUTION_FIXED); | |||
} | |||
private IssueDto newIssue() { | |||
RuleDto rule = ruleDbTester.insertRule(newRuleDto()); | |||
return newDto(rule, file, project); | |||
} | |||
private void addActions() { | |||
actions.add(new org.sonar.server.issue.AssignAction(db.getDbClient(), issueFieldsSetter)); | |||
actions.add(new org.sonar.server.issue.SetSeverityAction(issueFieldsSetter, userSession)); |
@@ -824,6 +824,16 @@ public class SearchActionTest { | |||
return rule; | |||
} | |||
private RuleDto newExternalRule() { | |||
RuleDto rule = RuleTesting.newDto(RuleTesting.EXTERNAL_XOO).setLanguage("xoo") | |||
.setName("Rule name") | |||
.setDescription("Rule desc") | |||
.setIsExternal(true) | |||
.setStatus(RuleStatus.READY); | |||
db.rules().insert(rule.getDefinition()); | |||
return rule; | |||
} | |||
private void indexPermissions() { | |||
permissionIndexer.indexOnStartup(permissionIndexer.getIndexTypes()); | |||
} |
@@ -3,7 +3,7 @@ | |||
{ | |||
"organization": "my-org-2", | |||
"key": "82fd47d4-b650-4037-80bc-7b112bd4eac2", | |||
"rule": "xoo:x1", | |||
"rule": "external_xoo:x1", | |||
"severity": "MAJOR", | |||
"component": "FILE_KEY", | |||
"resolution": "FIXED", | |||
@@ -19,7 +19,8 @@ | |||
"owasp" | |||
], | |||
"creationDate": "2014-09-04T01:00:00+0200", | |||
"updateDate": "2017-12-04T00:00:00+0100" | |||
"updateDate": "2017-12-04T00:00:00+0100", | |||
"externalRuleEngine" : "xoo" | |||
} | |||
] | |||
} |
@@ -81,6 +81,8 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure. | |||
private Set<String> tags = null; | |||
// temporarily an Object as long as DefaultIssue is used by sonar-batch | |||
private Object locations = null; | |||
private boolean isFromExternalRuleEngine; | |||
// FUNCTIONAL DATES | |||
private Date creationDate; | |||
private Date updateDate; | |||
@@ -239,6 +241,15 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure. | |||
return this; | |||
} | |||
public boolean isFromExternalRuleEngine() { | |||
return isFromExternalRuleEngine; | |||
} | |||
public DefaultIssue setFromExternalRuleEngine(boolean fromExternalRuleEngine) { | |||
isFromExternalRuleEngine = fromExternalRuleEngine; | |||
return this; | |||
} | |||
@Override | |||
@CheckForNull | |||
public String message() { |
@@ -44,6 +44,7 @@ public class DefaultIssueBuilder implements Issuable.IssueBuilder { | |||
private String assignee; | |||
private RuleType type; | |||
private Map<String, String> attributes; | |||
private boolean isFromExternalRuleEngine; | |||
public DefaultIssueBuilder componentKey(String componentKey) { | |||
this.componentKey = componentKey; | |||
@@ -55,6 +56,11 @@ public class DefaultIssueBuilder implements Issuable.IssueBuilder { | |||
return this; | |||
} | |||
public DefaultIssueBuilder fromExternalRuleEngine(boolean fromExternalRuleEngine) { | |||
isFromExternalRuleEngine = fromExternalRuleEngine; | |||
return this; | |||
} | |||
@Override | |||
public DefaultIssueBuilder ruleKey(RuleKey ruleKey) { | |||
this.ruleKey = ruleKey; | |||
@@ -164,6 +170,7 @@ public class DefaultIssueBuilder implements Issuable.IssueBuilder { | |||
issue.setCopied(false); | |||
issue.setBeingClosed(false); | |||
issue.setOnDisabledRule(false); | |||
issue.setFromExternalRuleEngine(isFromExternalRuleEngine); | |||
return issue; | |||
} | |||
} |
@@ -39,6 +39,7 @@ public class DefaultIssueBuilderTest { | |||
.line(123) | |||
.effortToFix(10000.0) | |||
.ruleKey(RuleKey.of("squid", "NullDereference")) | |||
.fromExternalRuleEngine(false) | |||
.severity(Severity.CRITICAL) | |||
.attribute("JIRA", "FOO-123") | |||
.attribute("YOUTRACK", "YT-123") | |||
@@ -53,6 +54,7 @@ public class DefaultIssueBuilderTest { | |||
assertThat(issue.line()).isEqualTo(123); | |||
assertThat(issue.ruleKey().repository()).isEqualTo("squid"); | |||
assertThat(issue.ruleKey().rule()).isEqualTo("NullDereference"); | |||
assertThat(issue.isFromExternalRuleEngine()).isFalse(); | |||
assertThat(issue.severity()).isEqualTo(Severity.CRITICAL); | |||
assertThat(issue.assignee()).isNull(); | |||
assertThat(issue.isNew()).isTrue(); |
@@ -157,6 +157,8 @@ message Issue { | |||
optional string organization = 29; | |||
optional string branch = 30; | |||
optional string pullRequest = 32; | |||
optional string externalRuleEngine = 33; | |||
} | |||
message Transitions { |