From d104ca20751117924dc7e74a9cf13445926b0acc Mon Sep 17 00:00:00 2001 From: Godin Date: Mon, 13 Dec 2010 21:55:49 +0000 Subject: [PATCH] SONAR-833: New Web Service to get history of measures (time machine) --- .../TimeMachineUnmarshaller.java | 34 ++-- .../controllers/api/timemachine_controller.rb | 166 ++++++++++++++---- .../wsclient/services/TimeMachineData.java | 38 ++-- .../wsclient/services/TimeMachineQuery.java | 11 ++ .../TimeMachineUnmarshaller.java | 34 ++-- .../services/TimeMachineDataTest.java | 13 +- .../TimeMachineUnmarshallerTest.java | 12 +- .../src/test/resources/timemachine/many.json | 10 +- .../resources/timemachine/timemachine.json | 6 +- 9 files changed, 206 insertions(+), 118 deletions(-) diff --git a/sonar-gwt-api/src/main/java/org/sonar/wsclient/gwt/unmarshallers/TimeMachineUnmarshaller.java b/sonar-gwt-api/src/main/java/org/sonar/wsclient/gwt/unmarshallers/TimeMachineUnmarshaller.java index 68ffda4ed06..f4c1cea3f3e 100644 --- a/sonar-gwt-api/src/main/java/org/sonar/wsclient/gwt/unmarshallers/TimeMachineUnmarshaller.java +++ b/sonar-gwt-api/src/main/java/org/sonar/wsclient/gwt/unmarshallers/TimeMachineUnmarshaller.java @@ -1,34 +1,28 @@ package org.sonar.wsclient.gwt.unmarshallers; -import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.json.client.JSONArray; import com.google.gwt.json.client.JSONObject; import com.google.gwt.json.client.JSONValue; import org.sonar.gwt.JsonUtils; import org.sonar.wsclient.services.TimeMachineData; -import java.util.*; +import java.util.ArrayList; +import java.util.List; -public class TimeMachineUnmarshaller implements Unmarshaller { +public class TimeMachineUnmarshaller extends AbstractUnmarshaller { - public TimeMachineData toModel(JavaScriptObject json) { - JSONObject map = new JSONObject(json); - Map> data = new HashMap>(); - for (String dateTimeStr : map.keySet()) { - JSONArray array = map.get(dateTimeStr).isArray(); - List measures = new ArrayList(); - for (int i = 0; i < JsonUtils.getArraySize(array); i++) { - // We can't use JsonUtils.getArray here, because it returns JSONObject instead of JSONValue - JSONValue elem = array.get(i); - measures.add(JsonUtils.getAsString(elem)); - } - data.put(JsonUtils.parseDateTime(dateTimeStr), measures); + protected TimeMachineData parse(JSONObject json) { + String dateTimeStr = (String) json.keySet().iterator().next(); + JSONArray array = json.get(dateTimeStr).isArray(); + List measures = new ArrayList(); + for (int i = 0; i < JsonUtils.getArraySize(array); i++) { + // We can't use JsonUtils.getArray here, because it returns JSONObject instead of JSONValue + JSONValue elem = array.get(i); + measures.add(JsonUtils.getAsString(elem)); } - return new TimeMachineData().setData(data); - } - - public List toModels(JavaScriptObject json) { - return Arrays.asList(toModel(json)); + return new TimeMachineData() + .setDate(JsonUtils.parseDateTime(dateTimeStr)) + .setValues(measures); } } diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/timemachine_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/timemachine_controller.rb index 92e2b51a3b2..042ba114f81 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/timemachine_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/timemachine_controller.rb @@ -19,62 +19,92 @@ # class Api::TimemachineController < Api::ApiController MAX_IN_ELEMENTS=990 + # # GET /api/timemachine # + # Required parameters : + # - resource (id or key) + # - metrics + # + # Optional parameters : + # - fromDateTime + # - toDateTime + # - model + # - characteristics + # def index begin resource_id = params[:resource] - metric_keys = params[:metrics].split(',') - metrics = Metric.by_keys(metric_keys) - first_date = parse_datetime(params[:fromDateTime]) - last_date = parse_datetime(params[:toDateTime]) @resource=Project.by_key(resource_id) if @resource.nil? raise ApiException.new 404, "Resource not found: #{resource_id}" end - snapshots = Snapshot.find(:all, - :conditions => ['created_at>=? AND created_at<=? AND project_id=? AND status=?', - first_date, last_date, @resource.id, Snapshot::STATUS_PROCESSED], - :order => 'created_at') + # ---------- PARAMETERS + measures_conditions = [] + measures_values = {} + snapshots_conditions = [] + snapshots_values = {} + + if params[:fromDateTime] + from = parse_datetime(params[:fromDateTime]) + end + if from + snapshots_conditions << 'created_at>=:from' + snapshots_values[:from] = from + end + + if params[:toDateTime] + to = parse_datetime(params[:toDateTime]) + end + if to + snapshots_conditions << 'created_at<=:to' + snapshots_values[:to] = to + end + + snapshots_conditions << 'project_id=:rid AND status=:status' + snapshots_values[:rid] = @resource.id + snapshots_values[:status] = Snapshot::STATUS_PROCESSED + snapshots = Snapshot.find(:all, + :conditions => [ snapshots_conditions.join(' AND '), snapshots_values], + :order => 'created_at') # Oracle limitation : no more than 1000 elements in IN clause if snapshots.length > MAX_IN_ELEMENTS size=snapshots.size snapshots=snapshots[size-MAX_IN_ELEMENTS .. size-1] end - measures = find_measures(metrics, snapshots) - - result = {} - if !measures.empty? - measures_by_sid = {} - measures.each do |measure| - measures_by_sid[measure.snapshot_id]||=[] - measures_by_sid[measure.snapshot_id]< 'project_measures.id,project_measures.value,project_measures.metric_id,project_measures.snapshot_id', + :conditions => [ measures_conditions.join(' AND '), measures_values]) + + # ---------- PREPARE RESPONSE + measures_by_sid = {} + measures.each do |measure| + measures_by_sid[measure.snapshot_id]||=[] + measures_by_sid[measure.snapshot_id]< snapshots, :measures_by_sid => measures_by_sid, :metric_keys => metric_keys } respond_to do |format| - format.json { render :json => jsonp(result) } - format.xml { render :xml => xml_not_supported } + format.json { render :json => jsonp(to_json(objects)) } + format.xml { render :xml => to_xml(objects) } format.text { render :text => text_not_supported } end rescue ApiException => e @@ -84,11 +114,71 @@ class Api::TimemachineController < Api::ApiController private - def find_measures(metrics, snapshots) - ProjectMeasure.find(:all, - :select => 'project_measures.id,project_measures.value,project_measures.metric_id,project_measures.snapshot_id', - :conditions => ['rules_category_id IS NULL AND rule_id IS NULL AND rule_priority IS NULL AND metric_id IN (?) AND snapshot_id IN (?)', - metrics.select{|m| m.id}, snapshots.map{|s| s.id}]) + def to_json(objects) + snapshots = objects[:snapshots] + measures_by_sid = objects[:measures_by_sid] + metric_keys = objects[:metric_keys] + + result = [] + snapshots.each do |snapshot| + result << snapshot_to_json(snapshot, measures_by_sid[snapshot.id] || [], metric_keys) + end + result + end + + def snapshot_to_json(snapshot, measures, metric_keys) + values_by_key = {} + measures.each do |measure| + values_by_key[measure.metric.name] = measure.value.to_f if measure.value + end + values = [] + metric_keys.each do |metric| + values << values_by_key[metric] + end + json = { format_datetime(snapshot.created_at) => values } + json + end + + def to_xml(objects) + snapshots = objects[:snapshots] + measures_by_sid = objects[:measures_by_sid] + metric_keys = objects[:metric_keys] + + xml = Builder::XmlMarkup.new(:indent => 0) + xml.instruct! + + xml.snapshots do + snapshots.each do |snapshot| + snapshot_to_xml(xml, snapshot, measures_by_sid[snapshot.id]) + end + end + end + + def snapshot_to_xml(xml, snapshot, measures) + xml.snapshot do + xml.date(format_datetime(snapshot.created_at)) + # TODO measures + end + end + + def add_characteristic_filters(measures_conditions, measures_values) + @characteristics=[] + @characteristic_by_id={} + if params[:model].present? && params[:characteristics].present? + @characteristics=Characteristic.find(:all, + :select => 'characteristics.id,characteristics.kee,characteristics.name', + :joins => :quality_model, + :conditions => ['quality_models.name=? AND characteristics.kee IN (?)', params[:model], params[:characteristics].split(',')]) + if @characteristics.empty? + measures_conditions<<'project_measures.characteristic_id=-1' + else + @characteristics.each { |c| @characteristic_by_id[c.id]=c } + measures_conditions<<'project_measures.characteristic_id IN (:characteristics)' + measures_values[:characteristics]=@characteristic_by_id.keys + end + else + measures_conditions<<'project_measures.characteristic_id IS NULL' + end end end diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineData.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineData.java index 4c68d2c3ca1..e0d669d7c2e 100644 --- a/sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineData.java +++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineData.java @@ -1,36 +1,42 @@ package org.sonar.wsclient.services; +import java.util.ArrayList; import java.util.Date; -import java.util.HashMap; import java.util.List; -import java.util.Map; public class TimeMachineData extends Model { + private Date date; /** * We use strings here in order to support measures with string value. */ - private Map> data = new HashMap>(); + private List values = new ArrayList(); - public Map> getData() { - return data; + public Date getDate() { + return date; } - public TimeMachineData setData(Map> data) { - this.data = data; + public TimeMachineData setDate(Date date) { + this.date = date; return this; } - public Double getValueAsDouble(Date date, int index) { - if (data.containsKey(date)) { - String valueStr = data.get(date).get(index); - try { - return valueStr == null ? null : Double.valueOf(valueStr); - } catch (NumberFormatException e) { - return null; - } + public List getValues() { + return values; + } + + public TimeMachineData setValues(List values) { + this.values = values; + return this; + } + + public Double getValueAsDouble(int index) { + String valueStr = values.get(index); + try { + return valueStr == null ? null : Double.valueOf(valueStr); + } catch (NumberFormatException e) { + return null; } - return null; } } diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineQuery.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineQuery.java index 6d4fee7fa96..04f2019d490 100644 --- a/sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineQuery.java +++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineQuery.java @@ -14,6 +14,9 @@ public class TimeMachineQuery extends Query { private Date from; private Date to; + private String model; + private String[] characteristicKeys; + public TimeMachineQuery(String resourceKeyOrId) { this.resourceKeyOrId = resourceKeyOrId; } @@ -45,6 +48,12 @@ public class TimeMachineQuery extends Query { return this; } + public TimeMachineQuery setCharacteristicKeys(String model, String... keys) { + this.model = model; + this.characteristicKeys = keys; + return this; + } + @Override public String getUrl() { StringBuilder url = new StringBuilder(BASE_URL); @@ -53,6 +62,8 @@ public class TimeMachineQuery extends Query { appendUrlParameter(url, "metrics", metrics); appendUrlParameter(url, "fromDateTime", from, true); appendUrlParameter(url, "toDateTime", to, true); + appendUrlParameter(url, "model", model); + appendUrlParameter(url, "characteristics", characteristicKeys); return url.toString(); } diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/unmarshallers/TimeMachineUnmarshaller.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/unmarshallers/TimeMachineUnmarshaller.java index dab000f069c..1289e63a5ae 100644 --- a/sonar-ws-client/src/main/java/org/sonar/wsclient/unmarshallers/TimeMachineUnmarshaller.java +++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/unmarshallers/TimeMachineUnmarshaller.java @@ -2,31 +2,25 @@ package org.sonar.wsclient.unmarshallers; import org.json.simple.JSONArray; import org.json.simple.JSONObject; -import org.json.simple.JSONValue; import org.sonar.wsclient.services.TimeMachineData; -import java.util.*; +import java.util.ArrayList; +import java.util.List; -public class TimeMachineUnmarshaller implements Unmarshaller { +public class TimeMachineUnmarshaller extends AbstractUnmarshaller { - public TimeMachineData toModel(String json) { - JSONObject map = (JSONObject) JSONValue.parse(json); - Map> data = new HashMap>(); - for (Object key : map.keySet()) { - JSONArray array = (JSONArray) map.get(key); - List measures = new ArrayList(); - for (int i = 0; i < array.size(); i++) { - Object elem = array.get(i); - String value = elem == null ? null : elem.toString(); - measures.add(value); - } - data.put(JsonUtils.parseDateTime((String) key), measures); + protected TimeMachineData parse(JSONObject json) { + String dateTimeStr = (String) json.keySet().iterator().next(); + JSONArray array = (JSONArray) json.get(dateTimeStr); + List measures = new ArrayList(); + for (int i = 0; i < array.size(); i++) { + Object elem = array.get(i); + String value = elem == null ? null : elem.toString(); + measures.add(value); } - return new TimeMachineData().setData(data); - } - - public List toModels(String json) { - throw new UnsupportedOperationException(); + return new TimeMachineData() + .setDate(JsonUtils.parseDateTime(dateTimeStr)) + .setValues(measures); } } diff --git a/sonar-ws-client/src/test/java/org/sonar/wsclient/services/TimeMachineDataTest.java b/sonar-ws-client/src/test/java/org/sonar/wsclient/services/TimeMachineDataTest.java index cd7fec9609f..6672a3acf5e 100644 --- a/sonar-ws-client/src/test/java/org/sonar/wsclient/services/TimeMachineDataTest.java +++ b/sonar-ws-client/src/test/java/org/sonar/wsclient/services/TimeMachineDataTest.java @@ -2,7 +2,7 @@ package org.sonar.wsclient.services; import org.junit.Test; -import java.util.*; +import java.util.Arrays; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; @@ -12,14 +12,11 @@ public class TimeMachineDataTest { @Test public void valueAsDouble() { - Map> map = new HashMap>(); - Date date = new Date(); - map.put(date, Arrays.asList(null, "20.3", "hello")); - TimeMachineData data = new TimeMachineData().setData(map); + TimeMachineData data = new TimeMachineData().setValues(Arrays.asList(null, "20.3", "hello")); - assertThat(data.getValueAsDouble(date, 0), nullValue()); - assertThat(data.getValueAsDouble(date, 1), is(20.3)); - assertThat(data.getValueAsDouble(date, 2), nullValue()); + assertThat(data.getValueAsDouble(0), nullValue()); + assertThat(data.getValueAsDouble(1), is(20.3)); + assertThat(data.getValueAsDouble(2), nullValue()); } } diff --git a/sonar-ws-client/src/test/java/org/sonar/wsclient/unmarshallers/TimeMachineUnmarshallerTest.java b/sonar-ws-client/src/test/java/org/sonar/wsclient/unmarshallers/TimeMachineUnmarshallerTest.java index 13a8b5c0482..908658f336b 100644 --- a/sonar-ws-client/src/test/java/org/sonar/wsclient/unmarshallers/TimeMachineUnmarshallerTest.java +++ b/sonar-ws-client/src/test/java/org/sonar/wsclient/unmarshallers/TimeMachineUnmarshallerTest.java @@ -6,7 +6,6 @@ import org.sonar.wsclient.services.TimeMachineData; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; -import java.util.Map; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; @@ -18,12 +17,10 @@ public class TimeMachineUnmarshallerTest { public void toModel() throws Exception { TimeMachineData data = new TimeMachineUnmarshaller().toModel(WSTestUtils.loadFile("/timemachine/timemachine.json")); - Map> map = data.getData(); - assertThat(map.size(), is(1)); - Date date = map.keySet().iterator().next(); + Date date = data.getDate(); final Date expectedDate = new SimpleDateFormat("yyyy-MM-dd'T'kk:mm:ssZZZZ").parse("2010-12-04T15:59:23+0000"); assertThat(date, is(expectedDate)); - List values = map.values().iterator().next(); + List values = data.getValues(); assertThat(values.size(), is(3)); assertThat(values.get(0), is("20.0")); assertThat(values.get(1), nullValue()); @@ -32,10 +29,9 @@ public class TimeMachineUnmarshallerTest { @Test public void many() throws Exception { - TimeMachineData data = new TimeMachineUnmarshaller().toModel(WSTestUtils.loadFile("/timemachine/many.json")); + List data = new TimeMachineUnmarshaller().toModels(WSTestUtils.loadFile("/timemachine/many.json")); - Map> map = data.getData(); - assertThat(map.size(), is(3)); + assertThat(data.size(), is(3)); } } diff --git a/sonar-ws-client/src/test/resources/timemachine/many.json b/sonar-ws-client/src/test/resources/timemachine/many.json index a65a8daa255..6ee5fa2312a 100644 --- a/sonar-ws-client/src/test/resources/timemachine/many.json +++ b/sonar-ws-client/src/test/resources/timemachine/many.json @@ -1,5 +1,5 @@ -{ - "2010-10-10T00:00:00+0000": [25.0, null, 14.6], - "2010-11-15T00:00:00+0000": [23.0, null, 10.3], - "2010-12-04T00:00:00+0000": [20.0, null, 12.8] -} +[ + {"2010-10-10T00:00:00+0000": [25.0, null, 14.6]}, + {"2010-11-15T00:00:00+0000": [23.0, null, 10.3]}, + {"2010-12-04T00:00:00+0000": [20.0, null, 12.8]} +] diff --git a/sonar-ws-client/src/test/resources/timemachine/timemachine.json b/sonar-ws-client/src/test/resources/timemachine/timemachine.json index 03130cc0286..17f064aeb0b 100644 --- a/sonar-ws-client/src/test/resources/timemachine/timemachine.json +++ b/sonar-ws-client/src/test/resources/timemachine/timemachine.json @@ -1,3 +1,3 @@ -{ - "2010-12-04T15:59:23+0000": [20.0, null, 12.8] -} +[ + {"2010-12-04T15:59:23+0000": [20.0, null, 12.8]} +] -- 2.39.5