]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9198 Support multi-valued properties containing comma
authorJulien HENRY <julien.henry@sonarsource.com>
Fri, 30 Jun 2017 12:51:35 +0000 (14:51 +0200)
committerJulien HENRY <julien.henry@sonarsource.com>
Tue, 4 Jul 2017 21:47:46 +0000 (23:47 +0200)
pom.xml
sonar-scanner-engine/pom.xml
sonar-scanner-engine/src/main/java/org/sonar/scanner/config/CsvParser.java [deleted file]
sonar-scanner-engine/src/main/java/org/sonar/scanner/config/DefaultConfiguration.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/settings/DefaultSettingsLoader.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/ModuleFileSystemInitializer.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/config/DefaultConfigurationTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/settings/DefaultSettingsLoaderTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/filesystem/ModuleFileSystemInitializerTest.java

diff --git a/pom.xml b/pom.xml
index 14228506e9cbc5c51f19648d55a5e58eaedb0f73..36be56adfa3af0392f9c1c0c626fac9f6ce5ff37 100644 (file)
--- a/pom.xml
+++ b/pom.xml
       <dependency>
         <groupId>com.google.code.findbugs</groupId>
         <artifactId>jsr305</artifactId>
-        <version>3.0.0</version>
+        <version>3.0.2</version>
       </dependency>
       <dependency>
         <groupId>commons-dbutils</groupId>
       <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-csv</artifactId>
-        <version>1.0</version>
+        <version>1.4</version>
       </dependency>
       <dependency>
         <groupId>commons-codec</groupId>
index e3b4186507e2bee5a2a2fd483ff949ad453f12e4..5b4192b82f3338282858d24d17fd3bbe49e5107d 100644 (file)
       <groupId>com.google.code.gson</groupId>
       <artifactId>gson</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-csv</artifactId>
+    </dependency>
     <!-- For HTML Report -->
     <dependency>
       <groupId>org.freemarker</groupId>
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/config/CsvParser.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/config/CsvParser.java
deleted file mode 100644 (file)
index efdfc5b..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program 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.
- *
- * This program 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 this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.scanner.config;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-
-public class CsvParser {
-
-  public static String[] parseLine(String line) {
-    List<String> result = new ArrayList<>();
-
-    AtomicInteger i = new AtomicInteger(0);
-    while (true) {
-      String cell = parseNextCell(line, i);
-      if (cell == null)
-        break;
-      result.add(cell);
-    }
-
-    return result.toArray(new String[0]);
-  }
-
-  // returns iterator after delimiter or after end of string
-  private static String parseNextCell(String line, AtomicInteger i) {
-    if (i.get() >= line.length())
-      return null;
-
-    if (line.charAt(i.get()) != '"')
-      return parseNotEscapedCell(line, i);
-    else
-      return parseEscapedCell(line, i);
-  }
-
-  // returns iterator after delimiter or after end of string
-  private static String parseNotEscapedCell(String line, AtomicInteger i) {
-    StringBuilder sb = new StringBuilder();
-    while (true) {
-      if (i.get() >= line.length()) {
-        // return iterator after end of string
-        break;
-      }
-      if (line.charAt(i.get()) == ',') {
-        // return iterator after delimiter
-        i.incrementAndGet();
-        break;
-      }
-      sb.append(line.charAt(i.get()));
-      i.incrementAndGet();
-    }
-    return sb.toString();
-  }
-
-  // returns iterator after delimiter or after end of string
-  private static String parseEscapedCell(String line, AtomicInteger i) {
-    i.incrementAndGet(); // omit first character (quotation mark)
-    StringBuilder sb = new StringBuilder();
-    while (true) {
-      if (i.get() >= line.length()) {
-        break;
-      }
-      if (line.charAt(i.get()) == '"') {
-        i.incrementAndGet(); // we're more interested in the next character
-        if (i.get() >= line.length()) {
-          // quotation mark was closing cell
-          // return iterator after end of string
-          break;
-        }
-        if (line.charAt(i.get()) == ',') {
-          // quotation mark was closing cell
-          // return iterator after delimiter
-          i.incrementAndGet();
-          break;
-        }
-        if (line.charAt(i.get()) == '"') {
-          // it was doubled (escaped) quotation mark
-          // do nothing -- we've already skipped first quotation mark
-        }
-
-      }
-      sb.append(line.charAt(i.get()));
-      i.incrementAndGet();
-    }
-
-    return sb.toString();
-  }
-}
index 35d9ac9b6835e4443454d5d64d431d30e5e41f2c..a58caf232de08b815bdf9cecf64996029dd1fb08 100644 (file)
  */
 package org.sonar.scanner.config;
 
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import javax.annotation.concurrent.Immutable;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVParser;
+import org.apache.commons.csv.CSVRecord;
 import org.apache.commons.lang.ArrayUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.sonar.api.batch.AnalysisMode;
 import org.sonar.api.config.Configuration;
 import org.sonar.api.config.Encryption;
 import org.sonar.api.config.PropertyDefinition;
 import org.sonar.api.config.PropertyDefinitions;
 import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
 
 import static java.util.Objects.requireNonNull;
 import static org.apache.commons.lang.StringUtils.trim;
@@ -40,7 +47,7 @@ import static org.apache.commons.lang.StringUtils.trim;
 @Immutable
 public abstract class DefaultConfiguration implements Configuration {
 
-  private static final Logger LOG = LoggerFactory.getLogger(DefaultConfiguration.class);
+  private static final Logger LOG = Loggers.get(DefaultConfiguration.class);
 
   private final PropertyDefinitions definitions;
   private final Encryption encryption;
@@ -83,7 +90,7 @@ public abstract class DefaultConfiguration implements Configuration {
     String effectiveKey = definitions.validKey(key);
     PropertyDefinition def = definitions.get(effectiveKey);
     if (def != null && def.multiValues()) {
-      LOG.warn("Access to the multi-valued property '{}' should be made using 'getStringArray' method. The SonarQube plugin using this property should be updated.");
+      LOG.warn("Access to the multi-valued property '{}' should be made using 'getStringArray' method. The SonarQube plugin using this property should be updated.", key);
     }
     return getInternal(effectiveKey);
   }
@@ -93,15 +100,32 @@ public abstract class DefaultConfiguration implements Configuration {
     String effectiveKey = definitions.validKey(key);
     PropertyDefinition def = definitions.get(effectiveKey);
     if (def != null && !def.multiValues()) {
-      LOG.warn("Property '{}' is not declared as multi-valued but was read using 'getStringArray' method. The SonarQube plugin declaring this property should be updated.");
+      LOG.warn("Property '{}' is not declared as multi-valued but was read using 'getStringArray' method. The SonarQube plugin declaring this property should be updated.", key);
     }
-    Optional<String> value = getInternal(key);
+    Optional<String> value = getInternal(effectiveKey);
     if (value.isPresent()) {
-      return CsvParser.parseLine(value.get());
+      return parseAsCsv(effectiveKey, value.get());
     }
     return ArrayUtils.EMPTY_STRING_ARRAY;
   }
 
+  public static String[] parseAsCsv(String key, String value) {
+    List<String> result = new ArrayList<>();
+    try (CSVParser csvParser = CSVFormat.RFC4180
+      .withHeader((String) null)
+      .withIgnoreSurroundingSpaces(true)
+      .parse(new StringReader(value))) {
+      List<CSVRecord> records = csvParser.getRecords();
+      if (records.isEmpty()) {
+        return ArrayUtils.EMPTY_STRING_ARRAY;
+      }
+      records.get(0).iterator().forEachRemaining(result::add);
+      return result.toArray(new String[result.size()]);
+    } catch (IOException e) {
+      throw new IllegalStateException("Property: '" + key + "' doesn't contain a valid CSV value: '" + value + "'", e);
+    }
+  }
+
   private Optional<String> getInternal(String key) {
     if (mode.isIssues() && key.endsWith(".secured") && !key.contains(".license")) {
       throw MessageException.of("Access to the secured property '" + key
index 00f0f8524902d27044598613aa81b15a8a378f1c..928d9c8ba1fff3fb7362690a2bd1ae971df7fe58 100644 (file)
 package org.sonar.scanner.repository.settings;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Joiner;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 import javax.annotation.Nullable;
+import org.apache.commons.lang.StringEscapeUtils;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.api.utils.log.Profiler;
@@ -76,7 +77,7 @@ public class DefaultSettingsLoader implements SettingsLoader {
             result.put(s.getKey(), s.getValue());
             break;
           case VALUES:
-            result.put(s.getKey(), Joiner.on(',').join(s.getValues().getValuesList()));
+            result.put(s.getKey(), s.getValues().getValuesList().stream().map(StringEscapeUtils::escapeCsv).collect(Collectors.joining(",")));
             break;
           case FIELDVALUES:
             convertPropertySetToProps(result, s);
@@ -99,6 +100,6 @@ public class DefaultSettingsLoader implements SettingsLoader {
       ids.add(String.valueOf(id));
       id++;
     }
-    result.put(s.getKey(), Joiner.on(',').join(ids));
+    result.put(s.getKey(), ids.stream().collect(Collectors.joining(",")));
   }
 }
index 6ead3ed7d39d1ddf38892bbcc446ea20d7eb4c07..2c5c9e54e8dfd68fc0e4ce12f217dcabf8f0a4f5 100644 (file)
@@ -28,6 +28,8 @@ import org.sonar.api.batch.bootstrap.ProjectDefinition;
 import org.sonar.api.scan.filesystem.PathResolver;
 import org.sonar.api.utils.TempFolder;
 
+import static org.sonar.scanner.config.DefaultConfiguration.parseAsCsv;
+
 /**
  * @since 3.5
  */
@@ -60,19 +62,25 @@ public class ModuleFileSystemInitializer {
   }
 
   private void initSources(ProjectDefinition module, PathResolver pathResolver) {
-    for (String sourcePath : module.sources()) {
-      File dirOrFile = pathResolver.relativeFile(module.getBaseDir(), sourcePath);
-      if (dirOrFile.exists()) {
-        sourceDirsOrFiles.add(dirOrFile);
+    String srcPropValue = module.properties().get(ProjectDefinition.SOURCES_PROPERTY);
+    if (srcPropValue != null) {
+      for (String sourcePath : parseAsCsv(ProjectDefinition.SOURCES_PROPERTY, srcPropValue)) {
+        File dirOrFile = pathResolver.relativeFile(module.getBaseDir(), sourcePath);
+        if (dirOrFile.exists()) {
+          sourceDirsOrFiles.add(dirOrFile);
+        }
       }
     }
   }
 
   private void initTests(ProjectDefinition module, PathResolver pathResolver) {
-    for (String testPath : module.tests()) {
-      File dirOrFile = pathResolver.relativeFile(module.getBaseDir(), testPath);
-      if (dirOrFile.exists()) {
-        testDirsOrFiles.add(dirOrFile);
+    String testPropValue = module.properties().get(ProjectDefinition.TESTS_PROPERTY);
+    if (testPropValue != null) {
+      for (String testPath : parseAsCsv(ProjectDefinition.TESTS_PROPERTY, testPropValue)) {
+        File dirOrFile = pathResolver.relativeFile(module.getBaseDir(), testPath);
+        if (dirOrFile.exists()) {
+          testDirsOrFiles.add(dirOrFile);
+        }
       }
     }
   }
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/config/DefaultConfigurationTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/config/DefaultConfigurationTest.java
new file mode 100644 (file)
index 0000000..2f65ebc
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.scanner.config;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Arrays;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.batch.AnalysisMode;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.config.Encryption;
+import org.sonar.api.config.PropertyDefinition;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.Mockito.mock;
+
+public class DefaultConfigurationTest {
+
+  @Rule
+  public LogTester logTester = new LogTester();
+
+  @Test
+  public void accessingMultiValuedPropertiesShouldBeConsistentWithDeclaration() {
+    Configuration config = new DefaultConfiguration(new PropertyDefinitions(Arrays.asList(
+      PropertyDefinition.builder("single").multiValues(false).build(),
+      PropertyDefinition.builder("multiA").multiValues(true).build())), new Encryption(null),
+      mock(AnalysisMode.class),
+      ImmutableMap.of("single", "foo", "multiA", "a,b", "notDeclared", "c,d")) {
+    };
+
+    assertThat(config.get("multiA")).hasValue("a,b");
+    assertThat(logTester.logs(LoggerLevel.WARN))
+      .contains("Access to the multi-valued property 'multiA' should be made using 'getStringArray' method. The SonarQube plugin using this property should be updated.");
+
+    logTester.clear();
+
+    assertThat(config.getStringArray("single")).containsExactly("foo");
+    assertThat(logTester.logs(LoggerLevel.WARN))
+      .contains("Property 'single' is not declared as multi-valued but was read using 'getStringArray' method. The SonarQube plugin declaring this property should be updated.");
+
+    logTester.clear();
+
+    assertThat(config.get("notDeclared")).hasValue("c,d");
+    assertThat(config.getStringArray("notDeclared")).containsExactly("c", "d");
+    assertThat(logTester.logs(LoggerLevel.WARN)).isEmpty();
+  }
+
+  @Test
+  public void getDefaultValues() {
+    Configuration config = new DefaultConfiguration(new PropertyDefinitions(Arrays.asList(
+      PropertyDefinition.builder("single").multiValues(false).defaultValue("default").build(),
+      PropertyDefinition.builder("multiA").multiValues(true).defaultValue("foo,bar").build())), new Encryption(null),
+      mock(AnalysisMode.class),
+      ImmutableMap.of()) {
+    };
+
+    assertThat(config.get("multiA")).hasValue("foo,bar");
+    assertThat(config.getStringArray("multiA")).containsExactly("foo", "bar");
+    assertThat(config.get("single")).hasValue("default");
+    assertThat(config.getStringArray("single")).containsExactly("default");
+  }
+
+  @Test
+  public void testParsingMultiValues() {
+    assertThat(getStringArray("")).isEmpty();
+    assertThat(getStringArray(",")).containsExactly("", "");
+    assertThat(getStringArray(",,")).containsExactly("", "", "");
+    assertThat(getStringArray("a")).containsExactly("a");
+    assertThat(getStringArray("a b")).containsExactly("a b");
+    assertThat(getStringArray("a , b")).containsExactly("a", "b");
+    assertThat(getStringArray("\"a \",\" b\"")).containsExactly("a ", " b");
+    assertThat(getStringArray("\"a,b\",c")).containsExactly("a,b", "c");
+    try {
+      getStringArray("\"a ,b");
+      fail("Expected exception");
+    } catch (Exception e) {
+      assertThat(e).hasMessage("Property: 'multi' doesn't contain a valid CSV value: '\"a ,b'");
+    }
+  }
+
+  private String[] getStringArray(String value) {
+    return new DefaultConfiguration(new PropertyDefinitions(Arrays.asList(
+      PropertyDefinition.builder("multi").multiValues(true).build())), new Encryption(null),
+      mock(AnalysisMode.class),
+      ImmutableMap.of("multi", value)) {
+    }.getStringArray("multi");
+  }
+}
index 49886eaca04140913c8d2009bf2b8e3feda4d9aa..f2ecbe4d5fba5622e3581f8775f8daca27c55fa9 100644 (file)
@@ -40,6 +40,14 @@ public class DefaultSettingsLoaderTest {
         .containsExactly(entry("sonar.preview.supportedPlugins", "java,php"));
   }
 
+  @Test
+  public void should_escape_global_multivalue_settings() {
+    assertThat(DefaultSettingsLoader.toMap(asList(Setting.newBuilder()
+      .setKey("sonar.preview.supportedPlugins")
+      .setValues(Values.newBuilder().addValues("ja,va").addValues("p\"hp")).build())))
+        .containsExactly(entry("sonar.preview.supportedPlugins", "\"ja,va\",\"p\"\"hp\""));
+  }
+
   @Test
   public void should_load_global_propertyset_settings() {
     Builder valuesBuilder = Value.newBuilder();
@@ -60,4 +68,5 @@ public class DefaultSettingsLoaderTest {
           entry("sonar.issue.exclusions.multicriteria.2.filepattern", "**/*.java"),
           entry("sonar.issue.exclusions.multicriteria.2.rulepattern", "*:S456"));
   }
+
 }
index 1c6db535f84fff50d74b6c1df7f87a836bfc0372..4790f48ae6e2fd38bfaa71324fbf6ac57075dad3 100644 (file)
@@ -78,6 +78,28 @@ public class ModuleFileSystemInitializerTest {
     assertThat(path(initializer.tests().get(0))).endsWith("src/test/java");
   }
 
+  @Test
+  public void supportFilenamesWithComma() throws IOException {
+    File baseDir = temp.newFolder("base");
+    File sourceFile = new File(baseDir, "my,File.cs");
+    sourceFile.createNewFile();
+    File testFile = new File(baseDir, "my,TestFile.cs");
+    testFile.createNewFile();
+
+    ProjectDefinition project = ProjectDefinition.create()
+      .setBaseDir(baseDir)
+      .addSources("\"my,File.cs\"")
+      .addTests("\"my,TestFile.cs\"");
+
+    ModuleFileSystemInitializer initializer = new ModuleFileSystemInitializer(project, mock(TempFolder.class), pathResolver);
+
+    assertThat(initializer.baseDir().getCanonicalPath()).isEqualTo(baseDir.getCanonicalPath());
+    assertThat(initializer.sources()).hasSize(1);
+    assertThat(initializer.sources().get(0)).isEqualTo(sourceFile);
+    assertThat(initializer.tests()).hasSize(1);
+    assertThat(initializer.tests().get(0)).isEqualTo(testFile);
+  }
+
   private String path(File f) throws IOException {
     return FilenameUtils.separatorsToUnix(f.getCanonicalPath());
   }