--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.hotspot.ws;
+
+import org.sonar.api.server.ws.WebService;
+
+public class HotspotsWs implements WebService {
+
+ private final HotspotsWsAction[] actions;
+
+ public HotspotsWs(HotspotsWsAction... actions) {
+ this.actions = actions;
+ }
+
+ @Override
+ public void define(Context context) {
+ NewController controller = context.createController("api/hotspots");
+ controller.setDescription("Read and update Security Hotspots.");
+ controller.setSince("8.1");
+ for (HotspotsWsAction action : actions) {
+ action.define(controller);
+ }
+ controller.done();
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.hotspot.ws;
+
+import org.sonar.server.ws.WsAction;
+
+public interface HotspotsWsAction extends WsAction {
+ // marker interface
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.hotspot.ws;
+
+import org.sonar.core.platform.Module;
+
+public class HotspotsWsModule extends Module {
+ @Override
+ protected void configureModule() {
+ add(
+ SearchAction.class,
+ HotspotsWs.class
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.hotspot.ws;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.search.SearchHit;
+import org.sonar.api.resources.Language;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.Scopes;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.issue.index.IssueIndex;
+import org.sonar.server.issue.index.IssueQuery;
+import org.sonar.server.organization.DefaultOrganizationProvider;
+import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.Hotspots;
+
+import static com.google.common.base.Strings.nullToEmpty;
+import static java.util.Optional.ofNullable;
+import static org.sonar.api.server.ws.WebService.Param.PAGE;
+import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
+import static org.sonar.api.utils.DateUtils.formatDateTime;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
+
+public class SearchAction implements HotspotsWsAction {
+ private static final String PARAM_PROJECT_KEY = "projectKey";
+
+ private final DbClient dbClient;
+ private final UserSession userSession;
+ private final IssueIndex issueIndex;
+ private final DefaultOrganizationProvider defaultOrganizationProvider;
+ private final Languages languages;
+
+ public SearchAction(DbClient dbClient, UserSession userSession, IssueIndex issueIndex, DefaultOrganizationProvider defaultOrganizationProvider, Languages languages) {
+ this.dbClient = dbClient;
+ this.userSession = userSession;
+ this.issueIndex = issueIndex;
+ this.defaultOrganizationProvider = defaultOrganizationProvider;
+ this.languages = languages;
+ }
+
+ @Override
+ public void define(WebService.NewController controller) {
+
+ WebService.NewAction action = controller
+ .createAction("search")
+ .setHandler(this)
+ .setDescription("Search for Security Hotpots.")
+ .setSince("8.1")
+ .setInternal(true);
+
+ action.addPagingParams(100);
+ action.createParam(PARAM_PROJECT_KEY)
+ .setDescription("Key of the project")
+ .setExampleValue(KEY_PROJECT_EXAMPLE_001)
+ .setRequired(true);
+ // FIXME add response example and test it
+ // action.setResponseExample()
+ }
+
+ @Override
+ public void handle(Request request, Response response) throws Exception {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ String projectKey = request.mandatoryParam(PARAM_PROJECT_KEY);
+ ComponentDto project = dbClient.componentDao().selectByKey(dbSession, projectKey)
+ .filter(t -> Scopes.PROJECT.equals(t.scope()) && Qualifiers.PROJECT.equals(t.qualifier()))
+ .filter(ComponentDto::isEnabled)
+ .filter(t -> t.getMainBranchProjectUuid() == null)
+ .orElseThrow(() -> new NotFoundException(String.format("Project '%s' not found", projectKey)));
+ userSession.checkComponentPermission(UserRole.USER, project);
+
+ List<IssueDto> orderedIssues = searchHotspots(request, dbSession, project);
+ SearchResponseData searchResponseData = new SearchResponseData(orderedIssues);
+ loadComponents(dbSession, searchResponseData);
+ loadRules(dbSession, searchResponseData);
+ writeProtobuf(formatResponse(searchResponseData), request, response);
+ }
+ }
+
+ private List<IssueDto> searchHotspots(Request request, DbSession dbSession, ComponentDto project) {
+ List<String> issueKeys = searchHotspots(request, project);
+
+ List<IssueDto> unorderedIssues = dbClient.issueDao().selectByKeys(dbSession, issueKeys);
+ Map<String, IssueDto> unorderedIssuesMap = unorderedIssues
+ .stream()
+ .collect(uniqueIndex(IssueDto::getKey, unorderedIssues.size()));
+
+ return issueKeys.stream()
+ .map(unorderedIssuesMap::get)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+
+ private List<String> searchHotspots(Request request, ComponentDto project) {
+ IssueQuery.Builder builder = IssueQuery.builder()
+ .projectUuids(Collections.singletonList(project.uuid()))
+ .organizationUuid(project.getOrganizationUuid())
+ .types(Collections.singleton(RuleType.SECURITY_HOTSPOT.name()))
+ .resolved(false);
+ IssueQuery query = builder.build();
+ SearchOptions searchOptions = new SearchOptions()
+ .setPage(request.mandatoryParamAsInt(PAGE), request.mandatoryParamAsInt(PAGE_SIZE));
+ SearchResponse result = issueIndex.search(query, searchOptions);
+ return Arrays.stream(result.getHits().getHits())
+ .map(SearchHit::getId)
+ .collect(MoreCollectors.toList(result.getHits().getHits().length));
+ }
+
+ private void loadComponents(DbSession dbSession, SearchResponseData searchResponseData) {
+ Set<String> componentKeys = searchResponseData.getOrderedHotspots()
+ .stream()
+ .flatMap(hotspot -> Stream.of(hotspot.getComponentKey(), hotspot.getProjectKey()))
+ .collect(Collectors.toSet());
+ if (!componentKeys.isEmpty()) {
+ searchResponseData.addComponents(dbClient.componentDao().selectByDbKeys(dbSession, componentKeys));
+ }
+ }
+
+ private void loadRules(DbSession dbSession, SearchResponseData searchResponseData) {
+ Set<RuleKey> ruleKeys = searchResponseData.getOrderedHotspots()
+ .stream()
+ .map(IssueDto::getRuleKey)
+ .collect(Collectors.toSet());
+ if (!ruleKeys.isEmpty()) {
+ searchResponseData.addRules(dbClient.ruleDao().selectDefinitionByKeys(dbSession, ruleKeys));
+ }
+ }
+
+ private Hotspots.SearchWsResponse formatResponse(SearchResponseData searchResponseData) {
+ Hotspots.SearchWsResponse.Builder responseBuilder = Hotspots.SearchWsResponse.newBuilder();
+ if (!searchResponseData.isEmpty()) {
+ formatHotspots(searchResponseData, responseBuilder);
+ formatComponents(searchResponseData, responseBuilder);
+ formatRules(searchResponseData, responseBuilder);
+ }
+ return responseBuilder.build();
+ }
+
+ private static void formatHotspots(SearchResponseData searchResponseData, Hotspots.SearchWsResponse.Builder responseBuilder) {
+ List<IssueDto> orderedHotspots = searchResponseData.getOrderedHotspots();
+ if (orderedHotspots.isEmpty()) {
+ return;
+ }
+
+ Hotspots.Hotspot.Builder builder = Hotspots.Hotspot.newBuilder();
+ for (IssueDto hotspot : orderedHotspots) {
+ builder
+ .clear()
+ .setKey(hotspot.getKey())
+ .setComponent(hotspot.getComponentKey())
+ .setProject(hotspot.getProjectKey())
+ .setRule(hotspot.getRuleKey().toString());
+ ofNullable(hotspot.getStatus()).ifPresent(builder::setStatus);
+// FIXME resolution field will be added later
+// ofNullable(hotspot.getResolution()).ifPresent(builder::setResolution);
+ ofNullable(hotspot.getLine()).ifPresent(builder::setLine);
+ builder.setMessage(nullToEmpty(hotspot.getMessage()));
+ ofNullable(hotspot.getAssigneeUuid()).ifPresent(builder::setAssignee);
+ // FIXME Filter author only if user is member of the organization (as done in issues/search WS)
+// if (data.getUserOrganizationUuids().contains(component.getOrganizationUuid())) {
+ builder.setAuthor(nullToEmpty(hotspot.getAuthorLogin()));
+// }
+ builder.setCreationDate(formatDateTime(hotspot.getIssueCreationDate()));
+ builder.setUpdateDate(formatDateTime(hotspot.getIssueUpdateDate()));
+
+ responseBuilder.addHotspots(builder.build());
+ }
+ }
+
+ private void formatComponents(SearchResponseData searchResponseData, Hotspots.SearchWsResponse.Builder responseBuilder) {
+ Set<ComponentDto> components = searchResponseData.getComponents();
+ if (components.isEmpty()) {
+ return;
+ }
+
+ Hotspots.Component.Builder builder = Hotspots.Component.newBuilder();
+ for (ComponentDto component : components) {
+ builder
+ .clear()
+ .setOrganization(defaultOrganizationProvider.get().getKey())
+ .setKey(component.getKey())
+ .setQualifier(component.qualifier())
+ .setName(component.name())
+ .setLongName(component.longName());
+ ofNullable(component.path()).ifPresent(builder::setPath);
+
+ responseBuilder.addComponents(builder.build());
+ }
+ }
+
+ private void formatRules(SearchResponseData searchResponseData, Hotspots.SearchWsResponse.Builder responseBuilder) {
+ Set<RuleDefinitionDto> rules = searchResponseData.getRules();
+ if (rules.isEmpty()) {
+ return;
+ }
+
+ Common.Rules.Builder rulesBuilder = Common.Rules.newBuilder();
+ Common.Rule.Builder ruleBuilder = Common.Rule.newBuilder();
+ for (RuleDefinitionDto rule : rules) {
+ ruleBuilder
+ .clear()
+ .setKey(rule.getKey().toString())
+ .setName(nullToEmpty(rule.getName()))
+ .setStatus(Common.RuleStatus.valueOf(rule.getStatus().name()));
+
+ String language = rule.getLanguage();
+ if (language == null) {
+ ruleBuilder.setLang("");
+ } else {
+ ruleBuilder.setLang(language);
+ Language lang = languages.get(language);
+ if (lang != null) {
+ ruleBuilder.setLangName(lang.getName());
+ }
+ }
+ rulesBuilder.addRules(ruleBuilder.build());
+ }
+ responseBuilder.setRules(rulesBuilder.build());
+ }
+
+ private static final class SearchResponseData {
+ private final List<IssueDto> orderedHotspots;
+ private final Set<ComponentDto> components = new HashSet<>();
+ private final Set<RuleDefinitionDto> rules = new HashSet<>();
+
+ private SearchResponseData(List<IssueDto> orderedHotspots) {
+ this.orderedHotspots = orderedHotspots;
+ }
+
+ boolean isEmpty() {
+ return orderedHotspots.isEmpty();
+ }
+
+ List<IssueDto> getOrderedHotspots() {
+ return orderedHotspots;
+ }
+
+ void addComponents(Collection<ComponentDto> components) {
+ this.components.addAll(components);
+ }
+
+ Set<ComponentDto> getComponents() {
+ return components;
+ }
+
+ void addRules(Collection<RuleDefinitionDto> rules) {
+ this.rules.addAll(rules);
+ }
+
+ Set<RuleDefinitionDto> getRules() {
+ return rules;
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.hotspot.ws;
+
+import javax.annotation.ParametersAreNonnullByDefault;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.hotspot.ws;
+
+import org.junit.Test;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER;
+
+public class HotspotsWsModuleTest {
+ @Test
+ public void verify_count_of_added_components() {
+ ComponentContainer container = new ComponentContainer();
+ new HotspotsWsModule().configure(container);
+ assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 2);
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.hotspot.ws;
+
+import java.util.Arrays;
+import java.util.Random;
+import java.util.stream.IntStream;
+import org.junit.Test;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class HotspotsWsTest {
+
+ @Test
+ public void define_controller() {
+ String[] actionKeys = IntStream.range(0, 1 + new Random().nextInt(12))
+ .mapToObj(i -> i + randomAlphanumeric(10))
+ .toArray(String[]::new);
+ HotspotsWsAction[] actions = Arrays.stream(actionKeys)
+ .map(actionKey -> new HotspotsWsAction() {
+ @Override
+ public void define(WebService.NewController context) {
+ context.createAction(actionKey).setHandler(this);
+ }
+
+ @Override
+ public void handle(Request request, Response response) {
+
+ }
+ })
+ .toArray(HotspotsWsAction[]::new);
+ WebService.Context context = new WebService.Context();
+ new HotspotsWs(actions).define(context);
+ WebService.Controller controller = context.controller("api/hotspots");
+
+ assertThat(controller).isNotNull();
+ assertThat(controller.description()).isNotEmpty();
+ assertThat(controller.since()).isEqualTo("8.1");
+ assertThat(controller.actions()).extracting(WebService.Action::key).containsOnly(actionKeys);
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.hotspot.ws;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Random;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.resources.AbstractLanguage;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentTesting;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleTesting;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.es.StartupIndexer;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.issue.index.IssueIndex;
+import org.sonar.server.issue.index.IssueIndexer;
+import org.sonar.server.issue.index.IssueIteratorFactory;
+import org.sonar.server.organization.TestDefaultOrganizationProvider;
+import org.sonar.server.permission.index.PermissionIndexer;
+import org.sonar.server.permission.index.WebAuthorizationTypeSupport;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.Common.RuleStatus;
+import org.sonarqube.ws.Hotspots;
+import org.sonarqube.ws.Hotspots.Component;
+import org.sonarqube.ws.Hotspots.SearchWsResponse;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
+import static org.sonar.api.utils.DateUtils.formatDateTime;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+import static org.sonar.db.component.ComponentTesting.newDirectory;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+
+public class SearchActionTest {
+ private static final Random RANDOM = new Random();
+
+ @Rule
+ public DbTester dbTester = DbTester.create(System2.INSTANCE);
+ @Rule
+ public EsTester es = EsTester.create();
+ @Rule
+ public UserSessionRule userSessionRule = UserSessionRule.standalone();
+
+ private DbClient dbClient = dbTester.getDbClient();
+ private TestDefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(dbTester);
+
+ private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSessionRule, new WebAuthorizationTypeSupport(userSessionRule));
+ private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient));
+ private StartupIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer);
+
+ private Languages languages = mock(Languages.class);
+ private SearchAction underTest = new SearchAction(dbClient, userSessionRule, issueIndex, defaultOrganizationProvider, languages);
+ private WsActionTester actionTester = new WsActionTester(underTest);
+
+ @Test
+ public void ws_is_internal() {
+ assertThat(actionTester.getDef().isInternal()).isTrue();
+ }
+
+ @Test
+ public void fails_with_IAE_if_parameter_projectKey_is_missing() {
+ TestRequest request = actionTester.newRequest();
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("The 'projectKey' parameter is missing");
+ }
+
+ @Test
+ public void fails_with_NotFoundException_if_project_does_not_exist() {
+ String key = randomAlphabetic(12);
+ TestRequest request = actionTester.newRequest()
+ .setParam("projectKey", key);
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(NotFoundException.class)
+ .hasMessage("Project '%s' not found", key);
+ }
+
+ @Test
+ public void fails_with_NotFoundException_if_project_is_not_a_project() {
+ ComponentDto project = dbTester.components().insertPrivateProject();
+ ComponentDto directory = dbTester.components().insertComponent(ComponentTesting.newDirectory(project, "foo"));
+ ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project));
+ ComponentDto portfolio = dbTester.components().insertPrivatePortfolio(dbTester.getDefaultOrganization());
+ ComponentDto application = dbTester.components().insertPrivateApplication(dbTester.getDefaultOrganization());
+ TestRequest request = actionTester.newRequest();
+
+ for (ComponentDto component : Arrays.asList(directory, file, portfolio, application)) {
+ request.setParam("projectKey", component.getKey());
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(NotFoundException.class)
+ .hasMessage("Project '%s' not found", component.getKey());
+ }
+ }
+
+ @Test
+ public void fails_with_ForbiddenException_if_project_is_private_and_not_allowed() {
+ ComponentDto project = dbTester.components().insertPrivateProject();
+ userSessionRule.registerComponents(project);
+ TestRequest request = actionTester.newRequest()
+ .setParam("projectKey", project.getKey());
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(ForbiddenException.class)
+ .hasMessage("Insufficient privileges");
+ }
+
+ @Test
+ public void succeeds_on_public_project() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+
+ SearchWsResponse response = actionTester.newRequest()
+ .setParam("projectKey", project.getKey())
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList()).isEmpty();
+ assertThat(response.getComponentsList()).isEmpty();
+ assertThat(response.getRules().getRulesList()).isEmpty();
+ }
+
+ @Test
+ public void succeeds_on_private_project_with_permission() {
+ ComponentDto project = dbTester.components().insertPrivateProject();
+ userSessionRule.registerComponents(project);
+ userSessionRule.logIn().addProjectPermission(UserRole.USER, project);
+
+ SearchWsResponse response = actionTester.newRequest()
+ .setParam("projectKey", project.getKey())
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList()).isEmpty();
+ assertThat(response.getComponentsList()).isEmpty();
+ assertThat(response.getRules().getRulesList()).isEmpty();
+ }
+
+ @Test
+ public void returns_no_hotspot_component_nor_rule_when_project_has_no_hotspot() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ indexPermissions();
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ Arrays.stream(RuleType.values())
+ .filter(t -> t != SECURITY_HOTSPOT)
+ .forEach(ruleType -> {
+ RuleDefinitionDto rule = newRule(ruleType);
+ dbTester.issues().insert(rule, project, file, t -> t.setType(ruleType));
+ });
+ indexIssues();
+
+ SearchWsResponse response = actionTester.newRequest()
+ .setParam("projectKey", project.getKey())
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList()).isEmpty();
+ assertThat(response.getComponentsList()).isEmpty();
+ assertThat(response.getRules().getRulesList()).isEmpty();
+ }
+
+ @Test
+ public void returns_hotspot_component_and_rule_when_project_has_hotspots() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ indexPermissions();
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ ComponentDto fileWithHotspot = dbTester.components().insertComponent(newFileDto(project));
+ Arrays.stream(RuleType.values())
+ .filter(t -> t != SECURITY_HOTSPOT)
+ .forEach(ruleType -> {
+ RuleDefinitionDto rule = newRule(ruleType);
+ dbTester.issues().insert(rule, project, file, t -> t.setType(ruleType));
+ });
+ IssueDto[] hotspots = IntStream.range(0, 1 + RANDOM.nextInt(10))
+ .mapToObj(i -> {
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ return dbTester.issues().insert(rule, project, fileWithHotspot, t -> t.setType(SECURITY_HOTSPOT));
+ })
+ .toArray(IssueDto[]::new);
+ indexIssues();
+
+ SearchWsResponse response = actionTester.newRequest()
+ .setParam("projectKey", project.getKey())
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList())
+ .extracting(Hotspots.Hotspot::getKey)
+ .containsOnly(Arrays.stream(hotspots).map(IssueDto::getKey).toArray(String[]::new));
+ assertThat(response.getComponentsList())
+ .extracting(Component::getKey)
+ .containsOnly(project.getKey(), fileWithHotspot.getKey());
+ assertThat(response.getRules().getRulesList())
+ .extracting(Common.Rule::getKey)
+ .containsOnly(Arrays.stream(hotspots).map(t -> t.getRuleKey().toString()).toArray(String[]::new));
+ }
+
+ @Test
+ public void returns_hotspots_of_specified_project() {
+ ComponentDto project1 = dbTester.components().insertPublicProject();
+ ComponentDto project2 = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project1, project2);
+ indexPermissions();
+ ComponentDto file1 = dbTester.components().insertComponent(newFileDto(project1));
+ ComponentDto file2 = dbTester.components().insertComponent(newFileDto(project2));
+ IssueDto[] hotspots2 = IntStream.range(0, 1 + RANDOM.nextInt(10))
+ .mapToObj(i -> {
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ dbTester.issues().insert(rule, project1, file1, t -> t.setType(SECURITY_HOTSPOT));
+ return dbTester.issues().insert(rule, project2, file2, t -> t.setType(SECURITY_HOTSPOT));
+ })
+ .toArray(IssueDto[]::new);
+ indexIssues();
+
+ SearchWsResponse responseProject1 = actionTester.newRequest()
+ .setParam("projectKey", project1.getKey())
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(responseProject1.getHotspotsList())
+ .extracting(Hotspots.Hotspot::getKey)
+ .doesNotContainAnyElementsOf(Arrays.stream(hotspots2).map(IssueDto::getKey).collect(Collectors.toList()));
+ assertThat(responseProject1.getComponentsList())
+ .extracting(Component::getKey)
+ .containsOnly(project1.getKey(), file1.getKey());
+ assertThat(responseProject1.getRules().getRulesList())
+ .extracting(Common.Rule::getKey)
+ .containsOnly(Arrays.stream(hotspots2).map(t -> t.getRuleKey().toString()).toArray(String[]::new));
+
+ SearchWsResponse responseProject2 = actionTester.newRequest()
+ .setParam("projectKey", project2.getKey())
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(responseProject2.getHotspotsList())
+ .extracting(Hotspots.Hotspot::getKey)
+ .containsOnly(Arrays.stream(hotspots2).map(IssueDto::getKey).toArray(String[]::new));
+ assertThat(responseProject2.getComponentsList())
+ .extracting(Component::getKey)
+ .containsOnly(project2.getKey(), file2.getKey());
+ assertThat(responseProject2.getRules().getRulesList())
+ .extracting(Common.Rule::getKey)
+ .containsOnly(Arrays.stream(hotspots2).map(t -> t.getRuleKey().toString()).toArray(String[]::new));
+ }
+
+ @Test
+ public void returns_only_unresolved_hotspots() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ indexPermissions();
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ IssueDto unresolvedHotspot = dbTester.issues().insert(rule, project, file,
+ t -> t.setType(SECURITY_HOTSPOT).setResolution(null));
+ // unrealistic case since a resolution must be set, but shows a limit of current implementation
+ IssueDto badlyClosedHotspot = dbTester.issues().insert(rule, project, file,
+ t -> t.setType(SECURITY_HOTSPOT).setStatus(Issue.STATUS_CLOSED).setResolution(null));
+ IssueDto resolvedHotspot = dbTester.issues().insert(rule, project, file,
+ t -> t.setType(SECURITY_HOTSPOT).setResolution(randomAlphabetic(5)));
+ indexIssues();
+
+ SearchWsResponse response = actionTester.newRequest()
+ .setParam("projectKey", project.getKey())
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList())
+ .extracting(Hotspots.Hotspot::getKey)
+ .containsOnly(unresolvedHotspot.getKey(), badlyClosedHotspot.getKey());
+ }
+
+ @Test
+ public void returns_fields_of_hotspot() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ indexPermissions();
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ IssueDto hotspot = dbTester.issues().insert(rule, project, file,
+ t -> t.setType(SECURITY_HOTSPOT)
+ .setStatus(randomAlphabetic(11))
+ .setLine(RANDOM.nextInt(230))
+ .setMessage(randomAlphabetic(10))
+ .setAssigneeUuid(randomAlphabetic(9))
+ .setAuthorLogin(randomAlphabetic(8)));
+ indexIssues();
+
+ SearchWsResponse response = actionTester.newRequest()
+ .setParam("projectKey", project.getKey())
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList()).hasSize(1);
+ Hotspots.Hotspot actual = response.getHotspots(0);
+ assertThat(actual.getComponent()).isEqualTo(file.getKey());
+ assertThat(actual.getProject()).isEqualTo(project.getKey());
+ assertThat(actual.getRule()).isEqualTo(hotspot.getRuleKey().toString());
+ assertThat(actual.getStatus()).isEqualTo(hotspot.getStatus());
+ // FIXME resolution field will be added later
+ // assertThat(actual.getResolution()).isEqualTo(hotspot.getResolution());
+ assertThat(actual.getLine()).isEqualTo(hotspot.getLine());
+ assertThat(actual.getMessage()).isEqualTo(hotspot.getMessage());
+ assertThat(actual.getAssignee()).isEqualTo(hotspot.getAssigneeUuid());
+ assertThat(actual.getAuthor()).isEqualTo(hotspot.getAuthorLogin());
+ assertThat(actual.getCreationDate()).isEqualTo(formatDateTime(hotspot.getIssueCreationDate()));
+ assertThat(actual.getUpdateDate()).isEqualTo(formatDateTime(hotspot.getIssueUpdateDate()));
+ }
+
+ @Test
+ public void does_not_fail_when_hotspot_has_none_of_the_nullable_fields() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ indexPermissions();
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ dbTester.issues().insert(rule, project, file,
+ t -> t.setType(SECURITY_HOTSPOT)
+ .setStatus(null)
+ // FIXME resolution field will be added later
+ // .setResolution(null)
+ .setLine(null)
+ .setMessage(null)
+ .setAssigneeUuid(null)
+ .setAuthorLogin(null));
+ indexIssues();
+
+ SearchWsResponse response = actionTester.newRequest()
+ .setParam("projectKey", project.getKey())
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList())
+ .hasSize(1);
+ Hotspots.Hotspot actual = response.getHotspots(0);
+ assertThat(actual.hasStatus()).isFalse();
+ // FIXME resolution field will be added later
+ // assertThat(actual.hasResolution()).isFalse();
+ assertThat(actual.hasLine()).isFalse();
+ assertThat(actual.getMessage()).isEmpty();
+ assertThat(actual.hasAssignee()).isFalse();
+ assertThat(actual.getAuthor()).isEmpty();
+ }
+
+ @Test
+ public void returns_details_of_components() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ indexPermissions();
+ ComponentDto directory = dbTester.components().insertComponent(newDirectory(project, "donut/acme"));
+ ComponentDto directory2 = dbTester.components().insertComponent(newDirectory(project, "foo/bar"));
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ ComponentDto file2 = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ IssueDto fileHotspot = dbTester.issues().insert(rule, project, file, t -> t.setType(SECURITY_HOTSPOT));
+ IssueDto dirHotspot = dbTester.issues().insert(rule, project, directory, t -> t.setType(SECURITY_HOTSPOT));
+ IssueDto projectHotspot = dbTester.issues().insert(rule, project, project, t -> t.setType(SECURITY_HOTSPOT));
+ indexIssues();
+
+ SearchWsResponse response = actionTester.newRequest()
+ .setParam("projectKey", project.getKey())
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList())
+ .extracting(Hotspots.Hotspot::getKey)
+ .containsOnly(fileHotspot.getKey(), dirHotspot.getKey(), projectHotspot.getKey());
+ assertThat(response.getComponentsList()).hasSize(3);
+ assertThat(response.getComponentsList())
+ .extracting(Component::getOrganization)
+ .containsOnly(defaultOrganizationProvider.get().getKey());
+ assertThat(response.getComponentsList())
+ .extracting(Component::getKey)
+ .containsOnly(project.getKey(), directory.getKey(), file.getKey());
+ Map<String, Component> componentByKey = response.getComponentsList().stream().collect(uniqueIndex(Component::getKey));
+ Component actualProject = componentByKey.get(project.getKey());
+ assertThat(actualProject.getQualifier()).isEqualTo(project.qualifier());
+ assertThat(actualProject.getName()).isEqualTo(project.name());
+ assertThat(actualProject.getLongName()).isEqualTo(project.longName());
+ assertThat(actualProject.hasPath()).isFalse();
+ Component actualDirectory = componentByKey.get(directory.getKey());
+ assertThat(actualDirectory.getQualifier()).isEqualTo(directory.qualifier());
+ assertThat(actualDirectory.getName()).isEqualTo(directory.name());
+ assertThat(actualDirectory.getLongName()).isEqualTo(directory.longName());
+ assertThat(actualDirectory.getPath()).isEqualTo(directory.path());
+ Component actualFile = componentByKey.get(file.getKey());
+ assertThat(actualFile.getQualifier()).isEqualTo(file.qualifier());
+ assertThat(actualFile.getName()).isEqualTo(file.name());
+ assertThat(actualFile.getLongName()).isEqualTo(file.longName());
+ assertThat(actualFile.getPath()).isEqualTo(file.path());
+ }
+
+ @Test
+ public void returns_details_of_rule_with_language_name_when_available() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ indexPermissions();
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ String language1 = randomAlphabetic(3);
+ String language2 = randomAlphabetic(2);
+ RuleDefinitionDto rule1a = newRule(SECURITY_HOTSPOT, t -> t.setLanguage(language1));
+ RuleDefinitionDto rule1b = newRule(SECURITY_HOTSPOT, t -> t.setLanguage(language1));
+ RuleDefinitionDto rule2 = newRule(SECURITY_HOTSPOT, t -> t.setLanguage(language2));
+ when(languages.get(language1)).thenReturn(new AbstractLanguage(language1, language1 + "_name") {
+ @Override
+ public String[] getFileSuffixes() {
+ return new String[0];
+ }
+ });
+ IssueDto hotspot1a = dbTester.issues().insert(rule1a, project, file, t -> t.setType(SECURITY_HOTSPOT));
+ IssueDto hotspot1b = dbTester.issues().insert(rule1b, project, file, t -> t.setType(SECURITY_HOTSPOT));
+ IssueDto hotspot2 = dbTester.issues().insert(rule2, project, file, t -> t.setType(SECURITY_HOTSPOT));
+ indexIssues();
+
+ SearchWsResponse response = actionTester.newRequest()
+ .setParam("projectKey", project.getKey())
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList()).hasSize(3);
+ assertThat(response.getRules().getRulesList()).hasSize(3);
+ Map<RuleKey, Common.Rule> rulesByKey = response.getRules().getRulesList()
+ .stream()
+ .collect(uniqueIndex(t -> RuleKey.parse(t.getKey())));
+ Common.Rule actualRule1a = rulesByKey.get(hotspot1a.getRuleKey());
+ assertThat(actualRule1a.getName()).isEqualTo(rule1a.getName());
+ assertThat(actualRule1a.getLang()).isEqualTo(rule1a.getLanguage());
+ assertThat(actualRule1a.getLangName()).isEqualTo(rule1a.getLanguage() + "_name");
+ assertThat(actualRule1a.getStatus()).isEqualTo(RuleStatus.valueOf(rule1a.getStatus().name()));
+ Common.Rule actualRule1b = rulesByKey.get(hotspot1b.getRuleKey());
+ assertThat(actualRule1b.getName()).isEqualTo(rule1b.getName());
+ assertThat(actualRule1b.getLang()).isEqualTo(rule1b.getLanguage());
+ assertThat(actualRule1b.getLangName()).isEqualTo(rule1b.getLanguage() + "_name");
+ assertThat(actualRule1b.getStatus()).isEqualTo(RuleStatus.valueOf(rule1b.getStatus().name()));
+ Common.Rule actualRule2 = rulesByKey.get(hotspot2.getRuleKey());
+ assertThat(actualRule2.getName()).isEqualTo(rule2.getName());
+ assertThat(actualRule2.getLang()).isEqualTo(rule2.getLanguage());
+ assertThat(actualRule2.hasLangName()).isFalse();
+ assertThat(actualRule2.getStatus()).isEqualTo(RuleStatus.valueOf(rule2.getStatus().name()));
+ }
+
+ private void indexPermissions() {
+ permissionIndexer.indexOnStartup(permissionIndexer.getIndexTypes());
+ }
+
+ private void indexIssues() {
+ issueIndexer.indexOnStartup(issueIndexer.getIndexTypes());
+ }
+
+ private RuleDefinitionDto newRule(RuleType ruleType) {
+ return newRule(ruleType, t -> {
+ });
+ }
+
+ private RuleDefinitionDto newRule(RuleType ruleType, Consumer<RuleDefinitionDto> populate) {
+ RuleDefinitionDto ruleDefinition = RuleTesting.newRule()
+ .setType(ruleType);
+ populate.accept(ruleDefinition);
+ dbTester.rules().insert(ruleDefinition);
+ return ruleDefinition;
+ }
+}
import org.sonar.server.favorite.FavoriteModule;
import org.sonar.server.favorite.ws.FavoriteWsModule;
import org.sonar.server.health.NodeHealthModule;
+import org.sonar.server.hotspot.ws.HotspotsWsModule;
import org.sonar.server.issue.AddTagsAction;
import org.sonar.server.issue.AssignAction;
import org.sonar.server.issue.CommentAction;
RemoveTagsAction.class,
IssueChangePostProcessorImpl.class,
+ // hotspots
+ HotspotsWsModule.class,
+
// source
SourceWsModule.class,
--- /dev/null
+// SonarQube, open source software quality management tool.
+// Copyright (C) 2008-2016 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.
+
+syntax = "proto2";
+
+package sonarqube.ws.hotspots;
+
+import "ws-commons.proto";
+
+option java_package = "org.sonarqube.ws";
+option java_outer_classname = "Hotspots";
+option optimize_for = SPEED;
+
+// Response of GET api/hotspots/search
+message SearchWsResponse {
+ optional sonarqube.ws.commons.Paging paging = 1;
+ repeated Hotspot hotspots = 2;
+ repeated Component components = 3;
+ optional sonarqube.ws.commons.Rules rules = 4;
+}
+
+message Hotspot {
+ optional string key = 1;
+ optional string component = 2;
+ optional string project = 3;
+ optional string rule = 4;
+ optional string status = 5;
+// FIXME resolution field will be added later
+// optional string resolution = 6;
+ optional int32 line = 7;
+ optional string message = 8;
+ optional string assignee = 9;
+ // SCM login of the committer who introduced the issue
+ optional string author = 10;
+ optional string creationDate = 11;
+ optional string updateDate = 12;
+}
+
+message Component {
+ optional string organization = 1;
+ optional string key = 2;
+ optional string qualifier = 3;
+ optional string name = 4;
+ optional string longName = 5;
+ optional string path = 6;
+}