// joins
private String ruleKey;
private String ruleRepo;
+ private boolean isExternal;
private String language;
private String componentKey;
private String moduleUuid;
.setAssignee(issue.assignee())
.setRuleId(ruleId)
.setRuleKey(issue.ruleKey().repository(), issue.ruleKey().rule())
+ .setExternal(issue.isFromExternalRuleEngine())
.setTags(issue.tags())
.setComponentUuid(issue.componentUuid())
.setComponentKey(issue.componentKey())
.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())
public static IssueDto createFor(Project project, RuleDto rule) {
return new IssueDto()
.setProjectUuid(project.getUuid())
+ .setExternal(rule.isExternal())
.setRuleId(rule.getId())
.setKee(Uuids.create());
}
this.ruleKey = rule.getRuleKey();
this.ruleRepo = rule.getRepositoryKey();
this.language = rule.getLanguage();
+ this.isExternal = rule.isExternal();
return this;
}
return this;
}
+ public boolean isExternal() {
+ return isExternal;
+ }
+
+ public IssueDto setExternal(boolean external) {
+ isExternal = external;
+ return this;
+ }
+
public String getComponentKey() {
return componentKey;
}
issue.setUpdateDate(longToDate(issueUpdateDate));
issue.setSelectedAt(selectedAt);
issue.setLocations(parseLocations());
+ issue.setFromExternalRuleEngine(isExternal);
return issue;
}
}
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,
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,
assertThat(issue.getProjectKey()).isEqualTo(PROJECT_KEY);
assertThat(issue.getLocations()).isNull();
assertThat(issue.parseLocations()).isNull();
+ assertThat(issue.isExternal()).isTrue();
}
@Test
}
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));
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);
assertThat(dto.getRule()).isEqualTo("AvoidCycle");
assertThat(dto.getRuleKey().toString()).isEqualTo("squid:AvoidCycle");
assertThat(dto.getLanguage()).isEqualTo("xoo");
+ assertThat(dto.isExternal()).isEqualTo(true);
}
@Test
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");
issue.setSeverity(activeRule.get().getSeverity());
issue.setLine(null);
issue.setChecksum("");
+ issue.setFromExternalRuleEngine(false);
}
}
return 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;
}
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)
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)
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;
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 {
}
}
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);
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) {
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 {
.setScope(ALL)
.setStatus(RuleStatus.READY)
.setSeverity(external.getSeverity())
+ .setStatus(READY)
.setCreatedAt(system2.now())
.setUpdatedAt(system2.now()));
"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>]");
}
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;
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;
@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
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();
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;
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 {
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;
.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");
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));
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());
}
{
"organization": "my-org-2",
"key": "82fd47d4-b650-4037-80bc-7b112bd4eac2",
- "rule": "xoo:x1",
+ "rule": "external_xoo:x1",
"severity": "MAJOR",
"component": "FILE_KEY",
"resolution": "FIXED",
"owasp"
],
"creationDate": "2014-09-04T01:00:00+0200",
- "updateDate": "2017-12-04T00:00:00+0100"
+ "updateDate": "2017-12-04T00:00:00+0100",
+ "externalRuleEngine" : "xoo"
}
]
}
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;
return this;
}
+ public boolean isFromExternalRuleEngine() {
+ return isFromExternalRuleEngine;
+ }
+
+ public DefaultIssue setFromExternalRuleEngine(boolean fromExternalRuleEngine) {
+ isFromExternalRuleEngine = fromExternalRuleEngine;
+ return this;
+ }
+
@Override
@CheckForNull
public String message() {
private String assignee;
private RuleType type;
private Map<String, String> attributes;
+ private boolean isFromExternalRuleEngine;
public DefaultIssueBuilder componentKey(String componentKey) {
this.componentKey = componentKey;
return this;
}
+ public DefaultIssueBuilder fromExternalRuleEngine(boolean fromExternalRuleEngine) {
+ isFromExternalRuleEngine = fromExternalRuleEngine;
+ return this;
+ }
+
@Override
public DefaultIssueBuilder ruleKey(RuleKey ruleKey) {
this.ruleKey = ruleKey;
issue.setCopied(false);
issue.setBeingClosed(false);
issue.setOnDisabledRule(false);
+ issue.setFromExternalRuleEngine(isFromExternalRuleEngine);
return issue;
}
}
.line(123)
.effortToFix(10000.0)
.ruleKey(RuleKey.of("squid", "NullDereference"))
+ .fromExternalRuleEngine(false)
.severity(Severity.CRITICAL)
.attribute("JIRA", "FOO-123")
.attribute("YOUTRACK", "YT-123")
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();
optional string organization = 29;
optional string branch = 30;
optional string pullRequest = 32;
+
+ optional string externalRuleEngine = 33;
}
message Transitions {