]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-2218 add NewCoverageDecorator + add DateUtils to API
authorsimonbrandhof <simon.brandhof@gmail.com>
Fri, 25 Feb 2011 17:16:54 +0000 (18:16 +0100)
committersimonbrandhof <simon.brandhof@gmail.com>
Fri, 25 Feb 2011 17:16:54 +0000 (18:16 +0100)
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageDecorator.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewCoverageDecoratorTest.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java
sonar-plugin-api/src/main/java/org/sonar/api/utils/DateUtils.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/utils/KeyValueFormat.java

index 4d6063a9d7149747fc0dea84ae00b0ed6f6365af..a343985c1dead93d8d6e306c931c101f59f0c2dd 100644 (file)
@@ -237,6 +237,7 @@ public class CorePlugin implements Plugin {
     extensions.add(ViolationPersisterDecorator.class);
     extensions.add(NewViolationsDecorator.class);
     extensions.add(TimeMachineConfigurationPersister.class);
+    extensions.add(NewCoverageDecorator.class);
 
     return extensions;
   }
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageDecorator.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageDecorator.java
new file mode 100644 (file)
index 0000000..1829771
--- /dev/null
@@ -0,0 +1,210 @@
+/*
+ * 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.timemachine;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.apache.commons.lang.NumberUtils;
+import org.apache.commons.lang.ObjectUtils;
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.DependedUpon;
+import org.sonar.api.batch.DependsUpon;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.Scopes;
+import org.sonar.api.utils.KeyValueFormat;
+import org.sonar.batch.components.PastSnapshot;
+import org.sonar.batch.components.TimeMachineConfiguration;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @since 2.7
+ */
+public final class NewCoverageDecorator implements Decorator {
+
+  private List<PeriodStruct> structs;
+
+  public NewCoverageDecorator(TimeMachineConfiguration timeMachineConfiguration) {
+    structs = Lists.newArrayList();
+    for (PastSnapshot pastSnapshot : timeMachineConfiguration.getProjectPastSnapshots()) {
+      structs.add(new PeriodStruct(pastSnapshot));
+    }
+  }
+
+  NewCoverageDecorator(List<PeriodStruct> structs) {
+    this.structs = structs;
+  }
+
+
+  public boolean shouldExecuteOnProject(Project project) {
+    return project.isLatestAnalysis() && !structs.isEmpty();
+  }
+
+  private boolean shouldDecorate(Resource resource) {
+    return isFile(resource);
+  }
+
+  @DependsUpon
+  public List<Metric> dependsOnMetrics() {
+    return Arrays.asList(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, CoreMetrics.COVERAGE_LINE_HITS_DATA,
+        CoreMetrics.CONDITIONS_BY_LINE, CoreMetrics.COVERED_CONDITIONS_BY_LINE);
+  }
+
+  @DependedUpon
+  public List<Metric> generatesNewCoverageMetrics() {
+    return Arrays.asList(CoreMetrics.NEW_LINES_TO_COVER, CoreMetrics.NEW_UNCOVERED_LINES,
+        CoreMetrics.NEW_CONDITIONS_TO_COVER, CoreMetrics.NEW_UNCOVERED_CONDITIONS);
+  }
+
+  public void decorate(Resource resource, DecoratorContext context) {
+    if (shouldDecorate(resource)) {
+      doDecorate(context);
+    }
+  }
+
+  void doDecorate(DecoratorContext context) {
+    if (parse(context)) {
+      compute(context);
+    }
+  }
+
+  private boolean parse(DecoratorContext context) {
+    Measure lastCommits = context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE);
+    Measure hitsByLineMeasure = context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA);
+
+    if (lastCommits != null && lastCommits.hasData() && hitsByLineMeasure != null && hitsByLineMeasure.hasData()) {
+      Map<Integer, Date> datesByLine = KeyValueFormat.parseIntDateTime(lastCommits.getData());
+      Map<Integer, Integer> hitsByLine = parseCountByLine(hitsByLineMeasure);
+      Map<Integer, Integer> conditionsByLine = parseCountByLine(context.getMeasure(CoreMetrics.CONDITIONS_BY_LINE));
+      Map<Integer, Integer> coveredConditionsByLine = parseCountByLine(context.getMeasure(CoreMetrics.COVERED_CONDITIONS_BY_LINE));
+
+      reset();
+
+      for (Map.Entry<Integer, Integer> entry : hitsByLine.entrySet()) {
+        int lineId = entry.getKey();
+        int hits = entry.getValue();
+        int conditions = (Integer)ObjectUtils.defaultIfNull(conditionsByLine.get(lineId), 0);
+        int coveredConditions = (Integer)ObjectUtils.defaultIfNull(coveredConditionsByLine.get(lineId), 0);
+        Date date = datesByLine.get(lineId);
+        for (PeriodStruct struct : structs) {
+          struct.analyze(date, hits, conditions, coveredConditions);
+        }
+      }
+
+      return true;
+    }
+    return false;
+  }
+
+  private void reset() {
+    for (PeriodStruct struct : structs) {
+      struct.reset();
+    }
+  }
+
+  private void compute(DecoratorContext context) {
+    Measure newLines = new Measure(CoreMetrics.NEW_LINES_TO_COVER);
+    Measure newUncoveredLines = new Measure(CoreMetrics.NEW_UNCOVERED_LINES);
+    Measure newConditions = new Measure(CoreMetrics.NEW_CONDITIONS_TO_COVER);
+    Measure newUncoveredConditions = new Measure(CoreMetrics.NEW_UNCOVERED_CONDITIONS);
+
+    for (PeriodStruct struct : structs) {
+      newLines.setVariation(struct.index, (double)struct.newLines);
+      newUncoveredLines.setVariation(struct.index, (double) (struct.newLines - struct.newCoveredLines));
+      newConditions.setVariation(struct.index, (double)struct.newConditions);
+      newUncoveredConditions.setVariation(struct.index, (double)struct.newConditions-struct.newCoveredConditions);
+    }
+
+    context.saveMeasure(newLines);
+    context.saveMeasure(newUncoveredLines);
+    context.saveMeasure(newConditions);
+    context.saveMeasure(newUncoveredConditions);
+  }
+
+
+  private Map<Integer, Integer> parseCountByLine(Measure measure) {
+    if (measure != null && measure.hasData()) {
+      return KeyValueFormat.parseIntInt(measure.getData());
+    }
+    return Maps.newHashMap();
+  }
+
+
+  private boolean isFile(Resource resource) {
+    return Scopes.isFile(resource) && !Qualifiers.UNIT_TEST_FILE.equals(resource.getQualifier());
+  }
+
+  public static final class PeriodStruct {
+    int index;
+    Date date;
+    int newLines = 0, newCoveredLines = 0, newConditions = 0, newCoveredConditions = 0;
+
+    PeriodStruct(PastSnapshot pastSnapshot) {
+      this.index = pastSnapshot.getIndex();
+      this.date = pastSnapshot.getDate();
+    }
+
+    PeriodStruct(int index, Date date) {
+      this.index = index;
+      this.date = date;
+    }
+
+    void reset() {
+      newLines = 0;
+      newCoveredLines = 0;
+      newConditions = 0;
+      newCoveredConditions = 0;
+    }
+
+    void analyze(Date lineDate, int hits, int conditions, int coveredConditions) {
+      if (lineDate == null) {
+        //TODO warning
+
+      } else if (lineDate.after(date)) {
+        // TODO experiment if date comparison is faster or not
+        addLine(hits > 0);
+        addConditions(conditions, coveredConditions);
+      }
+    }
+
+    void addLine(boolean covered) {
+      newLines += 1;
+      if (covered) {
+        newCoveredLines += 1;
+      }
+    }
+
+    void addConditions(int count, int countCovered) {
+      newConditions += count;
+      if (count > 0) {
+        newCoveredConditions += countCovered;
+      }
+    }
+  }
+}
diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewCoverageDecoratorTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewCoverageDecoratorTest.java
new file mode 100644 (file)
index 0000000..e94cb5b
--- /dev/null
@@ -0,0 +1,183 @@
+/*
+ * 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.timemachine;
+
+import org.apache.commons.lang.NumberUtils;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.junit.Test;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.utils.DateUtils;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+import static org.mockito.Mockito.*;
+
+public class NewCoverageDecoratorTest {
+
+  @Test
+  public void shouldDoNothingIfNoScmData() throws ParseException {
+    DecoratorContext context = mock(DecoratorContext.class);
+    when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA))
+        .thenReturn(new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "1=10"));
+
+    NewCoverageDecorator decorator = newDecorator();
+    decorator.doDecorate(context);
+    verify(context, never()).saveMeasure((Measure) anyObject());
+  }
+
+  @Test
+  public void shouldDoNothingIfNoCoverageData() throws ParseException {
+    DecoratorContext context = mock(DecoratorContext.class);
+    when(context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE))
+        .thenReturn(new Measure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, "10=2008-05-18T00:00:00+0000"));
+
+    NewCoverageDecorator decorator = newDecorator();
+    decorator.doDecorate(context);
+
+    verify(context, never()).saveMeasure((Measure) anyObject());
+  }
+
+  @Test
+  public void shouldGetNewLines() throws ParseException {
+    DecoratorContext context = mock(DecoratorContext.class);
+    when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA)).thenReturn(
+        new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "10=2;11=3"));
+    when(context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE)).thenReturn(
+        new Measure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, "10=2007-01-15T00:00:00+0000;11=2011-01-01T00:00:00+0000"));
+
+    NewCoverageDecorator decorator = newDecorator();
+    decorator.doDecorate(context);
+
+    // line 11 has been updated after date1 (2009-12-25). This line is covered.
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 1, 1.0)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 1, 0.0)));
+
+    // no line have been updated after date3 (2011-02-18)
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 3, 0.0)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 3, 0.0)));
+
+    // no data on other periods
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 2, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 4, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 5, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 2, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 4, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 5, null)));
+  }
+
+  @Test
+  public void shouldGetNewConditions() throws ParseException {
+    DecoratorContext context = mock(DecoratorContext.class);
+    when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA)).thenReturn(
+        new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "10=2;11=3"));
+    when(context.getMeasure(CoreMetrics.CONDITIONS_BY_LINE)).thenReturn(
+        new Measure(CoreMetrics.CONDITIONS_BY_LINE, "11=4"));
+    when(context.getMeasure(CoreMetrics.COVERED_CONDITIONS_BY_LINE)).thenReturn(
+        new Measure(CoreMetrics.COVERED_CONDITIONS_BY_LINE, "11=1"));
+    when(context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE)).thenReturn(
+        new Measure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, "10=2007-01-15T00:00:00+0000;11=2011-01-01T00:00:00+0000"));
+
+    NewCoverageDecorator decorator = newDecorator();
+    decorator.doDecorate(context);
+
+    // line 11 has been updated after date1 (2009-12-25). This line has 1 covered condition amongst 4
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 1, 4.0)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 1, 3.0)));
+
+    // no line have been updated after date3 (2011-02-18)
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 3, 0.0)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 3, 0.0)));
+
+    // no data on other periods
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 2, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 4, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 5, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 2, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 4, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 5, null)));
+  }
+
+  @Test
+  public void shouldNotGetNewConditionsWhenNewLineHasNoConditions() throws ParseException {
+    DecoratorContext context = mock(DecoratorContext.class);
+    when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA)).thenReturn(
+        new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "10=2;11=3"));
+    when(context.getMeasure(CoreMetrics.CONDITIONS_BY_LINE)).thenReturn(
+        new Measure(CoreMetrics.CONDITIONS_BY_LINE, "10=1"));
+    when(context.getMeasure(CoreMetrics.COVERED_CONDITIONS_BY_LINE)).thenReturn(
+        new Measure(CoreMetrics.COVERED_CONDITIONS_BY_LINE, "10=1"));
+    when(context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE)).thenReturn(
+        new Measure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, "10=2007-01-15T00:00:00+0000;11=2011-01-01T00:00:00+0000"));
+
+    NewCoverageDecorator decorator = newDecorator();
+    decorator.doDecorate(context);
+
+    // line 11 has been updated after date1 (2009-12-25) but it has no conditions
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 1, 0.0)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 1, 0.0)));
+  }
+
+
+  static class VariationMatcher extends BaseMatcher<Measure> {
+    Metric metric;
+    int index;
+    Double variation;
+
+    VariationMatcher(Metric metric, int index, Double variation) {
+      this.metric = metric;
+      this.index = index;
+      this.variation = variation;
+    }
+
+    public boolean matches(Object o) {
+      Measure m = (Measure)o;
+      if (m.getMetric().equals(metric)) {
+        if ((variation==null && m.getVariation(index)==null) ||
+            (variation!=null && variation.equals(m.getVariation(index)))) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    public void describeTo(Description description) {
+
+    }
+  }
+
+  private NewCoverageDecorator newDecorator() throws ParseException {
+    List<NewCoverageDecorator.PeriodStruct> structs = Arrays.asList(
+        new NewCoverageDecorator.PeriodStruct(1, newDate("2009-12-25")),
+        new NewCoverageDecorator.PeriodStruct(3, newDate("2011-02-18")));
+    return new NewCoverageDecorator(structs);
+  }
+
+  private Date newDate(String s) throws ParseException {
+    return new SimpleDateFormat(DateUtils.DATE_FORMAT).parse(s);
+  }
+}
index e88512934b921932aa2f3801e0d6e47a3c4b236f..59c1f66ed9aae8f9761cb9914829bbd142ae2ba9 100644 (file)
@@ -224,6 +224,15 @@ public final class CoreMetrics {
   public static final Metric LINES_TO_COVER = new Metric(LINES_TO_COVER_KEY, "Lines to cover", "Lines to cover", Metric.ValueType.INT,
       Metric.DIRECTION_BETTER, false, DOMAIN_TESTS).setFormula(new SumChildValuesFormula(false)).setHidden(true);
 
+  public static final String NEW_LINES_TO_COVER_KEY = "new_lines_to_cover";
+  public static final Metric NEW_LINES_TO_COVER = new Metric.Builder(NEW_LINES_TO_COVER_KEY, Metric.ValueType.INT)
+      .setName("New lines to cover")
+      .setDescription("New lines to cover")
+      .setDirection(Metric.DIRECTION_BETTER)
+      .setDomain(DOMAIN_TESTS)
+      .setFormula(new SumChildValuesFormula(false))
+      .setHidden(true)
+      .create();
 
   public static final String UNCOVERED_LINES_KEY = "uncovered_lines";
   public static final Metric UNCOVERED_LINES = new Metric.Builder(UNCOVERED_LINES_KEY, Metric.ValueType.INT)
@@ -256,7 +265,15 @@ public final class CoreMetrics {
   public static final Metric CONDITIONS_TO_COVER = new Metric.Builder(CONDITIONS_TO_COVER_KEY, Metric.ValueType.INT)
       .setName("Conditions to cover")
       .setDescription("Conditions to cover")
-      .setDirection(Metric.DIRECTION_BETTER)
+      .setDomain(DOMAIN_TESTS)
+      .setFormula(new SumChildValuesFormula(false))
+      .setHidden(true)
+      .create();
+
+  public static final String NEW_CONDITIONS_TO_COVER_KEY = "new_conditions_to_cover";
+  public static final Metric NEW_CONDITIONS_TO_COVER = new Metric.Builder(NEW_CONDITIONS_TO_COVER_KEY, Metric.ValueType.INT)
+      .setName("New conditions to cover")
+      .setDescription("New conditions to cover")
       .setDomain(DOMAIN_TESTS)
       .setFormula(new SumChildValuesFormula(false))
       .setHidden(true)
@@ -565,7 +582,6 @@ public final class CoreMetrics {
       Metric.ValueType.INT, Metric.DIRECTION_BETTER, false, DOMAIN_DESIGN).setHidden(true);
 
 
-
   // Alerts
   public static final String ALERT_STATUS_KEY = "alert_status";
   public static final Metric ALERT_STATUS = new Metric.Builder(ALERT_STATUS_KEY, Metric.ValueType.LEVEL)
@@ -575,7 +591,7 @@ public final class CoreMetrics {
       .setQualitative(true)
       .setDomain(DOMAIN_GENERAL)
       .create();
-  
+
 
   /* quality profile */
   public static final String PROFILE_KEY = "profile";
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/DateUtils.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/DateUtils.java
new file mode 100644 (file)
index 0000000..510cb0c
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2011 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.api.utils;
+
+/**
+ * @since 2.7
+ */
+public interface DateUtils {
+  String DATE_FORMAT = "yyyy-MM-dd";
+  String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
+}
index 8ba231af888ce14ce35f9247afc8abbf9faa3e60..165152c353747e1dbf5ccee7d7dfe1cfa2b5826e 100644 (file)
@@ -141,10 +141,10 @@ public final class KeyValueFormat {
   }
 
   public static class DateConverter extends Converter<Date> {
-    private DateFormat dateFormat;
+    private SimpleDateFormat dateFormat;
 
     public DateConverter() {
-      this("yyyy-MM-dd");
+      this(DateUtils.DATE_FORMAT);
     }
 
     DateConverter(String format) {
@@ -161,14 +161,14 @@ public final class KeyValueFormat {
       try {
         return StringUtils.isBlank(s) ? null : dateFormat.parse(s);
       } catch (ParseException e) {
-        throw new SonarException("Not a date: " + s, e);
+        throw new SonarException("Not a date with format: " + dateFormat.toPattern(), e);
       }
     }
   }
 
   public static class DateTimeConverter extends DateConverter {
     public DateTimeConverter() {
-      super("yyyy-MM-dd'T'HH:mm:ssZ");
+      super(DateUtils.DATETIME_FORMAT);
     }
   }
 
@@ -225,6 +225,13 @@ public final class KeyValueFormat {
     return parse(data, IntegerConverter.INSTANCE, new DateConverter());
   }
 
+  /**
+   * @since 2.7
+   */
+  public static Map<Integer, Integer> parseIntInt(String data) {
+    return parse(data, IntegerConverter.INSTANCE, IntegerConverter.INSTANCE);
+  }
+
   /**
    * @since 2.7
    */