001    /*
002     * Sonar, open source software quality management tool.
003     * Copyright (C) 2008-2011 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 org.slf4j.Logger;
023    import org.slf4j.LoggerFactory;
024    import org.sonar.api.batch.*;
025    import org.sonar.api.database.DatabaseSession;
026    import org.sonar.api.database.model.Snapshot;
027    import org.sonar.api.database.model.User;
028    import org.sonar.api.notifications.Notification;
029    import org.sonar.api.notifications.NotificationManager;
030    import org.sonar.api.resources.Project;
031    import org.sonar.api.resources.Resource;
032    import org.sonar.api.resources.ResourceUtils;
033    import org.sonar.api.security.UserFinder;
034    import org.sonar.batch.index.ResourcePersister;
035    import org.sonar.core.NotDryRun;
036    import org.sonar.jpa.entity.Review;
037    
038    import java.util.List;
039    
040    /**
041     * Decorator that handles the life cycle of a review (for instance, closes a review when its corresponding violation has been fixed).
042     */
043    @NotDryRun
044    @DependsUpon(DecoratorBarriers.END_OF_VIOLATION_PERSISTENCE)
045    @DependedUpon(CloseReviewsDecorator.REVIEW_LIFECYCLE_BARRIER)
046    public class CloseReviewsDecorator implements Decorator {
047    
048      private static final Logger LOG = LoggerFactory.getLogger(CloseReviewsDecorator.class);
049      public static final String REVIEW_LIFECYCLE_BARRIER = "REVIEW_LIFECYCLE_BARRIER";
050    
051      private Project project;
052      private ResourcePersister resourcePersister;
053      private DatabaseSession databaseSession;
054      private NotificationManager notificationManager;
055      private UserFinder userFinder;
056    
057      public CloseReviewsDecorator(Project project, ResourcePersister resourcePersister, DatabaseSession databaseSession,
058                                   NotificationManager notificationManager, UserFinder userFinder) {
059        this.project = project;
060        this.resourcePersister = resourcePersister;
061        this.databaseSession = databaseSession;
062        this.notificationManager = notificationManager;
063        this.userFinder = userFinder;
064      }
065    
066      public boolean shouldExecuteOnProject(Project project) {
067        return project.isLatestAnalysis();
068      }
069    
070      public void decorate(Resource resource, DecoratorContext context) {
071        Snapshot currentSnapshot = resourcePersister.getSnapshot(resource);
072        if (currentSnapshot != null) {
073          int resourceId = currentSnapshot.getResourceId();
074          int snapshotId = currentSnapshot.getId();
075    
076          closeReviewsOnResolvedViolations(resource, resourceId, snapshotId);
077          reopenReviewsOnUnresolvedViolations(resource, resourceId);
078    
079          if (ResourceUtils.isRootProject(resource)) {
080            closeReviewsForDeletedResources(resourceId, currentSnapshot.getId());
081          }
082    
083          databaseSession.commit();
084        }
085      }
086    
087      /**
088       * Close reviews for which violations have been fixed.
089       */
090      protected int closeReviewsOnResolvedViolations(Resource resource, int resourceId, int snapshotId) {
091        String conditions = " WHERE resource_id=" + resourceId + " AND "
092          + "( "
093          + "  ( "
094          + "    manual_violation=:automaticViolation AND status!='CLOSED' AND "
095          + "    rule_failure_permanent_id NOT IN (SELECT permanent_id FROM rule_failures WHERE snapshot_id=" + snapshotId + " AND permanent_id IS NOT NULL)"
096          + "  )"
097          + "  OR "
098          + "  (manual_violation=:manualViolation AND status='RESOLVED')"
099          + ")";
100        List<Review> reviews = databaseSession.getEntityManager().createNativeQuery("SELECT * FROM reviews " + conditions, Review.class)
101          .setParameter("automaticViolation", false)
102          .setParameter("manualViolation", true)
103          .getResultList();
104    
105        for (Review review : reviews) {
106          notifyClosed(resource, review);
107        }
108    
109        int rowUpdated = databaseSession.createNativeQuery("UPDATE reviews SET status='CLOSED', updated_at=CURRENT_TIMESTAMP" + conditions)
110          .setParameter("automaticViolation", false)
111          .setParameter("manualViolation", true)
112          .executeUpdate();
113        if (rowUpdated > 0) {
114          LOG.debug("- {} reviews closed on #{}", rowUpdated, resourceId);
115        }
116        return rowUpdated;
117      }
118    
119      /**
120       * Reopen reviews that had been set to resolved but for which the violation is still here.
121       * Manual violations are ignored.
122       */
123      protected int reopenReviewsOnUnresolvedViolations(Resource resource, int resourceId) {
124        String conditions = " WHERE status='RESOLVED' AND resolution<>'FALSE-POSITIVE' AND manual_violation=:manualViolation AND resource_id=" + resourceId;
125        List<Review> reviews = databaseSession.getEntityManager().createNativeQuery("SELECT * FROM reviews " + conditions, Review.class)
126          .setParameter("manualViolation", false)
127          .getResultList();
128        for (Review review : reviews) {
129          notifyReopened(resource, review);
130        }
131    
132        int rowUpdated = databaseSession.createNativeQuery("UPDATE reviews SET status='REOPENED', resolution=NULL, updated_at=CURRENT_TIMESTAMP" + conditions)
133          .setParameter("manualViolation", false)
134          .executeUpdate();
135        if (rowUpdated > 0) {
136          LOG.debug("- {} reviews reopened on #{}", rowUpdated, resourceId);
137        }
138        return rowUpdated;
139      }
140    
141      /**
142       * Close reviews that relate to resources that have been deleted or renamed.
143       */
144      protected int closeReviewsForDeletedResources(int projectId, int projectSnapshotId) {
145        String conditions = " WHERE status!='CLOSED' AND project_id=" + projectId
146          + " AND resource_id IN ( SELECT prev.project_id FROM snapshots prev  WHERE prev.root_project_id=" + projectId
147          + " AND prev.islast=? AND NOT EXISTS ( SELECT cur.id FROM snapshots cur WHERE cur.root_snapshot_id=" + projectSnapshotId
148          + " AND cur.created_at > prev.created_at AND cur.root_project_id=" + projectId + " AND cur.project_id=prev.project_id ) )";
149        List<Review> reviews = databaseSession.getEntityManager().createNativeQuery("SELECT * FROM reviews " + conditions, Review.class)
150          .setParameter(1, Boolean.TRUE)
151          .getResultList();
152        for (Review review : reviews) {
153          notifyClosed(null, review);
154        }
155        int rowUpdated = databaseSession.createNativeQuery("UPDATE reviews SET status='CLOSED', updated_at=CURRENT_TIMESTAMP" + conditions)
156          .setParameter(1, Boolean.TRUE)
157          .executeUpdate();
158        LOG.debug("- {} reviews set to 'closed' on project #{}", rowUpdated, projectSnapshotId);
159        return rowUpdated;
160      }
161    
162      private String getCreator(Review review) {
163        if (review.getUserId() == null) { // no creator and in fact this should never happen in real-life, however happens during unit tests
164          return null;
165        }
166        User user = userFinder.findById(review.getUserId());
167        return user != null ? user.getLogin() : null;
168      }
169    
170      private String getAssignee(Review review) {
171        if (review.getAssigneeId() == null) { // not assigned
172          return null;
173        }
174        User user = userFinder.findById(review.getAssigneeId());
175        return user != null ? user.getLogin() : null;
176      }
177    
178      void notifyReopened(Resource resource, Review review) {
179        Notification notification = createReviewNotification(resource, review)
180          .setFieldValue("old.status", review.getStatus())
181          .setFieldValue("new.status", "REOPENED")
182          .setFieldValue("old.resolution", review.getResolution())
183          .setFieldValue("new.resolution", null);
184        notificationManager.scheduleForSending(notification);
185      }
186    
187      void notifyClosed(Resource resource, Review review) {
188        Notification notification = createReviewNotification(resource, review)
189          .setFieldValue("old.status", review.getStatus())
190          .setFieldValue("new.status", "CLOSED");
191        notificationManager.scheduleForSending(notification);
192      }
193    
194      private Notification createReviewNotification(Resource resource, Review review) {
195        return new Notification("review-changed")
196          .setFieldValue("reviewId", String.valueOf(review.getId()))
197          .setFieldValue("project", project.getRoot().getLongName())
198          .setFieldValue("resource", resource != null ? resource.getLongName() : null)
199          .setFieldValue("title", review.getTitle())
200          .setFieldValue("creator", getCreator(review))
201          .setFieldValue("assignee", getAssignee(review));
202      }
203    
204    }