]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-833 time machine WS : improve SQL requests + better support of Q model characte...
authorsimonbrandhof <simon.brandhof@gmail.com>
Mon, 20 Dec 2010 21:46:50 +0000 (21:46 +0000)
committersimonbrandhof <simon.brandhof@gmail.com>
Mon, 20 Dec 2010 21:46:50 +0000 (21:46 +0000)
20 files changed:
sonar-gwt-api/src/main/java/org/sonar/wsclient/gwt/unmarshallers/TimeMachineUnmarshaller.java
sonar-gwt-api/src/main/java/org/sonar/wsclient/gwt/unmarshallers/Unmarshallers.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/api/timemachine_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/models/project_measure.rb
sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachine.java [new file with mode: 0644]
sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineCell.java [new file with mode: 0644]
sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineColumn.java [new file with mode: 0644]
sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineData.java [deleted file]
sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineQuery.java
sonar-ws-client/src/main/java/org/sonar/wsclient/services/WSUtils.java
sonar-ws-client/src/main/java/org/sonar/wsclient/unmarshallers/TimeMachineUnmarshaller.java
sonar-ws-client/src/main/java/org/sonar/wsclient/unmarshallers/Unmarshallers.java
sonar-ws-client/src/test/java/org/sonar/wsclient/services/TimeMachineDataTest.java [deleted file]
sonar-ws-client/src/test/java/org/sonar/wsclient/services/TimeMachineQueryTest.java [new file with mode: 0644]
sonar-ws-client/src/test/java/org/sonar/wsclient/unmarshallers/TimeMachineUnmarshallerTest.java
sonar-ws-client/src/test/resources/timemachine/many.json [deleted file]
sonar-ws-client/src/test/resources/timemachine/null-values.json [new file with mode: 0644]
sonar-ws-client/src/test/resources/timemachine/timemachine.json
sonar-ws-client/src/test/resources/timemachine/typed-values.json [new file with mode: 0644]
tests/integration/tests/src/test/java/org/sonar/tests/integration/ViolationsTimeMachineIT.java

index f4c1cea3f3e9114f79f9498a5deeae9a50bf3d89..1fddae26e76db373b132d26d1ccb0ded943dd54d 100644 (file)
@@ -2,27 +2,43 @@ package org.sonar.wsclient.gwt.unmarshallers;
 
 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 org.sonar.wsclient.services.TimeMachine;
+import org.sonar.wsclient.services.TimeMachineCell;
+import org.sonar.wsclient.services.TimeMachineColumn;
 
-import java.util.ArrayList;
-import java.util.List;
+public class TimeMachineUnmarshaller extends AbstractUnmarshaller<TimeMachine> {
 
-public class TimeMachineUnmarshaller extends AbstractUnmarshaller<TimeMachineData> {
+  protected TimeMachine parse(JSONObject json) {
+    JSONArray cols = json.get("cols").isArray();
+    JSONArray cells = json.get("cells").isArray();
+    return new TimeMachine(toColumns(cols), toCells(cells));
+  }
+
+  private TimeMachineColumn[] toColumns(JSONArray cols) {
+    int size = cols.size();
+    TimeMachineColumn[] result = new TimeMachineColumn[size];
+    for (int index = 0; index < JsonUtils.getArraySize(cols); index++) {
+      JSONObject elem = JsonUtils.getArray(cols, index);
+      result[index] = new TimeMachineColumn(index, JsonUtils.getString(elem, "metric"), null, null);
+    }
+    return result;
+  }
 
-  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));
+  private TimeMachineCell[] toCells(JSONArray cells) {
+    int size = JsonUtils.getArraySize(cells);
+    TimeMachineCell[] result = new TimeMachineCell[size];
+    for (int i = 0; i < size; i++) {
+      JSONObject cellJson = JsonUtils.getArray(cells, i);
+      JSONArray valuesJson = cellJson.get("v").isArray();
+      Object[] resultValues = new Object[valuesJson.size()];
+      for (int indexValue = 0; indexValue < JsonUtils.getArraySize(valuesJson); indexValue++) {
+        Object value = valuesJson.get(indexValue);
+        resultValues[indexValue] = value;
+      }
+      result[i] = new TimeMachineCell(JsonUtils.getDate(cellJson, "d"), resultValues);
     }
-    return new TimeMachineData()
-        .setDate(JsonUtils.parseDateTime(dateTimeStr))
-        .setValues(measures);
+    return result;
   }
 
 }
index 637af109357283642a9294258639d3615d16ec01..d28a90ccee85e2a21e3cf26cb93f038df146c808 100644 (file)
@@ -46,7 +46,7 @@ public final class Unmarshallers {
     // FavouriteUnmarshaller
     // PluginUnmarshaller
     // RuleUnmarshaller
-    unmarshallers.put(TimeMachineData.class, new TimeMachineUnmarshaller());
+    unmarshallers.put(TimeMachine.class, new TimeMachineUnmarshaller());
   }
 
   public static Unmarshaller forModel(Class modelClass) {
index 746c27cadf4c57cb6f2bf5d2b8312badebac3c89..d795f6bfddd575f9dc6e5332cdaf5d23ac55606e 100644 (file)
@@ -22,6 +22,27 @@ require 'fastercsv'
 class Api::TimemachineController < Api::ApiController
   MAX_IN_ELEMENTS=990
 
+  class MetadataId < Struct.new(:metric_id, :characteristic_id)
+  end
+
+  class Metadata < Struct.new(:metric, :characteristic)
+    def to_id
+      @id ||=
+        begin
+          MetadataId.new(self.metric.id, self.characteristic ? self.characteristic.id : nil)
+        end
+    end
+
+    def to_s
+      label=self.metric.key
+      if self.characteristic
+        label+="(#{characteristic.key})"
+      end
+      label
+    end
+  end
+
+
   #
   # GET /api/timemachine
   #
@@ -45,71 +66,70 @@ class Api::TimemachineController < Api::ApiController
       end
 
       # ---------- 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_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
+      load_metadata()
+
+      @sids=[]
+      @dates_by_sid={}
+      @measures_by_sid={}
+
+      unless @metrics.empty?
+        sql_conditions = ['snapshots.project_id=:rid AND snapshots.status=:status AND project_measures.rules_category_id IS NULL AND project_measures.rule_id IS NULL AND project_measures.rule_priority IS NULL']
+        sql_values = {:rid => @resource.id, :status => Snapshot::STATUS_PROCESSED}
+
+        if params[:fromDateTime]
+          from = parse_datetime(params[:fromDateTime])
+          if from
+            sql_conditions << 'snapshots.created_at>=:from'
+            sql_values[:from] = from
+          end
+        end
+
+        if params[:toDateTime]
+          to = parse_datetime(params[:toDateTime])
+          if to
+            sql_conditions << 'snapshots.created_at<=:to'
+            sql_values[:to] = to
+          end
+        end
+
+        sql_conditions << 'project_measures.metric_id IN (:metrics)'
+        sql_values[:metrics] = @metrics.select{|m| m.id}
+
+        if @characteristics.empty?
+          sql_conditions<<'project_measures.characteristic_id IS NULL'
+        else
+          sql_conditions<<'project_measures.characteristic_id IN (:characteristics)'
+          sql_values[:characteristics]=@characteristics.select{|c| c.id}
+        end
+
+        measures = ProjectMeasure.find(:all,
+          :joins => :snapshot,
+          :select => 'project_measures.id,project_measures.value,project_measures.text_value,project_measures.metric_id,project_measures.snapshot_id,project_measures.characteristic_id,snapshots.created_at',
+          :conditions => [sql_conditions.join(' AND '), sql_values],
+          :order => 'snapshots.created_at')
+
+        # ---------- PROCESS RESPONSE
+        # sorted array of unique snapshot ids
+
+        # workaround to convert snapshot date from string to datetime
+        date_column=Snapshot.connection.columns('snapshots')[1]
+
+        measures.each do |m|
+          @sids<<m.snapshot_id
+          @dates_by_sid[m.snapshot_id]=date_column.type_cast(m.attributes['created_at'])
+          @measures_by_sid[m.snapshot_id]||={}
+          @measures_by_sid[m.snapshot_id][MetadataId.new(m.metric_id, m.characteristic_id)]=m
+        end
+        @sids.uniq!
       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(to_json(objects)) }
+        format.json { render :json => jsonp(to_json) }
         format.csv  {
-          send_data(to_csv(objects),
+          send_data(to_csv,
             :type => 'text/csv; charset=utf-8; header=present',
-            :disposition => 'attachment; filename=measures.csv')
+            :disposition => 'attachment; filename=timemachine.csv')
         }
         format.text { render :text => text_not_supported }
       end
@@ -120,77 +140,94 @@ class Api::TimemachineController < Api::ApiController
 
   private
 
-  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)
+  def load_metrics
+    if params[:metrics]
+      @metrics = Metric.by_keys(params[:metrics].split(','))
+    else
+      @metrics=[]
     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]
+  def load_characteristics
+    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(',')])
+    else
+      @characteristics=[]
     end
-    json = { format_datetime(snapshot.created_at) => values }
-    json
   end
 
-  def to_csv(objects)
-    snapshots = objects[:snapshots]
-    measures_by_sid = objects[:measures_by_sid]
-    metric_keys = objects[:metric_keys]
+  def load_metadata
+    load_metrics
+    load_characteristics
 
-    FasterCSV.generate do |csv|
-      header = ['date']
-      header.concat(metric_keys)
-      csv << header
-      snapshots.each do |snapshot|
-        snapshot_to_csv(csv, snapshot, measures_by_sid[snapshot.id], metric_keys)
+    @metadata=[]
+    @metrics.each do |metric|
+      if @characteristics.empty?
+        @metadata<<Metadata.new(metric, nil)
+      else
+        @characteristics.each do |characteristic|
+          @metadata<<Metadata.new(metric, characteristic)
+        end
       end
     end
+    @metadata
   end
 
-  def snapshot_to_csv(csv, snapshot, measures, metric_keys)
-    values_by_key = {}
-    measures.each do |measure|
-      values_by_key[measure.metric.name] = measure.value.to_f if measure.value
+  def to_json
+    cols=[]
+    cells=[]
+    result=[{:cols => cols, :cells => cells}]
+
+    @metadata.each do |metadata|
+      col={:metric => metadata.metric.key}
+      if metadata.characteristic
+        col[:model]=characteristic.model.name
+        col[:characteristic]=characteristic.kee
+      end
+      cols<<col
     end
-    values = []
-    values << format_datetime(snapshot.created_at)
-    metric_keys.each do |metric|
-      values << values_by_key[metric]
+
+    @sids.each do |snapshot_id|
+      cell={:d => format_datetime(@dates_by_sid[snapshot_id])}
+      cell_values=[]
+      cell[:v]=cell_values
+
+      @metadata.each do |metadata|
+        measure=@measures_by_sid[snapshot_id][metadata.to_id]
+        if measure
+          cell_values<<measure.typed_value
+        else
+          cell_values<<nil
+        end
+      end
+      cells<<cell
     end
-    csv << values
+    result
   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
+  def to_csv
+    FasterCSV.generate do |csv|
+      header = ['date']
+      @metadata.each do |metadata|
+        header<<metadata.to_s
+      end
+      csv << header
+      @sids.each do |snapshot_id|
+        row=[format_datetime(@dates_by_sid[snapshot_id])]
+        @metadata.each do |metadata|
+          measure=@measures_by_sid[snapshot_id][metadata.to_id]
+          if measure
+            row<<measure.typed_value
+          else
+            row<<nil
+          end
+        end
+        csv<<row
       end
-    else
-      measures_conditions<<'project_measures.characteristic_id IS NULL'
     end
   end
 
-end
+end
\ No newline at end of file
index 0089401ff7cb986ea756c07558ef3197200fc933..4f400045071b5a479e4ad91488a941da47cd2985 100644 (file)
@@ -174,6 +174,16 @@ class ProjectMeasure < ActiveRecord::Base
     measures
   end
 
+  def typed_value
+    if text_value
+      text_value
+    elsif value
+      value.to_f
+    else
+      nil
+    end
+  end
+
   # return reviews from the snapshot and also reviews created after the snapshot
   def self.find_reviews_for_last_snapshot(snapshot)
     ProjectMeasure.find(:all, :include => [:async_measure_snapshots, :measure_data],
diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachine.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachine.java
new file mode 100644 (file)
index 0000000..fc85900
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2009 SonarSource SA
+ * 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.wsclient.services;
+
+/**
+ * Past values of a given resource
+ *
+ * @since 2.5
+ */
+public class TimeMachine extends Model {
+
+  private TimeMachineColumn[] columns;
+  private TimeMachineCell[] cells;
+
+  public TimeMachine(TimeMachineColumn[] columns, TimeMachineCell[] cells) {
+    this.columns = columns;
+    this.cells = cells;
+  }
+
+  public TimeMachineColumn[] getColumns() {
+    return columns;
+  }
+
+  public TimeMachineCell[] getCells() {
+    return cells;
+  }
+
+  public TimeMachineColumn getColumn(String metricKey) {
+    for (TimeMachineColumn column : columns) {
+      if (metricKey.equals(column.getMetricKey()) && column.getCharacteristicKey()==null) {
+        return column;
+      }
+    }
+    return null;
+  }
+
+  public int getColumnIndex(String metricKey) {
+    TimeMachineColumn col = getColumn(metricKey);
+    return col!=null ? col.getIndex() : -1;
+  }
+}
diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineCell.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineCell.java
new file mode 100644 (file)
index 0000000..3f3beb1
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2009 SonarSource SA
+ * 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.wsclient.services;
+
+import java.util.Date;
+
+/**
+ * @since 2.5
+ */
+public class TimeMachineCell {
+
+  private Date date;
+  public Object[] values;
+
+  public TimeMachineCell(Date date, Object[] values) {
+    this.date = date;
+    this.values = values;
+  }
+
+  public Date getDate() {
+    return date;
+  }
+
+  public Object[] getValues() {
+    return values;
+  }
+}
diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineColumn.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/services/TimeMachineColumn.java
new file mode 100644 (file)
index 0000000..1d9bd00
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2009 SonarSource SA
+ * 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.wsclient.services;
+
+/**
+ * @since 2.5
+ */
+public class TimeMachineColumn {
+
+  private int index;
+  private String metricKey;
+  private String modelName;
+  private String characteristicKey;
+
+  public TimeMachineColumn(int index, String metricKey, String modelName, String characteristicKey) {
+    this.index = index;
+    this.metricKey = metricKey;
+    this.modelName = modelName;
+    this.characteristicKey = characteristicKey;
+  }
+
+  public String getMetricKey() {
+    return metricKey;
+  }
+
+  public String getModelName() {
+    return modelName;
+  }
+
+  public String getCharacteristicKey() {
+    return characteristicKey;
+  }
+
+  public int getIndex() {
+    return index;
+  }
+}
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
deleted file mode 100644 (file)
index 23d3bfc..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Sonar, open source software quality management tool.
- * Copyright (C) 2009 SonarSource SA
- * 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.wsclient.services;
-
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-
-/**
- * @since 2.5
- */
-public class TimeMachineData extends Model {
-  private Date date;
-
-  /**
-   * We use strings here in order to support measures with string value.
-   */
-  private List<String> values = new ArrayList<String>();
-
-  public Date getDate() {
-    return date;
-  }
-
-  public TimeMachineData setDate(Date date) {
-    this.date = date;
-    return this;
-  }
-
-  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;
-    }
-  }
-
-}
index 285f977715a0603471af73718d37edd36ec60806..04fa7c4ea8f0d74b843f19534308217048247b42 100644 (file)
@@ -24,7 +24,7 @@ import java.util.Date;
 /**
  * @since 2.5
  */
-public class TimeMachineQuery extends Query<TimeMachineData> {
+public class TimeMachineQuery extends Query<TimeMachine> {
 
   public static final String BASE_URL = "/api/timemachine";
 
@@ -87,18 +87,16 @@ public class TimeMachineQuery extends Query<TimeMachineData> {
   }
 
   @Override
-  public Class<TimeMachineData> getModelClass() {
-    return TimeMachineData.class;
+  public Class<TimeMachine> getModelClass() {
+    return TimeMachine.class;
   }
 
   public static TimeMachineQuery createForMetrics(String resourceKeyOrId, String... metricKeys) {
-    return new TimeMachineQuery(resourceKeyOrId)
-        .setMetrics(metricKeys);
+    return new TimeMachineQuery(resourceKeyOrId).setMetrics(metricKeys);
   }
 
-  public static TimeMachineQuery createForResource(Resource resource, String... metricKeys) {
-    return new TimeMachineQuery(resource.getId().toString())
-        .setMetrics(metricKeys);
+  public static TimeMachineQuery createForMetrics(Resource resource, String... metricKeys) {
+    return new TimeMachineQuery(resource.getId().toString()).setMetrics(metricKeys);
   }
 
 }
index 61e5940cb0fc9b5dde9e2fa7014bfcee4a235f63..ab4cd8d102876e0138e6a43ab3e6d458a59d5cfa 100644 (file)
@@ -1,3 +1,22 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2009 SonarSource SA
+ * 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.wsclient.services;
 
 import java.util.Date;
index 1289e63a5aee8f42b84a425256b19a5c78937d56..72203450dfc32729e182758845a21209c727a758 100644 (file)
@@ -1,26 +1,63 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2009 SonarSource SA
+ * 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.wsclient.unmarshallers;
 
 import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
-import org.sonar.wsclient.services.TimeMachineData;
+import org.sonar.wsclient.services.TimeMachine;
+import org.sonar.wsclient.services.TimeMachineCell;
+import org.sonar.wsclient.services.TimeMachineColumn;
 
-import java.util.ArrayList;
-import java.util.List;
+public class TimeMachineUnmarshaller extends AbstractUnmarshaller<TimeMachine> {
 
-public class TimeMachineUnmarshaller extends AbstractUnmarshaller<TimeMachineData> {
+  protected TimeMachine parse(JSONObject json) {
+    JSONArray cols = JsonUtils.getArray(json, "cols");
+    JSONArray cells = JsonUtils.getArray(json, "cells");
+    return new TimeMachine(toColumns(cols), toCells(cells));
+  }
+
+  private TimeMachineColumn[] toColumns(JSONArray cols) {
+    int size = cols.size();
+    TimeMachineColumn[] result = new TimeMachineColumn[size];
+    for (int index = 0; index < size; index++) {
+      JSONObject colJson = (JSONObject)cols.get(index);
+      result[index]=new TimeMachineColumn(index, JsonUtils.getString(colJson, "metric"), null, null);
+    }
+    return result;
+  }
+
+  private TimeMachineCell[] toCells(JSONArray cells) {
+    int size = cells.size();
+    TimeMachineCell[] result = new TimeMachineCell[size];
+    for (int i = 0; i < size; i++) {
+      JSONObject cellJson = (JSONObject)cells.get(i);
+      JSONArray valuesJson = JsonUtils.getArray(cellJson, "v");
 
-  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);
+      Object[] resultValues = new Object[valuesJson.size()];
+      for (int indexValue = 0; indexValue < valuesJson.size(); indexValue++) {
+        Object value = valuesJson.get(indexValue);
+        resultValues[indexValue]=value;
+      }
+      result[i]=new TimeMachineCell(JsonUtils.getDateTime(cellJson, "d"), resultValues);
     }
-    return new TimeMachineData()
-        .setDate(JsonUtils.parseDateTime(dateTimeStr))
-        .setValues(measures);
+    return result;
   }
 
 }
index 9f16130116b5a5245be347e2d82c3801d358a3b8..ece84d4e2b317f3322086c31db59d3306680ae3b 100644 (file)
@@ -44,7 +44,7 @@ public final class Unmarshallers {
     unmarshallers.put(Favourite.class, new FavouriteUnmarshaller());
     unmarshallers.put(Plugin.class, new PluginUnmarshaller());
     unmarshallers.put(Rule.class, new RuleUnmarshaller());
-    unmarshallers.put(TimeMachineData.class, new TimeMachineUnmarshaller());
+    unmarshallers.put(TimeMachine.class, new TimeMachineUnmarshaller());
   }
 
   public static <MODEL extends Model> Unmarshaller<MODEL> forModel(Class<MODEL> modelClass) {
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
deleted file mode 100644 (file)
index 6672a3a..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.sonar.wsclient.services;
-
-import org.junit.Test;
-
-import java.util.Arrays;
-
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.nullValue;
-import static org.junit.Assert.assertThat;
-
-public class TimeMachineDataTest {
-
-  @Test
-  public void valueAsDouble() {
-    TimeMachineData data = new TimeMachineData().setValues(Arrays.asList(null, "20.3", "hello"));
-
-    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/services/TimeMachineQueryTest.java b/sonar-ws-client/src/test/java/org/sonar/wsclient/services/TimeMachineQueryTest.java
new file mode 100644 (file)
index 0000000..86bc13d
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2009 SonarSource SA
+ * 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.wsclient.services;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.wsclient.JdkUtils;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+public class TimeMachineQueryTest {
+
+  @Before
+  public void before() {
+    WSUtils.setInstance(new JdkUtils());
+  }
+  @Test
+  public void shouldGetUrl() {
+    TimeMachineQuery query = TimeMachineQuery.createForMetrics("12345", "ncloc", "coverage");
+    assertThat(query.getUrl(), is("/api/timemachine?resource=12345&metrics=ncloc,coverage&"));
+  }
+
+  @Test
+  public void shouldSetPeriod() throws ParseException {
+    Date from = new SimpleDateFormat("yyyy-MM-dd").parse("2010-02-18");
+    Date to = new SimpleDateFormat("yyyy-MM-dd HH:mm").parse("2010-03-25 14:59");
+    TimeMachineQuery query = TimeMachineQuery.createForMetrics("12345", "ncloc").setFrom(from).setTo(to);
+    assertThat(query.getUrl(), is("/api/timemachine?resource=12345&metrics=ncloc&fromDateTime=2010-02-18T00%3A00%3A00%2B0100&toDateTime=2010-03-25T14%3A59%3A00%2B0100&"));
+  }
+}
index 908658f336bc045bbf5765a8aa77e12596d8ad36..774dd1bfd81e64431af4bd96f2bcdedacc2810ba 100644 (file)
@@ -1,37 +1,74 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2009 SonarSource SA
+ * 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.wsclient.unmarshallers;
 
 import org.junit.Test;
-import org.sonar.wsclient.services.TimeMachineData;
-
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.List;
+import org.sonar.wsclient.services.TimeMachine;
 
+import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.nullValue;
 import static org.junit.Assert.assertThat;
 
 public class TimeMachineUnmarshallerTest {
 
   @Test
-  public void toModel() throws Exception {
-    TimeMachineData data = new TimeMachineUnmarshaller().toModel(WSTestUtils.loadFile("/timemachine/timemachine.json"));
-
-    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 = data.getValues();
-    assertThat(values.size(), is(3));
-    assertThat(values.get(0), is("20.0"));
-    assertThat(values.get(1), nullValue());
-    assertThat(values.get(2), is("12.8"));
+  public void testToModel() throws Exception {
+    TimeMachine timeMachine = new TimeMachineUnmarshaller().toModel(WSTestUtils.loadFile("/timemachine/timemachine.json"));
+
+    // columns
+    assertThat(timeMachine.getColumns().length, is(2));
+    assertThat(timeMachine.getColumns()[0].getIndex(), is(0));
+    assertThat(timeMachine.getColumns()[0].getMetricKey(), is("ncloc"));
+    assertThat(timeMachine.getColumns()[1].getIndex(), is(1));
+    assertThat(timeMachine.getColumns()[1].getMetricKey(), is("coverage"));
+
+    // values sorted by date
+    assertThat(timeMachine.getCells().length, is(3)); // 3 days
+    assertThat(timeMachine.getCells()[0].getDate().getDate(), is(19));
+    assertThat(timeMachine.getCells()[1].getDate().getDate(), is(21));
+    assertThat(timeMachine.getCells()[2].getDate().getDate(), is(25));
+
+    assertThat(timeMachine.getCells()[0].getValues().length, is(2));
+    assertThat((Double) timeMachine.getCells()[0].getValues()[0], is(21.0));
+    assertThat((Double) timeMachine.getCells()[0].getValues()[1], is(80.0));
   }
 
   @Test
-  public void many() throws Exception {
-    List<TimeMachineData> data = new TimeMachineUnmarshaller().toModels(WSTestUtils.loadFile("/timemachine/many.json"));
+  public void shouldAcceptNullValues() throws Exception {
+    TimeMachine timeMachine = new TimeMachineUnmarshaller().toModel(WSTestUtils.loadFile("/timemachine/null-values.json"));
+
+    assertThat(timeMachine.getCells()[0].getValues().length, is(2));
+    assertThat(timeMachine.getCells()[0].getValues()[0], nullValue());
+    assertThat((Double) timeMachine.getCells()[0].getValues()[1], is(80.0));
 
-    assertThat(data.size(), is(3));
+    assertThat((Double) timeMachine.getCells()[1].getValues()[0], is(29.0));
+    assertThat(timeMachine.getCells()[1].getValues()[1], nullValue());
   }
 
+  @Test
+  public void shouldCastValues() throws Exception {
+    TimeMachine timeMachine = new TimeMachineUnmarshaller().toModel(WSTestUtils.loadFile("/timemachine/typed-values.json"));
+
+    assertThat(timeMachine.getCells()[0].getValues().length, is(2));
+    assertThat((String)timeMachine.getCells()[0].getValues()[0], is("Sonar way"));
+    assertThat((Double) timeMachine.getCells()[0].getValues()[1], is(80.0));
+
+  }
 }
diff --git a/sonar-ws-client/src/test/resources/timemachine/many.json b/sonar-ws-client/src/test/resources/timemachine/many.json
deleted file mode 100644 (file)
index 6ee5fa2..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-[
-  {"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/null-values.json b/sonar-ws-client/src/test/resources/timemachine/null-values.json
new file mode 100644 (file)
index 0000000..394d008
--- /dev/null
@@ -0,0 +1,26 @@
+[
+  {
+    "cols":[
+      {
+        "metric":"ncloc"
+      },
+      {
+        "metric":"coverage"
+      }
+    ],
+    "cells":[
+      {
+        "d":"2010-10-19T00:01:00+0200",
+        "v":[null,80.0]
+      },
+      {
+        "d":"2010-10-21T00:01:00+0100",
+        "v":[29.0,null]
+      },
+      {
+        "d":"2010-10-25T00:04:00+0100",
+        "v":[22.6,null]
+      }
+    ]
+  }
+]
\ No newline at end of file
index 17f064aeb0b786ca59f07631adcb3d971051b201..376770398e9fe44224a22259299302cb72573b6b 100644 (file)
@@ -1,3 +1,26 @@
 [
-  {"2010-12-04T15:59:23+0000": [20.0, null, 12.8]}
-]
+  {
+    "cols":[
+      {
+        "metric":"ncloc"
+      },
+      {
+        "metric":"coverage"
+      }
+    ],
+    "cells":[
+      {
+        "d":"2010-10-19T00:01:00+0200",
+        "v":[21.0,80.0]
+      },
+      {
+        "d":"2010-10-21T00:01:00+0100",
+        "v":[29.0,70.0]
+      },
+      {
+        "d":"2010-10-25T00:04:00+0100",
+        "v":[22.6,75.0]
+      }
+    ]
+  }
+]
\ No newline at end of file
diff --git a/sonar-ws-client/src/test/resources/timemachine/typed-values.json b/sonar-ws-client/src/test/resources/timemachine/typed-values.json
new file mode 100644 (file)
index 0000000..b33d948
--- /dev/null
@@ -0,0 +1,26 @@
+[
+  {
+    "cols":[
+      {
+        "metric":"profile"
+      },
+      {
+        "metric":"coverage"
+      }
+    ],
+    "cells":[
+      {
+        "d":"2010-10-19T00:01:00+0200",
+        "v":["Sonar way",80.0]
+      },
+      {
+        "d":"2010-10-21T00:01:00+0100",
+        "v":["Sonar way",70.0]
+      },
+      {
+        "d":"2010-10-25T00:04:00+0100",
+        "v":["Sun checks",75.0]
+      }
+    ]
+  }
+]
\ No newline at end of file
index bc616289bf1b9550139c743efccc9af9b2aa1420..3d12a1612ca0d7e6a4158e476af0b4129bc174cf 100644 (file)
@@ -6,12 +6,12 @@ import org.sonar.api.measures.CoreMetrics;
 import org.sonar.wsclient.Sonar;
 import org.sonar.wsclient.services.*;
 
-import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
 
-import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
 
 public class ViolationsTimeMachineIT {
@@ -33,24 +33,24 @@ public class ViolationsTimeMachineIT {
   }
 
   @Test
-  public void timemachine() {
+  public void testHistoryOfViolations() {
     TimeMachineQuery query = TimeMachineQuery.createForMetrics(PROJECT,
         CoreMetrics.BLOCKER_VIOLATIONS_KEY,
         CoreMetrics.CRITICAL_VIOLATIONS_KEY,
         CoreMetrics.MAJOR_VIOLATIONS_KEY,
         CoreMetrics.MINOR_VIOLATIONS_KEY,
         CoreMetrics.INFO_VIOLATIONS_KEY);
-    List<TimeMachineData> snapshots = sonar.findAll(query);
-    assertThat(snapshots.size(), is(2));
+    TimeMachine timemachine = sonar.find(query);
+    assertThat(timemachine.getCells().length, is(2));
 
-    TimeMachineData snapshot1 = snapshots.get(0);
-    TimeMachineData snapshot2 = snapshots.get(1);
+    TimeMachineCell cell1 = timemachine.getCells()[0];
+    TimeMachineCell cell2 = timemachine.getCells()[1];
 
-    assertThat(snapshot1.getDate().getMonth(), is(9));
-    assertThat(snapshot1.getValues(), is(Arrays.asList("0.0", "0.0", "3.0", "4.0", "0.0")));
+    assertThat(cell1.getDate().getMonth(), is(9));
+    assertThat(cell1.getValues(), is(new Object[]{0.0, 0.0, 3.0, 4.0, 0.0}));
 
-    assertThat(snapshot2.getDate().getMonth(), is(10));
-    assertThat(snapshot2.getValues(), is(Arrays.asList("0.0", "0.0", "5.0", "4.0", "0.0")));
+    assertThat(cell2.getDate().getMonth(), is(10));
+    assertThat(cell2.getValues(), is(new Object[]{0.0, 0.0, 5.0, 4.0, 0.0}));
   }
 
   @Test
@@ -72,34 +72,35 @@ public class ViolationsTimeMachineIT {
 
   @Test
   public void unknownMetrics() {
-    List<TimeMachineData> snapshots = sonar.findAll(TimeMachineQuery.createForMetrics(PROJECT, "notfound"));
-    assertThat(snapshots.size(), is(2));
-    for (TimeMachineData data : snapshots) {
-      assertThat(data.getValues().get(0), nullValue());
+    TimeMachine timemachine = sonar.find(TimeMachineQuery.createForMetrics(PROJECT, "notfound"));
+    assertThat(timemachine.getCells().length, is(2));
+    for (TimeMachineCell cell : timemachine.getCells()) {
+      assertThat(cell.getValues()[0], nullValue());
     }
 
-    snapshots = sonar.findAll(TimeMachineQuery.createForMetrics(PROJECT, CoreMetrics.LINES_KEY, "notfound"));
-    assertThat(snapshots.size(), is(2));
-    for (TimeMachineData data : snapshots) {
-      assertThat(data.getValues().get(1), nullValue());
+    timemachine = sonar.find(TimeMachineQuery.createForMetrics(PROJECT, CoreMetrics.LINES_KEY, "notfound"));
+    assertThat(timemachine.getCells().length, is(2));
+    for (TimeMachineCell cell : timemachine.getCells()) {
+      assertThat(cell.getValues()[0], is(Double.class));
+      assertThat(cell.getValues()[1], nullValue());
     }
 
-    snapshots = sonar.findAll(TimeMachineQuery.createForMetrics(PROJECT));
-    assertThat(snapshots.size(), is(0));
+    timemachine = sonar.find(TimeMachineQuery.createForMetrics(PROJECT));
+    assertThat(timemachine.getCells().length, is(0));
   }
 
   @Test
   public void noDataForInterval() {
     Date date = new Date();
-    List<TimeMachineData> snapshots = sonar.findAll(TimeMachineQuery.createForMetrics(PROJECT, CoreMetrics.LINES_KEY)
+    TimeMachine timemachine = sonar.find(TimeMachineQuery.createForMetrics(PROJECT, CoreMetrics.LINES_KEY)
         .setFrom(date)
         .setTo(date));
-    assertThat(snapshots.size(), is(0));
+    assertThat(timemachine.getCells().length, is(0));
   }
 
   @Test
   public void unknownResource() {
-    List<TimeMachineData> snapshots = sonar.findAll(TimeMachineQuery.createForMetrics("notfound:notfound", CoreMetrics.LINES_KEY));
-    assertThat(snapshots.size(), is(0));
+    TimeMachine timemachine = sonar.find(TimeMachineQuery.createForMetrics("notfound:notfound", CoreMetrics.LINES_KEY));
+    assertNull(timemachine);
   }
 }