]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-833: New Web Service to get history of measures (time machine)
authorGodin <mandrikov@gmail.com>
Mon, 13 Dec 2010 21:55:49 +0000 (21:55 +0000)
committerGodin <mandrikov@gmail.com>
Mon, 13 Dec 2010 21:55:49 +0000 (21:55 +0000)
sonar-gwt-api/src/main/java/org/sonar/wsclient/gwt/unmarshallers/TimeMachineUnmarshaller.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/api/timemachine_controller.rb
sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineData.java
sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineQuery.java
sonar-ws-client/src/main/java/org/sonar/wsclient/unmarshallers/TimeMachineUnmarshaller.java
sonar-ws-client/src/test/java/org/sonar/wsclient/services/TimeMachineDataTest.java
sonar-ws-client/src/test/java/org/sonar/wsclient/unmarshallers/TimeMachineUnmarshallerTest.java
sonar-ws-client/src/test/resources/timemachine/many.json
sonar-ws-client/src/test/resources/timemachine/timemachine.json

index 68ffda4ed06573c7fc79b3cd7b667c4d8f962d9a..f4c1cea3f3e9114f79f9498a5deeae9a50bf3d89 100644 (file)
@@ -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<TimeMachineData> {
+public class TimeMachineUnmarshaller extends AbstractUnmarshaller<TimeMachineData> {
 
-  public TimeMachineData toModel(JavaScriptObject json) {
-    JSONObject map = new JSONObject(json);
-    Map<Date, List<String>> data = new HashMap<Date, List<String>>();
-    for (String dateTimeStr : map.keySet()) {
-      JSONArray array = map.get(dateTimeStr).isArray();
-      List<String> measures = new ArrayList<String>();
-      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<String> measures = new ArrayList<String>();
+    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<TimeMachineData> toModels(JavaScriptObject json) {
-    return Arrays.asList(toModel(json));
+    return new TimeMachineData()
+        .setDate(JsonUtils.parseDateTime(dateTimeStr))
+        .setValues(measures);
   }
 
 }
index 92e2b51a3b21ababd563d0d94923722a36b8706e..042ba114f81e70a4bd65db14b39794ad161f530c 100644 (file)
 #
 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]<<measure
-        end
-
-        snapshots.each do |snapshot|
-          snapshot_measures = measures_by_sid[snapshot.id] || []
-          values_by_key = {}
-          snapshot_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
-          result[format_datetime(snapshot.created_at)] = values
-        end
+      measures_conditions << 'project_measures.rules_category_id IS NULL AND project_measures.rule_id IS NULL AND project_measures.rule_priority IS NULL'
+
+      measures_conditions << 'project_measures.snapshot_id IN (:snapshots)'
+      measures_values[:snapshots] = snapshots.map{|s| s.id}
+
+      metric_keys = params[:metrics].split(',')
+      metrics = Metric.by_keys(metric_keys)
+      measures_conditions << 'project_measures.metric_id IN (:metrics)'
+      measures_values[:metrics] = metrics.select{|m| m.id}
+
+      add_characteristic_filters(measures_conditions, measures_values)
+
+      measures = ProjectMeasure.find(:all,
+        :select => '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]<<measure
       end
 
       # ---------- FORMAT RESPONSE
+      objects = { :snapshots => 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
index 4c68d2c3ca10d1a43843a6d54c6c65bac72ebbd8..e0d669d7c2ee6c22ac14417868c12fc7f4eb80be 100644 (file)
@@ -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<Date, List<String>> data = new HashMap<Date, List<String>>();
+  private List<String> values = new ArrayList<String>();
 
-  public Map<Date, List<String>> getData() {
-    return data;
+  public Date getDate() {
+    return date;
   }
 
-  public TimeMachineData setData(Map<Date, List<String>> 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<String> getValues() {
+    return values;
+  }
+
+  public TimeMachineData setValues(List<String> 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;
   }
 
 }
index 6d4fee7fa96143ff45cab8ad45761f3fb71be578..04f2019d490671ebb070dd7fd965fbd78a997273 100644 (file)
@@ -14,6 +14,9 @@ public class TimeMachineQuery extends Query<TimeMachineData> {
   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<TimeMachineData> {
     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<TimeMachineData> {
     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();
   }
 
index dab000f069ceeb4929c2c3727e21d7af9cd6c936..1289e63a5aee8f42b84a425256b19a5c78937d56 100644 (file)
@@ -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<TimeMachineData> {
+public class TimeMachineUnmarshaller extends AbstractUnmarshaller<TimeMachineData> {
 
-  public TimeMachineData toModel(String json) {
-    JSONObject map = (JSONObject) JSONValue.parse(json);
-    Map<Date, List<String>> data = new HashMap<Date, List<String>>();
-    for (Object key : map.keySet()) {
-      JSONArray array = (JSONArray) map.get(key);
-      List<String> measures = new ArrayList<String>();
-      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<String> measures = new ArrayList<String>();
+    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<TimeMachineData> toModels(String json) {
-    throw new UnsupportedOperationException();
+    return new TimeMachineData()
+        .setDate(JsonUtils.parseDateTime(dateTimeStr))
+        .setValues(measures);
   }
 
 }
index cd7fec9609f4fb0384bfa3b474d77b56cfa356b8..6672a3acf5e658f6c32794991e1fee4489930db7 100644 (file)
@@ -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<Date, List<String>> map = new HashMap<Date, List<String>>();
-    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());
   }
 
 }
index 13a8b5c048267fa42c6d027c5e0bd6afbb5db1af..908658f336bc045bbf5765a8aa77e12596d8ad36 100644 (file)
@@ -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<Date, List<String>> 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<String> values = map.values().iterator().next();
+    List<String> 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<TimeMachineData> data = new TimeMachineUnmarshaller().toModels(WSTestUtils.loadFile("/timemachine/many.json"));
 
-    Map<Date, List<String>> map = data.getData();
-    assertThat(map.size(), is(3));
+    assertThat(data.size(), is(3));
   }
 
 }
index a65a8daa255e7bfb6697f1991dac5478153c171f..6ee5fa2312a183a358b36d3c3494d57673da445e 100644 (file)
@@ -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]}
+]
index 03130cc0286d665fc87ff5d48240c7cf9ece3cc2..17f064aeb0b786ca59f07631adcb3d971051b201 100644 (file)
@@ -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]}
+]