*/
package org.sonar.server.computation.ws;
+import com.google.common.collect.Lists;
import java.util.Date;
import java.util.List;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.apache.ibatis.session.RowBounds;
+import org.sonar.api.resources.Qualifiers;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.db.ce.CeActivityDto;
import org.sonar.db.ce.CeActivityQuery;
import org.sonar.db.ce.CeTaskTypes;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentDtoFunctions;
+import org.sonar.db.component.ComponentQuery;
+import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.user.UserSession;
import org.sonar.server.ws.WsUtils;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.WsCe;
+import static java.lang.String.format;
+import static org.sonar.server.ws.WsUtils.checkRequest;
+
public class ActivityWsAction implements CeWsAction {
private static final String PARAM_COMPONENT_UUID = "componentId";
+ private static final String PARAM_COMPONENT_QUERY = "componentQuery";
private static final String PARAM_TYPE = "type";
private static final String PARAM_STATUS = "status";
private static final String PARAM_ONLY_CURRENTS = "onlyCurrents";
action.createParam(PARAM_COMPONENT_UUID)
.setDescription("Optional id of the component (project) to filter on")
.setExampleValue(Uuids.UUID_EXAMPLE_03);
+ action.createParam(PARAM_COMPONENT_QUERY)
+ .setDescription(format("Optional search by component name or key. Must not be set together with %s", PARAM_COMPONENT_UUID))
+ .setExampleValue("Apache");
action.createParam(PARAM_STATUS)
.setDescription("Optional filter on task status")
.setPossibleValues(CeActivityDto.Status.values());
public void handle(Request wsRequest, Response wsResponse) throws Exception {
DbSession dbSession = dbClient.openSession(false);
try {
- CeActivityQuery query = readQuery(wsRequest);
+ CeActivityQuery query = buildQuery(dbSession, wsRequest);
+ checkPermissions(query);
+
RowBounds rowBounds = readMyBatisRowBounds(wsRequest);
List<CeActivityDto> dtos = dbClient.ceActivityDao().selectByQuery(dbSession, query, rowBounds);
int total = dbClient.ceActivityDao().countByQuery(dbSession, query);
}
}
- private CeActivityQuery readQuery(Request wsRequest) {
+ private void checkPermissions(CeActivityQuery query) {
+ List<String> componentUuids = query.getComponentUuids();
+ if (componentUuids != null && componentUuids.size() == 1) {
+ if (!userSession.hasGlobalPermission(GlobalPermissions.SYSTEM_ADMIN) &&
+ !userSession.hasComponentUuidPermission(UserRole.ADMIN, componentUuids.get(0))) {
+ throw new ForbiddenException("Requires administration permission");
+ }
+ } else {
+ userSession.checkGlobalPermission(UserRole.ADMIN);
+ }
+ }
+
+ private CeActivityQuery buildQuery(DbSession dbSession, Request wsRequest) {
+ String componentUuid = wsRequest.param(PARAM_COMPONENT_UUID);
+ String componentQuery = wsRequest.param(PARAM_COMPONENT_QUERY);
+ checkRequest(componentUuid == null || componentQuery == null,
+ format("Only one of following parameters is accepted: %s or %s", PARAM_COMPONENT_UUID, PARAM_COMPONENT_QUERY));
+
CeActivityQuery query = new CeActivityQuery();
query.setType(wsRequest.param(PARAM_TYPE));
query.setOnlyCurrents(wsRequest.mandatoryParamAsBoolean(PARAM_ONLY_CURRENTS));
query.setStatus(CeActivityDto.Status.valueOf(status));
}
+ loadComponentUuids(dbSession, wsRequest, query);
+ return query;
+ }
+
+ private void loadComponentUuids(DbSession dbSession, Request wsRequest, CeActivityQuery query) {
String componentUuid = wsRequest.param(PARAM_COMPONENT_UUID);
- if (componentUuid == null) {
- userSession.checkGlobalPermission(UserRole.ADMIN);
- } else {
- if (userSession.hasGlobalPermission(GlobalPermissions.SYSTEM_ADMIN) || userSession.hasComponentUuidPermission(UserRole.ADMIN, componentUuid)) {
- query.setComponentUuid(componentUuid);
- } else {
- throw new ForbiddenException("Requires administration permission");
- }
+ String componentQuery = wsRequest.param(PARAM_COMPONENT_QUERY);
+ if (componentUuid != null && componentQuery != null) {
+ throw new BadRequestException(format("Only one of parameters must be set: %s or %s", PARAM_COMPONENT_UUID, PARAM_COMPONENT_QUERY));
+ }
+
+ if (componentUuid != null) {
+ query.setComponentUuid(componentUuid);
+ }
+ if (componentQuery != null) {
+ ComponentQuery componentDtoQuery = new ComponentQuery(dbClient.getDatabase(), componentQuery, Qualifiers.PROJECT, Qualifiers.VIEW);
+ List<ComponentDto> componentDtos = dbClient.componentDao().selectByQuery(dbSession, componentDtoQuery, 0, CeActivityQuery.MAX_COMPONENT_UUIDS);
+ query.setComponentUuids(Lists.transform(componentDtos, ComponentDtoFunctions.toUuid()));
}
- return query;
}
private static RowBounds readMyBatisRowBounds(Request wsRequest) {
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.plugins.MimeTypes;
+import static java.lang.String.format;
+
public class WsUtils {
private WsUtils() {
/**
* @throws BadRequestException
*/
- public static void checkRequest(boolean expression, String message) {
+ public static void checkRequest(boolean expression, String message, Object... messageArguments) {
if (!expression) {
- throw new BadRequestException(message);
+ throw new BadRequestException(format(message, messageArguments));
}
}
import com.google.common.base.Optional;
import java.io.File;
+import java.io.IOException;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.ExpectedException;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.System2;
import org.sonar.api.web.UserRole;
import org.sonar.db.ce.CeActivityDto;
import org.sonar.db.ce.CeQueueDto;
import org.sonar.db.ce.CeTaskTypes;
+import org.sonar.db.component.ComponentDbTester;
import org.sonar.server.computation.log.CeLogging;
import org.sonar.server.computation.log.LogFileRef;
+import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.plugins.MimeTypes;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.TestResponse;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import static org.sonar.db.component.ComponentTesting.newProjectDto;
public class ActivityWsActionTest {
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
@Rule
public UserSessionRule userSession = UserSessionRule.standalone();
@Rule
public DbTester dbTester = DbTester.create(System2.INSTANCE);
+ ComponentDbTester componentDb = new ComponentDbTester(dbTester);
+
CeLogging ceLogging = mock(CeLogging.class);
TaskFormatter formatter = new TaskFormatter(dbTester.getDbClient(), ceLogging);
ActivityWsAction underTest = new ActivityWsAction(userSession, dbTester.getDbClient(), formatter);
}
@Test
- public void filter_by_status() {
+ public void filter_by_status() {
userSession.setGlobalPermissions(UserRole.ADMIN);
insert("T1", "PROJECT_1", CeActivityDto.Status.SUCCESS);
insert("T2", "PROJECT_2", CeActivityDto.Status.FAILED);
}
@Test
- public void get_project_activity() {
+ public void project_administrator_can_access_his_project_activity() {
+ // no need to be a system admin
userSession.addComponentUuidPermission(UserRole.ADMIN, "PROJECT_1", "PROJECT_1");
insert("T1", "PROJECT_1", CeActivityDto.Status.SUCCESS);
insert("T2", "PROJECT_2", CeActivityDto.Status.FAILED);
.setMediaType(MimeTypes.PROTOBUF)
.execute();
- // verify the protobuf response
WsCe.ActivityResponse activityResponse = Protobuf.read(wsResponse.getInputStream(), WsCe.ActivityResponse.PARSER);
assertThat(activityResponse.getTasksCount()).isEqualTo(1);
assertThat(activityResponse.getTasks(0).getId()).isEqualTo("T1");
assertThat(activityResponse.getTasks(0).getComponentId()).isEqualTo("PROJECT_1");
}
+ @Test
+ public void search_activity_by_component_name() throws IOException {
+ componentDb.insertProjectAndSnapshot(dbTester.getSession(), newProjectDto().setName("apache struts").setUuid("P1"));
+ componentDb.insertProjectAndSnapshot(dbTester.getSession(), newProjectDto().setName("apache zookeeper").setUuid("P2"));
+ componentDb.insertProjectAndSnapshot(dbTester.getSession(), newProjectDto().setName("eclipse").setUuid("P3"));
+ dbTester.commit();
+ componentDb.indexProjects();
+ userSession.setGlobalPermissions(UserRole.ADMIN);
+ insert("T1", "P1", CeActivityDto.Status.SUCCESS);
+ insert("T2", "P2", CeActivityDto.Status.SUCCESS);
+ insert("T3", "P3", CeActivityDto.Status.SUCCESS);
+
+ TestResponse wsResponse = tester.newRequest()
+ .setParam("componentQuery", "apac")
+ .setMediaType(MimeTypes.PROTOBUF)
+ .execute();
+
+ WsCe.ActivityResponse activityResponse = WsCe.ActivityResponse.parseFrom(wsResponse.getInputStream());
+ assertThat(activityResponse.getTasksList()).extracting("id").containsOnly("T1", "T2");
+ }
+
+ @Test
+ public void fail_if_both_filters_on_component_id_and_name() {
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Only one of following parameters is accepted: componentId or componentQuery");
+
+ tester.newRequest()
+ .setParam("componentId", "ID1")
+ .setParam("componentQuery", "apache")
+ .setMediaType(MimeTypes.PROTOBUF)
+ .execute();
+ }
+
private CeActivityDto insert(String taskUuid, String componentUuid, CeActivityDto.Status status) {
CeQueueDto queueDto = new CeQueueDto();
queueDto.setTaskType(CeTaskTypes.REPORT);
activityDto.setStatus(status);
activityDto.setExecutionTimeMs(500L);
dbTester.getDbClient().ceActivityDao().insert(dbTester.getSession(), activityDto);
- dbTester.getSession().commit();
+ dbTester.commit();
return activityDto;
}
}
*/
package org.sonar.server.ws;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.plugins.MimeTypes;
import org.sonarqube.ws.Issues;
public class WsUtilsTest {
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
@Test
public void write_json_by_default() throws Exception {
TestRequest request = new TestRequest();
assertThat(response.stream().mediaType()).isEqualTo(MimeTypes.PROTOBUF);
assertThat(Issues.Issue.parseFrom(response.getFlushedOutput()).getKey()).isEqualTo("I1");
}
+
+ @Test
+ public void checkRequest_ok() {
+ WsUtils.checkRequest(true, "Missing param: %s", "foo");
+ // do not fail
+ }
+
+ @Test
+ public void checkRequest_ko() {
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Missing param: foo");
+
+ WsUtils.checkRequest(false, "Missing param: %s", "foo");
+ }
+
}
public class DatabaseUtils {
- private static final int PARTITION_SIZE_FOR_ORACLE = 1000;
+ public static final int PARTITION_SIZE_FOR_ORACLE = 1000;
public static void closeQuietly(@Nullable Connection connection) {
if (connection != null) {
package org.sonar.db.ce;
import com.google.common.base.Optional;
+import java.util.Collections;
import java.util.List;
import org.apache.ibatis.session.RowBounds;
import org.sonar.api.utils.System2;
* Ordered by id desc -> newest to oldest
*/
public List<CeActivityDto> selectByQuery(DbSession dbSession, CeActivityQuery query, RowBounds rowBounds) {
+ if (query.isShortCircuitedByComponentUuids()) {
+ return Collections.emptyList();
+ }
return mapper(dbSession).selectByQuery(query, rowBounds);
}
public int countByQuery(DbSession dbSession, CeActivityQuery query) {
+ if (query.isShortCircuitedByComponentUuids()) {
+ return 0;
+ }
return mapper(dbSession).countByQuery(query);
}
*/
package org.sonar.db.ce;
+import java.util.List;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
+import org.sonar.db.DatabaseUtils;
+
+import static java.util.Collections.singletonList;
public class CeActivityQuery {
+ public static final int MAX_COMPONENT_UUIDS = DatabaseUtils.PARTITION_SIZE_FOR_ORACLE;
+
private boolean onlyCurrents = false;
- private String componentUuid;
+ private List<String> componentUuids;
private CeActivityDto.Status status;
private String type;
private Long minSubmittedAt;
private Long maxExecutedAt;
@CheckForNull
- public String getComponentUuid() {
- return componentUuid;
+ public List<String> getComponentUuids() {
+ return componentUuids;
+ }
+
+ public CeActivityQuery setComponentUuids(@Nullable List<String> l) {
+ this.componentUuids = l;
+ return this;
+ }
+
+ public boolean isShortCircuitedByComponentUuids() {
+ return componentUuids != null && (componentUuids.isEmpty() || componentUuids.size() > MAX_COMPONENT_UUIDS);
}
- public CeActivityQuery setComponentUuid(@Nullable String componentUuid) {
- this.componentUuid = componentUuid;
+ public CeActivityQuery setComponentUuid(@Nullable String s) {
+ if (s == null) {
+ this.componentUuids = null;
+ } else {
+ this.componentUuids = singletonList(s);
+ }
return this;
}
<select id="selectByQuery" parameterType="map" resultType="org.sonar.db.ce.CeActivityDto">
select
<include refid="columns"/>
- from ce_activity ca
- <where>
- <if test="query.onlyCurrents">
- and ca.is_last=${_true}
- </if>
- <if test="query.componentUuid != null">
- and ca.component_uuid=#{query.componentUuid}
- </if>
- <if test="query.status != null">
- and ca.status=#{query.status}
- </if>
- <if test="query.type != null">
- and ca.task_type=#{query.type}
- </if>
- <if test="query.minSubmittedAt != null">
- and ca.submitted_at >= #{query.minSubmittedAt}
- </if>
- <if test="query.maxExecutedAt != null">
- and ca.executed_at <= #{query.maxExecutedAt}
- </if>
- </where>
+ <include refid="sqlSelectByQuery" />
order by ca.id desc
</select>
<select id="countByQuery" parameterType="map" resultType="int">
select count(ca.id)
+ <include refid="sqlSelectByQuery" />
+ </select>
+
+ <sql id="sqlSelectByQuery">
from ce_activity ca
<where>
<if test="query.onlyCurrents">
and ca.is_last=${_true}
</if>
- <if test="query.componentUuid != null">
- and ca.component_uuid=#{query.componentUuid}
+ <if test="query.componentUuids != null and query.componentUuids.size()>0">
+ and ca.component_uuid in
+ <foreach collection="query.componentUuids" open="(" close=")" item="cUuid" separator=",">
+ #{cUuid}
+ </foreach>
</if>
<if test="query.status != null">
and ca.status=#{query.status}
and ca.executed_at <= #{query.maxExecutedAt}
</if>
</where>
- </select>
+ </sql>
<select id="selectOlderThan" parameterType="long" resultType="org.sonar.db.ce.CeActivityDto">
select <include refid="columns"/>
package org.sonar.db.ce;
import com.google.common.base.Optional;
+import java.util.Collections;
import java.util.List;
import org.apache.ibatis.session.RowBounds;
import org.junit.Rule;
assertThat(underTest.countByQuery(db.getSession(), query)).isEqualTo(1);
}
+ @Test
+ public void selectByQuery_no_results_if_shortcircuited_by_component_uuids() {
+ insert("TASK_1", REPORT, "PROJECT_1", CeActivityDto.Status.SUCCESS);
+
+ CeActivityQuery query = new CeActivityQuery();
+ query.setComponentUuids(Collections.<String>emptyList());
+ assertThat(underTest.selectByQuery(db.getSession(), query, new RowBounds(0, 10))).isEmpty();
+ }
+
+ @Test
+ public void countByQuery_no_results_if_shortcircuited_by_component_uuids() {
+ insert("TASK_1", REPORT, "PROJECT_1", CeActivityDto.Status.SUCCESS);
+
+ CeActivityQuery query = new CeActivityQuery();
+ query.setComponentUuids(Collections.<String>emptyList());
+ assertThat(underTest.countByQuery(db.getSession(), query)).isEqualTo(0);
+ }
+
@Test
public void select_and_count_by_date() {
insertWithDates("UUID1", 1_450_000_000_000L, 1_470_000_000_000L);
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.db.ce;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CeActivityQueryTest {
+
+ CeActivityQuery underTest = new CeActivityQuery();
+
+ @Test
+ public void no_filter_on_component_uuids_by_default() {
+ assertThat(underTest.getComponentUuids()).isNull();
+ assertThat(underTest.isShortCircuitedByComponentUuids()).isFalse();
+ }
+
+ @Test
+ public void filter_on_component_uuid() {
+ underTest.setComponentUuid("UUID1");
+ assertThat(underTest.getComponentUuids()).containsOnly("UUID1");
+ assertThat(underTest.isShortCircuitedByComponentUuids()).isFalse();
+ }
+
+ @Test
+ public void filter_on_multiple_component_uuids() {
+ underTest.setComponentUuids(asList("UUID1", "UUID2"));
+ assertThat(underTest.getComponentUuids()).containsOnly("UUID1", "UUID2");
+ assertThat(underTest.isShortCircuitedByComponentUuids()).isFalse();
+ }
+
+ /**
+ * componentUuid is not null but is set to empty
+ * --> no results
+ */
+ @Test
+ public void short_circuited_if_empty_component_uuid_filter() {
+ underTest.setComponentUuids(Collections.<String>emptyList());
+ assertThat(underTest.getComponentUuids()).isEmpty();
+ assertThat(underTest.isShortCircuitedByComponentUuids()).isTrue();
+ }
+
+ /**
+ * too many componentUuids for SQL request. Waiting for ES to improve this use-case
+ * --> no results
+ */
+ @Test
+ public void short_circuited_if_too_many_component_uuid_filters() {
+ List<String> uuids = new ArrayList<>();
+ for (int i = 0; i < CeActivityQuery.MAX_COMPONENT_UUIDS + 2; i++) {
+ uuids.add(String.valueOf(i));
+ }
+ underTest.setComponentUuids(uuids);
+ assertThat(underTest.getComponentUuids()).hasSize(CeActivityQuery.MAX_COMPONENT_UUIDS + 2);
+ assertThat(underTest.isShortCircuitedByComponentUuids()).isTrue();
+ }
+}