]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7844 add ScannerContextDao
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Wed, 17 Aug 2016 14:49:34 +0000 (16:49 +0200)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Mon, 22 Aug 2016 08:25:43 +0000 (10:25 +0200)
12 files changed:
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
sonar-db/src/main/java/org/sonar/db/DaoModule.java
sonar-db/src/main/java/org/sonar/db/DbClient.java
sonar-db/src/main/java/org/sonar/db/MyBatis.java
sonar-db/src/main/java/org/sonar/db/scannercontext/LogsIteratorInputStream.java [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/scannercontext/ScannerContextDao.java [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/scannercontext/package-info.java [new file with mode: 0644]
sonar-db/src/main/resources/org/sonar/db/version/schema-h2.ddl
sonar-db/src/test/java/org/sonar/db/DaoModuleTest.java
sonar-db/src/test/java/org/sonar/db/DbTester.java
sonar-db/src/test/java/org/sonar/db/scannercontext/LogsIteratorInputStreamTest.java [new file with mode: 0644]
sonar-db/src/test/java/org/sonar/db/scannercontext/ScannerContextDaoTest.java [new file with mode: 0644]

index 6a4709b1260c497144255eb65c7242577ba47297..22db63201cedf8061cfaf3305a07d07fd39c635a 100644 (file)
@@ -105,7 +105,7 @@ public class ComputeEngineContainerImplTest {
     assertThat(picoContainer.getParent().getParent().getParent().getComponentAdapters()).hasSize(
       COMPONENTS_IN_LEVEL_1_AT_CONSTRUCTION
         + 26 // level 1
-        + 47 // content of DaoModule
+        + 48 // content of DaoModule
         + 2 // content of EsSearchModule
         + 54 // content of CorePropertyDefinitions
         + 1 // content of CePropertyDefinitions
index ba299a873ca1bb6c6786844e8cf1a98583bb0912..d18f35a86dde52ff2e7b4f7c003b1dadfad4a8ed 100644 (file)
@@ -27,10 +27,10 @@ import org.sonar.db.ce.CeActivityDao;
 import org.sonar.db.ce.CeQueueDao;
 import org.sonar.db.ce.CeTaskInputDao;
 import org.sonar.db.component.ComponentDao;
+import org.sonar.db.component.ComponentKeyUpdaterDao;
 import org.sonar.db.component.ComponentLinkDao;
 import org.sonar.db.component.ResourceDao;
 import org.sonar.db.component.ResourceIndexDao;
-import org.sonar.db.component.ComponentKeyUpdaterDao;
 import org.sonar.db.component.SnapshotDao;
 import org.sonar.db.dashboard.ActiveDashboardDao;
 import org.sonar.db.dashboard.DashboardDao;
@@ -50,8 +50,8 @@ import org.sonar.db.measure.custom.CustomMeasureDao;
 import org.sonar.db.metric.MetricDao;
 import org.sonar.db.notification.NotificationQueueDao;
 import org.sonar.db.permission.PermissionDao;
-import org.sonar.db.permission.template.PermissionTemplateDao;
 import org.sonar.db.permission.template.PermissionTemplateCharacteristicDao;
+import org.sonar.db.permission.template.PermissionTemplateDao;
 import org.sonar.db.property.PropertiesDao;
 import org.sonar.db.purge.PurgeDao;
 import org.sonar.db.qualitygate.ProjectQgateAssociationDao;
@@ -60,6 +60,7 @@ import org.sonar.db.qualitygate.QualityGateDao;
 import org.sonar.db.qualityprofile.ActiveRuleDao;
 import org.sonar.db.qualityprofile.QualityProfileDao;
 import org.sonar.db.rule.RuleDao;
+import org.sonar.db.scannercontext.ScannerContextDao;
 import org.sonar.db.source.FileSourceDao;
 import org.sonar.db.user.AuthorDao;
 import org.sonar.db.user.AuthorizationDao;
@@ -102,11 +103,12 @@ public class DaoModule extends Module {
     PermissionTemplateDao.class,
     PermissionTemplateCharacteristicDao.class,
     PropertiesDao.class,
+    ProjectQgateAssociationDao.class,
+    PurgeDao.class,
     QualityGateDao.class,
     QualityGateConditionDao.class,
-    ProjectQgateAssociationDao.class,
     QualityProfileDao.class,
-    PurgeDao.class,
+    ScannerContextDao.class,
     RuleDao.class,
     ActiveRuleDao.class,
     ResourceIndexDao.class,
index 527b3dd763011ed2852d76fc3f1b19841137b315..929813bfd66d6c368de726bacbe37a9896dd28d5 100644 (file)
@@ -60,6 +60,7 @@ import org.sonar.db.qualitygate.QualityGateDao;
 import org.sonar.db.qualityprofile.ActiveRuleDao;
 import org.sonar.db.qualityprofile.QualityProfileDao;
 import org.sonar.db.rule.RuleDao;
+import org.sonar.db.scannercontext.ScannerContextDao;
 import org.sonar.db.source.FileSourceDao;
 import org.sonar.db.user.AuthorDao;
 import org.sonar.db.user.AuthorizationDao;
@@ -121,6 +122,7 @@ public class DbClient {
   private final GroupDao groupDao;
   private final RuleDao ruleDao;
   private final ActiveRuleDao activeRuleDao;
+  private final ScannerContextDao scannerContextDao;
 
   public DbClient(Database database, MyBatis myBatis, Dao... daos) {
     this.database = database;
@@ -177,6 +179,7 @@ public class DbClient {
     groupDao = getDao(map, GroupDao.class);
     ruleDao = getDao(map, RuleDao.class);
     activeRuleDao = getDao(map, ActiveRuleDao.class);
+    scannerContextDao = getDao(map, ScannerContextDao.class);
     doOnLoad(map);
   }
 
@@ -385,6 +388,10 @@ public class DbClient {
     return activeRuleDao;
   }
 
+  public ScannerContextDao scannerContextDao() {
+    return scannerContextDao;
+  }
+
   protected <K extends Dao> K getDao(Map<Class, Dao> map, Class<K> clazz) {
     return (K) map.get(clazz);
   }
index a4666c5c1e97bd4f608c0ef993ee15f4ea64e6e4..9ea48e9e2cbfd4000b277cd2367439c9272c43b3 100644 (file)
@@ -36,6 +36,7 @@ import org.sonar.db.ce.CeQueueMapper;
 import org.sonar.db.ce.CeTaskInputMapper;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.ComponentDtoWithSnapshotId;
+import org.sonar.db.component.ComponentKeyUpdaterMapper;
 import org.sonar.db.component.ComponentLinkDto;
 import org.sonar.db.component.ComponentLinkMapper;
 import org.sonar.db.component.ComponentMapper;
@@ -43,7 +44,6 @@ import org.sonar.db.component.FilePathWithHashDto;
 import org.sonar.db.component.ResourceDto;
 import org.sonar.db.component.ResourceIndexDto;
 import org.sonar.db.component.ResourceIndexMapper;
-import org.sonar.db.component.ComponentKeyUpdaterMapper;
 import org.sonar.db.component.ResourceMapper;
 import org.sonar.db.component.SnapshotDto;
 import org.sonar.db.component.SnapshotMapper;
@@ -84,13 +84,13 @@ import org.sonar.db.metric.MetricMapper;
 import org.sonar.db.notification.NotificationQueueDto;
 import org.sonar.db.notification.NotificationQueueMapper;
 import org.sonar.db.permission.GroupWithPermissionDto;
+import org.sonar.db.permission.UserWithPermissionDto;
+import org.sonar.db.permission.template.PermissionTemplateCharacteristicDto;
+import org.sonar.db.permission.template.PermissionTemplateCharacteristicMapper;
 import org.sonar.db.permission.template.PermissionTemplateDto;
 import org.sonar.db.permission.template.PermissionTemplateGroupDto;
 import org.sonar.db.permission.template.PermissionTemplateMapper;
 import org.sonar.db.permission.template.PermissionTemplateUserDto;
-import org.sonar.db.permission.UserWithPermissionDto;
-import org.sonar.db.permission.template.PermissionTemplateCharacteristicDto;
-import org.sonar.db.permission.template.PermissionTemplateCharacteristicMapper;
 import org.sonar.db.property.PropertiesMapper;
 import org.sonar.db.property.PropertyDto;
 import org.sonar.db.purge.IdUuidPair;
diff --git a/sonar-db/src/main/java/org/sonar/db/scannercontext/LogsIteratorInputStream.java b/sonar-db/src/main/java/org/sonar/db/scannercontext/LogsIteratorInputStream.java
new file mode 100644 (file)
index 0000000..ab55e46
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.db.scannercontext;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import org.sonar.core.util.CloseableIterator;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * An {@link InputStream} that will read from a {@link CloseableIterator} of {@link String}, inserting {@code \n} between
+ * each element of the Iterator.
+ */
+final class LogsIteratorInputStream extends InputStream {
+  private static final int UNSET = -1;
+  private static final int END_OF_STREAM = -1;
+
+  private final Charset charset;
+  private final byte[] lineFeed;
+  private CloseableIterator<String> logsIterator;
+  private byte[] buf;
+  private int nextChar = UNSET;
+
+  LogsIteratorInputStream(CloseableIterator<String> logsIterator, Charset charset) {
+    checkArgument(logsIterator.hasNext(), "LogsIterator can't be empty or already read");
+    this.charset = charset;
+    this.lineFeed = "\n".getBytes(charset);
+    this.logsIterator = logsIterator;
+  }
+
+  @Override
+  public int read() throws IOException {
+    if (nextChar == UNSET || nextChar >= buf.length) {
+      fill();
+      if (nextChar == UNSET) {
+        return END_OF_STREAM;
+      }
+    }
+    return buf[nextChar++];
+  }
+
+  private void fill() {
+    if (logsIterator.hasNext()) {
+      byte[] line = logsIterator.next().getBytes(charset);
+      boolean hasNextLine = logsIterator.hasNext();
+      int bufLength = hasNextLine ? (line.length + lineFeed.length) : line.length;
+      // empty last line
+      if (bufLength == 0) {
+        this.buf = null;
+        this.nextChar = UNSET;
+      } else {
+        this.buf = new byte[bufLength];
+        System.arraycopy(line, 0, buf, 0, line.length);
+        if (hasNextLine) {
+          System.arraycopy(lineFeed, 0, buf, line.length, lineFeed.length);
+        }
+        this.nextChar = 0;
+      }
+    } else {
+      this.buf = null;
+      this.nextChar = UNSET;
+    }
+  }
+
+  @Override
+  public void close() throws IOException {
+    this.logsIterator.close();
+    this.buf = null;
+
+    super.close();
+  }
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/scannercontext/ScannerContextDao.java b/sonar-db/src/main/java/org/sonar/db/scannercontext/ScannerContextDao.java
new file mode 100644 (file)
index 0000000..c294922
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.db.scannercontext;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Optional;
+import org.apache.commons.io.IOUtils;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.CloseableIterator;
+import org.sonar.db.Dao;
+import org.sonar.db.DbSession;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+public class ScannerContextDao implements Dao {
+
+  private static final Charset UTF_8 = Charset.forName("UTF-8");
+
+  private final System2 system;
+
+  public ScannerContextDao(System2 system) {
+    this.system = system;
+  }
+
+  /**
+   * @throws IllegalArgumentException if {@code scannerContextLines} is empty or fully read.
+   */
+  public void insert(DbSession dbSession, String analysisUuid, CloseableIterator<String> scannerContextLines) {
+    checkArgument(scannerContextLines.hasNext(), "Scanner context can not be empty");
+    long now = system.now();
+    try (PreparedStatement stmt = dbSession.getConnection().prepareStatement(
+      "INSERT INTO scanner_context (analysis_uuid, created_at, updated_at, data) VALUES (?, ?, ?, ?)");
+      InputStream inputStream = new LogsIteratorInputStream(scannerContextLines, UTF_8)) {
+      stmt.setString(1, analysisUuid);
+      stmt.setLong(2, now);
+      stmt.setLong(3, now);
+      stmt.setBinaryStream(4, inputStream);
+      stmt.executeUpdate();
+    } catch (SQLException | IOException e) {
+      throw new IllegalStateException("Fail to insert scanner context for analysis " + analysisUuid, e);
+    }
+  }
+
+  /**
+   * The scanner context is very likely to contain lines, which are forcefully separated by {@code \n} characters,
+   * whichever the platform SQ is running on ({@see LogsIteratorInputStream}).
+   */
+  public Optional<String> selectScannerContext(DbSession dbSession, String analysisUuid) {
+    try (PreparedStatement stmt = dbSession.getConnection().prepareStatement("select data from scanner_context where analysis_uuid=?")) {
+      stmt.setString(1, analysisUuid);
+      try (ResultSet rs = stmt.executeQuery()) {
+        if (rs.next()) {
+          return Optional.of(IOUtils.toString(rs.getBinaryStream(1), UTF_8));
+        }
+        return Optional.empty();
+      }
+    } catch (SQLException | IOException e) {
+      throw new IllegalStateException("Fail to retrieve scanner context of analysis " + analysisUuid, e);
+    }
+  }
+
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/scannercontext/package-info.java b/sonar-db/src/main/java/org/sonar/db/scannercontext/package-info.java
new file mode 100644 (file)
index 0000000..de8b739
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.db.scannercontext;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
index 7559720c657eb091d216274b66992531d61cfe6e..3aeed281ed84660a7142a1419d3ddab56a7f853c 100644 (file)
@@ -544,6 +544,7 @@ CREATE TABLE "CE_ACTIVITY" (
 );
 
 CREATE TABLE "CE_TASK_INPUT" (
+  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
   "TASK_UUID" VARCHAR(40) NOT NULL,
   "DATA" BLOB(167772150),
   "CREATED_AT" BIGINT NOT NULL,
index 5e8d8910d50510d7aa5d23378b9d2951b40f37bc..f653c76bfcef058460a4ba1cc4391ddb756c569d 100644 (file)
@@ -29,6 +29,6 @@ public class DaoModuleTest {
   public void verify_count_of_added_components() {
     ComponentContainer container = new ComponentContainer();
     new DaoModule().configure(container);
-    assertThat(container.size()).isEqualTo(2 + 47);
+    assertThat(container.size()).isEqualTo(2 + 48);
   }
 }
index b729146fbf11968a4bb8bdc8c8d885ed5387d4f3..04e229a2d3f1c038eca70e2012d38144fcdf7e69 100644 (file)
@@ -20,8 +20,7 @@
 package org.sonar.db;
 
 import com.google.common.base.Joiner;
-import com.google.common.base.Preconditions;
-import com.google.common.collect.Collections2;
+import com.google.common.base.Supplier;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import java.io.InputStream;
@@ -59,6 +58,7 @@ import org.junit.rules.ExternalResource;
 import org.picocontainer.containers.TransientPicoContainer;
 import org.sonar.api.utils.System2;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.collect.Lists.newArrayList;
 import static com.google.common.collect.Maps.newHashMap;
 import static java.sql.ResultSetMetaData.columnNoNulls;
@@ -133,7 +133,7 @@ public class DbTester extends ExternalResource {
   }
 
   public void executeUpdateSql(String sql, Object... params) {
-    try (Connection connection = db.getDatabase().getDataSource().getConnection()) {
+    try (Connection connection = getConnection()) {
       new QueryRunner().update(connection, sql, params);
     } catch (Exception e) {
       throw new IllegalStateException("Fail to execute sql: " + sql, e);
@@ -186,8 +186,16 @@ public class DbTester extends ExternalResource {
    * <pre>int issues = countRowsOfTable("issues")</pre>
    */
   public int countRowsOfTable(String tableName) {
-    Preconditions.checkArgument(StringUtils.containsNone(tableName, " "), "Parameter must be the name of a table. Got " + tableName);
-    return countSql("select count(1) from " + tableName.toLowerCase(Locale.ENGLISH));
+    return countRowsOfTable(tableName, this::getConnection);
+  }
+
+  public int countRowsOfTable(DbSession dbSession, String tableName) {
+    return countRowsOfTable(tableName, () -> dbSession.getConnection());
+  }
+
+  private int countRowsOfTable(String tableName, SqlExceptionSupplier<Connection> connectionSupplier) {
+    checkArgument(StringUtils.containsNone(tableName, " "), "Parameter must be the name of a table. Got " + tableName);
+    return countSql("select count(1) from " + tableName.toLowerCase(Locale.ENGLISH), connectionSupplier);
   }
 
   /**
@@ -195,10 +203,18 @@ public class DbTester extends ExternalResource {
    * <pre>int OpenIssues = countSql("select count('id') from issues where status is not null")</pre>
    */
   public int countSql(String sql) {
-    Preconditions.checkArgument(StringUtils.contains(sql, "count("),
+    return countSql(sql, this::getConnection);
+  }
+
+  public int countSql(DbSession dbSession, String sql) {
+    return countSql(sql, () -> dbSession.getConnection());
+  }
+
+  private int countSql(String sql, SqlExceptionSupplier<Connection> connectionSupplier) {
+    checkArgument(StringUtils.contains(sql, "count("),
       "Parameter must be a SQL request containing 'count(x)' function. Got " + sql);
     try (
-      Connection connection = db.getDatabase().getDataSource().getConnection();
+      Connection connection = connectionSupplier.get();
       PreparedStatement stmt = connection.prepareStatement(sql);
       ResultSet rs = stmt.executeQuery()) {
       if (rs.next()) {
@@ -212,8 +228,12 @@ public class DbTester extends ExternalResource {
   }
 
   public List<Map<String, Object>> select(String selectSql) {
+    return select(selectSql, this::getConnection);
+  }
+
+  private List<Map<String, Object>> select(String selectSql, SqlExceptionSupplier<Connection> connectionSupplier) {
     try (
-      Connection connection = db.getDatabase().getDataSource().getConnection();
+      Connection connection = connectionSupplier.get();
       PreparedStatement stmt = connection.prepareStatement(selectSql);
       ResultSet rs = stmt.executeQuery()) {
       return getHashMap(rs);
@@ -223,7 +243,15 @@ public class DbTester extends ExternalResource {
   }
 
   public Map<String, Object> selectFirst(String selectSql) {
-    List<Map<String, Object>> rows = select(selectSql);
+    return selectFirst(selectSql, this::getConnection);
+  }
+
+  public Map<String, Object> selectFirst(DbSession dbSession, String selectSql) {
+    return selectFirst(selectSql, () -> dbSession.getConnection());
+  }
+
+  private Map<String, Object> selectFirst(String selectSql, SqlExceptionSupplier<Connection> connectionSupplier) {
+    List<Map<String, Object>> rows = select(selectSql, connectionSupplier);
     if (rows.isEmpty()) {
       throw new IllegalStateException("No results for " + selectSql);
     } else if (rows.size() > 1) {
@@ -365,7 +393,7 @@ public class DbTester extends ExternalResource {
   }
 
   public void assertColumnDefinition(String table, String column, int expectedType, @Nullable Integer expectedSize, @Nullable Boolean isNullable) {
-    try (Connection connection = db.getDatabase().getDataSource().getConnection();
+    try (Connection connection = getConnection();
       PreparedStatement stmt = connection.prepareStatement("select * from " + table);
       ResultSet res = stmt.executeQuery()) {
       Integer columnIndex = getColumnIndex(res, column);
@@ -456,6 +484,10 @@ public class DbTester extends ExternalResource {
 
   @Deprecated
   public Connection openConnection() throws Exception {
+    return getConnection();
+  }
+
+  private Connection getConnection() throws SQLException {
     return db.getDatabase().getDataSource().getConnection();
   }
 
@@ -468,4 +500,12 @@ public class DbTester extends ExternalResource {
     return db.getCommands();
   }
 
+  /**
+   * A {@link Supplier} that declares the checked exception {@link SQLException}.
+   */
+  @FunctionalInterface
+  private interface SqlExceptionSupplier<T> {
+    T get() throws SQLException;
+  }
+
 }
diff --git a/sonar-db/src/test/java/org/sonar/db/scannercontext/LogsIteratorInputStreamTest.java b/sonar-db/src/test/java/org/sonar/db/scannercontext/LogsIteratorInputStreamTest.java
new file mode 100644 (file)
index 0000000..06d6e1b
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.db.scannercontext;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import org.apache.commons.io.IOUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.core.util.CloseableIterator;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LogsIteratorInputStreamTest {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void read_from_ClosableIterator_with_several_lines() throws IOException {
+    assertThat(read(create("line1", "line2", "line3"))).isEqualTo("line1" + '\n' + "line2" + '\n' + "line3");
+  }
+
+  @Test
+  public void read_from_ClosableIterator_with_single_line() throws IOException {
+    assertThat(read(create("line1"))).isEqualTo("line1");
+  }
+
+  @Test
+  public void read_from_ClosableIterator_with_single_empty_line() throws IOException {
+    assertThat(read(create(""))).isEqualTo("");
+  }
+
+  @Test
+  public void read_from_ClosableIterator_with_several_empty_lines() throws IOException {
+    assertThat(read(create("", "line2", "", "line4", "", "", "", "line8", "")))
+      .isEqualTo('\n' + "line2" + '\n' + '\n' + "line4" + '\n' + '\n' + '\n' + '\n' + "line8" + '\n');
+  }
+
+  @Test
+  public void constructor_throws_IAE_when_ClosableIterator_is_empty() throws IOException {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("LogsIterator can't be empty or already read");
+
+    create();
+  }
+
+  @Test
+  public void constructor_throws_IAE_when_ClosableIterator_has_already_been_read() throws IOException {
+    CloseableIterator<String> iterator = CloseableIterator.from(Arrays.asList("line1").iterator());
+
+    // read iterator to the end
+    iterator.next();
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("LogsIterator can't be empty or already read");
+
+    new LogsIteratorInputStream(iterator, Charset.forName("UTF-8"));
+  }
+
+  private static LogsIteratorInputStream create(String... lines) {
+    return new LogsIteratorInputStream(CloseableIterator.from(Arrays.asList(lines).iterator()), Charset.forName("UTF-8"));
+  }
+
+  private static String read(LogsIteratorInputStream logsIteratorInputStream) throws IOException {
+    return IOUtils.toString(logsIteratorInputStream, "UTF-8");
+  }
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/scannercontext/ScannerContextDaoTest.java b/sonar-db/src/test/java/org/sonar/db/scannercontext/ScannerContextDaoTest.java
new file mode 100644 (file)
index 0000000..ef635a5
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.db.scannercontext;
+
+import java.util.Collections;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.CloseableIterator;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+
+import static java.lang.System.lineSeparator;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class ScannerContextDaoTest {
+
+  private static final String TABLE_NAME = "scanner_context";
+  private static final String SOME_UUID = "some UUID";
+
+  @Rule
+  public DbTester dbTester = DbTester.create(System2.INSTANCE);
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private System2 system = mock(System2.class);
+  private DbSession dbSession = dbTester.getSession();
+
+  private ScannerContextDao underTest = new ScannerContextDao(system);
+
+  @Test
+  public void selectScannerContext_returns_empty_on_empty_table() {
+    assertThat(underTest.selectScannerContext(dbSession, SOME_UUID)).isEmpty();
+  }
+
+  @Test
+  public void selectScannerContext_returns_empty_when_no_row_exist_for_analysisUuid() {
+    String data = "some data";
+    underTest.insert(dbSession, SOME_UUID, scannerContextInputStreamOf(data));
+    dbSession.commit();
+
+    assertThat(underTest.selectScannerContext(dbSession, "OTHER_uuid")).isEmpty();
+    assertThat(underTest.selectScannerContext(dbSession, SOME_UUID)).contains(data);
+  }
+
+  @Test
+  public void insert_fails_with_IAE_if_data_is_empty() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Scanner context can not be empty");
+
+    underTest.insert(dbSession, SOME_UUID, CloseableIterator.emptyCloseableIterator());
+  }
+
+  @Test
+  public void insert_fails_with_IAE_if_data_is_fully_read() {
+    CloseableIterator<String> iterator = scannerContextInputStreamOf("aa");
+    iterator.next();
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Scanner context can not be empty");
+
+    underTest.insert(dbSession, SOME_UUID, iterator);
+  }
+
+  @Test
+  public void insert_fails_if_row_already_exists_for_analysis_uuid() {
+    underTest.insert(dbSession, SOME_UUID, scannerContextInputStreamOf("bla"));
+    dbSession.commit();
+
+    assertThat(dbTester.countRowsOfTable(dbSession, TABLE_NAME)).isEqualTo(1);
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Fail to insert scanner context for analysis " + SOME_UUID);
+
+    underTest.insert(dbSession, SOME_UUID, scannerContextInputStreamOf("blo"));
+  }
+
+  @Test
+  public void insert_and_select_line_reader() {
+    String scannerContext = "line 1" + lineSeparator() + "line 2" + lineSeparator() + "line 3";
+    underTest.insert(dbSession, SOME_UUID, scannerContextInputStreamOf(scannerContext));
+    dbSession.commit(true);
+
+    assertThat(underTest.selectScannerContext(dbSession, SOME_UUID)).contains(scannerContext);
+  }
+
+  private static CloseableIterator<String> scannerContextInputStreamOf(String data) {
+    return CloseableIterator.from(Collections.singleton(data).iterator());
+  }
+
+}