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     */
020    package org.sonar.plugins.core.sensors;
021    
022    import com.google.common.annotations.VisibleForTesting;
023    import com.google.common.base.Function;
024    import com.google.common.collect.Collections2;
025    import com.google.common.collect.Maps;
026    import com.google.common.collect.Sets;
027    import org.apache.commons.lang.ObjectUtils;
028    import org.apache.commons.lang.StringUtils;
029    import org.sonar.api.batch.*;
030    import org.sonar.api.database.model.Snapshot;
031    import org.sonar.api.resources.Project;
032    import org.sonar.api.resources.Resource;
033    import org.sonar.api.resources.ResourceUtils;
034    import org.sonar.api.rules.Violation;
035    import org.sonar.api.violations.ViolationQuery;
036    import org.sonar.batch.index.ResourcePersister;
037    import org.sonar.core.NotDryRun;
038    import org.sonar.core.review.ReviewDao;
039    import org.sonar.core.review.ReviewDto;
040    
041    import java.util.*;
042    
043    @NotDryRun
044    @DependsUpon(DecoratorBarriers.END_OF_VIOLATION_TRACKING)
045    @DependedUpon(ReviewWorkflowDecorator.END_OF_REVIEWS_UPDATES)
046    public class ReviewWorkflowDecorator implements Decorator {
047    
048      public static final String END_OF_REVIEWS_UPDATES = "END_OF_REVIEWS_UPDATES";
049    
050      private ReviewNotifications notifications;
051      private ReviewDao reviewDao;
052      private ResourcePersister resourcePersister;
053    
054      public ReviewWorkflowDecorator(ReviewNotifications notifications, ReviewDao reviewDao, ResourcePersister resourcePersister) {
055        this.notifications = notifications;
056        this.reviewDao = reviewDao;
057        this.resourcePersister = resourcePersister;
058      }
059    
060      public boolean shouldExecuteOnProject(Project project) {
061        return project.isLatestAnalysis();
062      }
063    
064      public void decorate(Resource resource, DecoratorContext context) {
065        Snapshot snapshot = resourcePersister.getSnapshot(resource);
066        if (snapshot != null) {
067          Collection<ReviewDto> openReviews = reviewDao.selectOpenByResourceId(snapshot.getResourceId());
068          Set<ReviewDto> updated = Sets.newHashSet();
069          if (!openReviews.isEmpty()) {
070            List<Violation> violations = context.getViolations(ViolationQuery.create().forResource(resource).setSwitchMode(ViolationQuery.SwitchMode.BOTH));
071            closeResolvedStandardViolations(openReviews, violations, context.getProject(), resource, updated);
072            closeResolvedManualViolations(openReviews, context.getProject(), resource, updated);
073            reopenUnresolvedViolations(openReviews, context.getProject(), resource, updated);
074            updateReviewInformation(openReviews, violations, updated);
075          }
076          if (ResourceUtils.isRootProject(resource)) {
077            closeReviewsOnDeletedResources((Project) resource, snapshot.getResourceId(), snapshot.getId(), updated);
078          }
079          persistUpdates(updated);
080        }
081      }
082    
083      private void persistUpdates(Set<ReviewDto> updated) {
084        if (!updated.isEmpty()) {
085          reviewDao.update(updated);
086        }
087      }
088    
089      /**
090       * Close reviews that relate to resources that have been deleted or renamed.
091       */
092      private void closeReviewsOnDeletedResources(Project project, int rootProjectId, int rootSnapshotId, Set<ReviewDto> updated) {
093        Collection<ReviewDto> reviews = reviewDao.selectOnDeletedResources(rootProjectId, rootSnapshotId);
094        for (ReviewDto review : reviews) {
095          close(review, project, null);
096          updated.add(review);
097        }
098      }
099    
100      private void updateReviewInformation(Collection<ReviewDto> openReviews, Collection<Violation> violations, Set<ReviewDto> updated) {
101        Map<Integer, Violation> violationsByPermanentId = Maps.newHashMap();
102        for (Violation violation : violations) {
103          if (violation.getPermanentId()!=null) {
104            violationsByPermanentId.put(violation.getPermanentId(), violation);
105          }
106        }
107    
108        for (ReviewDto review : openReviews) {
109          Violation violation = violationsByPermanentId.get(review.getViolationPermanentId());
110          if (violation != null && !hasUpToDateInformation(review, violation)) {
111            review.setLine(violation.getLineId());
112            review.setTitle(violation.getMessage());
113            updated.add(review);
114          }
115        }
116      }
117    
118      @VisibleForTesting
119      static boolean hasUpToDateInformation(ReviewDto review, Violation violation) {
120        return StringUtils.equals(review.getTitle(), violation.getMessage()) && ObjectUtils.equals(review.getLine(), violation.getLineId());
121      }
122    
123      private void closeResolvedManualViolations(Collection<ReviewDto> openReviews, Project project, Resource resource, Set<ReviewDto> updated) {
124        for (ReviewDto openReview : openReviews) {
125          if (openReview.isManualViolation() && ReviewDto.STATUS_RESOLVED.equals(openReview.getStatus())) {
126            close(openReview, project, resource);
127            updated.add(openReview);
128          }
129        }
130      }
131    
132      private void closeResolvedStandardViolations(Collection<ReviewDto> openReviews, List<Violation> violations, Project project, Resource resource, Set<ReviewDto> updated) {
133        Set<Integer> violationIds = Sets.newHashSet(Collections2.transform(violations, new ViolationToPermanentIdFunction()));
134    
135        for (ReviewDto openReview : openReviews) {
136          if (!openReview.isManualViolation() && !violationIds.contains(openReview.getViolationPermanentId())) {
137            close(openReview, project, resource);
138            updated.add(openReview);
139          }
140        }
141      }
142    
143      private void reopenUnresolvedViolations(Collection<ReviewDto> openReviews, Project project, Resource resource, Set<ReviewDto> updated) {
144        for (ReviewDto openReview : openReviews) {
145          if (ReviewDto.STATUS_RESOLVED.equals(openReview.getStatus()) && !ReviewDto.RESOLUTION_FALSE_POSITIVE.equals(openReview.getResolution())
146              && !openReview.isManualViolation()) {
147            reopen(openReview, project, resource);
148            updated.add(openReview);
149          }
150        }
151      }
152    
153      private void close(ReviewDto review, Project project, Resource resource) {
154        notifications.notifyClosed(review, project, resource);
155        review.setStatus(ReviewDto.STATUS_CLOSED);
156        review.setUpdatedAt(new Date());
157      }
158    
159      private void reopen(ReviewDto review, Project project, Resource resource) {
160        notifications.notifyReopened(review, project, resource);
161        review.setStatus(ReviewDto.STATUS_REOPENED);
162        review.setResolution(null);
163        review.setUpdatedAt(new Date());
164      }
165    
166      private static final class ViolationToPermanentIdFunction implements Function<Violation, Integer> {
167        public Integer apply(Violation violation) {
168          return violation.getPermanentId();
169        }
170      }
171    }