Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* SonarQube
* Copyright (C) 2009-2018 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.component.ws;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Ordering;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.server.ws.Change;
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.server.ws.WebService.Param;
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.component.SnapshotDto;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.property.PropertyDto;
import org.sonar.db.property.PropertyQuery;
import org.sonar.server.component.ws.FilterParser.Criterion;
import org.sonar.server.es.Facets;
import org.sonar.server.es.SearchIdResult;
import org.sonar.server.es.SearchOptions;
import org.sonar.server.measure.index.ProjectMeasuresIndex;
import org.sonar.server.measure.index.ProjectMeasuresQuery;
import org.sonar.server.project.Visibility;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.Components.Component;
import org.sonarqube.ws.Components.SearchProjectsWsResponse;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Sets.newHashSet;
import static java.lang.String.format;
import static java.util.Collections.emptyMap;
import static java.util.Objects.requireNonNull;
import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
import static org.sonar.api.server.ws.WebService.Param.FIELDS;
import static org.sonar.api.utils.DateUtils.formatDateTime;
import static org.sonar.core.util.Protobuf.setNullable;
import static org.sonar.core.util.stream.MoreCollectors.toSet;
import static org.sonar.db.measure.ProjectMeasuresIndexerIterator.METRIC_KEYS;
import static org.sonar.server.component.ws.ProjectMeasuresQueryFactory.IS_FAVORITE_CRITERION;
import static org.sonar.server.component.ws.ProjectMeasuresQueryFactory.newProjectMeasuresQuery;
import static org.sonar.server.component.ws.ProjectMeasuresQueryValidator.NON_METRIC_SORT_KEYS;
import static org.sonar.server.measure.index.ProjectMeasuresIndex.SUPPORTED_FACETS;
import static org.sonar.server.measure.index.ProjectMeasuresQuery.SORT_BY_LAST_ANALYSIS_DATE;
import static org.sonar.server.measure.index.ProjectMeasuresQuery.SORT_BY_NAME;
import static org.sonar.server.ws.WsUtils.checkFound;
import static org.sonar.server.ws.WsUtils.checkFoundWithOptional;
import static org.sonar.server.ws.WsUtils.writeProtobuf;
import static org.sonarqube.ws.client.component.ComponentsWsParameters.ACTION_SEARCH_PROJECTS;
import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_FILTER;
import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_ORGANIZATION;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_LANGUAGES;
import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_TAGS;
public class SearchProjectsAction implements ComponentsWsAction {
public static final int MAX_PAGE_SIZE = 500;
public static final int DEFAULT_PAGE_SIZE = 100;
private static final String ANALYSIS_DATE = "analysisDate";
private static final String LEAK_PERIOD_DATE = "leakPeriodDate";
private static final Set POSSIBLE_FIELDS = newHashSet(ANALYSIS_DATE, LEAK_PERIOD_DATE);
private final DbClient dbClient;
private final ProjectMeasuresIndex index;
private final UserSession userSession;
public SearchProjectsAction(DbClient dbClient, ProjectMeasuresIndex index, UserSession userSession) {
this.dbClient = dbClient;
this.index = index;
this.userSession = userSession;
}
@Override
public void define(WebService.NewController context) {
WebService.NewAction action = context.createAction(ACTION_SEARCH_PROJECTS)
.setSince("6.2")
.setDescription("Search for projects")
.addPagingParams(DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE)
.setInternal(true)
.setResponseExample(getClass().getResource("search_projects-example.json"))
.setChangelog(
new Change("6.4", format("The '%s' parameter accepts '%s' to filter by language", FILTER_LANGUAGES, PARAM_FILTER)),
new Change("6.4", "The 'visibility' field is added"),
new Change("6.5", "The 'filter' parameter now allows 'NO_DATA' as value for numeric metrics"),
new Change("6.5", "Added the option 'analysisDate' for the 'sort' parameter"),
new Change("6.5", format("Value '%s' is added to parameter '%s'", LEAK_PERIOD_DATE, FIELDS)))
.setHandler(this);
action.createFieldsParam(POSSIBLE_FIELDS)
.setDescription("Comma-separated list of the fields to be returned in response")
.setSince("6.4");
action.createParam(PARAM_ORGANIZATION)
.setDescription("the organization to search projects in")
.setRequired(false)
.setInternal(true)
.setSince("6.3");
action.createParam(Param.FACETS)
.setDescription("Comma-separated list of the facets to be computed. No facet is computed by default.")
.setPossibleValues(SUPPORTED_FACETS.stream().sorted().collect(MoreCollectors.toList(SUPPORTED_FACETS.size())));
action
.createParam(PARAM_FILTER)
.setMinimumLength(2)
.setDescription("Filter of projects on name, key, measure value, quality gate, language, tag or whether a project is a favorite or not. " +
"The filter must be encoded to form a valid URL (for example '=' must be replaced by '%3D'). " +
"Examples of use:" +
"
" +
"
to filter my favorite projects with a failed quality gate and a coverage greater than or equals to 60% and a coverage strictly lower than 80%: " +
" filter=\"alert_status = ERROR and isFavorite and coverage >= 60 and coverage < 80\"
" +
"
to filter projects with a reliability, security and maintainability rating equals or worse than B: " +
" filter=\"reliability_rating>=2 and security_rating>=2 and sqale_rating>=2\"
" +
"
to filter projects without duplication data: " +
" filter=\"duplicated_lines_density = NO_DATA\"
" +
"
" +
"To filter on project name or key, use the 'query' keyword, for instance : filter='query = \"Sonar\"'. " +
" " +
"To filter on a numeric metric, provide the metric key. " +
"These are the supported metric keys: " +
"
" +
METRIC_KEYS.stream().sorted().map(key -> "
" + key + "
").collect(Collectors.joining()) +
"
" +
" " +
"To filter on a rating, provide the corresponding metric key (ex: reliability_rating for reliability rating). " +
"The possible values are:" +
"
" +
"
'1' for rating A
" +
"
'2' for rating B
" +
"
'3' for rating C
" +
"
'4' for rating D
" +
"
'5' for rating E
" +
"
" +
"To filter on a Quality Gate status use the metric key 'alert_status'. Only the '=' operator can be used. " +
"The possible values are:" +
"
" +
"
'OK' for Passed
" +
"
'WARN' for Warning
" +
"
'ERROR' for Failed
" +
"
" +
"To filter on language keys use the language key: " +
"
" +
"
to filter on a single language you can use 'language = java'
" +
"
to filter on several languages you must use 'language IN (java, js)'
" +
"
" +
"Use the WS api/languages/list to find the key of a language. " +
"To filter on tags use the 'tag' keyword:" +
"
" +
"
to filter on one tag you can use tag = finance
" +
"
to filter on several tags you must use tag in (offshore, java)
" +
"
");
action.createParam(Param.SORT)
.setDescription("Sort projects by numeric metric key, quality gate status (using '%s'), last analysis date (using '%s'), or by project name.",
ALERT_STATUS_KEY, SORT_BY_LAST_ANALYSIS_DATE, PARAM_FILTER)
.setDefaultValue(SORT_BY_NAME)
.setPossibleValues(
Stream.concat(METRIC_KEYS.stream(), NON_METRIC_SORT_KEYS.stream()).sorted().collect(MoreCollectors.toList(METRIC_KEYS.size() + NON_METRIC_SORT_KEYS.size())))
.setSince("6.4");
action.createParam(Param.ASCENDING)
.setDescription("Ascending sort")
.setBooleanPossibleValues()
.setDefaultValue(true);
}
@Override
public void handle(Request httpRequest, Response httpResponse) throws Exception {
SearchProjectsWsResponse response = doHandle(toRequest(httpRequest));
writeProtobuf(response, httpRequest, httpResponse);
}
private SearchProjectsWsResponse doHandle(SearchProjectsRequest request) {
try (DbSession dbSession = dbClient.openSession(false)) {
String organizationKey = request.getOrganization();
if (organizationKey == null) {
return handleForAnyOrganization(dbSession, request);
} else {
OrganizationDto organization = checkFoundWithOptional(
dbClient.organizationDao().selectByKey(dbSession, organizationKey),
"No organization for key '%s'", organizationKey);
return handleForOrganization(dbSession, request, organization);
}
}
}
private SearchProjectsWsResponse handleForAnyOrganization(DbSession dbSession, SearchProjectsRequest request) {
SearchResults searchResults = searchData(dbSession, request, null);
Set organizationUuids = searchResults.projects.stream().map(ComponentDto::getOrganizationUuid).collect(toSet());
Map organizationsByUuid = dbClient.organizationDao().selectByUuids(dbSession, organizationUuids)
.stream()
.collect(MoreCollectors.uniqueIndex(OrganizationDto::getUuid));
return buildResponse(request, searchResults, organizationsByUuid);
}
private SearchProjectsWsResponse handleForOrganization(DbSession dbSession, SearchProjectsRequest request, OrganizationDto organization) {
SearchResults searchResults = searchData(dbSession, request, organization);
return buildResponse(request, searchResults, ImmutableMap.of(organization.getUuid(), organization));
}
private SearchResults searchData(DbSession dbSession, SearchProjectsRequest request, @Nullable OrganizationDto organization) {
Set favoriteProjectUuids = loadFavoriteProjectUuids(dbSession);
List criteria = FilterParser.parse(firstNonNull(request.getFilter(), ""));
ProjectMeasuresQuery query = newProjectMeasuresQuery(criteria, hasFavoriteFilter(criteria) ? favoriteProjectUuids : null)
.setSort(request.getSort())
.setAsc(request.getAsc());
Optional.ofNullable(organization)
.map(OrganizationDto::getUuid)
.ifPresent(query::setOrganizationUuid);
ProjectMeasuresQueryValidator.validate(query);
SearchIdResult esResults = index.search(query, new SearchOptions()
.addFacets(request.getFacets())
.setPage(request.getPage(), request.getPageSize()));
List projectUuids = esResults.getIds();
Ordering ordering = Ordering.explicit(projectUuids).onResultOf(ComponentDto::uuid);
List projects = ordering.immutableSortedCopy(dbClient.componentDao().selectByUuids(dbSession, projectUuids));
Map analysisByProjectUuid = getSnapshots(dbSession, request, projectUuids);
return new SearchResults(projects, favoriteProjectUuids, esResults, analysisByProjectUuid, query);
}
private static boolean hasFavoriteFilter(List criteria) {
return criteria.stream()
.map(Criterion::getKey)
.anyMatch(IS_FAVORITE_CRITERION::equalsIgnoreCase);
}
private Set loadFavoriteProjectUuids(DbSession dbSession) {
if (!userSession.isLoggedIn()) {
return Collections.emptySet();
}
List props = dbClient.propertiesDao().selectByQuery(
PropertyQuery.builder()
.setUserId(userSession.getUserId())
.setKey("favourite")
.build(),
dbSession);
List favoriteDbIds = props.stream()
.map(PropertyDto::getResourceId)
.collect(MoreCollectors.toList(props.size()));
return dbClient.componentDao().selectByIds(dbSession, favoriteDbIds).stream()
.filter(ComponentDto::isEnabled)
.filter(f -> f.qualifier().equals(Qualifiers.PROJECT))
.map(ComponentDto::uuid)
.collect(MoreCollectors.toSet());
}
private Map getSnapshots(DbSession dbSession, SearchProjectsRequest request, List projectUuids) {
if (request.getAdditionalFields().contains(ANALYSIS_DATE) || request.getAdditionalFields().contains(LEAK_PERIOD_DATE)) {
return dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids(dbSession, projectUuids)
.stream()
.collect(MoreCollectors.uniqueIndex(SnapshotDto::getComponentUuid));
}
return emptyMap();
}
private static SearchProjectsRequest toRequest(Request httpRequest) {
RequestBuilder request = new RequestBuilder()
.setOrganization(httpRequest.param(PARAM_ORGANIZATION))
.setFilter(httpRequest.param(PARAM_FILTER))
.setSort(httpRequest.mandatoryParam(Param.SORT))
.setAsc(httpRequest.mandatoryParamAsBoolean(Param.ASCENDING))
.setPage(httpRequest.mandatoryParamAsInt(Param.PAGE))
.setPageSize(httpRequest.mandatoryParamAsInt(Param.PAGE_SIZE));
if (httpRequest.hasParam(Param.FACETS)) {
request.setFacets(httpRequest.paramAsStrings(Param.FACETS));
}
if (httpRequest.hasParam(FIELDS)) {
request.setAdditionalFields(httpRequest.paramAsStrings(FIELDS));
}
return request.build();
}
private SearchProjectsWsResponse buildResponse(SearchProjectsRequest request, SearchResults searchResults, Map organizationsByUuid) {
Function dbToWsComponent = new DbToWsComponent(request, organizationsByUuid, searchResults.favoriteProjectUuids, searchResults.analysisByProjectUuid,
userSession.isLoggedIn());
return Stream.of(SearchProjectsWsResponse.newBuilder())
.map(response -> response.setPaging(Common.Paging.newBuilder()
.setPageIndex(request.getPage())
.setPageSize(request.getPageSize())
.setTotal(searchResults.total)))
.map(response -> {
searchResults.projects.stream()
.map(dbToWsComponent)
.forEach(response::addComponents);
return response;
})
.map(response -> addFacets(searchResults, response))
.map(SearchProjectsWsResponse.Builder::build)
.findFirst()
.orElseThrow(() -> new IllegalStateException("SearchProjectsWsResponse not built"));
}
private static SearchProjectsWsResponse.Builder addFacets(SearchResults searchResults, SearchProjectsWsResponse.Builder wsResponse) {
Facets esFacets = searchResults.facets;
EsToWsFacet esToWsFacet = new EsToWsFacet();
searchResults.query.getLanguages().ifPresent(languages -> addMandatoryValuesToFacet(esFacets, FILTER_LANGUAGES, languages));
searchResults.query.getTags().ifPresent(tags -> addMandatoryValuesToFacet(esFacets, FILTER_TAGS, tags));
Common.Facets wsFacets = esFacets.getAll().entrySet().stream()
.map(esToWsFacet)
.collect(Collector.of(
Common.Facets::newBuilder,
Common.Facets.Builder::addFacets,
(result1, result2) -> {
throw new IllegalStateException("Parallel execution forbidden");
},
Common.Facets.Builder::build));
wsResponse.setFacets(wsFacets);
return wsResponse;
}
private static void addMandatoryValuesToFacet(Facets facets, String facetName, Iterable mandatoryValues) {
Map buckets = facets.get(facetName);
if (buckets == null) {
return;
}
for (String mandatoryValue : mandatoryValues) {
if (!buckets.containsKey(mandatoryValue)) {
buckets.put(mandatoryValue, 0L);
}
}
}
private static class EsToWsFacet implements Function>, Common.Facet> {
private final BucketToFacetValue bucketToFacetValue = new BucketToFacetValue();
private final Common.Facet.Builder wsFacet = Common.Facet.newBuilder();
@Override
public Common.Facet apply(Entry> esFacet) {
wsFacet
.clear()
.setProperty(esFacet.getKey());
LinkedHashMap buckets = esFacet.getValue();
if (buckets != null) {
buckets.entrySet()
.stream()
.map(bucketToFacetValue)
.forEach(wsFacet::addValues);
} else {
wsFacet.addAllValues(Collections.emptyList());
}
return wsFacet.build();
}
}
private static class BucketToFacetValue implements Function, Common.FacetValue> {
private final Common.FacetValue.Builder facetValue;
private BucketToFacetValue() {
this.facetValue = Common.FacetValue.newBuilder();
}
@Override
public Common.FacetValue apply(Entry bucket) {
return facetValue
.clear()
.setVal(bucket.getKey())
.setCount(bucket.getValue())
.build();
}
}
private static class DbToWsComponent implements Function {
private final SearchProjectsRequest request;
private final Component.Builder wsComponent;
private final Map organizationsByUuid;
private final Set favoriteProjectUuids;
private final boolean isUserLoggedIn;
private final Map analysisByProjectUuid;
private DbToWsComponent(SearchProjectsRequest request, Map organizationsByUuid, Set favoriteProjectUuids,
Map analysisByProjectUuid, boolean isUserLoggedIn) {
this.request = request;
this.analysisByProjectUuid = analysisByProjectUuid;
this.wsComponent = Component.newBuilder();
this.organizationsByUuid = organizationsByUuid;
this.favoriteProjectUuids = favoriteProjectUuids;
this.isUserLoggedIn = isUserLoggedIn;
}
@Override
public Component apply(ComponentDto dbComponent) {
String organizationUuid = dbComponent.getOrganizationUuid();
OrganizationDto organizationDto = organizationsByUuid.get(organizationUuid);
checkFound(organizationDto, "Organization with uuid '%s' not found", organizationUuid);
wsComponent
.clear()
.setOrganization(organizationDto.getKey())
.setId(dbComponent.uuid())
.setKey(dbComponent.getDbKey())
.setName(dbComponent.name())
.setVisibility(Visibility.getLabel(dbComponent.isPrivate()));
wsComponent.getTagsBuilder().addAllTags(dbComponent.getTags());
SnapshotDto snapshotDto = analysisByProjectUuid.get(dbComponent.uuid());
if (snapshotDto != null) {
if (request.getAdditionalFields().contains(ANALYSIS_DATE)) {
wsComponent.setAnalysisDate(formatDateTime(snapshotDto.getCreatedAt()));
}
if (request.getAdditionalFields().contains(LEAK_PERIOD_DATE)) {
setNullable(snapshotDto.getPeriodDate(), leakPeriodDate -> wsComponent.setLeakPeriodDate(formatDateTime(leakPeriodDate)));
}
}
if (isUserLoggedIn) {
wsComponent.setIsFavorite(favoriteProjectUuids.contains(dbComponent.uuid()));
}
return wsComponent.build();
}
}
private static class SearchResults {
private final List projects;
private final Set favoriteProjectUuids;
private final Facets facets;
private final Map analysisByProjectUuid;
private final ProjectMeasuresQuery query;
private final int total;
private SearchResults(List projects, Set favoriteProjectUuids, SearchIdResult searchResults, Map analysisByProjectUuid,
ProjectMeasuresQuery query) {
this.projects = projects;
this.favoriteProjectUuids = favoriteProjectUuids;
this.total = (int) searchResults.getTotal();
this.facets = searchResults.getFacets();
this.analysisByProjectUuid = analysisByProjectUuid;
this.query = query;
}
}
static class SearchProjectsRequest {
private final int page;
private final int pageSize;
private final String organization;
private final String filter;
private final List facets;
private final String sort;
private final Boolean asc;
private final List additionalFields;
private SearchProjectsRequest(RequestBuilder builder) {
this.page = builder.page;
this.pageSize = builder.pageSize;
this.organization = builder.organization;
this.filter = builder.filter;
this.facets = builder.facets;
this.sort = builder.sort;
this.asc = builder.asc;
this.additionalFields = builder.additionalFields;
}
@CheckForNull
public String getOrganization() {
return organization;
}
@CheckForNull
public String getFilter() {
return filter;
}
public List getFacets() {
return facets;
}
@CheckForNull
public String getSort() {
return sort;
}
public int getPageSize() {
return pageSize;
}
public int getPage() {
return page;
}
@CheckForNull
public Boolean getAsc() {
return asc;
}
public List getAdditionalFields() {
return additionalFields;
}
public static RequestBuilder builder() {
return new RequestBuilder();
}
}
static class RequestBuilder {
private String organization;
private Integer page;
private Integer pageSize;
private String filter;
private List facets = new ArrayList<>();
private String sort;
private Boolean asc;
private List additionalFields = new ArrayList<>();
private RequestBuilder() {
// enforce static factory method
}
public RequestBuilder setOrganization(@Nullable String organization) {
this.organization = organization;
return this;
}
public RequestBuilder setFilter(@Nullable String filter) {
this.filter = filter;
return this;
}
public RequestBuilder setFacets(List facets) {
this.facets = requireNonNull(facets);
return this;
}
public RequestBuilder setPage(int page) {
this.page = page;
return this;
}
public RequestBuilder setPageSize(int pageSize) {
this.pageSize = pageSize;
return this;
}
public RequestBuilder setSort(@Nullable String sort) {
this.sort = sort;
return this;
}
public RequestBuilder setAsc(boolean asc) {
this.asc = asc;
return this;
}
public RequestBuilder setAdditionalFields(List additionalFields) {
this.additionalFields = requireNonNull(additionalFields, "additional fields cannot be null");
return this;
}
public SearchProjectsRequest build() {
if (page == null) {
page = 1;
}
if (pageSize == null) {
pageSize = DEFAULT_PAGE_SIZE;
}
checkArgument(pageSize <= MAX_PAGE_SIZE, "Page size must not be greater than %s", MAX_PAGE_SIZE);
return new SearchProjectsRequest(this);
}
}
}