001/*
002 * Sonar, open source software quality management tool.
003 * Copyright (C) 2008-2012 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * Sonar is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * Sonar is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public
017 * License along with Sonar; if not, write to the Free Software
018 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
019 */
020package org.sonar.plugins.core.sensors;
021
022import com.google.common.annotations.VisibleForTesting;
023import com.google.common.base.Function;
024import com.google.common.collect.Collections2;
025import com.google.common.collect.Maps;
026import com.google.common.collect.Sets;
027import org.apache.commons.lang.ObjectUtils;
028import org.apache.commons.lang.StringUtils;
029import org.sonar.api.batch.*;
030import org.sonar.api.database.model.Snapshot;
031import org.sonar.api.resources.Project;
032import org.sonar.api.resources.Resource;
033import org.sonar.api.resources.ResourceUtils;
034import org.sonar.api.rules.Violation;
035import org.sonar.api.violations.ViolationQuery;
036import org.sonar.batch.index.ResourcePersister;
037import org.sonar.core.NotDryRun;
038import org.sonar.core.review.ReviewDao;
039import org.sonar.core.review.ReviewDto;
040
041import javax.annotation.Nullable;
042import java.util.*;
043
044@NotDryRun
045@DependsUpon(DecoratorBarriers.END_OF_VIOLATION_TRACKING)
046@DependedUpon(ReviewWorkflowDecorator.END_OF_REVIEWS_UPDATES)
047public class ReviewWorkflowDecorator implements Decorator {
048
049  public static final String END_OF_REVIEWS_UPDATES = "END_OF_REVIEWS_UPDATES";
050
051  private ReviewNotifications notifications;
052  private ReviewDao reviewDao;
053  private ResourcePersister resourcePersister;
054
055  public ReviewWorkflowDecorator(ReviewNotifications notifications, ReviewDao reviewDao, ResourcePersister resourcePersister) {
056    this.notifications = notifications;
057    this.reviewDao = reviewDao;
058    this.resourcePersister = resourcePersister;
059  }
060
061  public boolean shouldExecuteOnProject(Project project) {
062    return project.isLatestAnalysis();
063  }
064
065  public void decorate(Resource resource, DecoratorContext context) {
066    Snapshot snapshot = resourcePersister.getSnapshot(resource);
067    if (snapshot != null) {
068      Collection<ReviewDto> openReviews = reviewDao.selectOpenByResourceId(snapshot.getResourceId());
069      Set<ReviewDto> updated = Sets.newHashSet();
070      if (!openReviews.isEmpty()) {
071        List<Violation> violations = context.getViolations(ViolationQuery.create().forResource(resource).setSwitchMode(ViolationQuery.SwitchMode.BOTH));
072        closeResolvedStandardViolations(openReviews, violations, context.getProject(), resource, updated);
073        closeResolvedManualViolations(openReviews, context.getProject(), resource, updated);
074        reopenUnresolvedViolations(openReviews, context.getProject(), resource, updated);
075        updateReviewInformation(openReviews, violations, updated);
076      }
077      if (ResourceUtils.isRootProject(resource)) {
078        closeReviewsOnDeletedResources((Project) resource, snapshot.getResourceId(), snapshot.getId(), updated);
079      }
080      persistUpdates(updated);
081    }
082  }
083
084  private void persistUpdates(Set<ReviewDto> updated) {
085    if (!updated.isEmpty()) {
086      reviewDao.update(updated);
087    }
088  }
089
090  /**
091   * Close reviews that relate to resources that have been deleted or renamed.
092   */
093  private void closeReviewsOnDeletedResources(Project project, int rootProjectId, int rootSnapshotId, Set<ReviewDto> updated) {
094    Collection<ReviewDto> reviews = reviewDao.selectOnDeletedResources(rootProjectId, rootSnapshotId);
095    for (ReviewDto review : reviews) {
096      close(review, project, null);
097      updated.add(review);
098    }
099  }
100
101  private void updateReviewInformation(Collection<ReviewDto> openReviews, Collection<Violation> violations, Set<ReviewDto> updated) {
102    Map<Integer, Violation> violationsByPermanentId = Maps.newHashMap();
103    for (Violation violation : violations) {
104      if (violation.getPermanentId()!=null) {
105        violationsByPermanentId.put(violation.getPermanentId(), violation);
106      }
107    }
108
109    for (ReviewDto review : openReviews) {
110      Violation violation = violationsByPermanentId.get(review.getViolationPermanentId());
111      if (violation != null && !hasUpToDateInformation(review, violation)) {
112        review.setLine(violation.getLineId());
113        review.setTitle(violation.getMessage());
114        updated.add(review);
115      }
116    }
117  }
118
119  @VisibleForTesting
120  static boolean hasUpToDateInformation(ReviewDto review, Violation violation) {
121    return StringUtils.equals(review.getTitle(), violation.getMessage()) && ObjectUtils.equals(review.getLine(), violation.getLineId());
122  }
123
124  private void closeResolvedManualViolations(Collection<ReviewDto> openReviews, Project project, Resource resource, Set<ReviewDto> updated) {
125    for (ReviewDto openReview : openReviews) {
126      if (openReview.isManualViolation() && ReviewDto.STATUS_RESOLVED.equals(openReview.getStatus())) {
127        close(openReview, project, resource);
128        updated.add(openReview);
129      }
130    }
131  }
132
133  private void closeResolvedStandardViolations(Collection<ReviewDto> openReviews, List<Violation> violations, Project project, Resource resource, Set<ReviewDto> updated) {
134    Set<Integer> violationIds = Sets.newHashSet(Collections2.transform(violations, new ViolationToPermanentIdFunction()));
135
136    for (ReviewDto openReview : openReviews) {
137      if (!openReview.isManualViolation() && !violationIds.contains(openReview.getViolationPermanentId())) {
138        close(openReview, project, resource);
139        updated.add(openReview);
140      }
141    }
142  }
143
144  private void reopenUnresolvedViolations(Collection<ReviewDto> openReviews, Project project, Resource resource, Set<ReviewDto> updated) {
145    for (ReviewDto openReview : openReviews) {
146      if (ReviewDto.STATUS_RESOLVED.equals(openReview.getStatus()) && !ReviewDto.RESOLUTION_FALSE_POSITIVE.equals(openReview.getResolution())
147          && !openReview.isManualViolation()) {
148        reopen(openReview, project, resource);
149        updated.add(openReview);
150      }
151    }
152  }
153
154  private void close(ReviewDto review, Project project, Resource resource) {
155    notifications.notifyClosed(review, project, resource);
156    review.setStatus(ReviewDto.STATUS_CLOSED);
157    review.setUpdatedAt(new Date());
158  }
159
160  private void reopen(ReviewDto review, Project project, Resource resource) {
161    notifications.notifyReopened(review, project, resource);
162    review.setStatus(ReviewDto.STATUS_REOPENED);
163    review.setResolution(null);
164    review.setUpdatedAt(new Date());
165  }
166
167  private static final class ViolationToPermanentIdFunction implements Function<Violation, Integer> {
168    public Integer apply(@Nullable Violation violation) {
169      return (violation != null ? violation.getPermanentId() : null);
170    }
171  }
172}