diff options
author | simonbrandhof <simon.brandhof@gmail.com> | 2011-07-14 18:19:37 +0200 |
---|---|---|
committer | simonbrandhof <simon.brandhof@gmail.com> | 2011-07-14 18:19:52 +0200 |
commit | 8d542803129dc572828a4a12174c5c4d2c1c81c8 (patch) | |
tree | f539237e35b66ce4978e07a130078076c0678c3a | |
parent | 75eeadcdaa600a5d2e2729125f7258ebddeb8101 (diff) | |
download | sonarqube-8d542803129dc572828a4a12174c5c4d2c1c81c8.tar.gz sonarqube-8d542803129dc572828a4a12174c5c4d2c1c81c8.zip |
SONAR-2610 web service for manual measures + copy manual measures during analysis
13 files changed, 448 insertions, 36 deletions
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java index 5b4358eb5cb..807e8834852 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java @@ -214,6 +214,7 @@ public class CorePlugin extends SonarPlugin { extensions.add(FilesDecorator.class); extensions.add(CloseReviewsDecorator.class); extensions.add(ReferenceAnalysis.class); + extensions.add(ManualMeasureDecorator.class); // time machine extensions.add(TendencyDecorator.class); diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ManualMeasureDecorator.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ManualMeasureDecorator.java new file mode 100644 index 00000000000..5d26067deee --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ManualMeasureDecorator.java @@ -0,0 +1,69 @@ +/* + * 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.Decorator; +import org.sonar.api.batch.DecoratorContext; +import org.sonar.api.batch.Phase; +import org.sonar.api.database.DatabaseSession; +import org.sonar.api.measures.Measure; +import org.sonar.api.measures.MetricFinder; +import org.sonar.api.resources.Project; +import org.sonar.api.resources.Resource; +import org.sonar.jpa.entity.ManualMeasure; + +import java.util.List; + +@Phase(name = Phase.Name.PRE) +public class ManualMeasureDecorator implements Decorator { + + private DatabaseSession session; + private MetricFinder metricFinder; + + public ManualMeasureDecorator(DatabaseSession session, MetricFinder metricFinder) { + this.session = session; + this.metricFinder = metricFinder; + } + + public boolean shouldExecuteOnProject(Project project) { + return true; + } + + public void decorate(Resource resource, DecoratorContext context) { + if (resource.getId() != null) { + List<ManualMeasure> manualMeasures = session.getResults(ManualMeasure.class, "resourceId", resource.getId()); + for (ManualMeasure manualMeasure : manualMeasures) { + context.saveMeasure(copy(manualMeasure)); + } + } + } + + private Measure copy(ManualMeasure manualMeasure) { + Measure measure = new Measure(metricFinder.findById(manualMeasure.getMetricId())); + measure.setValue(manualMeasure.getValue(), 5); + measure.setData(manualMeasure.getTextValue()); + return measure; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ManualMeasureDecoratorTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ManualMeasureDecoratorTest.java new file mode 100644 index 00000000000..4ca1719de50 --- /dev/null +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ManualMeasureDecoratorTest.java @@ -0,0 +1,52 @@ +/* + * 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.junit.Test; +import org.sonar.api.batch.DecoratorContext; +import org.sonar.api.measures.Metric; +import org.sonar.api.resources.JavaFile; +import org.sonar.api.test.IsMeasure; +import org.sonar.core.components.DefaultMetricFinder; +import org.sonar.jpa.test.AbstractDbUnitTestCase; + +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class ManualMeasureDecoratorTest extends AbstractDbUnitTestCase { + + private Metric reviewNote = new Metric.Builder("review_note", "Note", Metric.ValueType.FLOAT).create().setId(2); + + @Test + public void testCopyManualMeasures() throws Exception { + setupData("testCopyManualMeasures"); + + JavaFile javaFile = new JavaFile("Foo.java"); + javaFile.setId(40); + + ManualMeasureDecorator decorator = new ManualMeasureDecorator(getSession(), new DefaultMetricFinder(getSessionFactory())); + DecoratorContext context = mock(DecoratorContext.class); + decorator.decorate(javaFile, context); + + verify(context).saveMeasure(argThat(new IsMeasure(reviewNote, 6.0, "six"))); + } + +} diff --git a/plugins/sonar-core-plugin/src/test/resources/org/sonar/plugins/core/sensors/ManualMeasureDecoratorTest/testCopyManualMeasures.xml b/plugins/sonar-core-plugin/src/test/resources/org/sonar/plugins/core/sensors/ManualMeasureDecoratorTest/testCopyManualMeasures.xml new file mode 100644 index 00000000000..9c772983186 --- /dev/null +++ b/plugins/sonar-core-plugin/src/test/resources/org/sonar/plugins/core/sensors/ManualMeasureDecoratorTest/testCopyManualMeasures.xml @@ -0,0 +1,11 @@ +<dataset> + <metrics id="1" NAME="ncloc" VAL_TYPE="INT" DESCRIPTION="[null]" domain="[null]" short_name="" + enabled="true" worst_value="[null]" optimized_best_value="[null]" best_value="[null]" direction="0" hidden="false"/> + <metrics id="2" NAME="review_note" VAL_TYPE="INT" DESCRIPTION="[null]" domain="[null]" short_name="" + enabled="true" worst_value="[null]" optimized_best_value="[null]" best_value="[null]" direction="0" hidden="false"/> + + + <manual_measures id="1" metric_id="2" resource_id="30" value="3.14" text_value="pi" created_at="[null]" updated_at="[null]"/> + <manual_measures id="2" metric_id="2" resource_id="40" value="6" text_value="six" created_at="[null]" updated_at="[null]"/> + +</dataset>
\ No newline at end of file diff --git a/sonar-core/src/main/java/org/sonar/jpa/entity/ManualMeasure.java b/sonar-core/src/main/java/org/sonar/jpa/entity/ManualMeasure.java new file mode 100644 index 00000000000..9f0bbf94ba9 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/jpa/entity/ManualMeasure.java @@ -0,0 +1,78 @@ +/* + * 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.jpa.entity; + +import javax.persistence.*; +import java.util.Date; + +@Entity +@Table(name = "manual_measures") +public final class ManualMeasure { + @Id + @Column(name = "id") + @GeneratedValue + private Long id; + + @Column(name = "value", updatable = true, nullable = true, precision = 30, scale = 20) + private Double value = null; + + @Column(name = "text_value", updatable = true, nullable = true, length = 4000) + private String textValue; + + @Column(name = "metric_id", updatable = false, nullable = false) + private Integer metricId; + + @Column(name = "resource_id", updatable = true, nullable = true) + private Integer resourceId; + + @Column(name = "created_at", updatable = true, nullable = true) + private Date createdAt; + + @Column(name = "updated_at", updatable = true, nullable = true) + private Date updatedAt; + + public Long getId() { + return id; + } + + public Double getValue() { + return value; + } + + public String getTextValue() { + return textValue; + } + + public Integer getMetricId() { + return metricId; + } + + public Integer getResourceId() { + return resourceId; + } + + public Date getCreatedAt() { + return createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } +} diff --git a/sonar-core/src/main/resources/META-INF/persistence.xml b/sonar-core/src/main/resources/META-INF/persistence.xml index a4a8d36b3e4..cb768e0e986 100644 --- a/sonar-core/src/main/resources/META-INF/persistence.xml +++ b/sonar-core/src/main/resources/META-INF/persistence.xml @@ -7,6 +7,7 @@ <provider>org.hibernate.ejb.HibernatePersistence</provider> <class>org.sonar.jpa.entity.SchemaMigration</class> + <class>org.sonar.jpa.entity.ManualMeasure</class> <class>org.sonar.api.database.configuration.Property</class> <class>org.sonar.api.qualitymodel.Model</class> <class>org.sonar.api.qualitymodel.Characteristic</class> diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/api_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/api_controller.rb index 594635c62a1..f1e0100884c 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/api_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/api_controller.rb @@ -20,42 +20,93 @@ require 'json' require 'time' class Api::ApiController < ApplicationController - - protected - + + class ApiException < Exception + attr_reader :code, :msg + + def initialize(code, msg) + @code = code + @msg = msg + end + end + + rescue_from ApiException do |exception| + render_error(exception.msg, exception.code) + end + + rescue_from ActiveRecord::RecordInvalid do |exception| + render_error(exception.message, 400) + end + + rescue_from ActiveRecord::RecordNotFound do |exception| + render_error(exception.message, 404) + end + + protected + def text_not_supported "Not supported" end - + def xml_not_supported xml = Builder::XmlMarkup.new(:indent => 0) xml.instruct! xml.not_supported end - + def json_not_supported JSON({:not_supported => true}) end - + def jsonp(json) - text=( (json.is_a? String) ? json : JSON(json)) - + text=((json.is_a? String) ? json : JSON(json)) + if params['callback'] - params['callback'] + '(' + text + ');' + params['callback'] + '(' + text + ');' else text end end - + + # deprecated. Use Api::Utils.format_datetime + def format_datetime(datetime) + Api::Utils.format_datetime(datetime) + end + + # deprecated. Use Api::Utils.parse_datetime + def parse_datetime(datetime_string, default_is_now=true) + Api::Utils.parse_datetime(datetime_string, default_is_now) + end + + def load_resource(resource_key, role=nil) + resource=Project.by_key(resource_key) + not_found("Resource not found: #{resource_key}") if resource.nil? + access_denied if role && !has_role?(role, resource) + resource + end + + + + #---------------------------------------------------------------------------- + # ERRORS + #---------------------------------------------------------------------------- + def not_found(message) + raise ApiException.new(404, message) + end + + def bad_request(message) + raise ApiException.new(400, message) + end + def access_denied - render_error('Unauthorized', 401) + raise ApiException.new(401, 'Unauthorized') end - + def render_error(msg, http_status=400) - respond_to do |format| - format.json{ render :json => error_to_json(msg, http_status), :status => http_status } - format.xml{ render :xml => error_to_xml(msg, http_status), :status => http_status} - format.text{ render :text => msg, :status => http_status } + respond_to do |format| + format.json { render :json => error_to_json(msg, http_status), :status => http_status } + format.xml { render :xml => error_to_xml(msg, http_status), :status => http_status } + format.text { render :text => msg, :status => http_status } end end @@ -68,7 +119,7 @@ class Api::ApiController < ApplicationController def error_to_xml(msg, error_code=nil) xml = Builder::XmlMarkup.new(:indent => 0) - xml.error do + xml.error do xml.code(error_code) if error_code xml.msg(msg) if msg end @@ -78,23 +129,5 @@ class Api::ApiController < ApplicationController render_error(msg, 200) end - # deprecated. Use Api::Utils.format_datetime - def format_datetime(datetime) - Api::Utils.format_datetime(datetime) - end - - # deprecated. Use Api::Utils.parse_datetime - def parse_datetime(datetime_string, default_is_now=true) - Api::Utils.parse_datetime(datetime_string, default_is_now) - end - - class ApiException < Exception - attr_reader :code, :msg - def initialize(code, msg) - @code = code - @msg = msg - end - end - end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/events_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/events_controller.rb index afc773baa48..8f842296cb4 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/events_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/events_controller.rb @@ -43,6 +43,7 @@ class Api::EventsController < Api::ApiController conditions<<'resource_id IS NULL' end + from=nil if params[:fromDateTime] from=parse_datetime(params[:fromDateTime], false) elsif params[:fromDate] @@ -53,6 +54,7 @@ class Api::EventsController < Api::ApiController values[:from]=from end + to=nil if params[:toDateTime] to=parse_datetime(params[:toDateTime], false) elsif params[:toDate] diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/manual_measures_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/manual_measures_controller.rb new file mode 100644 index 00000000000..e5ad10169e6 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/manual_measures_controller.rb @@ -0,0 +1,113 @@ +# +# 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 +# + +require 'json' + +class Api::ManualMeasuresController < Api::ApiController + + # + # GET /api/manual_measures?resource=<resource>&metric=<optional metric> + # + def index + resource=load_resource(params[:resource], :user) + + metric=nil + if params[:metric].present? + metric=Metric.by_key(params[:metric]) + bad_request("Unknown metric: #{params[:metric]}") if metric.nil? + end + + result = resource.manual_measures + if metric + result = result.select{|m| m.metric_id==metric.id} + end + + respond_to do |format| + format.json { render :json => jsonp(manual_measures_to_json(result)) } + format.xml { render :xml => xml_not_supported } + end + end + + # + # POST /api/manual_measures?resource=<resource>&metric=<metric>&val=<optional decimal value>&text=<optional text> + # Create or update measure. + # + def create + resource=load_resource(params[:resource], :admin) + + metric=Metric.by_key(params[:metric]) + bad_request("Unknown metric: #{params[:metric]}") if metric.nil? + + value=params[:val] + bad_request("Not a numeric value: #{value}") if value && !Api::Utils.is_number?(value) + + measure=ManualMeasure.find(:first, :conditions => ['resource_id=? and metric_id=?', resource.id, metric.id]) + if measure.nil? + measure=ManualMeasure.new(:resource => resource, :user => current_user, :metric_id => metric.id) + end + + measure.value = value + measure.text_value = params[:text] + measure.save! + + respond_to do |format| + format.json { render :json => jsonp(manual_measure_to_json(measure)) } + format.xml { render :xml => xml_not_supported } + end + end + + + # + # DELETE /api/manual_measures?resource=<resource>&metric=<metric> + # + def destroy + resource=load_resource(params[:resource], :admin) + + metric=Metric.by_key(params[:metric]) + bad_request("Unknown metric: #{params[:metric]}") if metric.nil? + + count = ManualMeasure.delete_all(['resource_id=? and metric_id=?', resource.id, metric.id]) + + render_success "Deleted #{count} measures" + end + + private + + def manual_measures_to_json(manual_measures) + json = [] + manual_measures.each do |m| + json<<manual_measure_to_json(m) + end + json + end + + def manual_measure_to_json(manual_measure) + hash={:id => manual_measure.id, :metric => manual_measure.metric.key} + hash[:val]=manual_measure.value if manual_measure.value + hash[:text]=manual_measure.text_value if manual_measure.text_value + hash[:created_at]=format_datetime(manual_measure.created_at) + hash[:updated_at]=format_datetime(manual_measure.updated_at) if manual_measure.updated_at + if manual_measure.user + hash[:login]=manual_measure.user.login + hash[:username]=manual_measure.user.name + end + hash + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/manual_measure.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/manual_measure.rb new file mode 100644 index 00000000000..b458c336e2a --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/manual_measure.rb @@ -0,0 +1,42 @@ +# +# 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 ManualMeasure < ActiveRecord::Base + belongs_to :resource, :class_name => 'Project' + belongs_to :user + validates_uniqueness_of :metric_id, :scope => :resource_id + validates_length_of :text_value, :maximum => 4000, :allow_nil => true, :allow_blank => true + validate :validate_metric + + def metric + @metric ||= + begin + Metric.by_id(metric_id) + end + end + + def metric=(m) + @metric = m + write_attribute(:metric_id, m.id) if m.id + end + + def validate_metric + errors.add_to_base("Not a valid metric") unless metric && metric.enabled? + end +end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/project.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/project.rb index 9658c9dfcb3..5c7273c0296 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/project.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/project.rb @@ -28,6 +28,7 @@ class Project < ActiveRecord::Base belongs_to :profile, :class_name => 'Profile', :foreign_key => 'profile_id' has_many :user_roles, :foreign_key => 'resource_id' has_many :group_roles, :foreign_key => 'resource_id' + has_many :manual_measures, :foreign_key => 'resource_id' belongs_to :root, :class_name => 'Project', :foreign_key => 'root_id' def self.by_key(k) diff --git a/sonar-server/src/main/webapp/WEB-INF/config/routes.rb b/sonar-server/src/main/webapp/WEB-INF/config/routes.rb index ba3c53ba26b..e0e0afe2416 100644 --- a/sonar-server/src/main/webapp/WEB-INF/config/routes.rb +++ b/sonar-server/src/main/webapp/WEB-INF/config/routes.rb @@ -11,6 +11,7 @@ ActionController::Routing::Routes.draw do |map| api.resources :events, :only => [:index, :show, :create, :destroy] api.resources :user_properties, :only => [:index, :show, :create, :destroy], :requirements => { :id => /.*/ } api.resources :favorites, :only => [:index, :show, :create, :destroy], :requirements => { :id => /.*/ } + api.resources :manual_measures, :only => [:index, :create, :destroy], :requirements => { :id => /.*/ } api.resources :reviews, :only => [:index, :show, :create], :member => { :add_comment => :put, :reassign => :put, diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/211_create_manual_measures.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/211_create_manual_measures.rb index 72e065fc238..9a10482fa55 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/211_create_manual_measures.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/211_create_manual_measures.rb @@ -24,7 +24,15 @@ class CreateManualMeasures < ActiveRecord::Migration def self.up - # TODO + create_table 'manual_measures' do |t| + t.column 'metric_id', :integer, :null => false + t.column 'resource_id', :integer, :null => true + t.column 'value', :decimal, :null => true, :precision => 30, :scale => 20 + t.column 'text_value', :string, :null => true, :limit => 4000 + t.column 'user_id', :integer, :null => true + t.timestamps + end + alter_to_big_primary_key('manual_measures') end end |