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 }