org.sonar.server.issue.IssueQueryFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.sonar.server.issue.IssueQueryFactory.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.issue;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.apache.commons.lang.BooleanUtils;
import org.joda.time.DateTime;
import org.joda.time.format.ISOPeriodFormat;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.System2;
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.component.SnapshotDto;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.client.issue.SearchWsRequest;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Lists.newArrayList;
import static java.lang.String.format;
import static java.util.Collections.singletonList;
import static org.sonar.api.utils.DateUtils.longToDate;
import static org.sonar.api.utils.DateUtils.parseDateOrDateTime;
import static org.sonar.api.utils.DateUtils.parseEndingDateOrDateTime;
import static org.sonar.api.utils.DateUtils.parseStartingDateOrDateTime;
import static org.sonar.server.ws.WsUtils.checkFoundWithOptional;
import static org.sonar.server.ws.WsUtils.checkRequest;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENTS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_KEYS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_ROOTS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_UUIDS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_AFTER;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_IN_LAST;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SINCE_LEAK_PERIOD;

/**
 * This component is used to create an IssueQuery, in order to transform the component and component roots keys into uuid.
 */
@ServerSide
public class IssueQueryFactory {

    public static final String LOGIN_MYSELF = "__me__";

    private static final String UNKNOWN = "<UNKNOWN>";
    private final DbClient dbClient;
    private final System2 system;
    private final UserSession userSession;

    public IssueQueryFactory(DbClient dbClient, System2 system, UserSession userSession) {
        this.dbClient = dbClient;
        this.system = system;
        this.userSession = userSession;
    }

    public IssueQuery create(SearchWsRequest request) {
        try (DbSession dbSession = dbClient.openSession(false)) {
            IssueQuery.Builder builder = IssueQuery.builder().issueKeys(request.getIssues())
                    .severities(request.getSeverities()).statuses(request.getStatuses())
                    .resolutions(request.getResolutions()).resolved(request.getResolved())
                    .rules(stringsToRules(request.getRules())).assignees(buildAssignees(request.getAssignees()))
                    .languages(request.getLanguages()).tags(request.getTags()).types(request.getTypes())
                    .assigned(request.getAssigned()).createdAt(parseDateOrDateTime(request.getCreatedAt()))
                    .createdBefore(parseEndingDateOrDateTime(request.getCreatedBefore()))
                    .facetMode(request.getFacetMode())
                    .organizationUuid(convertOrganizationKeyToUuid(dbSession, request.getOrganization()));

            Set<String> allComponentUuids = Sets.newHashSet();
            boolean effectiveOnComponentOnly = mergeDeprecatedComponentParameters(dbSession,
                    request.getOnComponentOnly(), request.getComponents(), request.getComponentUuids(),
                    request.getComponentKeys(), request.getComponentRootUuids(), request.getComponentRoots(),
                    allComponentUuids);

            addComponentParameters(builder, dbSession, effectiveOnComponentOnly, allComponentUuids,
                    request.getProjectUuids(), request.getProjectKeys(), request.getModuleUuids(),
                    request.getDirectories(), request.getFileUuids(), request.getAuthors());

            builder.createdAfter(buildCreatedAfterFromRequest(dbSession, request, allComponentUuids));

            String sort = request.getSort();
            if (!Strings.isNullOrEmpty(sort)) {
                builder.sort(sort);
                builder.asc(request.getAsc());
            }
            return builder.build();

        }
    }

    @CheckForNull
    private Date buildCreatedAfterFromDates(@Nullable Date createdAfter, @Nullable String createdInLast) {
        checkArgument(createdAfter == null || createdInLast == null, format(
                "Parameters %s and %s cannot be set simultaneously", PARAM_CREATED_AFTER, PARAM_CREATED_IN_LAST));

        Date actualCreatedAfter = createdAfter;
        if (createdInLast != null) {
            actualCreatedAfter = new DateTime(system.now())
                    .minus(ISOPeriodFormat.standard().parsePeriod("P" + createdInLast.toUpperCase(Locale.ENGLISH)))
                    .toDate();
        }
        return actualCreatedAfter;
    }

    @CheckForNull
    private String convertOrganizationKeyToUuid(DbSession dbSession, @Nullable String organizationKey) {
        if (organizationKey == null) {
            return null;
        }
        Optional<OrganizationDto> organization = dbClient.organizationDao().selectByKey(dbSession, organizationKey);
        return organization.map(OrganizationDto::getUuid).orElse(UNKNOWN);
    }

    private Date buildCreatedAfterFromRequest(DbSession dbSession, SearchWsRequest request,
            Set<String> componentUuids) {
        Date createdAfter = parseStartingDateOrDateTime(request.getCreatedAfter());
        String createdInLast = request.getCreatedInLast();

        if (request.getSinceLeakPeriod() == null || !request.getSinceLeakPeriod()) {
            return buildCreatedAfterFromDates(createdAfter, createdInLast);
        }

        checkRequest(createdAfter == null, "Parameters '%s' and '%s' cannot be set simultaneously",
                PARAM_CREATED_AFTER, PARAM_SINCE_LEAK_PERIOD);

        checkArgument(componentUuids.size() == 1,
                "One and only one component must be provided when searching since leak period");
        String uuid = componentUuids.iterator().next();
        Date createdAfterFromSnapshot = findCreatedAfterFromComponentUuid(dbSession, uuid);
        return buildCreatedAfterFromDates(createdAfterFromSnapshot, createdInLast);
    }

    @CheckForNull
    private Date findCreatedAfterFromComponentUuid(DbSession dbSession, String uuid) {
        ComponentDto component = checkFoundWithOptional(dbClient.componentDao().selectByUuid(dbSession, uuid),
                "Component with id '%s' not found", uuid);
        Optional<SnapshotDto> snapshot = dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession,
                component.uuid());
        return snapshot.map(s -> longToDate(s.getPeriodDate())).orElse(null);
    }

    private List<String> buildAssignees(@Nullable List<String> assigneesFromParams) {
        List<String> assignees = Lists.newArrayList();
        if (assigneesFromParams != null) {
            assignees.addAll(assigneesFromParams);
        }
        if (assignees.contains(LOGIN_MYSELF)) {
            String login = userSession.getLogin();
            if (login == null) {
                assignees.add(UNKNOWN);
            } else {
                assignees.add(login);
            }
        }
        return assignees;
    }

    private boolean mergeDeprecatedComponentParameters(DbSession session, @Nullable Boolean onComponentOnly,
            @Nullable Collection<String> components, @Nullable Collection<String> componentUuids,
            @Nullable Collection<String> componentKeys, @Nullable Collection<String> componentRootUuids,
            @Nullable Collection<String> componentRoots, Set<String> allComponentUuids) {
        boolean effectiveOnComponentOnly = false;

        checkArgument(
                atMostOneNonNullElement(components, componentUuids, componentKeys, componentRootUuids,
                        componentRoots),
                "At most one of the following parameters can be provided: %s, %s, %s, %s, %s", PARAM_COMPONENT_KEYS,
                PARAM_COMPONENT_UUIDS, PARAM_COMPONENTS, PARAM_COMPONENT_ROOTS, PARAM_COMPONENT_UUIDS);

        if (componentRootUuids != null) {
            allComponentUuids.addAll(componentRootUuids);
            effectiveOnComponentOnly = false;
        } else if (componentRoots != null) {
            allComponentUuids.addAll(convertComponentKeysToUuids(session, componentRoots));
            effectiveOnComponentOnly = false;
        } else if (components != null) {
            allComponentUuids.addAll(convertComponentKeysToUuids(session, components));
            effectiveOnComponentOnly = true;
        } else if (componentUuids != null) {
            allComponentUuids.addAll(componentUuids);
            effectiveOnComponentOnly = BooleanUtils.isTrue(onComponentOnly);
        } else if (componentKeys != null) {
            allComponentUuids.addAll(convertComponentKeysToUuids(session, componentKeys));
            effectiveOnComponentOnly = BooleanUtils.isTrue(onComponentOnly);
        }
        return effectiveOnComponentOnly;
    }

    private static boolean atMostOneNonNullElement(Object... objects) {
        return Arrays.stream(objects).filter(Objects::nonNull).count() <= 1;
    }

    private void addComponentParameters(IssueQuery.Builder builder, DbSession session, boolean onComponentOnly,
            Collection<String> componentUuids, @Nullable Collection<String> projectUuids,
            @Nullable Collection<String> projectKeys, @Nullable Collection<String> moduleUuids,
            @Nullable Collection<String> directories, @Nullable Collection<String> fileUuids,
            @Nullable Collection<String> authors) {

        builder.onComponentOnly(onComponentOnly);
        if (onComponentOnly) {
            builder.componentUuids(componentUuids);
            return;
        }

        builder.authors(authors);
        checkArgument(projectUuids == null || projectKeys == null,
                "Parameters projects and projectUuids cannot be set simultaneously");
        if (projectUuids != null) {
            builder.projectUuids(projectUuids);
        } else if (projectKeys != null) {
            builder.projectUuids(convertComponentKeysToUuids(session, projectKeys));
        }
        builder.moduleUuids(moduleUuids);
        builder.directories(directories);
        builder.fileUuids(fileUuids);

        if (!componentUuids.isEmpty()) {
            addComponentsBasedOnQualifier(builder, session, componentUuids);
        }
    }

    private void addComponentsBasedOnQualifier(IssueQuery.Builder builder, DbSession session,
            Collection<String> componentUuids) {
        if (componentUuids.isEmpty()) {
            builder.componentUuids(componentUuids);
            return;
        }

        List<ComponentDto> components = dbClient.componentDao().selectByUuids(session, componentUuids);
        if (components.isEmpty()) {
            builder.componentUuids(componentUuids);
            return;
        }

        Set<String> qualifiers = components.stream().map(ComponentDto::qualifier)
                .collect(MoreCollectors.toHashSet());
        if (qualifiers.size() > 1) {
            throw new IllegalArgumentException(
                    "All components must have the same qualifier, found " + Joiner.on(',').join(qualifiers));
        }

        String qualifier = qualifiers.iterator().next();
        switch (qualifier) {
        case Qualifiers.VIEW:
        case Qualifiers.SUBVIEW:
            addViewsOrSubViews(builder, componentUuids);
            break;
        case Qualifiers.PROJECT:
            builder.projectUuids(componentUuids);
            break;
        case Qualifiers.MODULE:
            builder.moduleRootUuids(componentUuids);
            break;
        case Qualifiers.DIRECTORY:
            addDirectories(builder, components);
            break;
        case Qualifiers.FILE:
        case Qualifiers.UNIT_TEST_FILE:
            builder.fileUuids(componentUuids);
            break;
        default:
            throw new IllegalArgumentException(
                    "Unable to set search root context for components " + Joiner.on(',').join(componentUuids));
        }
    }

    private void addViewsOrSubViews(IssueQuery.Builder builder, Collection<String> viewOrSubViewUuids) {
        List<String> filteredViewUuids = new ArrayList<>();
        for (String viewUuid : viewOrSubViewUuids) {
            if (userSession.hasComponentUuidPermission(UserRole.USER, viewUuid)) {
                filteredViewUuids.add(viewUuid);
            }
        }
        if (filteredViewUuids.isEmpty()) {
            filteredViewUuids.add(UNKNOWN);
        }
        builder.viewUuids(filteredViewUuids);
    }

    private static void addDirectories(IssueQuery.Builder builder, List<ComponentDto> directories) {
        Collection<String> directoryModuleUuids = Sets.newHashSet();
        Collection<String> directoryPaths = Sets.newHashSet();
        for (ComponentDto directory : directories) {
            directoryModuleUuids.add(directory.moduleUuid());
            directoryPaths.add(directory.path());
        }
        builder.moduleUuids(directoryModuleUuids);
        builder.directories(directoryPaths);
    }

    private Collection<String> convertComponentKeysToUuids(DbSession dbSession, Collection<String> componentKeys) {
        List<String> componentUuids = dbClient.componentDao().selectByKeys(dbSession, componentKeys).stream()
                .map(ComponentDto::uuid).collect(MoreCollectors.toList());
        // If unknown components are given, but no components are found, then all issues will be returned,
        // so we add this hack in order to return no issue in this case.
        if (!componentKeys.isEmpty() && componentUuids.isEmpty()) {
            return singletonList(UNKNOWN);
        }
        return componentUuids;
    }

    @VisibleForTesting
    static Collection<RuleKey> toRules(@Nullable Object o) {
        Collection<RuleKey> result = null;
        if (o != null) {
            if (o instanceof List) {
                // assume that it contains only strings
                result = stringsToRules((List<String>) o);
            } else if (o instanceof String) {
                result = stringsToRules(newArrayList(Splitter.on(',').omitEmptyStrings().split((String) o)));
            }
        }
        return result;
    }

    @CheckForNull
    private static Collection<RuleKey> stringsToRules(@Nullable Collection<String> rules) {
        if (rules != null) {
            return Collections2.transform(rules, RuleKey::parse);
        }
        return null;
    }
}