All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.server.issue.IssueQueryService Maven / Gradle / Ivy

There is a newer version: 7.0
Show newest version
/*
 * SonarQube
 * Copyright (C) 2009-2016 SonarSource SA
 * mailto:contact 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.Predicate;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.ObjectUtils;
import org.joda.time.DateTime;
import org.joda.time.format.ISOPeriodFormat;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.DateUtils;
import org.sonar.api.utils.SonarException;
import org.sonar.api.utils.System2;
import org.sonar.api.web.UserRole;
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.server.component.ComponentService;
import org.sonar.server.rule.RuleKeyFunctions;
import org.sonar.server.user.UserSession;
import org.sonar.server.util.RubyUtils;
import org.sonarqube.ws.client.issue.IssueFilterParameters;
import org.sonarqube.ws.client.issue.SearchWsRequest;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Predicates.notNull;
import static com.google.common.collect.FluentIterable.from;
import static com.google.common.collect.Lists.newArrayList;
import static java.lang.String.format;
import static org.sonar.api.utils.DateUtils.longToDate;
import static org.sonar.db.component.ComponentDtoFunctions.toCopyResourceId;
import static org.sonar.db.component.ComponentDtoFunctions.toProjectUuid;
import static org.sonar.db.component.ComponentDtoFunctions.toUuid;
import static org.sonar.server.ws.WsUtils.checkFoundWithOptional;
import static org.sonar.server.ws.WsUtils.checkRequest;
import static org.sonarqube.ws.client.issue.IssueFilterParameters.COMPONENTS;
import static org.sonarqube.ws.client.issue.IssueFilterParameters.COMPONENT_KEYS;
import static org.sonarqube.ws.client.issue.IssueFilterParameters.COMPONENT_ROOTS;
import static org.sonarqube.ws.client.issue.IssueFilterParameters.COMPONENT_UUIDS;
import static org.sonarqube.ws.client.issue.IssueFilterParameters.CREATED_AFTER;
import static org.sonarqube.ws.client.issue.IssueFilterParameters.CREATED_IN_LAST;
import static org.sonarqube.ws.client.issue.IssueFilterParameters.SINCE_LEAK_PERIOD;

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

  public static final String LOGIN_MYSELF = "__me__";

  private static final String UNKNOWN = "";
  private final DbClient dbClient;
  private final ComponentService componentService;
  private final System2 system;
  private final UserSession userSession;

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

  public IssueQuery createFromMap(Map params) {
    DbSession session = dbClient.openSession(false);
    try {

      IssueQuery.Builder builder = IssueQuery.builder(userSession)
        .issueKeys(RubyUtils.toStrings(params.get(IssueFilterParameters.ISSUES)))
        .severities(RubyUtils.toStrings(params.get(IssueFilterParameters.SEVERITIES)))
        .statuses(RubyUtils.toStrings(params.get(IssueFilterParameters.STATUSES)))
        .resolutions(RubyUtils.toStrings(params.get(IssueFilterParameters.RESOLUTIONS)))
        .resolved(RubyUtils.toBoolean(params.get(IssueFilterParameters.RESOLVED)))
        .rules(toRules(params.get(IssueFilterParameters.RULES)))
        .assignees(buildAssignees(RubyUtils.toStrings(params.get(IssueFilterParameters.ASSIGNEES))))
        .languages(RubyUtils.toStrings(params.get(IssueFilterParameters.LANGUAGES)))
        .tags(RubyUtils.toStrings(params.get(IssueFilterParameters.TAGS)))
        .types(RubyUtils.toStrings(params.get(IssueFilterParameters.TYPES)))
        .assigned(RubyUtils.toBoolean(params.get(IssueFilterParameters.ASSIGNED)))
        .hideRules(RubyUtils.toBoolean(params.get(IssueFilterParameters.HIDE_RULES)))
        .createdAt(RubyUtils.toDate(params.get(IssueFilterParameters.CREATED_AT)))
        .createdAfter(buildCreatedAfterFromDates(RubyUtils.toDate(params.get(CREATED_AFTER)), (String) params.get(CREATED_IN_LAST)))
        .createdBefore(RubyUtils.toDate(params.get(IssueFilterParameters.CREATED_BEFORE)));

      Set allComponentUuids = Sets.newHashSet();
      boolean effectiveOnComponentOnly = mergeDeprecatedComponentParameters(session,
        RubyUtils.toBoolean(params.get(IssueFilterParameters.ON_COMPONENT_ONLY)),
        RubyUtils.toStrings(params.get(IssueFilterParameters.COMPONENTS)),
        RubyUtils.toStrings(params.get(IssueFilterParameters.COMPONENT_UUIDS)),
        RubyUtils.toStrings(params.get(IssueFilterParameters.COMPONENT_KEYS)),
        RubyUtils.toStrings(params.get(IssueFilterParameters.COMPONENT_ROOT_UUIDS)),
        RubyUtils.toStrings(params.get(IssueFilterParameters.COMPONENT_ROOTS)),
        allComponentUuids);

      addComponentParameters(builder, session,
        effectiveOnComponentOnly,
        allComponentUuids,
        RubyUtils.toStrings(params.get(IssueFilterParameters.PROJECT_UUIDS)),
        RubyUtils.toStrings(
          ObjectUtils.defaultIfNull(
            params.get(IssueFilterParameters.PROJECT_KEYS),
            params.get(IssueFilterParameters.PROJECTS))),
        RubyUtils.toStrings(params.get(IssueFilterParameters.MODULE_UUIDS)),
        RubyUtils.toStrings(params.get(IssueFilterParameters.DIRECTORIES)),
        RubyUtils.toStrings(params.get(IssueFilterParameters.FILE_UUIDS)),
        RubyUtils.toStrings(params.get(IssueFilterParameters.AUTHORS)));

      String sort = (String) params.get(IssueFilterParameters.SORT);
      if (!Strings.isNullOrEmpty(sort)) {
        builder.sort(sort);
        builder.asc(RubyUtils.toBoolean(params.get(IssueFilterParameters.ASC)));
      }
      String facetMode = (String) params.get(IssueFilterParameters.FACET_MODE);
      if (!Strings.isNullOrEmpty(facetMode)) {
        builder.facetMode(facetMode);
      } else {
        builder.facetMode(IssueFilterParameters.FACET_MODE_COUNT);
      }
      return builder.build();

    } finally {
      session.close();
    }
  }

  private Date buildCreatedAfterFromDates(@Nullable Date createdAfter, @Nullable String createdInLast) {
    checkArgument(createdAfter == null || createdInLast == null, format("%s and %s cannot be set simultaneously", CREATED_AFTER, 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;
  }

  public IssueQuery createFromRequest(SearchWsRequest request) {
    DbSession session = dbClient.openSession(false);
    try {
      IssueQuery.Builder builder = IssueQuery.builder(userSession)
        .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(parseAsDateTime(request.getCreatedAt()))
        .createdBefore(parseAsDateTime(request.getCreatedBefore()))
        .facetMode(request.getFacetMode());

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

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

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

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

    } finally {
      session.close();
    }
  }

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

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

    checkRequest(createdAfter == null, "'%s' and '%s' cannot be set simultaneously", CREATED_AFTER, 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();
    // TODO use ComponentFinder instead
    Date createdAfterFromSnapshot = findCreatedAfterFromComponentUuid(dbSession, uuid);
    return buildCreatedAfterFromDates(createdAfterFromSnapshot, createdInLast);
  }

  @CheckForNull
  private Date findCreatedAfterFromComponentUuid(DbSession dbSession, String uuid) {
    ComponentDto component = checkFoundWithOptional(componentService.getByUuid(uuid), "Component with id '%s' not found", uuid);
    SnapshotDto snapshot = dbClient.snapshotDao().selectLastSnapshotByComponentId(dbSession, component.getId());
    Long projectSnapshotId = snapshot == null ? null : snapshot.getRootId();
    SnapshotDto projectSnapshot = projectSnapshotId == null ? snapshot : dbClient.snapshotDao().selectById(dbSession, projectSnapshotId);
    return projectSnapshot == null ? null : longToDate(projectSnapshot.getPeriodDate(1));
  }

  private List buildAssignees(@Nullable List assigneesFromParams) {
    List 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, Boolean onComponentOnly,
    @Nullable Collection components,
    @Nullable Collection componentUuids,
    @Nullable Collection componentKeys,
    @Nullable Collection componentRootUuids,
    @Nullable Collection componentRoots,
    Set 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",
      COMPONENT_KEYS, COMPONENT_UUIDS, COMPONENTS, COMPONENT_ROOTS, COMPONENT_UUIDS);

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

  private static boolean atMostOneNonNullElement(Object... objects) {
    return !from(Arrays.asList(objects))
      .filter(notNull())
      .anyMatch(new HasTwoOrMoreElements());
  }

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

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

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

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

  protected void addComponentsBasedOnQualifier(IssueQuery.Builder builder, DbSession session, Collection componentUuids, Collection authors) {
    Set qualifiers = componentService.getDistinctQualifiers(session, componentUuids);
    if (qualifiers.isEmpty()) {
      // Qualifier not found, defaulting to componentUuids (e.g )
      builder.componentUuids(componentUuids);
      return;
    }
    if (qualifiers.size() > 1) {
      throw new IllegalArgumentException("All components must have the same qualifier, found " + Joiner.on(',').join(qualifiers));
    }

    String uniqueQualifier = qualifiers.iterator().next();
    switch (uniqueQualifier) {
      case Qualifiers.VIEW:
      case Qualifiers.SUBVIEW:
        addViewsOrSubViews(builder, componentUuids, uniqueQualifier);
        break;
      case "DEV":
        // XXX No constant for developer !!!
        Collection actualAuthors = authorsFromParamsOrFromDeveloper(session, componentUuids, authors);
        builder.authors(actualAuthors);
        break;
      case "DEV_PRJ":
        addDeveloperTechnicalProjects(builder, session, componentUuids, authors);
        break;
      case Qualifiers.PROJECT:
        builder.projectUuids(componentUuids);
        break;
      case Qualifiers.MODULE:
        builder.moduleRootUuids(componentUuids);
        break;
      case Qualifiers.DIRECTORY:
        addDirectories(builder, session, componentUuids);
        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 componentUuids, String uniqueQualifier) {
    List filteredViewUuids = newArrayList();
    for (String viewUuid : componentUuids) {
      if ((Qualifiers.VIEW.equals(uniqueQualifier) && userSession.hasComponentUuidPermission(UserRole.USER, viewUuid))
        || (Qualifiers.SUBVIEW.equals(uniqueQualifier) && userSession.hasComponentUuidPermission(UserRole.USER, viewUuid))) {
        filteredViewUuids.add(viewUuid);
      }
    }
    if (filteredViewUuids.isEmpty()) {
      filteredViewUuids.add(UNKNOWN);
    }
    builder.viewUuids(filteredViewUuids);
  }

  private void addDeveloperTechnicalProjects(IssueQuery.Builder builder, DbSession session, Collection componentUuids, Collection authors) {
    Collection technicalProjects = dbClient.componentDao().selectByUuids(session, componentUuids);
    Collection developerUuids = Collections2.transform(technicalProjects, toProjectUuid());
    Collection authorsFromProjects = authorsFromParamsOrFromDeveloper(session, developerUuids, authors);
    builder.authors(authorsFromProjects);
    Collection projectIds = Collections2.transform(technicalProjects, toCopyResourceId());
    List originalProjects = dbClient.componentDao().selectByIds(session, projectIds);
    Collection projectUuids = Collections2.transform(originalProjects, toUuid());
    builder.projectUuids(projectUuids);
  }

  private Collection authorsFromParamsOrFromDeveloper(DbSession session, Collection componentUuids, Collection authors) {
    return authors == null ? dbClient.authorDao().selectScmAccountsByDeveloperUuids(session, componentUuids) : authors;
  }

  private void addDirectories(IssueQuery.Builder builder, DbSession session, Collection componentUuids) {
    Collection directoryModuleUuids = Sets.newHashSet();
    Collection directoryPaths = Sets.newHashSet();
    for (ComponentDto directory : componentService.getByUuids(session, componentUuids)) {
      directoryModuleUuids.add(directory.moduleUuid());
      directoryPaths.add(directory.path());
    }
    builder.moduleUuids(directoryModuleUuids);
    builder.directories(directoryPaths);
  }

  private Collection componentUuids(DbSession session, @Nullable Collection componentKeys) {
    Collection componentUuids = Lists.newArrayList();
    componentUuids.addAll(componentService.componentUuids(session, componentKeys, true));
    // 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 != null && !componentKeys.isEmpty() && componentUuids.isEmpty()) {
      componentUuids.add(UNKNOWN);
    }
    return componentUuids;
  }

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

  @CheckForNull
  private static Collection stringsToRules(@Nullable Collection rules) {
    if (rules != null) {
      return newArrayList(Iterables.transform(rules, RuleKeyFunctions.stringToRuleKey()));
    }
    return null;
  }

  @CheckForNull
  private static Date parseAsDateTime(@Nullable String stringDate) {
    if (stringDate == null) {
      return null;
    }

    try {
      return DateUtils.parseDateTime(stringDate);
    } catch (SonarException notDateTime) {
      try {
        return DateUtils.parseDate(stringDate);
      } catch (SonarException notDateEither) {
        throw new IllegalArgumentException(String.format("'%s' cannot be parsed as either a date or date+time", stringDate));
      }
    }
  }

  private static class HasTwoOrMoreElements implements Predicate {
    private AtomicInteger counter;

    private HasTwoOrMoreElements() {
      this.counter = new AtomicInteger();
    }

    @Override
    public boolean apply(@Nonnull Object input) {
      Objects.requireNonNull(input);
      return counter.incrementAndGet() >= 2;
    }
  }
}