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