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 }