]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-1922 Add a kind of version control for quality profiles
authorEvgeny Mandrikov <mandrikov@gmail.com>
Thu, 26 May 2011 22:07:33 +0000 (02:07 +0400)
committerEvgeny Mandrikov <mandrikov@gmail.com>
Thu, 26 May 2011 22:54:10 +0000 (02:54 +0400)
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.

63 files changed:
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileEventsSensor.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ProfileSensor.java
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileEventsSensorTest.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ProfileSensorTest.java
sonar-core/src/main/java/org/sonar/jpa/entity/SchemaMigration.java
sonar-core/src/main/resources/META-INF/persistence.xml
sonar-core/src/test/java/org/sonar/jpa/test/AbstractDbUnitTestCase.java
sonar-plugin-api/src/main/java/org/sonar/api/batch/Event.java
sonar-plugin-api/src/main/java/org/sonar/api/batch/TimeMachineQuery.java
sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java
sonar-plugin-api/src/main/java/org/sonar/api/profiles/RulesProfile.java
sonar-plugin-api/src/main/java/org/sonar/api/rules/ActiveRuleChange.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/rules/ActiveRuleParamChange.java [new file with mode: 0644]
sonar-plugin-api/src/test/java/org/sonar/api/profiles/RulesProfileTest.java
sonar-server/src/main/java/org/sonar/server/configuration/ProfilesBackup.java
sonar-server/src/main/java/org/sonar/server/configuration/ProfilesManager.java
sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/profiles_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/controllers/rules_configuration_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/models/active_rule_change.rb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/models/active_rule_param_change.rb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/models/event_category.rb
sonar-server/src/main/webapp/WEB-INF/app/views/profiles/_tabs.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/profiles/changelog.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/profiles/index.html.erb
sonar-server/src/main/webapp/WEB-INF/db/migrate/202_create_rule_changes.rb [new file with mode: 0644]
sonar-server/src/test/java/org/sonar/server/configuration/InheritedProfilesTest.java
sonar-server/src/test/java/org/sonar/server/configuration/RuleChangeTest.java [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/configuration/BackupTest/backup-valid.xml
sonar-server/src/test/resources/org/sonar/server/configuration/InheritedProfilesTest/shouldActivateInChildren-result.xml
sonar-server/src/test/resources/org/sonar/server/configuration/InheritedProfilesTest/shouldActivateInChildren.xml
sonar-server/src/test/resources/org/sonar/server/configuration/InheritedProfilesTest/shouldChangeParent-result.xml
sonar-server/src/test/resources/org/sonar/server/configuration/InheritedProfilesTest/shouldChangeParent.xml
sonar-server/src/test/resources/org/sonar/server/configuration/InheritedProfilesTest/shouldCheckCycles.xml
sonar-server/src/test/resources/org/sonar/server/configuration/InheritedProfilesTest/shouldDeactivateInChildren-result.xml
sonar-server/src/test/resources/org/sonar/server/configuration/InheritedProfilesTest/shouldDeactivateInChildren.xml
sonar-server/src/test/resources/org/sonar/server/configuration/InheritedProfilesTest/shouldNotDeleteInheritedProfile-result.xml
sonar-server/src/test/resources/org/sonar/server/configuration/InheritedProfilesTest/shouldRemoveParent-result.xml
sonar-server/src/test/resources/org/sonar/server/configuration/InheritedProfilesTest/shouldRemoveParent.xml
sonar-server/src/test/resources/org/sonar/server/configuration/InheritedProfilesTest/shouldRenameInheritedProfile-result.xml
sonar-server/src/test/resources/org/sonar/server/configuration/InheritedProfilesTest/shouldSetParent-result.xml
sonar-server/src/test/resources/org/sonar/server/configuration/InheritedProfilesTest/shouldSetParent.xml
sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/changeParentProfile-result.xml [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/changeParentProfile.xml [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/initialData.xml [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleActivated-result.xml [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleDeactivated-result.xml [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleParamChanged-result.xml [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleReverted-result.xml [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleReverted.xml [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleSeverityChanged-result.xml [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/versionIncreaseIfUsed-result.xml [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/versionIncreaseIfUsedAndInChildren-result.xml [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/startup/EnableProfilesTest/shouldDisableProfilesWithMissingLanguages-result.xml
sonar-server/src/test/resources/org/sonar/server/startup/EnableProfilesTest/shouldDisableProfilesWithMissingLanguages.xml
sonar-server/src/test/resources/org/sonar/server/startup/EnableProfilesTest/shouldEnableProfilesWithKnownLanguages-result.xml
sonar-server/src/test/resources/org/sonar/server/startup/EnableProfilesTest/shouldEnableProfilesWithKnownLanguages.xml
sonar-server/src/test/resources/org/sonar/server/startup/RegisterMetricsTest/cleanAlerts-result.xml
sonar-server/src/test/resources/org/sonar/server/startup/RegisterMetricsTest/cleanAlerts.xml
sonar-server/src/test/resources/org/sonar/server/startup/RegisterRulesTest/disableDeprecatedActiveRuleParameters.xml
sonar-server/src/test/resources/org/sonar/server/startup/RegisterRulesTest/disableDeprecatedActiveRules.xml
sonar-testing-harness/src/main/resources/org/sonar/test/persistence/sonar-test.ddl

index 4634bcb01fe18cd6eaa11d359cdd7da07e25a8d4..61d8a1380e1e442f859f46aab05ab531fe066328 100644 (file)
@@ -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 (file)
index 0000000..76283ba
--- /dev/null
@@ -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<Measure> measures = timeMachine.getMeasures(query);
+    if (measures.isEmpty()) {
+      return defaultValue;
+    }
+    return measures.get(0).getIntValue();
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName();
+  }
+}
index fb8787355d8a55a875222b22c9cd95011b3207c8..5468467055ddd076df9d2a053fb02e2bb4b6d04d 100644 (file)
@@ -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 (file)
index 0000000..d0b4431
--- /dev/null
@@ -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.<Measure> emptyList() : Arrays.asList(result1))
+        .thenReturn(result2 == null ? Collections.<Measure> emptyList() : Arrays.asList(result2));
+
+    return timeMachine;
+  }
+
+}
index 29ac9fad4ecbf7fd53c0a3cffdbd1ea6586676c1..9357657db9d7af6e541f93921f3c9f9efc645ec5 100644 (file)
@@ -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)));
   }
 }
index 039029276d7315e9ec6dfaa3d06773704262341f..25351acce1263e535fd8d740f73951a26a0df2fe 100644 (file)
@@ -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";
 
index 304887e52b7c4030f740e10ef5bd081e5be4a796..4d287d8a0df98d2295b0ab550778e2546c969651 100644 (file)
@@ -35,7 +35,9 @@
     <class>org.sonar.api.database.model.AsyncMeasureSnapshot</class>
     <class>org.sonar.api.batch.Event</class>
     <class>org.sonar.api.profiles.Alert</class>
-
+    <class>org.sonar.api.rules.ActiveRuleChange</class>
+    <class>org.sonar.api.rules.ActiveRuleParamChange</class>
+    
     <properties>
       <property name="hibernate.current_session_context_class" value="thread"/>
       <property name="hibernate.connection.release_mode" value="after_transaction"/>
index 6e9aa22013a626b6ff8a72d9c3b357e7445b391f..834657bb7f9fa5e00817e54961fe2c5374ac65a4 100644 (file)
@@ -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);
index b1a7f1e19b9ccf8a68924bb4b4f3807d00b60163..fa2263548d67f70b2ca237c69a34b343d03ec736 100644 (file)
@@ -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;
   }
index 13f8818d0bc775e4690a65659fcc68e928936bb5..20b0ee861c084a26286203a97f0a09f042b4af28 100644 (file)
@@ -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);
+  }
+  
 }
index 4e265bac406e9d9b5b80fc242d5d98e54e1609e0..2cf3d598b402f2d18e8523025feb18248cc1778a 100644 (file)
@@ -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<Metric> metrics = Lists.newLinkedList();
 
index 5b34326daf696f6312cc77f42eb4b7d04c4a4d57..e7f61d92df685ab48b8a10016bda7024e6dcf390 100644 (file)
@@ -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 (file)
index 0000000..a3b9521
--- /dev/null
@@ -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<ActiveRuleParamChange> activeRuleParamChanges = new ArrayList<ActiveRuleParamChange>();
+
+  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<ActiveRuleParamChange> 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 (file)
index 0000000..be7d2ce
--- /dev/null
@@ -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();
+  }
+
+}
index 944805f866cd717105dbb2126f88c1578df12cdf..dd364486646c7ae6c371e27c7699d391f5b7220a 100644 (file)
@@ -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));
+  }
 }
index 4dcd6e1b1e37b3e282a021295445c573f5485951..958fe22220b74c7da9f5e7ddbcda21b46038889a 100644 (file)
@@ -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);
index 4734352e97945fb1dae68edbb7e2e278dcaf87eb..52549bc52c07e9307cecec08e13d8f41ede79105 100644 (file)
@@ -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);
       }
     }
   }
index dbadf5ac3496d914dac9dc123cfaabd048b9cd45..8b8cf857bbb98e15aaeb97d018c25a12efffef39 100644 (file)
@@ -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<Footer> getWebFooters() {
index 24339483e4a282d5602345d74f5e9ab5bcf8c797..81160982d5247929d6926ba50bcbf1d4cb78cfa7 100644 (file)
@@ -196,7 +196,18 @@ class ProfilesController < ApplicationController
     profiles=Profile.find(:all, :conditions => ['language=? and id<>? and (parent_name is null or parent_name<>?) and enabled=?', @profile.language, @profile.id, @profile.name, true], :order => 'name')
     @select_parent = [['None', nil]] + profiles.collect{ |profile| [profile.name, profile.name] }
   end
-
+  
+  #
+  #
+  # GET /profiles/changelog?id=<profile id>
+  #
+  #
+  def changelog
+    @profile = Profile.find(params[:id])
+    
+    @changes=ActiveRuleChange.find(:all, :conditions => ['profile_id=?', @profile.id], :order => 'id desc')
+    
+  end
   
   
   #
@@ -208,9 +219,9 @@ class ProfilesController < ApplicationController
     id = params[:id].to_i
     parent_name = params[:parent_name]
     if parent_name.blank?
-      messages = java_facade.changeParentProfile(id, nil)
+      messages = java_facade.changeParentProfile(id, nil, current_user.login)
     else
-      messages = java_facade.changeParentProfile(id, parent_name)
+      messages = java_facade.changeParentProfile(id, parent_name, current_user.login)
     end
     flash_validation_messages(messages)
     redirect_to :action => 'inheritance', :id => id
index b268f71715c791fc27467d9515659fe78c443356..6659d8d0cc0a7229cfcf57e3cfffb6066483f088 100644 (file)
@@ -84,7 +84,7 @@ class RulesConfigurationController < ApplicationController
   def revert_rule
     id = params[:id].to_i
     rule_id = params[:active_rule_id].to_i
-    java_facade.revertRule(id, rule_id)
+    java_facade.revertRule(id, rule_id, current_user.login)
     redirect_to request.query_parameters.merge({:action => 'index', :id => params[:id], :commit => nil})
   end
 
@@ -105,20 +105,29 @@ class RulesConfigurationController < ApplicationController
       active_rule=profile.active_by_rule_id(rule.id)
       if priority.blank?
         # deactivate the rule
-        active_rule.destroy if active_rule
-        active_rule=nil
-        java_facade.ruleDeactivated(profile.id, rule.id)
+        if active_rule
+          java_facade.ruleDeactivated(profile.id, active_rule.id, current_user.login)
+          active_rule.destroy
+          active_rule=nil
+        end
       else
         # activate the rule
+        activated = false
         if active_rule.nil?
           active_rule = ActiveRule.new(:profile_id => profile.id, :rule => rule)
           rule.parameters.select{|p| p.default_value.present?}.each do |p|
             active_rule.active_rule_parameters.build(:rules_parameter => p, :value => p.default_value)
           end
+          activated = true
         end
+        old_severity = active_rule.failure_level
         active_rule.failure_level=Sonar::RulePriority.id(priority)
         active_rule.save!
-        java_facade.ruleActivatedOrChanged(profile.id, active_rule.id)
+        if activated
+            java_facade.ruleActivated(profile.id, active_rule.id, current_user.login)
+        else
+            java_facade.ruleSeverityChanged(profile.id, active_rule.id, old_severity, active_rule.failure_level, current_user.login)
+        end
       end
       if active_rule
         active_rule.reload
@@ -286,15 +295,18 @@ class RulesConfigurationController < ApplicationController
     if !profile.provided?
       if value != ""
         active_param = ActiveRuleParameter.new(:rules_parameter => rule_param, :active_rule => active_rule ) if active_param.nil?
+        old_value = active_param.value
         active_param.value = value
         active_param.save
         active_param.valid?
         active_param.reload
+        java_facade.ruleParamChanged(profile.id, active_rule.id, rule_param.name, old_value, value, current_user.login)
       elsif !active_param.nil?
+        old_value = active_param.value
         active_param.destroy
         active_param = nil
+        java_facade.ruleParamChanged(profile.id, active_rule.id, rule_param.name, old_value, nil, current_user.login)
       end
-      java_facade.ruleActivatedOrChanged(profile.id, active_rule.id)
     end
     render :partial => 'rule_param', :object => nil,
       :locals => {:parameter => rule_param, :active_parameter => active_param, :profile => profile, :active_rule => active_rule, :is_admin => is_admin }
@@ -311,7 +323,7 @@ class RulesConfigurationController < ApplicationController
       count = rules_to_activate.size
       rules_to_activate.each do |rule|
         active_rule = profile.active_rules.create(:rule => rule, :failure_level => rule.priority)
-        java_facade.ruleActivatedOrChanged(profile.id, active_rule.id)
+        java_facade.ruleActivated(profile.id, active_rule.id, current_user.login)
       end
     end
     count
@@ -322,7 +334,7 @@ class RulesConfigurationController < ApplicationController
     profile.active_rules.each do |ar|
       if rule_ids.include?(ar.rule_id) && !ar.inheritance.present?
         ar.destroy
-        java_facade.ruleDeactivated(profile.id, ar.rule_id)
+        java_facade.ruleDeactivated(profile.id, ar.rule_id, current_user.login)
         count+=1
       end
     end
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/active_rule_change.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/active_rule_change.rb
new file mode 100644 (file)
index 0000000..d52c149
--- /dev/null
@@ -0,0 +1,37 @@
+#
+# Sonar, entreprise quality control 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
+#
+class ActiveRuleChange < ActiveRecord::Base
+  belongs_to :rules_profile, :class_name => 'Profile', :foreign_key => 'profile_id'
+  belongs_to :rule
+  has_many :active_rule_param_changes, :dependent => :destroy
+
+  def old_severity_text
+    Sonar::RulePriority.to_s old_severity
+  end
+
+  def new_severity_text
+    Sonar::RulePriority.to_s new_severity
+  end
+
+  def parameters
+    active_rule_param_changes
+  end
+
+end
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/active_rule_param_change.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/active_rule_param_change.rb
new file mode 100644 (file)
index 0000000..5d91dc7
--- /dev/null
@@ -0,0 +1,32 @@
+#
+# Sonar, entreprise quality control 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 {library}; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+#
+ class ActiveRuleParamChange < ActiveRecord::Base
+   belongs_to :active_rule_change
+   belongs_to :rules_parameter
+
+   def name
+    rules_parameter.name
+   end
+
+   def parameter
+     rules_parameter
+   end
+
+ end
index 85d76c17fc322f51bb7c4f8267f377f494c2e27f..89e957bc7df6a35530751335a0bf3ac15e044419 100644 (file)
@@ -23,6 +23,7 @@ class EventCategory
 
   KEY_VERSION='Version'
   KEY_ALERT='Alert'
+  KEY_PROFILE='Profile'
 
   def initialize(name=nil, description=nil)
     @name=name
@@ -80,13 +81,14 @@ class EventCategory
   end
 
   def editable?
-    !([KEY_VERSION, KEY_ALERT].include?(name))
+    !([KEY_VERSION, KEY_ALERT, KEY_PROFILE].include?(name))
   end
 
   def self.defaults
     [
       EventCategory.new(KEY_VERSION, 'Application version'),
-      EventCategory.new(KEY_ALERT, 'Alert')
+      EventCategory.new(KEY_ALERT, 'Alert'),
+      EventCategory.new(KEY_PROFILE, 'Profile change')
     ]
   end
 
index 2c42cc69e818684c92927a7dd4728b7676e0c035..5fa8fce9926f839983dd017e1c4feadf42572141 100644 (file)
@@ -18,6 +18,9 @@
   <li>
     <a href="<%= url_for :controller => 'profiles', :action => 'inheritance', :id => @profile.id -%>" <%= "class='selected'" if selected_tab=='inheritance' -%>>Profile inheritance</a>
   </li>
+  <li>
+    <a href="<%= url_for :controller => 'profiles', :action => 'changelog', :id => @profile.id -%>" <%= "class='selected'" if selected_tab=='changelog' -%>>Changelog</a>
+  </li>
   <% if new_tab %>
   <li>
     <a href="#" class='selected'><%= new_tab -%></a>
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/profiles/changelog.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/profiles/changelog.html.erb
new file mode 100644 (file)
index 0000000..6a616bf
--- /dev/null
@@ -0,0 +1,77 @@
+<h1 class="marginbottom10"><%= link_to 'Quality profiles', :controller => 'profiles', :action => 'index' -%> / <%= h @profile.language -%> / <%= h @profile.name %></h1>
+<%= render :partial => 'profiles/tabs', :locals => {:selected_tab=>'changelog'} %>
+
+<div class="tabs-panel marginbottom10">
+
+  <table  class="data width100">
+    <thead>
+      <tr>
+        <th>Action</th>
+        <th>By</th>
+        <th>Date</th>
+        <th>Rule name</th>
+        <th>Parameters</th>
+      </tr>
+    </thead>
+    <% current_version = -1 
+      @changes.each do |change|
+    %>
+        <% if current_version != change.profile_version %>
+    <tr>
+      <td align="left" colspan="5">
+       <div class="line-block">
+             <h2>Version <%=change.profile_version%></h2>
+               </div>
+      </td>
+    </tr>
+        <%   current_version = change.profile_version
+           end
+        %> 
+    <tr class="<%= cycle 'even', 'odd', :name => change.profile_version -%>">
+      <td valign="top"><%=case change.enabled
+                when true then "Enabled"
+                when false then "Disabled"
+                when nil then "Modified"
+             end%></td>
+      <td valign="top"><%=change.user_login%></td>
+      <td valign="top"><%=change.change_date.strftime("%Y-%m-%d %H:%M:%S")%></td>
+      <td valign="top"><%=change.rule.name%></td>
+      <td valign="top">
+        <% if change.old_severity
+             if change.new_severity %>
+            Severity changed from <i><%= change.old_severity_text %></i> to
+          <% else %>
+            Severity was <i><%= change.old_severity_text %></i>
+          <% end
+           end %>
+        <% if change.new_severity 
+             if change.old_severity %>
+           <i><%= change.new_severity_text %></i>
+          <% else %>
+           Severity set to <i><%= change.new_severity_text %></i>
+          <% end
+           end %>
+        <% if (change.old_severity or change.new_severity) and change.parameters.size > 0 %>
+          <br/>
+        <% end %>
+        <% change.parameters.each do |param_change| %>
+          Parameter <i><%=param_change.name %></i>
+          <% if not param_change.old_value %>
+             set to <i><%= param_change.new_value %></i>
+          <% elsif not param_change.new_value 
+               if change.enabled == false %>
+                 was <i><%= param_change.old_value %></i>
+            <% else %>
+                 reset to default value (was <i><%= param_change.old_value %></i>)
+            <% end
+             else %>
+             changed from <i><%= param_change.old_value %></i> to <i><%= param_change.new_value %></i>
+          <% end %>
+          <%= "<br/>" unless param_change == change.parameters.last %>
+        <% end%>
+      </td>
+    </tr>
+    <% end %>
+  </table>
+
+</div>
index c3d2cf5104ee0d5f8b9289cd0fb29e621f661f8e..c434bd19949f1e1160efbb7116e5c01831bea22b 100644 (file)
@@ -76,6 +76,7 @@
     <thead>
       <tr>
         <th class="left">Name</th>
+        <th align="left">Version</th>
         <th class="right">Rules</th>
         <th class="right">Alerts</th>
         <th class="right">Projects</th>
@@ -90,6 +91,8 @@
     <tr class="<%= cycle 'even', 'odd', :name => language.getKey() -%>" id="<%= u profile.key %>">
       <td><a href="<%= url_for :controller => 'rules_configuration', :action => 'index', :id => profile.id -%>" id="rules-<%= language.getKey() -%>-<%= u(profile.name) -%>"><%= h profile.name %></a></td>
       
+      <td align="left"><span id="version_<%= u profile.key -%>"><%= profile.version -%></span></td>
+      
       <td align="right">
         <span id="activated_rules_<%= u profile.key -%>"><%= profile.count_active_rules -%></span>
       </td>
diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/202_create_rule_changes.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/202_create_rule_changes.rb
new file mode 100644 (file)
index 0000000..d9a03e5
--- /dev/null
@@ -0,0 +1,53 @@
+#
+# Sonar, entreprise quality control 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
+#
+
+#
+# Sonar 2.9
+#
+class CreateRuleChanges < ActiveRecord::Migration
+
+  def self.up
+    create_table :active_rule_changes do |t|
+      t.column :user_login,              :string,    :limit => 40,  :null => false
+      t.column :profile_id,              :integer,   :null => false
+      t.column :profile_version,         :integer,   :null => false
+      t.column :rule_id,                 :integer,   :null => false
+      t.column :change_date,             :datetime,  :null => false
+      t.column :enabled,                 :boolean,   :null => true
+      t.column :old_severity,            :integer,   :null => true
+      t.column :new_severity,            :integer,   :null => true
+    end
+    add_index :active_rule_changes, [:profile_id], :name => 'active_rule_changes_pid'
+
+    create_table :active_rule_param_changes do |t|
+      t.column :active_rule_change_id,   :integer,   :null => false
+      t.column :rules_parameter_id,      :integer,   :null => false
+      t.column :old_value,               :string,    :limit => 4000, :null => true
+      t.column :new_value,               :string,    :limit => 4000, :null => true
+    end
+    add_index :active_rule_param_changes, [:active_rule_change_id], :name => 'active_rule_param_changes_cid'
+
+    add_column 'rules_profiles', 'version',      :integer, :default => 1
+    add_column 'rules_profiles', 'used_profile', :boolean, :default => false
+    Profile.reset_column_information
+    Profile.update_all(Profile.sanitize_sql_for_assignment({:used_profile => false, :version => 1}))
+  end
+
+end
index 1b95129ad952b1c1b25778e6fe6d9dcc2c75863d..55eb7b378f93b97778e0ef6ad8a6c4118719538e 100644 (file)
@@ -71,35 +71,35 @@ public class InheritedProfilesTest extends AbstractDbUnitTestCase {
   @Test
   public void shouldSetParent() {
     setupData("shouldSetParent");
-    profilesManager.changeParentProfile(2, "parent");
+    profilesManager.changeParentProfile(2, "parent", "admin");
     checkTables("shouldSetParent", "active_rules", "rules_profiles");
   }
 
   @Test
   public void shouldChangeParent() {
     setupData("shouldChangeParent");
-    profilesManager.changeParentProfile(3, "new_parent");
+    profilesManager.changeParentProfile(3, "new_parent", "admin");
     checkTables("shouldChangeParent", "active_rules", "rules_profiles");
   }
 
   @Test
   public void shouldRemoveParent() {
     setupData("shouldRemoveParent");
-    profilesManager.changeParentProfile(2, null);
+    profilesManager.changeParentProfile(2, null, "admin");
     checkTables("shouldRemoveParent", "active_rules", "rules_profiles");
   }
 
   @Test
   public void shouldDeactivateInChildren() {
     setupData("shouldDeactivateInChildren");
-    profilesManager.deactivated(1, 1);
+    profilesManager.deactivated(1, 1, "admin");
     checkTables("shouldDeactivateInChildren", "active_rules", "rules_profiles");
   }
 
   @Test
   public void shouldActivateInChildren() {
     setupData("shouldActivateInChildren");
-    profilesManager.activatedOrChanged(1, 1);
+    profilesManager.activated(1, 1, "admin");
     checkTables("shouldActivateInChildren", "active_rules", "rules_profiles", "active_rule_parameters");
   }
 
diff --git a/sonar-server/src/test/java/org/sonar/server/configuration/RuleChangeTest.java b/sonar-server/src/test/java/org/sonar/server/configuration/RuleChangeTest.java
new file mode 100644 (file)
index 0000000..d6f3bfd
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * 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.server.configuration;
+
+import org.sonar.api.rules.RulePriority;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.profiles.RulesProfile;
+import org.sonar.api.utils.ValidationMessages;
+import org.sonar.jpa.test.AbstractDbUnitTestCase;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+public class RuleChangeTest extends AbstractDbUnitTestCase {
+  private ProfilesManager profilesManager;
+
+  @Before
+  public void setUp() {
+    profilesManager = new ProfilesManager(getSession(), null);
+  }
+
+  @Test
+  public void testVersionIncreaseIfUsed() {
+    setupData("initialData");
+    profilesManager.activated(2, 3, "admin");
+    checkTables("versionIncreaseIfUsed", "rules_profiles");
+  }
+
+  @Test
+  public void testVersionIncreaseIfUsedAndInChildren() {
+    setupData("initialData");
+    profilesManager.activated(1, 1, "admin");
+    checkTables("versionIncreaseIfUsedAndInChildren", "rules_profiles");
+  }
+
+  @Test
+  public void testRuleActivated() {
+    setupData("initialData");
+    profilesManager.activated(2, 3, "admin");
+    checkTables("ruleActivated", new String[] {"change_date"}, "active_rule_changes");
+  }
+
+  @Test
+  public void testRuleDeactivated() {
+    setupData("initialData");
+    profilesManager.deactivated(2, 3, "admin");
+    checkTables("ruleDeactivated", new String[] {"change_date"}, "active_rule_changes");
+  }
+
+  @Test
+  public void testRuleParamChanged() {
+    setupData("initialData");
+    profilesManager.ruleParamChanged(2, 3, "param1", "20", "30", "admin");
+    checkTables("ruleParamChanged", new String[] {"change_date"}, "active_rule_changes", "active_rule_param_changes");
+  }
+  
+  @Test
+  public void testRuleSeverityChanged() {
+    setupData("initialData");
+    profilesManager.ruleSeverityChanged(2, 3, RulePriority.BLOCKER, RulePriority.CRITICAL, "admin");
+    checkTables("ruleSeverityChanged", new String[] {"change_date"}, "active_rule_changes");
+  }
+  
+  @Test
+  public void testRuleReverted() {
+    setupData("ruleReverted");
+    profilesManager.revert(2, 3, "admin");
+    checkTables("ruleReverted", new String[] {"change_date"}, "active_rule_changes", "active_rule_param_changes");
+  }
+  
+  @Test
+  public void testChangeParentProfile() {
+    setupData("changeParentProfile");
+    profilesManager.changeParentProfile(2, "parent", "admin");
+    checkTables("changeParentProfile", new String[] {"change_date"}, "active_rule_changes");
+  }
+
+
+}
index fb33ff6a535280c416b7670f49aa3069b32e17b0..b5df287f5ce06b0feb03f75957b6807f5dc48b18 100644 (file)
   <profiles>
     <profile>
       <name><![CDATA[test name]]></name>
+      <version><![CDATA[1]]></version>
       <default-profile><![CDATA[true]]></default-profile>
       <provided><![CDATA[true]]></provided>
+      <used><![CDATA[false]]></used>
       <language><![CDATA[test language]]></language>
       <enabled><![CDATA[true]]></enabled>
       <active-rules>
     </profile>
     <profile>
       <name><![CDATA[test2 name]]></name>
+      <version><![CDATA[1]]></version>
       <default-profile><![CDATA[false]]></default-profile>
       <provided><![CDATA[false]]></provided>
       <language><![CDATA[test language]]></language>
       <enabled><![CDATA[true]]></enabled>
+      <used><![CDATA[false]]></used>
       <parentName><![CDATA[test name]]></parentName>
       <active-rules>
         <active-rule>
index c6e3fb5692264279b6277dea0be7a1ed4bd7ab0d..f34a0b494fdc82238c458e9e3b69b672cab5d87d 100644 (file)
@@ -5,9 +5,9 @@
   
   <rules_parameters id="1" rule_id="1" name="param1" description="[null]" param_type="r"/>
 
-  <rules_profiles id="1" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="1" version="2" used_profile="false" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
   
-  <rules_profiles id="2" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
+  <rules_profiles id="2" version="2" used_profile="false" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
 
   <active_rules id="1" profile_id="1" rule_id="1" failure_level="2" inheritance="[null]"/>
   <active_rule_parameters id="1" active_rule_id="1" rules_parameter_id="1" value="30"/>
index 130072bb538e869da4df059d0e759190ddf91c11..62081abc4038c6e362a4323bacff7b222cb1aca3 100644 (file)
@@ -5,9 +5,9 @@
 
   <rules_parameters id="1" rule_id="1" name="param1" description="[null]" param_type="r"/>
 
-  <rules_profiles id="1" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="1" version="1" used_profile="true" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
   
-  <rules_profiles id="2" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
 
   <active_rules id="1" profile_id="1" rule_id="1" failure_level="2" inheritance="[null]"/>
   <active_rule_parameters id="1" active_rule_id="1" rules_parameter_id="1" value="30"/>
index 524f5db30ea72c0752bd482d74458fdc33d24577..84ca4c97e9d61800fbee55cd648750048cc9f3e5 100644 (file)
@@ -6,11 +6,11 @@
   <rules id="2" name="bar" description="test" plugin_config_key="checker/bar"
          plugin_rule_key="checkstyle.rule2" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
 
-  <rules_profiles id="1" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="1" version="1" used_profile="true" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
   
-  <rules_profiles id="2" provided="false" name="new_parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="new_parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
   
-  <rules_profiles id="3" provided="false" name="child" default_profile="0" language="java" parent_name="new_parent" enabled="true"/>
+  <rules_profiles id="3" version="2" used_profile="false" provided="false" name="child" default_profile="0" language="java" parent_name="new_parent" enabled="true"/>
 
   <active_rules id="1" profile_id="1" rule_id="1" failure_level="2" inheritance="[null]"/>
 
index a5d444cf42ef4943aa15ef82a13f73404edc8368..b2daef7a8ebe3b526bafb54d13c861d658e2bb92 100644 (file)
@@ -6,11 +6,11 @@
   <rules id="2" name="bar" description="test" plugin_config_key="checker/bar"
          plugin_rule_key="checkstyle.rule2" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
 
-  <rules_profiles id="1" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="1" version="1" used_profile="true" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
   
-  <rules_profiles id="2" provided="false" name="new_parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="new_parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
   
-  <rules_profiles id="3" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
+  <rules_profiles id="3" version="1" used_profile="true" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
 
   <active_rules id="1" profile_id="1" rule_id="1" failure_level="2" inheritance="[null]"/>
 
index 38df95e09956f10c278b86df81ddd6d744d1f553..5200875be2149a9a5397dedbfbb4d33c561bf83d 100644 (file)
@@ -3,10 +3,10 @@
   <rules id="1" name="foo" description="test" plugin_config_key="checker/foo"
          plugin_rule_key="checkstyle.rule1" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
 
-  <rules_profiles id="1" provided="false" name="level1" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="1" version="1" used_profile="true" provided="false" name="level1" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
   
-  <rules_profiles id="2" provided="false" name="level2" default_profile="0" language="java" parent_name="level1" enabled="true"/>
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="level2" default_profile="0" language="java" parent_name="level1" enabled="true"/>
   
-  <rules_profiles id="3" provided="false" name="level3" default_profile="0" language="java" parent_name="level2" enabled="true"/>
+  <rules_profiles id="3" version="1" used_profile="true" provided="false" name="level3" default_profile="0" language="java" parent_name="level2" enabled="true"/>
 
 </dataset>
index 18e99729416ae6b0eb772443c6126216018eee29..3371d019f580f00319e1cb458b0a38db52b61ee9 100644 (file)
@@ -3,9 +3,9 @@
   <rules id="1" name="foo" description="test" plugin_config_key="checker/foo"
          plugin_rule_key="checkstyle.rule1" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
 
-  <rules_profiles id="1" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="1" version="2" used_profile="false" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
   
-  <rules_profiles id="2" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
+  <rules_profiles id="2" version="2" used_profile="false" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
 
   <active_rules id="1" profile_id="1" rule_id="1" failure_level="2" inheritance="[null]"/>
 
index b8489c1fbd27cb574af52b8b37df6328128d708a..8d4e2c9072c505cd8da5d9d607f407b36e60cffb 100644 (file)
@@ -3,9 +3,9 @@
   <rules id="1" name="foo" description="test" plugin_config_key="checker/foo"
          plugin_rule_key="checkstyle.rule1" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
 
-  <rules_profiles id="1" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="1" version="1" used_profile="true" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
   
-  <rules_profiles id="2" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
 
   <active_rules id="1" profile_id="1" rule_id="1" failure_level="2" inheritance="[null]"/>
 
index 38df95e09956f10c278b86df81ddd6d744d1f553..5200875be2149a9a5397dedbfbb4d33c561bf83d 100644 (file)
@@ -3,10 +3,10 @@
   <rules id="1" name="foo" description="test" plugin_config_key="checker/foo"
          plugin_rule_key="checkstyle.rule1" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
 
-  <rules_profiles id="1" provided="false" name="level1" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="1" version="1" used_profile="true" provided="false" name="level1" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
   
-  <rules_profiles id="2" provided="false" name="level2" default_profile="0" language="java" parent_name="level1" enabled="true"/>
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="level2" default_profile="0" language="java" parent_name="level1" enabled="true"/>
   
-  <rules_profiles id="3" provided="false" name="level3" default_profile="0" language="java" parent_name="level2" enabled="true"/>
+  <rules_profiles id="3" version="1" used_profile="true" provided="false" name="level3" default_profile="0" language="java" parent_name="level2" enabled="true"/>
 
 </dataset>
index 540b18c3ace956a0802aa003b33d046e234de55a..a27f357b5565c441affb5c1136c222f9253b8a32 100644 (file)
@@ -3,9 +3,9 @@
   <rules id="1" name="foo" description="test" plugin_config_key="checker/foo"
          plugin_rule_key="checkstyle.rule1" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
 
-  <rules_profiles id="1" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="1" version="1" used_profile="true" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
   
-  <rules_profiles id="2" provided="false" name="child" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="2" version="2" used_profile="false" provided="false" name="child" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
 
   <active_rules id="1" profile_id="1" rule_id="1" failure_level="2" inheritance="[null]"/>
 
index b8489c1fbd27cb574af52b8b37df6328128d708a..8d4e2c9072c505cd8da5d9d607f407b36e60cffb 100644 (file)
@@ -3,9 +3,9 @@
   <rules id="1" name="foo" description="test" plugin_config_key="checker/foo"
          plugin_rule_key="checkstyle.rule1" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
 
-  <rules_profiles id="1" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="1" version="1" used_profile="true" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
   
-  <rules_profiles id="2" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
 
   <active_rules id="1" profile_id="1" rule_id="1" failure_level="2" inheritance="[null]"/>
 
index a7374525f59263f5e47e649533a3e981ba989be9..c7a2355a55ad0b818b9950f662f33ae071cc37f9 100644 (file)
@@ -3,10 +3,10 @@
   <rules id="1" name="foo" description="test" plugin_config_key="checker/foo"
          plugin_rule_key="checkstyle.rule1" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
 
-  <rules_profiles id="1" provided="false" name="newName" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="1" version="1" used_profile="true" provided="false" name="newName" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
   
-  <rules_profiles id="2" provided="false" name="level2" default_profile="0" language="java" parent_name="newName" enabled="true"/>
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="level2" default_profile="0" language="java" parent_name="newName" enabled="true"/>
   
-  <rules_profiles id="3" provided="false" name="level3" default_profile="0" language="java" parent_name="level2" enabled="true"/>
+  <rules_profiles id="3" version="1" used_profile="true" provided="false" name="level3" default_profile="0" language="java" parent_name="level2" enabled="true"/>
 
 </dataset>
index b8489c1fbd27cb574af52b8b37df6328128d708a..5d9be47123bda687950fb852c6c163915868adb1 100644 (file)
@@ -3,9 +3,9 @@
   <rules id="1" name="foo" description="test" plugin_config_key="checker/foo"
          plugin_rule_key="checkstyle.rule1" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
 
-  <rules_profiles id="1" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="1" version="1" used_profile="true" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
   
-  <rules_profiles id="2" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
+  <rules_profiles id="2" version="2" used_profile="false" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
 
   <active_rules id="1" profile_id="1" rule_id="1" failure_level="2" inheritance="[null]"/>
 
index 540b18c3ace956a0802aa003b33d046e234de55a..253e69aac65dab209aaea1f811844d173bea2ae3 100644 (file)
@@ -3,9 +3,9 @@
   <rules id="1" name="foo" description="test" plugin_config_key="checker/foo"
          plugin_rule_key="checkstyle.rule1" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
 
-  <rules_profiles id="1" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="1" version="1" used_profile="true" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
   
-  <rules_profiles id="2" provided="false" name="child" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="child" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
 
   <active_rules id="1" profile_id="1" rule_id="1" failure_level="2" inheritance="[null]"/>
 
diff --git a/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/changeParentProfile-result.xml b/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/changeParentProfile-result.xml
new file mode 100644 (file)
index 0000000..62e8b33
--- /dev/null
@@ -0,0 +1,5 @@
+<dataset>
+
+  <active_rule_changes id="1" user_login="admin" profile_id="2" profile_version="2" rule_id="1" enabled="true" old_severity="[null]" new_severity="2"/>
+
+</dataset>
diff --git a/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/changeParentProfile.xml b/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/changeParentProfile.xml
new file mode 100644 (file)
index 0000000..7c4d381
--- /dev/null
@@ -0,0 +1,22 @@
+<dataset>
+
+  <rules id="1" name="foo" description="test" plugin_config_key="checker/foo"
+         plugin_rule_key="checkstyle.rule1" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
+
+  <rules id="2" name="bar" description="test2" plugin_config_key="checker/bar"
+         plugin_rule_key="checkstyle.rule2" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
+  <rules_parameters id="13" rule_id="2" name="param1" description="[null]" param_type="r"/>
+
+  <rules_profiles id="1" version="1" used_profile="true" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="child" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+
+  <active_rules id="1" profile_id="1" rule_id="1" failure_level="2" inheritance="[null]"/>
+
+  <active_rules id="3" profile_id="1" rule_id="2" failure_level="2" inheritance="[null]"/>
+  <active_rule_parameters id="1" active_rule_id="3" rules_parameter_id="13" value="30"/>
+
+  <active_rules id="4" profile_id="2" rule_id="2" failure_level="2" inheritance="[null]"/>
+  <active_rule_parameters id="2" active_rule_id="4" rules_parameter_id="13" value="60"/>
+
+</dataset>
diff --git a/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/initialData.xml b/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/initialData.xml
new file mode 100644 (file)
index 0000000..aa0cb98
--- /dev/null
@@ -0,0 +1,21 @@
+<dataset>
+
+  <rules id="1" name="foo" description="test" plugin_config_key="checker/foo"
+         plugin_rule_key="checkstyle.rule1" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
+
+  <rules id="2" name="bar" description="test2" plugin_config_key="checker/bar"
+         plugin_rule_key="checkstyle.rule2" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
+  <rules_parameters id="13" rule_id="2" name="param1" description="[null]" param_type="r"/>
+
+  <rules_profiles id="1" version="1" used_profile="true" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
+
+  <active_rules id="1" profile_id="1" rule_id="1" failure_level="2" inheritance="[null]"/>
+
+  <active_rules id="2" profile_id="2" rule_id="1" failure_level="2" inheritance="INHERITED"/>
+  
+  <active_rules id="3" profile_id="2" rule_id="2" failure_level="2" inheritance="[null]"/>
+  <active_rule_parameters id="1" active_rule_id="3" rules_parameter_id="13" value="30"/>
+
+</dataset>
diff --git a/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleActivated-result.xml b/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleActivated-result.xml
new file mode 100644 (file)
index 0000000..9af4ad4
--- /dev/null
@@ -0,0 +1,5 @@
+<dataset>
+
+  <active_rule_changes id="1" user_login="admin" profile_id="2" profile_version="2" rule_id="2" enabled="true" old_severity="[null]" new_severity="2"/>
+
+</dataset>
diff --git a/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleDeactivated-result.xml b/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleDeactivated-result.xml
new file mode 100644 (file)
index 0000000..179a8ff
--- /dev/null
@@ -0,0 +1,7 @@
+<dataset>
+
+  <active_rule_changes id="1" user_login="admin" profile_id="2" profile_version="2" rule_id="2" enabled="false" old_severity="2" new_severity="[null]"/>
+  
+  <active_rule_param_changes id="1" active_rule_change_id="1" rules_parameter_id="13" old_value="30" new_value="[null]"/>
+
+</dataset>
diff --git a/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleParamChanged-result.xml b/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleParamChanged-result.xml
new file mode 100644 (file)
index 0000000..6704d22
--- /dev/null
@@ -0,0 +1,7 @@
+<dataset>
+
+  <active_rule_changes id="1" user_login="admin" profile_id="2" profile_version="2" rule_id="2" enabled="[null]" old_severity="[null]" new_severity="[null]"/>
+
+  <active_rule_param_changes id="1" active_rule_change_id="1" rules_parameter_id="13" old_value="20" new_value="30"/>
+  
+</dataset>
diff --git a/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleReverted-result.xml b/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleReverted-result.xml
new file mode 100644 (file)
index 0000000..14b1c9d
--- /dev/null
@@ -0,0 +1,8 @@
+<dataset>
+
+  <active_rule_changes id="1" user_login="admin" profile_id="2" profile_version="2" rule_id="2" enabled="[null]" old_severity="3" new_severity="2"/>
+
+  <active_rule_param_changes id="1" active_rule_change_id="1" rules_parameter_id="13" old_value="30" new_value="[null]"/>
+  <active_rule_param_changes id="2" active_rule_change_id="1" rules_parameter_id="14" old_value="100" new_value="50"/>
+  
+</dataset>
diff --git a/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleReverted.xml b/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleReverted.xml
new file mode 100644 (file)
index 0000000..2ec87b2
--- /dev/null
@@ -0,0 +1,19 @@
+<dataset>
+
+  <rules id="2" name="bar" description="test2" plugin_config_key="checker/bar"
+         plugin_rule_key="checkstyle.rule2" plugin_name="plugin" enabled="true" cardinality="SINGLE" parent_id="[null]"/>
+  <rules_parameters id="13" rule_id="2" name="param1" description="[null]" param_type="r"/>
+  <rules_parameters id="14" rule_id="2" name="param2" description="[null]" param_type="r"/>
+
+  <rules_profiles id="1" version="1" used_profile="true" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
+
+  <active_rules id="2" profile_id="1" rule_id="2" failure_level="2" inheritance="[null]"/>
+  <active_rule_parameters id="3" active_rule_id="2" rules_parameter_id="14" value="50"/>
+
+  <active_rules id="3" profile_id="2" rule_id="2" failure_level="3" inheritance="OVERRIDES"/>
+  <active_rule_parameters id="1" active_rule_id="3" rules_parameter_id="13" value="30"/>
+  <active_rule_parameters id="2" active_rule_id="3" rules_parameter_id="14" value="100"/>
+
+</dataset>
diff --git a/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleSeverityChanged-result.xml b/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/ruleSeverityChanged-result.xml
new file mode 100644 (file)
index 0000000..cd39547
--- /dev/null
@@ -0,0 +1,5 @@
+<dataset>
+
+  <active_rule_changes id="1" user_login="admin" profile_id="2" profile_version="2" rule_id="2" enabled="[null]" old_severity="4" new_severity="3"/>
+
+</dataset>
diff --git a/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/versionIncreaseIfUsed-result.xml b/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/versionIncreaseIfUsed-result.xml
new file mode 100644 (file)
index 0000000..b5be928
--- /dev/null
@@ -0,0 +1,7 @@
+<dataset>
+
+  <rules_profiles id="1" version="1" used_profile="true" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  
+  <rules_profiles id="2" version="2" used_profile="false" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
+
+</dataset>
diff --git a/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/versionIncreaseIfUsedAndInChildren-result.xml b/sonar-server/src/test/resources/org/sonar/server/configuration/RuleChangeTest/versionIncreaseIfUsedAndInChildren-result.xml
new file mode 100644 (file)
index 0000000..8c3d85e
--- /dev/null
@@ -0,0 +1,7 @@
+<dataset>
+
+  <rules_profiles id="1" version="2" used_profile="false" provided="false" name="parent" default_profile="0" language="java" parent_name="[null]" enabled="true"/>
+  
+  <rules_profiles id="2" version="2" used_profile="false" provided="false" name="child" default_profile="0" language="java" parent_name="parent" enabled="true"/>
+
+</dataset>
index 62dce741d801a78ba6bfd1277f7da2794ad046ac..23de3e4d49184834addb20eadf56bb5cbac72ea3 100644 (file)
@@ -1,9 +1,9 @@
 <dataset>
-  <rules_profiles id="1" provided="true" name="Java 1" default_profile="0" language="java" enabled="true" PARENT_NAME="[null]"/>
-  <rules_profiles id="2" provided="false" name="Java 2" default_profile="1" language="java" enabled="true" PARENT_NAME="[null]"/>
-  <rules_profiles id="3" provided="true" name="Php" default_profile="0" language="php" enabled="true" PARENT_NAME="[null]"/>
+  <rules_profiles id="1" version="1" used_profile="true" provided="true" name="Java 1" default_profile="0" language="java" enabled="true" PARENT_NAME="[null]"/>
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="Java 2" default_profile="1" language="java" enabled="true" PARENT_NAME="[null]"/>
+  <rules_profiles id="3" version="1" used_profile="true" provided="true" name="Php" default_profile="0" language="php" enabled="true" PARENT_NAME="[null]"/>
 
   <!-- disabled -->
-  <rules_profiles id="4" provided="true" name="Cobol 1" default_profile="1" language="cobol" enabled="false" PARENT_NAME="[null]"/>
-  <rules_profiles id="5" provided="false" name="Cobol 2" default_profile="0" language="cobol" enabled="false" PARENT_NAME="[null]"/>
+  <rules_profiles id="4" version="1" used_profile="true" provided="true" name="Cobol 1" default_profile="1" language="cobol" enabled="false" PARENT_NAME="[null]"/>
+  <rules_profiles id="5" version="1" used_profile="true" provided="false" name="Cobol 2" default_profile="0" language="cobol" enabled="false" PARENT_NAME="[null]"/>
 </dataset>
\ No newline at end of file
index f2c684b5735ad9f7912cd65cb8437e26c4a42c0d..082f8785417e505a60bd96ecc24bdea6dd5684d3 100644 (file)
@@ -1,7 +1,7 @@
 <dataset>
-  <rules_profiles id="1" provided="true" name="Java 1" default_profile="0" language="java" enabled="true" PARENT_NAME="[null]"/>
-  <rules_profiles id="2" provided="false" name="Java 2" default_profile="1" language="java" enabled="true" PARENT_NAME="[null]"/>
-  <rules_profiles id="3" provided="true" name="Php" default_profile="0" language="php" enabled="true" PARENT_NAME="[null]"/>
-  <rules_profiles id="4" provided="true" name="Cobol 1" default_profile="1" language="cobol" enabled="true" PARENT_NAME="[null]"/>
-  <rules_profiles id="5" provided="false" name="Cobol 2" default_profile="0" language="cobol" enabled="true" PARENT_NAME="[null]"/>
+  <rules_profiles id="1" version="1" used_profile="true" provided="true" name="Java 1" default_profile="0" language="java" enabled="true" PARENT_NAME="[null]"/>
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="Java 2" default_profile="1" language="java" enabled="true" PARENT_NAME="[null]"/>
+  <rules_profiles id="3" version="1" used_profile="true" provided="true" name="Php" default_profile="0" language="php" enabled="true" PARENT_NAME="[null]"/>
+  <rules_profiles id="4" version="1" used_profile="true" provided="true" name="Cobol 1" default_profile="1" language="cobol" enabled="true" PARENT_NAME="[null]"/>
+  <rules_profiles id="5" version="1" used_profile="true" provided="false" name="Cobol 2" default_profile="0" language="cobol" enabled="true" PARENT_NAME="[null]"/>
 </dataset>
\ No newline at end of file
index d89acc7ae0bf2e4c14fff439b9e085be35eb57cc..505d4bc123f09d42cd971c29f925c50328b52161 100644 (file)
@@ -1,11 +1,11 @@
 <dataset>
-  <rules_profiles id="1" provided="true" name="Java 1" default_profile="0" language="java" enabled="true" PARENT_NAME="[null]"/>
-  <rules_profiles id="2" provided="false" name="Java 2" default_profile="1" language="java" enabled="true" PARENT_NAME="[null]"/>
+  <rules_profiles id="1" version="1" used_profile="true" provided="true" name="Java 1" default_profile="0" language="java" enabled="true" PARENT_NAME="[null]"/>
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="Java 2" default_profile="1" language="java" enabled="true" PARENT_NAME="[null]"/>
 
   <!-- enabled -->
-  <rules_profiles id="3" provided="true" name="Disabled Php 1" default_profile="0" language="php" enabled="true" PARENT_NAME="[null]"/>
-  <rules_profiles id="4" provided="false" name="Disabled Php 2" default_profile="1" language="php" enabled="true" PARENT_NAME="[null]"/>
+  <rules_profiles id="3" version="1" used_profile="true" provided="true" name="Disabled Php 1" default_profile="0" language="php" enabled="true" PARENT_NAME="[null]"/>
+  <rules_profiles id="4" version="1" used_profile="true" provided="false" name="Disabled Php 2" default_profile="1" language="php" enabled="true" PARENT_NAME="[null]"/>
 
   <!-- keep disabled -->
-  <rules_profiles id="5" provided="true" name="Disabled Cobol" default_profile="1" language="cobol" enabled="false" PARENT_NAME="[null]"/>
+  <rules_profiles id="5" version="1" used_profile="true" provided="true" name="Disabled Cobol" default_profile="1" language="cobol" enabled="false" PARENT_NAME="[null]"/>
 </dataset>
\ No newline at end of file
index c32281a33db1b269e216154810d11b0d9c50a16c..7d26e5127f092345cb2690cc89c60828e6834a4c 100644 (file)
@@ -1,7 +1,7 @@
 <dataset>
-  <rules_profiles id="1" provided="true" name="Java 1" default_profile="0" language="java" enabled="true" PARENT_NAME="[null]"/>
-  <rules_profiles id="2" provided="false" name="Java 2" default_profile="1" language="java" enabled="true" PARENT_NAME="[null]"/>
-  <rules_profiles id="3" provided="true" name="Disabled Php 1" default_profile="0" language="php" enabled="false" PARENT_NAME="[null]"/>
-  <rules_profiles id="4" provided="false" name="Disabled Php 2" default_profile="1" language="php" enabled="false" PARENT_NAME="[null]"/>
-  <rules_profiles id="5" provided="true" name="Disabled Cobol" default_profile="1" language="cobol" enabled="false" PARENT_NAME="[null]"/>
+  <rules_profiles id="1" version="1" used_profile="true" provided="true" name="Java 1" default_profile="0" language="java" enabled="true" PARENT_NAME="[null]"/>
+  <rules_profiles id="2" version="1" used_profile="true" provided="false" name="Java 2" default_profile="1" language="java" enabled="true" PARENT_NAME="[null]"/>
+  <rules_profiles id="3" version="1" used_profile="true" provided="true" name="Disabled Php 1" default_profile="0" language="php" enabled="false" PARENT_NAME="[null]"/>
+  <rules_profiles id="4" version="1" used_profile="true" provided="false" name="Disabled Php 2" default_profile="1" language="php" enabled="false" PARENT_NAME="[null]"/>
+  <rules_profiles id="5" version="1" used_profile="true" provided="true" name="Disabled Cobol" default_profile="1" language="cobol" enabled="false" PARENT_NAME="[null]"/>
 </dataset>
\ No newline at end of file
index 1bf29a3a0d2656f4573f326ee33582abf070b74a..a17525b594e7a168e2287c64374a75ab546b7b4f 100644 (file)
@@ -6,8 +6,8 @@
   <metrics id="2" name="disabledkey2" val_type="INT" description="description"  domain="domain"
            short_name="name2" qualitative="false" user_managed="false" enabled="false" origin="JAV" worst_value="[null]" optimized_best_value="[null]" best_value="[null]" direction="1" hidden="false"/>
 
-  <rules_profiles id="1" name="profile1" default_profile="true" provided="true" language="JAV" enabled="true"/>
-  <rules_profiles id="2" name="profile2" default_profile="false" provided="false" language="JAV" enabled="true"/>
+  <rules_profiles id="1" version="1" used_profile="true" name="profile1" default_profile="true" provided="true" language="JAV" enabled="true"/>
+  <rules_profiles id="2" version="1" used_profile="true" name="profile2" default_profile="false" provided="false" language="JAV" enabled="true"/>
 
   <!-- ok -->
   <alerts id="1" profile_id="1" metric_id="1" operator=">" value_error="30" value_warning="[null]"/>
index 67ea704c42083e86e8483c1aa6d8b258157398bf..9dc524b15d30d03bdfe45adc177cafc756aaeead 100644 (file)
@@ -7,8 +7,8 @@
            short_name="name2" qualitative="false" user_managed="false" enabled="false" origin="JAV" worst_value="[null]" optimized_best_value="[null]" best_value="[null]" direction="1" hidden="false"/>
 
 
-  <rules_profiles id="1" name="profile1" default_profile="true" provided="true" language="JAV" enabled="true"/>
-  <rules_profiles id="2" name="profile2" default_profile="false" provided="false" language="JAV" enabled="true"/>
+  <rules_profiles id="1" version="1" used_profile="true" name="profile1" default_profile="true" provided="true" language="JAV" enabled="true"/>
+  <rules_profiles id="2" version="1" used_profile="true" name="profile2" default_profile="false" provided="false" language="JAV" enabled="true"/>
 
   <!-- ok -->
   <alerts id="1" profile_id="1" metric_id="1" operator=">" value_error="30" value_warning="[null]"/>
index df722428cb45eca94e8b424a2adaa3d01ed4e5dc..e85839686b36cdbde698b3456d3e80105afcf672 100644 (file)
@@ -12,7 +12,7 @@
   <rules_parameters id="3" rule_id="1" name="param3" description="[null]" param_type="[null]"/>
 
 
-  <rules_profiles id="1" name="profile name" language="java" default_profile="false" provided="false"  enabled="true"/>
+  <rules_profiles id="1" version="1" used_profile="true" name="profile name" language="java" default_profile="false" provided="false"  enabled="true"/>
   <active_rules id="1" rule_id="1" profile_id="1" failure_level="4" />
   <active_rule_parameters id="1" active_rule_id="1" rules_parameter_id="1" value="one" />
   <active_rule_parameters id="2" active_rule_id="1" rules_parameter_id="2" value="two" />
index ae76f8c4ba6759d1edf4d6b6802cd7583f56578d..0b3b68a9880e71d329386a7e625e0ae653761c9f 100644 (file)
@@ -8,7 +8,7 @@
 
   <rules_parameters id="1" rule_id="1" name="deprecated-prop" description="[null]" param_type="[null]"/>
 
-  <rules_profiles id="1" name="sonar way" language="java" default_profile="false" provided="true"  enabled="true"/>
+  <rules_profiles id="1" version="1" used_profile="true" name="sonar way" language="java" default_profile="false" provided="true"  enabled="true"/>
   <active_rules id="1" rule_id="1" profile_id="1" failure_level="4" />
   <active_rules id="2" rule_id="2" profile_id="1" failure_level="0" />
 
index 2391427880f5c768cdb7e5ce875cf65ad5cd9d4e..e8d802938084d80ee12c028ac70092f1766b9702 100644 (file)
@@ -25,6 +25,20 @@ create table ACTIVE_RULES (
   primary key (id)
 );
 
+create table ACTIVE_RULE_CHANGES (
+  ID INTEGER not null,
+  USER_LOGIN VARCHAR(40) not null,
+  PROFILE_ID INTEGER not null,
+  PROFILE_VERSION INTEGER not null,
+  RULE_ID INTEGER not null,
+  CHANGE_DATE TIMESTAMP not null,
+  ENABLED SMALLINT,
+  OLD_SEVERITY INTEGER,
+  NEW_SEVERITY INTEGER,
+  primary key (id)
+);
+CREATE INDEX ACTIVE_RULE_CHANGES_PID ON ACTIVE_RULE_CHANGES (PROFILE_ID);
+
 create table ACTIVE_RULE_PARAMETERS (
   ID INTEGER not null,
   ACTIVE_RULE_ID INTEGER not null,
@@ -33,6 +47,16 @@ create table ACTIVE_RULE_PARAMETERS (
   primary key (id)
 );
 
+create table ACTIVE_RULE_PARAM_CHANGES (
+  ID INTEGER not null,
+  ACTIVE_RULE_CHANGE_ID INTEGER not null,
+  RULES_PARAMETER_ID INTEGER not null,
+  OLD_VALUE VARCHAR(4000),
+  NEW_VALUE VARCHAR(4000),
+  primary key (id)
+);
+CREATE INDEX ACTIVE_RULE_PARAM_CHANGES_CID ON ACTIVE_RULE_PARAM_CHANGES (ACTIVE_RULE_CHANGE_ID);
+
 create table ALERTS (
   ID INTEGER not null,
   PROFILE_ID INTEGER,
@@ -362,6 +386,8 @@ create table RULES_PROFILES (
   LANGUAGE VARCHAR(16),
   PARENT_NAME VARCHAR(100),
   ENABLED SMALLINT,
+  VERSION INTEGER not null,
+  USED_PROFILE SMALLINT not null,
   primary key (id)
 );