From 90a3277d3f57a4414fc6bbc48986345d56af23b6 Mon Sep 17 00:00:00 2001 From: Evgeny Mandrikov Date: Thu, 9 Feb 2012 17:23:05 +0400 Subject: [PATCH] SONAR-3209,SONAR-3210 Provide API to save and retrieve measures by line + Use this API to save LoC in Java files --- .../java/ast/visitor/FileLinesVisitor.java | 21 +-- .../ast/visitor/FileLinesVisitorTest.java | 20 +-- .../sonar/batch/DefaultFileLinesContext.java | 151 ++++++++++++++++++ .../org/sonar/batch/DefaultSensorContext.java | 9 +- .../batch/DefaultFileLinesContextTest.java | 112 +++++++++++++ .../org/sonar/api/batch/SensorContext.java | 8 + .../sonar/api/measures/FileLinesContext.java | 58 +++++++ 7 files changed, 355 insertions(+), 24 deletions(-) create mode 100644 sonar-batch/src/main/java/org/sonar/batch/DefaultFileLinesContext.java create mode 100644 sonar-batch/src/test/java/org/sonar/batch/DefaultFileLinesContextTest.java create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/measures/FileLinesContext.java diff --git a/plugins/sonar-squid-java-plugin/src/main/java/org/sonar/java/ast/visitor/FileLinesVisitor.java b/plugins/sonar-squid-java-plugin/src/main/java/org/sonar/java/ast/visitor/FileLinesVisitor.java index 31202f01d51..3c1788cba46 100644 --- a/plugins/sonar-squid-java-plugin/src/main/java/org/sonar/java/ast/visitor/FileLinesVisitor.java +++ b/plugins/sonar-squid-java-plugin/src/main/java/org/sonar/java/ast/visitor/FileLinesVisitor.java @@ -22,8 +22,7 @@ package org.sonar.java.ast.visitor; import com.puppycrawl.tools.checkstyle.api.DetailAST; import org.sonar.api.batch.SquidUtils; import org.sonar.api.measures.CoreMetrics; -import org.sonar.api.measures.Measure; -import org.sonar.api.measures.PersistenceMode; +import org.sonar.api.measures.FileLinesContext; import org.sonar.api.resources.JavaFile; import org.sonar.plugins.squid.SonarAccessor; import org.sonar.squid.api.SourceFile; @@ -31,7 +30,7 @@ import org.sonar.squid.measures.Metric; import org.sonar.squid.text.Source; /** - * Saves information about lines directly into {@link org.sonar.api.batch.SensorContext}. + * Saves information about lines directly into Sonar index by using {@link FileLinesContext}. */ public class FileLinesVisitor extends JavaAstVisitor { @@ -57,21 +56,15 @@ public class FileLinesVisitor extends JavaAstVisitor { private void processFile() { SourceFile file = (SourceFile) peekSourceCode(); + JavaFile javaFile = SquidUtils.convertJavaFileKeyFromSquidFormat(file.getKey()); + FileLinesContext measures = sonarAccessor.getSensorContext().createFileLinesContext(javaFile); + Source source = getSource(); - StringBuilder data = new StringBuilder(); for (int line = 1; line <= source.getNumberOfLines(); line++) { int linesOfCode = source.getMeasure(Metric.LINES_OF_CODE, line, line); - if (linesOfCode == 1) { - if (data.length() > 0) { - data.append(','); - } - data.append(line); - } + measures.setIntValue(CoreMetrics.NCLOC_DATA_KEY, line, linesOfCode); } - JavaFile javaFile = SquidUtils.convertJavaFileKeyFromSquidFormat(file.getKey()); - Measure measure = new Measure(CoreMetrics.NCLOC_DATA, data.toString()) - .setPersistenceMode(PersistenceMode.DATABASE); - sonarAccessor.getSensorContext().saveMeasure(javaFile, measure); + measures.save(); } } diff --git a/plugins/sonar-squid-java-plugin/src/test/java/org/sonar/java/ast/visitor/FileLinesVisitorTest.java b/plugins/sonar-squid-java-plugin/src/test/java/org/sonar/java/ast/visitor/FileLinesVisitorTest.java index 75edd60fb84..eb2d0808932 100644 --- a/plugins/sonar-squid-java-plugin/src/test/java/org/sonar/java/ast/visitor/FileLinesVisitorTest.java +++ b/plugins/sonar-squid-java-plugin/src/test/java/org/sonar/java/ast/visitor/FileLinesVisitorTest.java @@ -24,8 +24,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.sonar.api.batch.SensorContext; import org.sonar.api.measures.CoreMetrics; -import org.sonar.api.measures.Measure; -import org.sonar.api.measures.PersistenceMode; +import org.sonar.api.measures.FileLinesContext; import org.sonar.api.resources.Resource; import org.sonar.java.ast.JavaAstScanner; import org.sonar.java.ast.SquidTestUtils; @@ -41,26 +40,29 @@ public class FileLinesVisitorTest { private Squid squid; private SensorContext context; + private FileLinesContext measures; @Before public void setUp() { squid = new Squid(new JavaSquidConfiguration()); context = mock(SensorContext.class); + measures = mock(FileLinesContext.class); } @Test public void analyseTestNcloc() { + ArgumentCaptor resourceCaptor = ArgumentCaptor.forClass(Resource.class); + when(context.createFileLinesContext(resourceCaptor.capture())) + .thenReturn(measures); + squid.register(SonarAccessor.class).setSensorContext(context); squid.register(JavaAstScanner.class).scanFile(SquidTestUtils.getInputFile("/metrics/ncloc/TestNcloc.java")); - ArgumentCaptor resourceCaptor = ArgumentCaptor.forClass(Resource.class); - ArgumentCaptor measureCaptor = ArgumentCaptor.forClass(Measure.class); - verify(context, times(1)).saveMeasure(resourceCaptor.capture(), measureCaptor.capture()); assertThat(resourceCaptor.getValue().getKey(), is("[default].TestNcloc")); - Measure measure = measureCaptor.getValue(); - assertThat(measure.getMetricKey(), is(CoreMetrics.NCLOC_DATA_KEY)); - assertThat(measure.getPersistenceMode(), is(PersistenceMode.DATABASE)); - assertThat(measure.getData(), is("1,3,4,5,6,7,8,13,14,15,16,17,19,20,21,22,23,24,25,26,27,28,29,30,32,39")); + verify(measures).setIntValue(CoreMetrics.NCLOC_DATA_KEY, 1, 1); + verify(measures).setIntValue(CoreMetrics.NCLOC_DATA_KEY, 2, 0); + verify(measures).setIntValue(CoreMetrics.NCLOC_DATA_KEY, 3, 1); + verify(measures).save(); } } diff --git a/sonar-batch/src/main/java/org/sonar/batch/DefaultFileLinesContext.java b/sonar-batch/src/main/java/org/sonar/batch/DefaultFileLinesContext.java new file mode 100644 index 00000000000..c4a8e9a7579 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/DefaultFileLinesContext.java @@ -0,0 +1,151 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 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.batch; + +import com.google.common.annotations.Beta; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import org.sonar.api.batch.SonarIndex; +import org.sonar.api.measures.FileLinesContext; +import org.sonar.api.measures.Measure; +import org.sonar.api.measures.Metric; +import org.sonar.api.measures.PersistenceMode; +import org.sonar.api.resources.Resource; +import org.sonar.api.utils.KeyValueFormat; +import org.sonar.api.utils.KeyValueFormat.Converter; + +import java.util.Map; + +/** + * @since 2.14 + */ +@Beta +public class DefaultFileLinesContext implements FileLinesContext { + + private final SonarIndex index; + private final Resource resource; + + /** + * metric key -> line -> value + */ + private final Map> map = Maps.newHashMap(); + + public DefaultFileLinesContext(SonarIndex index, Resource resource) { + this.index = index; + this.resource = resource; + } + + public void setIntValue(String metricKey, int line, int value) { + Preconditions.checkNotNull(metricKey); + Preconditions.checkArgument(line > 0); + + setValue(metricKey, line, value); + } + + public Integer getIntValue(String metricKey, int line) { + Preconditions.checkNotNull(metricKey); + Preconditions.checkArgument(line > 0); + + Map lines = map.get(metricKey); + if (lines == null) { + // not in memory, so load + lines = loadData(metricKey, KeyValueFormat.newIntegerConverter()); + map.put(metricKey, lines); + } + return (Integer) lines.get(line); + } + + public void setStringValue(String metricKey, int line, String value) { + Preconditions.checkNotNull(metricKey); + Preconditions.checkArgument(line > 0); + Preconditions.checkNotNull(value); + + setValue(metricKey, line, value); + } + + public String getStringValue(String metricKey, int line) { + Preconditions.checkNotNull(metricKey); + Preconditions.checkArgument(line > 0); + + Map lines = map.get(metricKey); + if (lines == null) { + // not in memory, so load + lines = loadData(metricKey, KeyValueFormat.newStringConverter()); + map.put(metricKey, lines); + } + return (String) lines.get(line); + } + + private Map getOrCreateLines(String metricKey) { + Map lines = map.get(metricKey); + if (lines == null) { + lines = Maps.newHashMap(); + map.put(metricKey, lines); + } + return lines; + } + + private void setValue(String metricKey, int line, Object value) { + getOrCreateLines(metricKey).put(line, value); + } + + public void save() { + for (Map.Entry> entry : map.entrySet()) { + String metricKey = entry.getKey(); + Map lines = entry.getValue(); + if (shouldSave(lines)) { + String data = KeyValueFormat.format(lines); + Measure measure = new Measure(metricKey) + .setPersistenceMode(PersistenceMode.DATABASE) + .setData(data); + index.addMeasure(resource, measure); + } + } + } + + private Map loadData(String metricKey, Converter converter) { + // FIXME no way to load measure only by key + Measure measure = index.getMeasure(resource, new Metric(metricKey)); + if (measure == null || measure.getData() == null) { + // no such measure + return ImmutableMap.of(); + } + return ImmutableMap.copyOf(KeyValueFormat.parse(measure.getData(), KeyValueFormat.newIntegerConverter(), converter)); + } + + /** + * Checks that measure was not loaded. + * + * @see #loadData(String, Converter) + */ + private boolean shouldSave(Map lines) { + return !(lines instanceof ImmutableMap); + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("map", map) + .toString(); + } + +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/DefaultSensorContext.java b/sonar-batch/src/main/java/org/sonar/batch/DefaultSensorContext.java index 1a5ecd309c6..2a75b837b6d 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/DefaultSensorContext.java +++ b/sonar-batch/src/main/java/org/sonar/batch/DefaultSensorContext.java @@ -23,10 +23,13 @@ import org.sonar.api.batch.Event; import org.sonar.api.batch.SensorContext; import org.sonar.api.batch.SonarIndex; import org.sonar.api.design.Dependency; +import org.sonar.api.measures.FileLinesContext; import org.sonar.api.measures.Measure; import org.sonar.api.measures.MeasuresFilter; import org.sonar.api.measures.Metric; -import org.sonar.api.resources.*; +import org.sonar.api.resources.Project; +import org.sonar.api.resources.ProjectLink; +import org.sonar.api.resources.Resource; import org.sonar.api.rules.Violation; import java.util.Collection; @@ -120,6 +123,10 @@ public class DefaultSensorContext implements SensorContext { return index.addMeasure(resourceOrProject(resource), measure); } + public FileLinesContext createFileLinesContext(Resource resource) { + return new DefaultFileLinesContext(index, resource); + } + public void saveViolation(Violation violation, boolean force) { if (violation.getResource() == null) { violation.setResource(resourceOrProject(violation.getResource())); diff --git a/sonar-batch/src/test/java/org/sonar/batch/DefaultFileLinesContextTest.java b/sonar-batch/src/test/java/org/sonar/batch/DefaultFileLinesContextTest.java new file mode 100644 index 00000000000..2cf15e2bd68 --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/DefaultFileLinesContextTest.java @@ -0,0 +1,112 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 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.batch; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.sonar.api.batch.SonarIndex; +import org.sonar.api.measures.Measure; +import org.sonar.api.measures.Metric; +import org.sonar.api.measures.PersistenceMode; +import org.sonar.api.resources.Resource; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.*; + +public class DefaultFileLinesContextTest { + + private SonarIndex index; + private Resource resource; + private DefaultFileLinesContext fileLineMeasures; + + @Before + public void setUp() { + index = mock(SonarIndex.class); + resource = mock(Resource.class); + fileLineMeasures = new DefaultFileLinesContext(index, resource); + } + + @Test + public void shouldSave() { + fileLineMeasures.setIntValue("hits", 1, 2); + fileLineMeasures.setIntValue("hits", 3, 4); + fileLineMeasures.save(); + + ArgumentCaptor measureCaptor = ArgumentCaptor.forClass(Measure.class); + verify(index).addMeasure(Mockito.eq(resource), measureCaptor.capture()); + Measure measure = measureCaptor.getValue(); + assertThat(measure.getMetricKey(), is("hits")); + assertThat(measure.getPersistenceMode(), is(PersistenceMode.DATABASE)); + assertThat(measure.getData(), is("1=2;3=4")); + } + + @Test + public void shouldSaveSeveral() { + fileLineMeasures.setIntValue("hits", 1, 2); + fileLineMeasures.setIntValue("hits", 3, 4); + fileLineMeasures.setStringValue("author", 1, "simon"); + fileLineMeasures.setStringValue("author", 3, "evgeny"); + fileLineMeasures.save(); + + verify(index, times(2)).addMeasure(Mockito.eq(resource), Mockito.any(Measure.class)); + } + + @Test + public void shouldLoadIntValues() { + when(index.getMeasure(Mockito.any(Resource.class), Mockito.any(Metric.class))) + .thenReturn(new Measure("hits").setData("1=2;3=4")); + + assertThat(fileLineMeasures.getIntValue("hits", 1), is(2)); + assertThat(fileLineMeasures.getIntValue("hits", 3), is(4)); + assertThat("no measure on line", fileLineMeasures.getIntValue("hits", 5), nullValue()); + } + + @Test + public void shouldLoadStringValues() { + when(index.getMeasure(Mockito.any(Resource.class), Mockito.any(Metric.class))) + .thenReturn(new Measure("author").setData("1=simon;3=evgeny")); + + assertThat(fileLineMeasures.getStringValue("author", 1), is("simon")); + assertThat(fileLineMeasures.getStringValue("author", 3), is("evgeny")); + assertThat("no measure on line", fileLineMeasures.getStringValue("author", 5), nullValue()); + } + + @Test + public void shouldNotSaveAfterLoad() { + when(index.getMeasure(Mockito.any(Resource.class), Mockito.any(Metric.class))) + .thenReturn(new Measure("author").setData("1=simon;3=evgeny")); + + fileLineMeasures.getStringValue("author", 1); + fileLineMeasures.save(); + + verify(index, never()).addMeasure(Mockito.eq(resource), Mockito.any(Measure.class)); + } + + @Test + public void shouldNotFailIfNoMeasureInIndex() { + assertThat(fileLineMeasures.getIntValue("hits", 1), nullValue()); + assertThat(fileLineMeasures.getStringValue("author", 1), nullValue()); + } + +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/SensorContext.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/SensorContext.java index fc9189410c2..1cc5b291403 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/SensorContext.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/SensorContext.java @@ -19,7 +19,9 @@ */ package org.sonar.api.batch; +import com.google.common.annotations.Beta; import org.sonar.api.design.Dependency; +import org.sonar.api.measures.FileLinesContext; import org.sonar.api.measures.Measure; import org.sonar.api.measures.MeasuresFilter; import org.sonar.api.measures.Metric; @@ -150,6 +152,12 @@ public interface SensorContext { */ Measure saveMeasure(Resource resource, Measure measure); + /** + * @since 2.14 + */ + @Beta + FileLinesContext createFileLinesContext(Resource resource); + // ----------- RULE VIOLATIONS -------------- /** diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/measures/FileLinesContext.java b/sonar-plugin-api/src/main/java/org/sonar/api/measures/FileLinesContext.java new file mode 100644 index 00000000000..f2fe4dcaf18 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/measures/FileLinesContext.java @@ -0,0 +1,58 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 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.measures; + +import com.google.common.annotations.Beta; + +/** + * Provides access to measures for the lines of file. + * Examples: + *
    + *
  • line 1 is a line of code
  • + *
  • line 2 contains comment
  • + *
  • line 3 contains 2 branches
  • + *
  • author of line 4 is Simon
  • + *
+ * Numbering of lines starts from 1. + * + *

This interface is not intended to be implemented by clients.

+ * + * @since 2.14 + */ +@Beta +public interface FileLinesContext { + + void setIntValue(String metricKey, int line, int value); + + /** + * @return value, or null if no such metric for given line + */ + Integer getIntValue(String metricKey, int line); + + void setStringValue(String metricKey, int line, String value); + + /** + * @return value, or null if no such metric for given line + */ + String getStringValue(String metricKey, int line); + + void save(); + +} -- 2.39.5