From 2dbed652688d87b303f7821ea619ed36ba654a19 Mon Sep 17 00:00:00 2001 From: Evgeny Mandrikov Date: Fri, 27 May 2011 02:07:33 +0400 Subject: [PATCH] SONAR-1922 Add a kind of version control for quality profiles Apply patch, which was contributed by Julien Henry: * Following algorithm was implemented: Every profile starts with version=1 and used=false. As soon as there is an analysis of a project, the involved profile is set to used=true. Every modification to a quality profile (activation, deactivation or modification of rule) is logged in DB in dedicated tables. When a modification is done on a profile that is used=true, then version number is increased and profile is set to used=false. * Introduced new metric to store profile version, which was used during analysis. * If profile for project is different than the one used during previous analysis, then event would be created. * Introduced new tab 'changelog' for profiles. Following fixes were applied on original patch: * Index name limited to 30 characters in Oracle DB, so names were reduced. * Field ActiveRuleChange.profileVersion never read locally, because ruby read it directly from DB, so getter added. * Direction doesn't make sense for 'profile_version' metric, so was removed. * Fixed ProfileEventsSensor: it seems that TimeMachine not guarantee that the order of measures would be the same as in query, so we should perform two sequential queries. * Fixed handling of null values during migration. --- .../org/sonar/plugins/core/CorePlugin.java | 1 + .../core/sensors/ProfileEventsSensor.java | 82 +++++++ .../plugins/core/sensors/ProfileSensor.java | 12 +- .../core/sensors/ProfileEventsSensorTest.java | 162 +++++++++++++ .../core/sensors/ProfileSensorTest.java | 6 +- .../org/sonar/jpa/entity/SchemaMigration.java | 2 +- .../main/resources/META-INF/persistence.xml | 4 +- .../jpa/test/AbstractDbUnitTestCase.java | 9 +- .../main/java/org/sonar/api/batch/Event.java | 5 + .../org/sonar/api/batch/TimeMachineQuery.java | 8 + .../org/sonar/api/measures/CoreMetrics.java | 6 + .../org/sonar/api/profiles/RulesProfile.java | 24 ++ .../org/sonar/api/rules/ActiveRuleChange.java | 213 +++++++++++++++++ .../api/rules/ActiveRuleParamChange.java | 97 ++++++++ .../sonar/api/profiles/RulesProfileTest.java | 6 + .../server/configuration/ProfilesBackup.java | 10 +- .../server/configuration/ProfilesManager.java | 224 +++++++++++++++--- .../java/org/sonar/server/ui/JRubyFacade.java | 26 +- .../app/controllers/profiles_controller.rb | 17 +- .../rules_configuration_controller.rb | 28 ++- .../WEB-INF/app/models/active_rule_change.rb | 37 +++ .../app/models/active_rule_param_change.rb | 32 +++ .../WEB-INF/app/models/event_category.rb | 6 +- .../WEB-INF/app/views/profiles/_tabs.html.erb | 3 + .../app/views/profiles/changelog.html.erb | 77 ++++++ .../WEB-INF/app/views/profiles/index.html.erb | 3 + .../db/migrate/202_create_rule_changes.rb | 53 +++++ .../configuration/InheritedProfilesTest.java | 10 +- .../server/configuration/RuleChangeTest.java | 99 ++++++++ .../configuration/BackupTest/backup-valid.xml | 4 + .../shouldActivateInChildren-result.xml | 4 +- .../shouldActivateInChildren.xml | 4 +- .../shouldChangeParent-result.xml | 6 +- .../shouldChangeParent.xml | 6 +- .../shouldCheckCycles.xml | 6 +- .../shouldDeactivateInChildren-result.xml | 4 +- .../shouldDeactivateInChildren.xml | 4 +- ...shouldNotDeleteInheritedProfile-result.xml | 6 +- .../shouldRemoveParent-result.xml | 4 +- .../shouldRemoveParent.xml | 4 +- .../shouldRenameInheritedProfile-result.xml | 6 +- .../shouldSetParent-result.xml | 4 +- .../InheritedProfilesTest/shouldSetParent.xml | 4 +- .../changeParentProfile-result.xml | 5 + .../RuleChangeTest/changeParentProfile.xml | 22 ++ .../RuleChangeTest/initialData.xml | 21 ++ .../RuleChangeTest/ruleActivated-result.xml | 5 + .../RuleChangeTest/ruleDeactivated-result.xml | 7 + .../ruleParamChanged-result.xml | 7 + .../RuleChangeTest/ruleReverted-result.xml | 8 + .../RuleChangeTest/ruleReverted.xml | 19 ++ .../ruleSeverityChanged-result.xml | 5 + .../versionIncreaseIfUsed-result.xml | 7 + ...sionIncreaseIfUsedAndInChildren-result.xml | 7 + ...bleProfilesWithMissingLanguages-result.xml | 10 +- ...uldDisableProfilesWithMissingLanguages.xml | 10 +- ...nableProfilesWithKnownLanguages-result.xml | 10 +- ...shouldEnableProfilesWithKnownLanguages.xml | 10 +- .../cleanAlerts-result.xml | 4 +- .../RegisterMetricsTest/cleanAlerts.xml | 4 +- .../disableDeprecatedActiveRuleParameters.xml | 2 +- .../disableDeprecatedActiveRules.xml | 2 +- .../org/sonar/test/persistence/sonar-test.ddl | 26 ++ 63 files changed, 1395 insertions(+), 124 deletions(-) create mode 100644 plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileEventsSensor.java create mode 100644 plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileEventsSensorTest.java create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/rules/ActiveRuleChange.java create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/rules/ActiveRuleParamChange.java create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/models/active_rule_change.rb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/models/active_rule_param_change.rb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/views/profiles/changelog.html.erb create mode 100644 sonar-server/src/main/webapp/WEB-INF/db/migrate/202_create_rule_changes.rb create mode 100644 sonar-server/src/test/java/org/sonar/server/configuration/RuleChangeTest.java create mode 100644 sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/changeParentProfile-result.xml create mode 100644 sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/changeParentProfile.xml create mode 100644 sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/initialData.xml create mode 100644 sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleActivated-result.xml create mode 100644 sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleDeactivated-result.xml create mode 100644 sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleParamChanged-result.xml create mode 100644 sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleReverted-result.xml create mode 100644 sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleReverted.xml create mode 100644 sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleSeverityChanged-result.xml create mode 100644 sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/versionIncreaseIfUsed-result.xml create mode 100644 sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/versionIncreaseIfUsedAndInChildren-result.xml diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java index 4634bcb01fe..61d8a1380e1 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java @@ -196,6 +196,7 @@ public class CorePlugin extends SonarPlugin { // batch extensions.add(ProfileSensor.class); + extensions.add(ProfileEventsSensor.class); extensions.add(ProjectLinksSensor.class); extensions.add(AsynchronousMeasuresSensor.class); extensions.add(UnitTestDecorator.class); diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileEventsSensor.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileEventsSensor.java new file mode 100644 index 00000000000..76283ba34b9 --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileEventsSensor.java @@ -0,0 +1,82 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.core.sensors; + +import org.sonar.api.batch.*; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Measure; +import org.sonar.api.measures.Metric; +import org.sonar.api.profiles.RulesProfile; +import org.sonar.api.resources.Project; + +import java.util.List; + +public class ProfileEventsSensor implements Sensor { + + private final RulesProfile profile; + private final TimeMachine timeMachine; + + public ProfileEventsSensor(RulesProfile profile, TimeMachine timeMachine) { + this.profile = profile; + this.timeMachine = timeMachine; + } + + public boolean shouldExecuteOnProject(Project project) { + return true; + } + + public void analyse(Project project, SensorContext context) { + if (profile == null) { + return; + } + String currentProfile = profile.getName(); + int currentProfileId = profile.getId(); + int currentProfileVersion = profile.getVersion(); + + int pastProfileId = getPreviousMeasureValue(project, CoreMetrics.PROFILE, -1); + int pastProfileVersion = getPreviousMeasureValue(project, CoreMetrics.PROFILE, 1); + + if (pastProfileId != currentProfileId) { + // A different profile is used for this project + context.createEvent(project, currentProfile + " V" + currentProfileVersion, + "A different quality profile was used", Event.CATEGORY_PROFILE, null); + } else if (pastProfileVersion != currentProfileVersion) { + // Same profile but new version + context.createEvent(project, currentProfile + " V" + currentProfileVersion, + "A new version of the quality profile was used", Event.CATEGORY_PROFILE, null); + } + } + + private int getPreviousMeasureValue(Project project, Metric metric, int defaultValue) { + TimeMachineQuery query = new TimeMachineQuery(project) + .setOnlyLastAnalysis(true) + .setMetrics(metric); + List measures = timeMachine.getMeasures(query); + if (measures.isEmpty()) { + return defaultValue; + } + return measures.get(0).getIntValue(); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileSensor.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileSensor.java index fb8787355d8..5468467055d 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileSensor.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileSensor.java @@ -21,6 +21,7 @@ package org.sonar.plugins.core.sensors; import org.sonar.api.batch.Sensor; import org.sonar.api.batch.SensorContext; +import org.sonar.api.database.DatabaseSession; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.Measure; import org.sonar.api.profiles.RulesProfile; @@ -28,10 +29,12 @@ import org.sonar.api.resources.Project; public class ProfileSensor implements Sensor { - private RulesProfile profile; + private final RulesProfile profile; + private final DatabaseSession session; - public ProfileSensor(RulesProfile profile) { + public ProfileSensor(RulesProfile profile, DatabaseSession session) { this.profile = profile; + this.session = session; } public boolean shouldExecuteOnProject(Project project) { @@ -41,10 +44,15 @@ public class ProfileSensor implements Sensor { public void analyse(Project project, SensorContext context) { if (profile != null) { Measure measure = new Measure(CoreMetrics.PROFILE, profile.getName()); + Measure measureVersion = new Measure(CoreMetrics.PROFILE_VERSION, Integer.valueOf(profile.getVersion()).doubleValue()); if (profile.getId() != null) { measure.setValue(profile.getId().doubleValue()); + + profile.setUsed(true); + session.merge(profile); } context.saveMeasure(measure); + context.saveMeasure(measureVersion); } } diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileEventsSensorTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileEventsSensorTest.java new file mode 100644 index 00000000000..d0b44316eaf --- /dev/null +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileEventsSensorTest.java @@ -0,0 +1,162 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.core.sensors; + +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.batch.Event; +import org.sonar.api.batch.SensorContext; +import org.sonar.api.batch.TimeMachine; +import org.sonar.api.batch.TimeMachineQuery; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Measure; +import org.sonar.api.profiles.RulesProfile; +import org.sonar.api.resources.Project; +import org.sonar.api.resources.Resource; + +import java.text.ParseException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; + +public class ProfileEventsSensorTest { + + private Project project; + private SensorContext context; + + @Before + public void prepare() { + project = mock(Project.class); + context = mock(SensorContext.class); + } + + @Test + public void shouldDoNothingIfNoProfile() throws ParseException { + ProfileEventsSensor sensor = new ProfileEventsSensor(null, null); + + sensor.analyse(project, context); + + verify(context, never()).createEvent((Resource) anyObject(), anyString(), anyString(), anyString(), (Date) anyObject()); + } + + @Test + public void shouldDoNothingIfNoProfileChange() throws ParseException { + RulesProfile profile = mockProfile(1); + TimeMachine timeMachine = mockTM(project, 22.0, 1.0); // Same profile, same version + ProfileEventsSensor sensor = new ProfileEventsSensor(profile, timeMachine); + + sensor.analyse(project, context); + + verify(context, never()).createEvent((Resource) anyObject(), anyString(), anyString(), anyString(), (Date) anyObject()); + } + + @Test + public void shouldCreateEventIfProfileChange() throws ParseException { + RulesProfile profile = mockProfile(1); + TimeMachine timeMachine = mockTM(project, 21.0, 1.0); // Different profile + ProfileEventsSensor sensor = new ProfileEventsSensor(profile, timeMachine); + + sensor.analyse(project, context); + + verify(context).createEvent(same(project), eq("Profile V1"), eq("A different quality profile was used"), + same(Event.CATEGORY_PROFILE), (Date) anyObject()); + } + + @Test + public void shouldCreateEventIfProfileVersionChange() throws ParseException { + RulesProfile profile = mockProfile(2); + TimeMachine timeMachine = mockTM(project, 22.0, 1.0); // Same profile, different version + ProfileEventsSensor sensor = new ProfileEventsSensor(profile, timeMachine); + + sensor.analyse(project, context); + + verify(context).createEvent(same(project), eq("Profile V2"), eq("A new version of the quality profile was used"), + same(Event.CATEGORY_PROFILE), (Date) anyObject()); + } + + @Test + public void shouldCreateEventIfFirstAnalysis() throws ParseException { + RulesProfile profile = mockProfile(2); + TimeMachine timeMachine = mockTM(project, null, null); + ProfileEventsSensor sensor = new ProfileEventsSensor(profile, timeMachine); + + sensor.analyse(project, context); + + verify(context).createEvent(same(project), eq("Profile V2"), eq("A different quality profile was used"), + same(Event.CATEGORY_PROFILE), (Date) anyObject()); + } + + @Test + public void shouldNotCreateEventIfFirstProfileVersionAndStillV1() throws ParseException { + RulesProfile profile = mockProfile(1); + TimeMachine timeMachine = mockTMWithNullVersion(project, 22.0); + ProfileEventsSensor sensor = new ProfileEventsSensor(profile, timeMachine); + + sensor.analyse(project, context); + + verify(context, never()).createEvent((Resource) anyObject(), anyString(), anyString(), anyString(), (Date) anyObject()); + } + + @Test + public void shouldCreateEventIfFirstProfileVersionAndMoreThanV1() throws ParseException { + RulesProfile profile = mockProfile(2); + TimeMachine timeMachine = mockTMWithNullVersion(project, 22.0); + ProfileEventsSensor sensor = new ProfileEventsSensor(profile, timeMachine); + + sensor.analyse(project, context); + + verify(context).createEvent(same(project), eq("Profile V2"), eq("A new version of the quality profile was used"), + same(Event.CATEGORY_PROFILE), (Date) anyObject()); + } + + private RulesProfile mockProfile(int version) { + RulesProfile profile = mock(RulesProfile.class); + when(profile.getId()).thenReturn(22); + when(profile.getName()).thenReturn("Profile"); + when(profile.getVersion()).thenReturn(version); // New version + return profile; + } + + private TimeMachine mockTM(Project project, double profileValue, double versionValue) { + return mockTM(project, new Measure(CoreMetrics.PROFILE, profileValue), + new Measure(CoreMetrics.PROFILE_VERSION, versionValue)); + } + + private TimeMachine mockTMWithNullVersion(Project project, double profileValue) { + return mockTM(project, new Measure(CoreMetrics.PROFILE, profileValue), null); + } + + private TimeMachine mockTM(Project project, Measure result1, Measure result2) { + TimeMachine timeMachine = mock(TimeMachine.class); + + when(timeMachine.getMeasures(any(TimeMachineQuery.class))) + .thenReturn(result1 == null ? Collections. emptyList() : Arrays.asList(result1)) + .thenReturn(result2 == null ? Collections. emptyList() : Arrays.asList(result2)); + + return timeMachine; + } + +} diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileSensorTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileSensorTest.java index 29ac9fad4ec..9357657db9d 100644 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileSensorTest.java +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileSensorTest.java @@ -23,6 +23,7 @@ import org.junit.Test; import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.*; import org.sonar.api.batch.SensorContext; +import org.sonar.api.database.DatabaseSession; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.profiles.RulesProfile; import org.sonar.api.test.IsMeasure; @@ -34,11 +35,14 @@ public class ProfileSensorTest { RulesProfile profile = mock(RulesProfile.class); when(profile.getId()).thenReturn(22); when(profile.getName()).thenReturn("fake"); + when(profile.getVersion()).thenReturn(2); SensorContext context = mock(SensorContext.class); + DatabaseSession session = mock(DatabaseSession.class); - ProfileSensor sensor = new ProfileSensor(profile); + ProfileSensor sensor = new ProfileSensor(profile, session); sensor.analyse(null, context); verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.PROFILE, 22d))); + verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.PROFILE_VERSION, 2d))); } } diff --git a/sonar-core/src/main/java/org/sonar/jpa/entity/SchemaMigration.java b/sonar-core/src/main/java/org/sonar/jpa/entity/SchemaMigration.java index 039029276d7..25351acce12 100644 --- a/sonar-core/src/main/java/org/sonar/jpa/entity/SchemaMigration.java +++ b/sonar-core/src/main/java/org/sonar/jpa/entity/SchemaMigration.java @@ -40,7 +40,7 @@ public class SchemaMigration { - complete the Derby DDL file used for unit tests : sonar-testing-harness/src/main/resources/org/sonar/test/persistence/sonar-test.ddl */ - public static final int LAST_VERSION = 201; + public static final int LAST_VERSION = 202; public final static String TABLE_NAME = "schema_migrations"; diff --git a/sonar-core/src/main/resources/META-INF/persistence.xml b/sonar-core/src/main/resources/META-INF/persistence.xml index 304887e52b7..4d287d8a0df 100644 --- a/sonar-core/src/main/resources/META-INF/persistence.xml +++ b/sonar-core/src/main/resources/META-INF/persistence.xml @@ -35,7 +35,9 @@ org.sonar.api.database.model.AsyncMeasureSnapshot org.sonar.api.batch.Event org.sonar.api.profiles.Alert - + org.sonar.api.rules.ActiveRuleChange + org.sonar.api.rules.ActiveRuleParamChange + diff --git a/sonar-core/src/test/java/org/sonar/jpa/test/AbstractDbUnitTestCase.java b/sonar-core/src/test/java/org/sonar/jpa/test/AbstractDbUnitTestCase.java index 6e9aa22013a..834657bb7f9 100644 --- a/sonar-core/src/test/java/org/sonar/jpa/test/AbstractDbUnitTestCase.java +++ b/sonar-core/src/test/java/org/sonar/jpa/test/AbstractDbUnitTestCase.java @@ -29,7 +29,9 @@ import org.dbunit.database.IDatabaseConnection; import org.dbunit.dataset.CompositeDataSet; import org.dbunit.dataset.DataSetException; import org.dbunit.dataset.IDataSet; +import org.dbunit.dataset.ITable; import org.dbunit.dataset.ReplacementDataSet; +import org.dbunit.dataset.filter.DefaultColumnFilter; import org.dbunit.dataset.xml.FlatXmlDataSet; import org.dbunit.ext.hsqldb.HsqldbDataTypeFactory; import org.dbunit.operation.DatabaseOperation; @@ -147,12 +149,17 @@ public abstract class AbstractDbUnitTestCase { } protected final void checkTables(String testName, String... tables) { + checkTables(testName, new String[] {}, tables); + } + + protected final void checkTables(String testName, String[] excludedColumnNames, String... tables) { getSession().commit(); try { IDataSet dataSet = getCurrentDataSet(); IDataSet expectedDataSet = getExpectedData(testName); for (String table : tables) { - Assertion.assertEquals(expectedDataSet.getTable(table), dataSet.getTable(table)); + ITable filteredTable = DefaultColumnFilter.excludedColumnsTable(dataSet.getTable(table), excludedColumnNames); + Assertion.assertEquals(expectedDataSet.getTable(table), filteredTable); } } catch (DataSetException e) { throw translateException("Error while checking results", e); diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/Event.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/Event.java index b1a7f1e19b9..fa2263548d6 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/Event.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/Event.java @@ -35,6 +35,7 @@ import javax.persistence.*; public class Event extends BaseIdentifiable { public static final String CATEGORY_VERSION = "Version"; public static final String CATEGORY_ALERT = "Alert"; + public static final String CATEGORY_PROFILE = "Profile"; @Column(name = "name", updatable = true, nullable = true, length = 50) private String name; @@ -121,6 +122,10 @@ public class Event extends BaseIdentifiable { return CATEGORY_VERSION.equalsIgnoreCase(category); } + public boolean isProfileCategory() { + return CATEGORY_PROFILE.equalsIgnoreCase(category); + } + public Date getDate() { return date; } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/TimeMachineQuery.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/TimeMachineQuery.java index 13f8818d0bc..20b0ee861c0 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/TimeMachineQuery.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/TimeMachineQuery.java @@ -19,6 +19,8 @@ */ package org.sonar.api.batch; +import org.apache.commons.lang.builder.EqualsBuilder; + import com.google.common.collect.Lists; import org.apache.commons.lang.builder.ToStringBuilder; import org.sonar.api.measures.Metric; @@ -231,4 +233,10 @@ public class TimeMachineQuery { .append("to", to) .toString(); } + + @Override + public boolean equals(Object obj) { + return EqualsBuilder.reflectionEquals(this, obj); + } + } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java b/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java index 4e265bac406..2cf3d598b40 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java @@ -1141,6 +1141,12 @@ public final class CoreMetrics { .setDomain(DOMAIN_GENERAL) .create(); + public static final String PROFILE_VERSION_KEY = "profile_version"; + public static final Metric PROFILE_VERSION = new Metric.Builder(PROFILE_VERSION_KEY, "Profile version", Metric.ValueType.INT) + .setDescription("Selected quality profile version") + .setQualitative(false) + .setDomain(DOMAIN_GENERAL) + .create(); public static List metrics = Lists.newLinkedList(); diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/profiles/RulesProfile.java b/sonar-plugin-api/src/main/java/org/sonar/api/profiles/RulesProfile.java index 5b34326daf6..e7f61d92df6 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/profiles/RulesProfile.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/profiles/RulesProfile.java @@ -64,6 +64,9 @@ public class RulesProfile implements Cloneable { @Column(name = "name", updatable = true, nullable = false) private String name; + @Column(name = "version", updatable = true, nullable = false) + private int version = 1; + @Column(name = "default_profile", updatable = true, nullable = false) private Boolean defaultProfile = Boolean.FALSE; @@ -73,6 +76,9 @@ public class RulesProfile implements Cloneable { @Column(name = "enabled", updatable = true, nullable = false) private Boolean enabled = Boolean.TRUE; + @Column(name = "used_profile", updatable = true, nullable = false) + private Boolean used = Boolean.FALSE; + @Column(name = "language", updatable = true, nullable = false) private String language; @@ -135,6 +141,24 @@ public class RulesProfile implements Cloneable { this.name = s; return this; } + + public int getVersion() { + return version; + } + + public RulesProfile setVersion(int version) { + this.version = version; + return this; + } + + public Boolean getUsed() { + return used; + } + + public RulesProfile setUsed(Boolean used) { + this.used = used; + return this; + } /** * @return the list of active rules diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/rules/ActiveRuleChange.java b/sonar-plugin-api/src/main/java/org/sonar/api/rules/ActiveRuleChange.java new file mode 100644 index 00000000000..a3b95213220 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/rules/ActiveRuleChange.java @@ -0,0 +1,213 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.api.rules; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; +import org.sonar.api.database.BaseIdentifiable; +import org.sonar.api.profiles.RulesProfile; + +/** + * A class to map a RuleChange to the hibernate model + * + * @since 2.9 + */ +@Entity +@Table(name = "active_rule_changes") +public class ActiveRuleChange extends BaseIdentifiable { + + @Column(name = "user_login", updatable = false, nullable = false) + private String modifierLogin; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "profile_id", updatable = false, nullable = false) + private RulesProfile rulesProfile; + + @Column(name = "profile_version", updatable = false, nullable = false) + private int profileVersion; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "rule_id", updatable = false, nullable = false) + private Rule rule; + + @Column(name = "change_date", updatable = false, nullable = false) + private Date date; + + /** + * true means rule was enabled + * false means rule was disabled + * null means rule stay enabled (another param was changed) + */ + @Column(name = "enabled") + private Boolean enabled; + + @Column(name = "old_severity", updatable = false, nullable = true) + @Enumerated(EnumType.ORDINAL) + private RulePriority oldSeverity; + + @Column(name = "new_severity", updatable = false, nullable = true) + @Enumerated(EnumType.ORDINAL) + private RulePriority newSeverity; + + @OneToMany(mappedBy = "activeRuleChange", fetch = FetchType.LAZY, cascade = { CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE }) + private List activeRuleParamChanges = new ArrayList(); + + public ActiveRuleChange(String modifierLogin, RulesProfile profile, Rule rule) { + this.modifierLogin = modifierLogin; + this.rulesProfile = profile; + this.profileVersion = profile.getVersion(); + this.rule = rule; + this.date = Calendar.getInstance().getTime(); + } + + public Rule getRule() { + return rule; + } + + public RulePriority getOldSeverity() { + return oldSeverity; + } + + public void setOldSeverity(RulePriority oldSeverity) { + this.oldSeverity = oldSeverity; + } + + public RulePriority getNewSeverity() { + return newSeverity; + } + + public void setNewSeverity(RulePriority newSeverity) { + this.newSeverity = newSeverity; + } + + public RulesProfile getRulesProfile() { + return rulesProfile; + } + + public int getProfileVersion() { + return profileVersion; + } + + public String getRepositoryKey() { + return rule.getRepositoryKey(); + } + + /** + * @return the config key the changed rule belongs to + */ + public String getConfigKey() { + return rule.getConfigKey(); + } + + /** + * @return the key of the changed rule + */ + public String getRuleKey() { + return rule.getKey(); + } + + public Boolean isEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public List getActiveRuleParamChanges() { + return activeRuleParamChanges; + } + + public String getModifierLogin() { + return modifierLogin; + } + + public ActiveRuleChange setParameterChange(String key, String oldValue, String newValue) { + RuleParam ruleParameter = rule.getParam(key); + if (ruleParameter != null) { + activeRuleParamChanges.add(new ActiveRuleParamChange(this, ruleParameter, oldValue, newValue)); + } + return this; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (obj.getClass() != getClass()) { + return false; + } + ActiveRuleChange rhs = (ActiveRuleChange) obj; + return new EqualsBuilder() + .appendSuper(super.equals(obj)) + .append(modifierLogin, rhs.modifierLogin) + .append(rulesProfile, rhs.rulesProfile) + .append(rule, rhs.rule) + .append(date, rhs.date) + .append(enabled, rhs.enabled) + .append(newSeverity, rhs.newSeverity) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(41, 33) + .append(modifierLogin) + .append(rulesProfile) + .append(rule) + .append(date) + .append(enabled) + .append(newSeverity) + .toHashCode(); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("id", getId()) + .append("profile", rulesProfile) + .append("rule", rule) + .append("modifier", modifierLogin) + .append("changed at", date) + .append("enabled", enabled) + .append("new severity", newSeverity) + .toString(); + } + +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/rules/ActiveRuleParamChange.java b/sonar-plugin-api/src/main/java/org/sonar/api/rules/ActiveRuleParamChange.java new file mode 100644 index 00000000000..be7d2cef003 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/rules/ActiveRuleParamChange.java @@ -0,0 +1,97 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.api.rules; + +import org.sonar.api.database.BaseIdentifiable; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; + +import javax.persistence.*; + +/** + * @since 2.9 + */ +@Entity +@Table(name = "active_rule_param_changes") +public class ActiveRuleParamChange extends BaseIdentifiable { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "active_rule_change_id") + private ActiveRuleChange activeRuleChange; + + @ManyToOne(fetch = FetchType.LAZY, optional = true) + @JoinColumn(name = "rules_parameter_id") + private RuleParam ruleParam; + + @Column(name = "old_value", updatable = false, nullable = true, length = 4000) + private String oldValue; + + @Column(name = "new_value", updatable = false, nullable = true, length = 4000) + private String newValue; + + ActiveRuleParamChange(ActiveRuleChange activeRuleChange, RuleParam ruleParam, String oldValue, String newValue) { + this.activeRuleChange = activeRuleChange; + this.ruleParam = ruleParam; + this.oldValue = oldValue; + this.newValue = newValue; + } + + public ActiveRuleChange getActiveRuleChange() { + return activeRuleChange; + } + + public RuleParam getRuleParam() { + return ruleParam; + } + + public String getOldValue() { + return oldValue; + } + + public String getNewValue() { + return newValue; + } + + public String getKey() { + return ruleParam.getKey(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ActiveRuleParamChange)) { + return false; + } + if (this == obj) { + return true; + } + ActiveRuleParamChange other = (ActiveRuleParamChange) obj; + return new EqualsBuilder() + .append(getId(), other.getId()).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 57) + .append(getId()) + .toHashCode(); + } + +} diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/profiles/RulesProfileTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/profiles/RulesProfileTest.java index 944805f866c..dd364486646 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/profiles/RulesProfileTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/profiles/RulesProfileTest.java @@ -54,4 +54,10 @@ public class RulesProfileTest { profile.activateRule(rule, RulePriority.MINOR); assertThat(profile.getActiveRule("repo", "key1").getSeverity(), is(RulePriority.MINOR)); } + + @Test + public void defaultVersionIs1() { + RulesProfile profile = RulesProfile.create(); + assertThat(profile.getVersion(), is(1)); + } } diff --git a/sonar-server/src/main/java/org/sonar/server/configuration/ProfilesBackup.java b/sonar-server/src/main/java/org/sonar/server/configuration/ProfilesBackup.java index 4dcd6e1b1e3..958fe22220b 100644 --- a/sonar-server/src/main/java/org/sonar/server/configuration/ProfilesBackup.java +++ b/sonar-server/src/main/java/org/sonar/server/configuration/ProfilesBackup.java @@ -85,10 +85,18 @@ public class ProfilesBackup implements Backupable { } public void importProfile(RulesDao rulesDao, RulesProfile toImport) { - if (toImport.getEnabled()==null) { + if (toImport.getEnabled() == null) { // backward-compatibility with versions < 2.6. The field "enabled" did not exist. Default value is true. toImport.setEnabled(true); } + if (toImport.getVersion() == 0) { + // backward-compatibility with versions < 2.9. The field "version" did not exist. Default value is 1. + toImport.setVersion(1); + } + if (toImport.getUsed() == null) { + // backward-compatibility with versions < 2.9. The field "used_profile" did not exist. Default value is false. + toImport.setUsed(false); + } importActiveRules(rulesDao, toImport); importAlerts(toImport); session.save(toImport); diff --git a/sonar-server/src/main/java/org/sonar/server/configuration/ProfilesManager.java b/sonar-server/src/main/java/org/sonar/server/configuration/ProfilesManager.java index 4734352e979..52549bc52c0 100644 --- a/sonar-server/src/main/java/org/sonar/server/configuration/ProfilesManager.java +++ b/sonar-server/src/main/java/org/sonar/server/configuration/ProfilesManager.java @@ -23,7 +23,10 @@ import org.sonar.api.database.DatabaseSession; import org.sonar.api.database.model.ResourceModel; import org.sonar.api.profiles.RulesProfile; import org.sonar.api.rules.ActiveRule; +import org.sonar.api.rules.ActiveRuleChange; import org.sonar.api.rules.Rule; +import org.sonar.api.rules.RuleParam; +import org.sonar.api.rules.RulePriority; import org.sonar.api.utils.ValidationMessages; import org.sonar.jpa.dao.BaseDao; import org.sonar.jpa.dao.RulesDao; @@ -67,6 +70,10 @@ public class ProfilesManager extends BaseDao { public void deleteProfile(int profileId) { RulesProfile profile = getSession().getEntity(RulesProfile.class, profileId); if (profile != null && !profile.getProvided() && getChildren(profile).isEmpty()) { + //Remove history of rule changes + String hqlDeleteRc = "DELETE " + ActiveRuleChange.class.getSimpleName() + " rc WHERE rc.rulesProfile=:rulesProfile"; + getSession().createQuery(hqlDeleteRc).setParameter("rulesProfile", profile).executeUpdate(); + String hql = "UPDATE " + ResourceModel.class.getSimpleName() + " o SET o.rulesProfile=null WHERE o.rulesProfile=:rulesProfile"; getSession().createQuery(hql).setParameter("rulesProfile", profile).executeUpdate(); getSession().remove(profile); @@ -75,6 +82,10 @@ public class ProfilesManager extends BaseDao { } public void deleteAllProfiles() { + //Remove history of rule changes + String hqlDeleteRc = "DELETE " + ActiveRuleChange.class.getSimpleName() + " rc"; + getSession().createQuery(hqlDeleteRc).executeUpdate(); + String hql = "UPDATE " + ResourceModel.class.getSimpleName() + " o SET o.rulesProfile = null WHERE o.rulesProfile IS NOT NULL"; getSession().createQuery(hql).executeUpdate(); List profiles = getSession().createQuery("FROM " + RulesProfile.class.getSimpleName()).getResultList(); @@ -86,7 +97,7 @@ public class ProfilesManager extends BaseDao { // Managing inheritance of profiles - public ValidationMessages changeParentProfile(Integer profileId, String parentName) { + public ValidationMessages changeParentProfile(Integer profileId, String parentName, String userLogin) { ValidationMessages messages = ValidationMessages.create(); RulesProfile profile = getSession().getEntity(RulesProfile.class, profileId); if (profile != null && !profile.getProvided()) { @@ -99,13 +110,13 @@ public class ProfilesManager extends BaseDao { // Deactivate all inherited rules if (oldParent != null) { for (ActiveRule activeRule : oldParent.getActiveRules()) { - deactivate(profile, activeRule.getRule()); + deactivate(profile, activeRule.getRule(), userLogin); } } // Activate all inherited rules if (newParent != null) { for (ActiveRule activeRule : newParent.getActiveRules()) { - activateOrChange(profile, activeRule); + activateOrChange(profile, activeRule, userLogin); } } profile.setParentName(newParent == null ? null : newParent.getName()); @@ -115,17 +126,54 @@ public class ProfilesManager extends BaseDao { return messages; } + /** + * Rule was activated + */ + public void activated(int profileId, int activeRuleId, String userLogin) { + ActiveRule activeRule = getSession().getEntity(ActiveRule.class, activeRuleId); + RulesProfile profile = getSession().getEntity(RulesProfile.class, profileId); + ruleEnabled(profile, activeRule, userLogin); + //Notify child profiles + activatedOrChanged(profileId, activeRuleId, userLogin); + } + + /** + * Rule param was changed + */ + public void ruleParamChanged(int profileId, int activeRuleId, String paramKey, String oldValue, String newValue, String userLogin) { + ActiveRule activeRule = getSession().getEntity(ActiveRule.class, activeRuleId); + RulesProfile profile = getSession().getEntity(RulesProfile.class, profileId); + + ruleParamChanged(profile, activeRule.getRule(), paramKey, oldValue, newValue, userLogin); + + //Notify child profiles + activatedOrChanged(profileId, activeRuleId, userLogin); + } + + /** + * Rule severity was changed + */ + public void ruleSeverityChanged(int profileId, int activeRuleId, RulePriority oldSeverity, RulePriority newSeverity, String userLogin) { + ActiveRule activeRule = getSession().getEntity(ActiveRule.class, activeRuleId); + RulesProfile profile = getSession().getEntity(RulesProfile.class, profileId); + + ruleSeverityChanged(profile, activeRule.getRule(), oldSeverity, newSeverity, userLogin); + + //Notify child profiles + activatedOrChanged(profileId, activeRuleId, userLogin); + } + /** * Rule was activated/changed in parent profile. */ - public void activatedOrChanged(int parentProfileId, int activeRuleId) { + private void activatedOrChanged(int parentProfileId, int activeRuleId, String userLogin) { ActiveRule parentActiveRule = getSession().getEntity(ActiveRule.class, activeRuleId); if (parentActiveRule.isInherited()) { parentActiveRule.setInheritance(ActiveRule.OVERRIDES); getSession().saveWithoutFlush(parentActiveRule); } for (RulesProfile child : getChildren(parentProfileId)) { - activateOrChange(child, parentActiveRule); + activateOrChange(child, parentActiveRule, userLogin); } getSession().commit(); } @@ -133,10 +181,12 @@ public class ProfilesManager extends BaseDao { /** * Rule was deactivated in parent profile. */ - public void deactivated(int parentProfileId, int ruleId) { - Rule rule = getSession().getEntity(Rule.class, ruleId); + public void deactivated(int parentProfileId, int deactivatedRuleId, String userLogin) { + ActiveRule parentActiveRule = getSession().getEntity(ActiveRule.class, deactivatedRuleId); + RulesProfile profile = getSession().getEntity(RulesProfile.class, parentProfileId); + ruleDisabled(profile, parentActiveRule, userLogin); for (RulesProfile child : getChildren(parentProfileId)) { - deactivate(child, rule); + deactivate(child, parentActiveRule.getRule(), userLogin); } getSession().commit(); } @@ -154,52 +204,158 @@ public class ProfilesManager extends BaseDao { return false; } - public void revert(int profileId, int activeRuleId) { + public void revert(int profileId, int activeRuleId, String userLogin) { RulesProfile profile = getSession().getEntity(RulesProfile.class, profileId); - ActiveRule activeRule = getSession().getEntity(ActiveRule.class, activeRuleId); - if (activeRule != null && activeRule.doesOverride()) { - ActiveRule parentActiveRule = getParentProfile(profile).getActiveRule(activeRule.getRule()); - removeActiveRule(profile, activeRule); - activeRule = (ActiveRule) parentActiveRule.clone(); - activeRule.setRulesProfile(profile); - activeRule.setInheritance(ActiveRule.INHERITED); - profile.addActiveRule(activeRule); - getSession().saveWithoutFlush(activeRule); + ActiveRule oldActiveRule = getSession().getEntity(ActiveRule.class, activeRuleId); + if (oldActiveRule != null && oldActiveRule.doesOverride()) { + ActiveRule parentActiveRule = getParentProfile(profile).getActiveRule(oldActiveRule.getRule()); + removeActiveRule(profile, oldActiveRule); + ActiveRule newActiveRule = (ActiveRule) parentActiveRule.clone(); + newActiveRule.setRulesProfile(profile); + newActiveRule.setInheritance(ActiveRule.INHERITED); + profile.addActiveRule(newActiveRule); + getSession().saveWithoutFlush(newActiveRule); + + //Compute change + ruleChanged(profile, oldActiveRule, newActiveRule, userLogin); for (RulesProfile child : getChildren(profile)) { - activateOrChange(child, activeRule); + activateOrChange(child, newActiveRule, userLogin); } getSession().commit(); } } + + private synchronized void incrementProfileVersionIfNeeded(RulesProfile profile) { + if (profile.getUsed()) { + profile.setVersion(profile.getVersion() + 1); + profile.setUsed(false); + getSession().saveWithoutFlush(profile); + } + } + + /** + * Deal with creation of ActiveRuleChange item when a rule param is changed on a profile + */ + private void ruleParamChanged(RulesProfile profile, Rule rule, String paramKey, String oldValue, String newValue, String userLogin) { + incrementProfileVersionIfNeeded(profile); + ActiveRuleChange rc = new ActiveRuleChange(userLogin, profile, rule); + if (oldValue != newValue) { + rc.setParameterChange(paramKey, oldValue, newValue); + getSession().saveWithoutFlush(rc); + } + } - private void activateOrChange(RulesProfile profile, ActiveRule parentActiveRule) { - ActiveRule activeRule = profile.getActiveRule(parentActiveRule.getRule()); - if (activeRule != null) { - if (activeRule.isInherited()) { - removeActiveRule(profile, activeRule); + /** + * Deal with creation of ActiveRuleChange item when a rule severity is changed on a profile + */ + private void ruleSeverityChanged(RulesProfile profile, Rule rule, RulePriority oldSeverity, RulePriority newSeverity, String userLogin) { + incrementProfileVersionIfNeeded(profile); + ActiveRuleChange rc = new ActiveRuleChange(userLogin, profile, rule); + if (oldSeverity != newSeverity) { + rc.setOldSeverity(oldSeverity); + rc.setNewSeverity(newSeverity); + getSession().saveWithoutFlush(rc); + } + } + + /** + * Deal with creation of ActiveRuleChange item when a rule is changed (severity and/or param(s)) on a profile + */ + private void ruleChanged(RulesProfile profile, ActiveRule oldActiveRule, ActiveRule newActiveRule, String userLogin) { + incrementProfileVersionIfNeeded(profile); + ActiveRuleChange rc = new ActiveRuleChange(userLogin, profile, newActiveRule.getRule()); + + if (oldActiveRule.getSeverity() != newActiveRule.getSeverity()) { + rc.setOldSeverity(oldActiveRule.getSeverity()); + rc.setNewSeverity(newActiveRule.getSeverity()); + } + if (oldActiveRule.getRule().getParams() != null) { + for (RuleParam p : oldActiveRule.getRule().getParams()) { + String oldParam = oldActiveRule.getParameter(p.getKey()); + String newParam = newActiveRule.getParameter(p.getKey()); + if (oldParam != newParam) { + rc.setParameterChange(p.getKey(), oldParam, newParam); + } + } + } + + getSession().saveWithoutFlush(rc); + } + + /** + * Deal with creation of ActiveRuleChange item when a rule is enabled on a profile + */ + private void ruleEnabled(RulesProfile profile, ActiveRule newActiveRule, String userLogin) { + incrementProfileVersionIfNeeded(profile); + ActiveRuleChange rc = new ActiveRuleChange(userLogin, profile, newActiveRule.getRule()); + rc.setEnabled(true); + rc.setNewSeverity(newActiveRule.getSeverity()); + if (newActiveRule.getRule().getParams() != null) { + for (RuleParam p : newActiveRule.getRule().getParams()) { + String newParam = newActiveRule.getParameter(p.getKey()); + if (newParam != null) { + rc.setParameterChange(p.getKey(), null, newParam); + } + } + } + getSession().saveWithoutFlush(rc); + } + + /** + * Deal with creation of ActiveRuleChange item when a rule is disabled on a profile + */ + private void ruleDisabled(RulesProfile profile, ActiveRule disabledRule, String userLogin) { + incrementProfileVersionIfNeeded(profile); + ActiveRuleChange rc = new ActiveRuleChange(userLogin, profile, disabledRule.getRule()); + rc.setEnabled(false); + rc.setOldSeverity(disabledRule.getSeverity()); + if (disabledRule.getRule().getParams() != null) { + for (RuleParam p : disabledRule.getRule().getParams()) { + String oldParam = disabledRule.getParameter(p.getKey()); + if (oldParam != null) { + rc.setParameterChange(p.getKey(), oldParam, null); + } + } + } + getSession().saveWithoutFlush(rc); + } + + private void activateOrChange(RulesProfile profile, ActiveRule parentActiveRule, String userLogin) { + ActiveRule oldActiveRule = profile.getActiveRule(parentActiveRule.getRule()); + if (oldActiveRule != null) { + if (oldActiveRule.isInherited()) { + removeActiveRule(profile, oldActiveRule); } else { - activeRule.setInheritance(ActiveRule.OVERRIDES); - getSession().saveWithoutFlush(activeRule); + oldActiveRule.setInheritance(ActiveRule.OVERRIDES); + getSession().saveWithoutFlush(oldActiveRule); return; // no need to change in children } } - activeRule = (ActiveRule) parentActiveRule.clone(); - activeRule.setRulesProfile(profile); - activeRule.setInheritance(ActiveRule.INHERITED); - profile.addActiveRule(activeRule); - getSession().saveWithoutFlush(activeRule); + ActiveRule newActiveRule = (ActiveRule) parentActiveRule.clone(); + newActiveRule.setRulesProfile(profile); + newActiveRule.setInheritance(ActiveRule.INHERITED); + profile.addActiveRule(newActiveRule); + getSession().saveWithoutFlush(newActiveRule); + + if (oldActiveRule != null) { + ruleChanged(profile, oldActiveRule, newActiveRule, userLogin); + } + else { + ruleEnabled(profile, newActiveRule, userLogin); + } for (RulesProfile child : getChildren(profile)) { - activateOrChange(child, activeRule); + activateOrChange(child, newActiveRule, userLogin); } } - private void deactivate(RulesProfile profile, Rule rule) { + private void deactivate(RulesProfile profile, Rule rule, String userLogin) { ActiveRule activeRule = profile.getActiveRule(rule); if (activeRule != null) { if (activeRule.isInherited()) { + ruleDisabled(profile, activeRule, userLogin); removeActiveRule(profile, activeRule); } else { activeRule.setInheritance(null); @@ -208,7 +364,7 @@ public class ProfilesManager extends BaseDao { } for (RulesProfile child : getChildren(profile)) { - deactivate(child, rule); + deactivate(child, rule, userLogin); } } } diff --git a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java index dbadf5ac349..8b8cf857bbb 100644 --- a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java +++ b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java @@ -27,6 +27,7 @@ import org.sonar.api.Property; import org.sonar.api.profiles.ProfileExporter; import org.sonar.api.profiles.ProfileImporter; import org.sonar.api.resources.Language; +import org.sonar.api.rules.RulePriority; import org.sonar.api.rules.RuleRepository; import org.sonar.api.utils.ValidationMessages; import org.sonar.api.web.*; @@ -228,20 +229,29 @@ public final class JRubyFacade { getProfilesManager().deleteProfile((int) profileId); } - public ValidationMessages changeParentProfile(int profileId, String parentName) { - return getProfilesManager().changeParentProfile(profileId, parentName); + public ValidationMessages changeParentProfile(int profileId, String parentName, String userLogin) { + return getProfilesManager().changeParentProfile(profileId, parentName, userLogin); } - public void ruleActivatedOrChanged(int parentProfileId, int activeRuleId) { - getProfilesManager().activatedOrChanged(parentProfileId, activeRuleId); + public void ruleActivated(int parentProfileId, int activeRuleId, String userLogin) { + getProfilesManager().activated(parentProfileId, activeRuleId, userLogin); } - public void ruleDeactivated(int parentProfileId, int ruleId) { - getProfilesManager().deactivated(parentProfileId, ruleId); + public void ruleParamChanged(int parentProfileId, int activeRuleId, String paramKey, String oldValue, String newValue, String userLogin) { + getProfilesManager().ruleParamChanged(parentProfileId, activeRuleId, paramKey, oldValue, newValue, userLogin); } - public void revertRule(int profileId, int activeRuleId) { - getProfilesManager().revert(profileId, activeRuleId); + public void ruleSeverityChanged(int parentProfileId, int activeRuleId, int oldSeverityId, int newSeverityId, String userLogin) { + getProfilesManager().ruleSeverityChanged(parentProfileId, activeRuleId, RulePriority.values()[oldSeverityId], + RulePriority.values()[newSeverityId], userLogin); + } + + public void ruleDeactivated(int parentProfileId, int deactivatedRuleId, String userLogin) { + getProfilesManager().deactivated(parentProfileId, deactivatedRuleId, userLogin); + } + + public void revertRule(int profileId, int activeRuleId, String userLogin) { + getProfilesManager().revert(profileId, activeRuleId, userLogin); } public List