org.sonar.server.rule.ws.SearchAction.java Source code

Java tutorial

Introduction

Here is the source code for org.sonar.server.rule.ws.SearchAction.java

Source

/*
 * SonarQube
 * Copyright (C) 2009-2017 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.rule.ws;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.io.Resources;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.RuleStatus;
import org.sonar.api.rule.Severity;
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.core.util.stream.MoreCollectors;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.rule.RuleDefinitionDto;
import org.sonar.db.rule.RuleDto;
import org.sonar.db.rule.RuleParamDto;
import org.sonar.server.es.Facets;
import org.sonar.server.es.SearchIdResult;
import org.sonar.server.es.SearchOptions;
import org.sonar.server.qualityprofile.ActiveRule;
import org.sonar.server.rule.index.RuleIndex;
import org.sonar.server.rule.index.RuleIndexDefinition;
import org.sonar.server.rule.index.RuleQuery;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.Rules.SearchResponse;
import org.sonarqube.ws.client.rule.SearchWsRequest;

import static java.lang.String.format;
import static org.sonar.api.server.ws.WebService.Param.ASCENDING;
import static org.sonar.api.server.ws.WebService.Param.FACETS;
import static org.sonar.api.server.ws.WebService.Param.FIELDS;
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.server.ws.WebService.Param.SORT;
import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY;
import static org.sonar.server.es.SearchOptions.MAX_LIMIT;
import static org.sonar.server.rule.index.RuleIndex.ALL_STATUSES_EXCEPT_REMOVED;
import static org.sonar.server.rule.index.RuleIndex.FACET_ACTIVE_SEVERITIES;
import static org.sonar.server.rule.index.RuleIndex.FACET_LANGUAGES;
import static org.sonar.server.rule.index.RuleIndex.FACET_OLD_DEFAULT;
import static org.sonar.server.rule.index.RuleIndex.FACET_REPOSITORIES;
import static org.sonar.server.rule.index.RuleIndex.FACET_SEVERITIES;
import static org.sonar.server.rule.index.RuleIndex.FACET_STATUSES;
import static org.sonar.server.rule.index.RuleIndex.FACET_TAGS;
import static org.sonar.server.rule.index.RuleIndex.FACET_TYPES;
import static org.sonar.server.ws.WsUtils.writeProtobuf;
import static org.sonarqube.ws.client.rule.RulesWsParameters.OPTIONAL_FIELDS;
import static org.sonarqube.ws.client.rule.RulesWsParameters.PARAM_ACTIVATION;
import static org.sonarqube.ws.client.rule.RulesWsParameters.PARAM_ACTIVE_SEVERITIES;
import static org.sonarqube.ws.client.rule.RulesWsParameters.PARAM_AVAILABLE_SINCE;
import static org.sonarqube.ws.client.rule.RulesWsParameters.PARAM_INHERITANCE;
import static org.sonarqube.ws.client.rule.RulesWsParameters.PARAM_IS_TEMPLATE;
import static org.sonarqube.ws.client.rule.RulesWsParameters.PARAM_LANGUAGES;
import static org.sonarqube.ws.client.rule.RulesWsParameters.PARAM_ORGANIZATION;
import static org.sonarqube.ws.client.rule.RulesWsParameters.PARAM_QPROFILE;
import static org.sonarqube.ws.client.rule.RulesWsParameters.PARAM_REPOSITORIES;
import static org.sonarqube.ws.client.rule.RulesWsParameters.PARAM_RULE_KEY;
import static org.sonarqube.ws.client.rule.RulesWsParameters.PARAM_SEVERITIES;
import static org.sonarqube.ws.client.rule.RulesWsParameters.PARAM_STATUSES;
import static org.sonarqube.ws.client.rule.RulesWsParameters.PARAM_TAGS;
import static org.sonarqube.ws.client.rule.RulesWsParameters.PARAM_TEMPLATE_KEY;
import static org.sonarqube.ws.client.rule.RulesWsParameters.PARAM_TYPES;

public class SearchAction implements RulesWsAction {
    public static final String ACTION = "search";

    private static final Collection<String> DEFAULT_FACETS = ImmutableSet.of(PARAM_LANGUAGES, PARAM_REPOSITORIES,
            "tags");
    private static final String[] POSSIBLE_FACETS = new String[] { FACET_LANGUAGES, FACET_REPOSITORIES, FACET_TAGS,
            FACET_SEVERITIES, FACET_ACTIVE_SEVERITIES, FACET_STATUSES, FACET_TYPES, FACET_OLD_DEFAULT };

    private final RuleQueryFactory ruleQueryFactory;
    private final DbClient dbClient;
    private final RuleIndex ruleIndex;
    private final ActiveRuleCompleter activeRuleCompleter;
    private final RuleMapper mapper;

    public SearchAction(RuleIndex ruleIndex, ActiveRuleCompleter activeRuleCompleter,
            RuleQueryFactory ruleQueryFactory, DbClient dbClient, RuleMapper mapper) {
        this.ruleIndex = ruleIndex;
        this.activeRuleCompleter = activeRuleCompleter;
        this.ruleQueryFactory = ruleQueryFactory;
        this.dbClient = dbClient;
        this.mapper = mapper;
    }

    @Override
    public void define(WebService.NewController controller) {
        WebService.NewAction action = controller.createAction(ACTION).addPagingParams(100, MAX_LIMIT)
                .setHandler(this);

        action.createParam(FACETS)
                .setDescription(
                        "Comma-separated list of the facets to be computed. No facet is computed by default.")
                .setPossibleValues(POSSIBLE_FACETS)
                .setExampleValue(format("%s,%s", POSSIBLE_FACETS[0], POSSIBLE_FACETS[1]));

        WebService.NewParam paramFields = action.createParam(FIELDS).setDescription(
                "Comma-separated list of the fields to be returned in response. All the fields are returned by default, except actives."
                        + "Since 5.5, following fields have been deprecated :" + "<ul>"
                        + "<li>\"defaultDebtRemFn\" becomes \"defaultRemFn\"</li>"
                        + "<li>\"debtRemFn\" becomes \"remFn\"</li>"
                        + "<li>\"effortToFixDescription\" becomes \"gapDescription\"</li>"
                        + "<li>\"debtOverloaded\" becomes \"remFnOverloaded\"</li>" + "</ul>")
                .setPossibleValues(Ordering.natural().sortedCopy(OPTIONAL_FIELDS));
        Iterator<String> it = OPTIONAL_FIELDS.iterator();
        paramFields.setExampleValue(format("%s,%s", it.next(), it.next()));

        doDefinition(action);
    }

    @Override
    public void handle(Request request, Response response) throws Exception {
        try (DbSession dbSession = dbClient.openSession(false)) {
            SearchWsRequest searchWsRequest = toSearchWsRequest(request);
            SearchOptions context = buildSearchOptions(searchWsRequest);
            RuleQuery query = ruleQueryFactory.createRuleQuery(dbSession, request);
            SearchResult searchResult = doSearch(dbSession, query, context);
            SearchResponse responseBuilder = buildResponse(dbSession, searchWsRequest, context, searchResult,
                    query);
            writeProtobuf(responseBuilder, request, response);
        }
    }

    private SearchResponse buildResponse(DbSession dbSession, SearchWsRequest request, SearchOptions context,
            SearchResult result, RuleQuery query) {
        SearchResponse.Builder responseBuilder = SearchResponse.newBuilder();
        writeStatistics(responseBuilder, result, context);
        doContextResponse(dbSession, request, result, responseBuilder, query);
        if (!context.getFacets().isEmpty()) {
            writeFacets(responseBuilder, request, context, result);
        }
        return responseBuilder.build();
    }

    private static void writeStatistics(SearchResponse.Builder response, SearchResult searchResult,
            SearchOptions context) {
        response.setTotal(searchResult.total);
        response.setP(context.getPage());
        response.setPs(context.getLimit());
    }

    private void doDefinition(WebService.NewAction action) {
        action.setDescription("Search for a collection of relevant rules matching a specified query.<br/>"
                + "Since 5.5, following fields in the response have been deprecated :" + "<ul>"
                + "<li>\"effortToFixDescription\" becomes \"gapDescription\"</li>"
                + "<li>\"debtRemFnCoeff\" becomes \"remFnGapMultiplier\"</li>"
                + "<li>\"defaultDebtRemFnCoeff\" becomes \"defaultRemFnGapMultiplier\"</li>"
                + "<li>\"debtRemFnOffset\" becomes \"remFnBaseEffort\"</li>"
                + "<li>\"defaultDebtRemFnOffset\" becomes \"defaultRemFnBaseEffort\"</li>"
                + "<li>\"debtOverloaded\" becomes \"remFnOverloaded\"</li>" + "</ul>")
                .setResponseExample(Resources.getResource(getClass(), "example-search.json")).setSince("4.4")
                .setHandler(this);

        // Rule-specific search parameters
        defineRuleSearchParameters(action);
    }

    public static void defineRuleSearchParameters(WebService.NewAction action) {
        action.createParam(TEXT_QUERY).setDescription("UTF-8 search query").setExampleValue("xpath");

        action.createParam(PARAM_RULE_KEY).setDescription("Key of rule to search for")
                .setExampleValue("squid:S001");

        action.createParam(PARAM_REPOSITORIES).setDescription("Comma-separated list of repositories")
                .setExampleValue("checkstyle,findbugs");

        action.createParam(PARAM_SEVERITIES).setDescription(
                "Comma-separated list of default severities. Not the same than severity of rules in Quality profiles.")
                .setPossibleValues(Severity.ALL).setExampleValue("CRITICAL,BLOCKER");

        action.createParam(PARAM_LANGUAGES).setDescription("Comma-separated list of languages")
                .setExampleValue("java,js");

        action.createParam(PARAM_STATUSES).setDescription("Comma-separated list of status codes")
                .setPossibleValues(RuleStatus.values()).setExampleValue(RuleStatus.READY);

        action.createParam(PARAM_AVAILABLE_SINCE)
                .setDescription("Filters rules added since date. Format is yyyy-MM-dd")
                .setExampleValue("2014-06-22");

        action.createParam(PARAM_TAGS)
                .setDescription("Comma-separated list of tags. Returned rules match any of the tags (OR operator)")
                .setExampleValue("security,java8");

        action.createParam(PARAM_TYPES).setSince("5.5")
                .setDescription("Comma-separated list of types. Returned rules match any of the tags (OR operator)")
                .setPossibleValues(RuleType.values()).setExampleValue(RuleType.BUG);

        action.createParam(PARAM_ACTIVATION).setDescription(
                "Filter rules that are activated or deactivated on the selected Quality profile. Ignored if "
                        + "the parameter '" + PARAM_QPROFILE + "' is not set.")
                .setBooleanPossibleValues();

        action.createParam(PARAM_QPROFILE)
                .setDescription("Key of Quality profile to filter on. Used only if the parameter '"
                        + PARAM_ACTIVATION + "' is set.")
                .setExampleValue("sonar-way-cs-12345");

        action.createParam(PARAM_INHERITANCE).setDescription(
                "Comma-separated list of values of inheritance for a rule within a quality profile. Used only if the parameter '"
                        + PARAM_ACTIVATION + "' is set.")
                .setPossibleValues(ActiveRule.Inheritance.NONE.name(), ActiveRule.Inheritance.INHERITED.name(),
                        ActiveRule.Inheritance.OVERRIDES.name())
                .setExampleValue(
                        ActiveRule.Inheritance.INHERITED.name() + "," + ActiveRule.Inheritance.OVERRIDES.name());

        action.createParam(PARAM_ACTIVE_SEVERITIES).setDescription(
                "Comma-separated list of activation severities, i.e the severity of rules in Quality profiles.")
                .setPossibleValues(Severity.ALL).setExampleValue("CRITICAL,BLOCKER");

        action.createParam(PARAM_IS_TEMPLATE).setDescription("Filter template rules").setBooleanPossibleValues();

        action.createParam(PARAM_TEMPLATE_KEY).setDescription(
                "Key of the template rule to filter on. Used to search for the custom rules based on this template.")
                .setExampleValue("java:S001");

        action.createParam(SORT).setDescription("Sort field").setPossibleValues(RuleIndexDefinition.SORT_FIELDS)
                .setExampleValue(RuleIndexDefinition.SORT_FIELDS.iterator().next());

        action.createParam(ASCENDING).setDescription("Ascending sort").setBooleanPossibleValues()
                .setDefaultValue(true);

        action.createParam(PARAM_ORGANIZATION).setDescription("Organization key").setRequired(false)
                .setInternal(true).setExampleValue("my-org").setSince("6.4");
    }

    private void writeRules(SearchResponse.Builder response, SearchResult result, SearchOptions context) {
        for (RuleDto rule : result.rules) {
            response.addRules(
                    mapper.toWsRule(rule.getDefinition(), result, context.getFields(), rule.getMetadata()));
        }
    }

    private static SearchOptions buildSearchOptions(SearchWsRequest request) {
        SearchOptions context = loadCommonContext(request);
        SearchOptions searchOptions = new SearchOptions().setLimit(context.getLimit())
                .setOffset(context.getOffset());
        if (context.getFacets().contains(RuleIndex.FACET_OLD_DEFAULT)) {
            searchOptions.addFacets(DEFAULT_FACETS);
        } else {
            searchOptions.addFacets(context.getFacets());
        }
        return searchOptions;
    }

    private static SearchOptions loadCommonContext(SearchWsRequest request) {
        int pageSize = request.getPageSize();
        SearchOptions context = new SearchOptions().addFields(request.getFields());
        if (request.getFacets() != null) {
            context.addFacets(request.getFacets());
        }
        if (pageSize < 1) {
            context.setPage(request.getPage(), 0).setLimit(MAX_LIMIT);
        } else {
            context.setPage(request.getPage(), pageSize);
        }
        return context;
    }

    private SearchResult doSearch(DbSession dbSession, RuleQuery query, SearchOptions context) {
        SearchIdResult<RuleKey> result = ruleIndex.search(query, context);
        List<RuleKey> ruleKeys = result.getIds();
        // rule order is managed by ES
        Map<RuleKey, RuleDto> rulesByRuleKey = Maps.uniqueIndex(
                dbClient.ruleDao().selectByKeys(dbSession, query.getOrganizationUuid(), ruleKeys), RuleDto::getKey);
        List<RuleDto> rules = new ArrayList<>();
        for (RuleKey ruleKey : ruleKeys) {
            RuleDto rule = rulesByRuleKey.get(ruleKey);
            if (rule != null) {
                rules.add(rule);
            }
        }
        List<Integer> ruleIds = rules.stream().map(RuleDto::getId).collect(MoreCollectors.toList());
        List<Integer> templateRuleIds = rules.stream().map(RuleDto::getTemplateId).filter(Objects::nonNull)
                .collect(MoreCollectors.toList());
        List<RuleDefinitionDto> templateRules = dbClient.ruleDao().selectDefinitionByIds(dbSession,
                templateRuleIds);
        List<RuleParamDto> ruleParamDtos = dbClient.ruleDao().selectRuleParamsByRuleIds(dbSession, ruleIds);
        return new SearchResult().setRules(rules).setRuleParameters(ruleParamDtos).setTemplateRules(templateRules)
                .setFacets(result.getFacets()).setTotal(result.getTotal());
    }

    private void doContextResponse(DbSession dbSession, SearchWsRequest request, SearchResult result,
            SearchResponse.Builder response, RuleQuery query) {
        SearchOptions contextForResponse = loadCommonContext(request);
        writeRules(response, result, contextForResponse);
        if (contextForResponse.getFields().contains("actives")) {
            activeRuleCompleter.completeSearch(dbSession, query, result.rules, response);
        }
    }

    private static void writeFacets(SearchResponse.Builder response, SearchWsRequest request, SearchOptions context,
            SearchResult results) {
        addMandatoryFacetValues(results, FACET_LANGUAGES, request.getLanguages());
        addMandatoryFacetValues(results, FACET_REPOSITORIES, request.getRepositories());
        addMandatoryFacetValues(results, FACET_STATUSES, ALL_STATUSES_EXCEPT_REMOVED);
        addMandatoryFacetValues(results, FACET_SEVERITIES, Severity.ALL);
        addMandatoryFacetValues(results, FACET_ACTIVE_SEVERITIES, Severity.ALL);
        addMandatoryFacetValues(results, FACET_TAGS, request.getTags());
        addMandatoryFacetValues(results, FACET_TYPES, RuleType.names());

        Common.Facet.Builder facet = Common.Facet.newBuilder();
        Common.FacetValue.Builder value = Common.FacetValue.newBuilder();
        Map<String, List<String>> facetValuesByFacetKey = new HashMap<>();
        facetValuesByFacetKey.put(FACET_LANGUAGES, request.getLanguages());
        facetValuesByFacetKey.put(FACET_REPOSITORIES, request.getRepositories());
        facetValuesByFacetKey.put(FACET_STATUSES, request.getStatuses());
        facetValuesByFacetKey.put(FACET_SEVERITIES, request.getSeverities());
        facetValuesByFacetKey.put(FACET_ACTIVE_SEVERITIES, request.getActiveSeverities());
        facetValuesByFacetKey.put(FACET_TAGS, request.getTags());
        facetValuesByFacetKey.put(FACET_TYPES, request.getTypes());

        for (String facetName : context.getFacets()) {
            facet.clear().setProperty(facetName);
            Map<String, Long> facets = results.facets.get(facetName);
            if (facets != null) {
                Set<String> itemsFromFacets = Sets.newHashSet();
                for (Map.Entry<String, Long> facetValue : facets.entrySet()) {
                    itemsFromFacets.add(facetValue.getKey());
                    facet.addValues(value.clear().setVal(facetValue.getKey()).setCount(facetValue.getValue()));
                }
                addZeroFacetsForSelectedItems(facet, facetValuesByFacetKey.get(facetName), itemsFromFacets);
            }
            response.getFacetsBuilder().addFacets(facet);
        }
    }

    private static void addZeroFacetsForSelectedItems(Common.Facet.Builder facet,
            @Nullable List<String> requestParams, Set<String> itemsFromFacets) {
        if (requestParams != null) {
            Common.FacetValue.Builder value = Common.FacetValue.newBuilder();
            for (String param : requestParams) {
                if (!itemsFromFacets.contains(param)) {
                    facet.addValues(value.clear().setVal(param).setCount(0L));
                }
            }
        }
    }

    private static void addMandatoryFacetValues(SearchResult results, String facetName,
            @Nullable Collection<String> mandatoryValues) {
        Map<String, Long> facetValues = results.facets.get(facetName);
        if (facetValues != null) {
            Collection<String> valuesToAdd = mandatoryValues == null ? Lists.newArrayList() : mandatoryValues;
            for (String item : valuesToAdd) {
                if (!facetValues.containsKey(item)) {
                    facetValues.put(item, 0L);
                }
            }
        }
    }

    private static SearchWsRequest toSearchWsRequest(Request request) {
        return new SearchWsRequest().setActivation(request.paramAsBoolean(PARAM_ACTIVATION))
                .setActiveSeverities(request.paramAsStrings(PARAM_ACTIVE_SEVERITIES))
                .setAsc(request.mandatoryParamAsBoolean(ASCENDING))
                .setAvailableSince(request.param(PARAM_AVAILABLE_SINCE)).setFields(request.paramAsStrings(FIELDS))
                .setFacets(request.paramAsStrings(FACETS)).setInheritance(request.paramAsStrings(PARAM_INHERITANCE))
                .setIsTemplate(request.paramAsBoolean(PARAM_IS_TEMPLATE))
                .setLanguages(request.paramAsStrings(PARAM_LANGUAGES)).setPage(request.mandatoryParamAsInt(PAGE))
                .setPageSize(request.mandatoryParamAsInt(PAGE_SIZE)).setQuery(request.param(TEXT_QUERY))
                .setQProfile(request.param(PARAM_QPROFILE))
                .setRepositories(request.paramAsStrings(PARAM_REPOSITORIES))
                .setRuleKey(request.param(PARAM_RULE_KEY)).setSort(request.param(SORT))
                .setSeverities(request.paramAsStrings(PARAM_SEVERITIES))
                .setStatuses(request.paramAsStrings(PARAM_STATUSES)).setTags(request.paramAsStrings(PARAM_TAGS))
                .setTemplateKey(request.param(PARAM_TEMPLATE_KEY)).setTypes(request.paramAsStrings(PARAM_TYPES));
    }

    static class SearchResult {
        private List<RuleDto> rules;
        private final ListMultimap<Integer, RuleParamDto> ruleParamsByRuleId;
        private final Map<Integer, RuleDefinitionDto> templateRulesByRuleId;
        private Long total;
        private Facets facets;

        public SearchResult() {
            this.rules = new ArrayList<>();
            this.ruleParamsByRuleId = ArrayListMultimap.create();
            this.templateRulesByRuleId = new HashMap<>();
        }

        public List<RuleDto> getRules() {
            return rules;
        }

        public SearchResult setRules(List<RuleDto> rules) {
            this.rules = rules;
            return this;
        }

        public ListMultimap<Integer, RuleParamDto> getRuleParamsByRuleId() {
            return ruleParamsByRuleId;
        }

        public SearchResult setRuleParameters(List<RuleParamDto> ruleParams) {
            ruleParamsByRuleId.clear();
            for (RuleParamDto ruleParam : ruleParams) {
                ruleParamsByRuleId.put(ruleParam.getRuleId(), ruleParam);
            }
            return this;
        }

        public Map<Integer, RuleDefinitionDto> getTemplateRulesByRuleId() {
            return templateRulesByRuleId;
        }

        public SearchResult setTemplateRules(List<RuleDefinitionDto> templateRules) {
            templateRulesByRuleId.clear();
            for (RuleDefinitionDto templateRule : templateRules) {
                templateRulesByRuleId.put(templateRule.getId(), templateRule);
            }
            return this;
        }

        @CheckForNull
        public Long getTotal() {
            return total;
        }

        public SearchResult setTotal(Long total) {
            this.total = total;
            return this;
        }

        @CheckForNull
        public Facets getFacets() {
            return facets;
        }

        public SearchResult setFacets(Facets facets) {
            this.facets = facets;
            return this;
        }
    }

}