All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonarsource.sonarlint.core.serverconnection.storage.XodusServerIssueStore Maven / Gradle / Ivy

/*
 * SonarLint Core - Server Connection
 * Copyright (C) 2016-2023 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.sonarsource.sonarlint.core.serverconnection.storage;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import jetbrains.exodus.entitystore.Entity;
import jetbrains.exodus.entitystore.EntityIterable;
import jetbrains.exodus.entitystore.PersistentEntityStore;
import jetbrains.exodus.entitystore.PersistentEntityStores;
import jetbrains.exodus.entitystore.StoreTransaction;
import jetbrains.exodus.entitystore.StoreTransactionalExecutable;
import jetbrains.exodus.env.EnvironmentConfig;
import jetbrains.exodus.env.Environments;
import jetbrains.exodus.util.CompressBackupUtil;
import org.apache.commons.io.FileUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.sonarsource.sonarlint.core.commons.HotspotReviewStatus;
import org.sonarsource.sonarlint.core.commons.IssueSeverity;
import org.sonarsource.sonarlint.core.commons.RuleType;
import org.sonarsource.sonarlint.core.commons.TextRangeWithHash;
import org.sonarsource.sonarlint.core.commons.VulnerabilityProbability;
import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger;
import org.sonarsource.sonarlint.core.serverapi.hotspot.ServerHotspot;
import org.sonarsource.sonarlint.core.serverconnection.issues.FileLevelServerIssue;
import org.sonarsource.sonarlint.core.serverconnection.issues.LineLevelServerIssue;
import org.sonarsource.sonarlint.core.serverconnection.issues.RangeLevelServerIssue;
import org.sonarsource.sonarlint.core.serverconnection.issues.ServerIssue;
import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue;
import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue.Flow;
import org.sonarsource.sonarlint.core.serverconnection.issues.ServerTaintIssue.ServerIssueLocation;
import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint;
import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint.Location;
import org.sonarsource.sonarlint.core.serverconnection.proto.Sonarlint.TextRange;

import static java.util.Objects.requireNonNull;

public class XodusServerIssueStore implements ProjectServerIssueStore {

  static final int CURRENT_SCHEMA_VERSION = 1;

  private static final String BACKUP_TAR_GZ = "backup.tar.gz";

  private static final SonarLintLogger LOG = SonarLintLogger.get();

  private static final String BRANCH_ENTITY_TYPE = "Branch";
  private static final String FILE_ENTITY_TYPE = "File";
  private static final String ISSUE_ENTITY_TYPE = "Issue";
  private static final String TAINT_ISSUE_ENTITY_TYPE = "TaintIssue";
  private static final String HOTSPOT_ENTITY_TYPE = "Hotspot";
  private static final String SCHEMA_ENTITY_TYPE = "Schema";

  private static final String BRANCH_TO_FILES_LINK_NAME = "files";
  private static final String BRANCH_TO_TAINT_ISSUES_LINK_NAME = "taintIssues";
  private static final String TAINT_ISSUE_TO_BRANCH_LINK_NAME = "branch";
  private static final String FILE_TO_ISSUES_LINK_NAME = "issues";
  private static final String FILE_TO_TAINT_ISSUES_LINK_NAME = "taintIssues";
  private static final String FILE_TO_HOTSPOTS_LINK_NAME = "hotspots";
  private static final String ISSUE_TO_FILE_LINK_NAME = "file";

  private static final String START_LINE_PROPERTY_NAME = "startLine";
  private static final String START_LINE_OFFSET_PROPERTY_NAME = "startLineOffset";
  private static final String END_LINE_PROPERTY_NAME = "endLine";
  private static final String END_LINE_OFFSET_PROPERTY_NAME = "endLineOffset";
  private static final String KEY_PROPERTY_NAME = "key";
  private static final String RESOLVED_PROPERTY_NAME = "resolved";
  private static final String REVIEW_STATUS_PROPERTY_NAME = "status";
  private static final String RULE_KEY_PROPERTY_NAME = "ruleKey";
  private static final String LINE_HASH_PROPERTY_NAME = "lineHash";
  private static final String RANGE_HASH_PROPERTY_NAME = "rangeHash";
  private static final String CREATION_DATE_PROPERTY_NAME = "creationDate";
  private static final String USER_SEVERITY_PROPERTY_NAME = "userSeverity";
  private static final String SEVERITY_PROPERTY_NAME = "severity";
  private static final String VULNERABILITY_PROBABILITY_PROPERTY_NAME = "vulnerabilityProbability";
  private static final String TYPE_PROPERTY_NAME = "type";
  private static final String PATH_PROPERTY_NAME = "path";
  private static final String NAME_PROPERTY_NAME = "name";
  private static final String LAST_ISSUE_SYNC_PROPERTY_NAME = "lastIssueSync";
  private static final String LAST_TAINT_SYNC_PROPERTY_NAME = "lastTaintSync";
  private static final String VERSION_PROPERTY_NAME = "version";

  private static final String MESSAGE_BLOB_NAME = "message";
  private static final String FLOWS_BLOB_NAME = "flows";
  private static final String RULE_DESCRIPTION_CONTEXT_KEY_PROPERTY_NAME = "ruleDescriptionContextKey";
  private final PersistentEntityStore entityStore;

  private final Path backupFile;

  private final Path xodusDbDir;

  public XodusServerIssueStore(Path backupDir, Path workDir) throws IOException {
    this(backupDir, workDir, XodusServerIssueStore::checkCurrentSchemaVersion);
  }

  XodusServerIssueStore(Path backupDir, Path workDir, StoreTransactionalExecutable afterInit) throws IOException {
    xodusDbDir = Files.createTempDirectory(workDir, "xodus-issue-store");
    backupFile = backupDir.resolve(BACKUP_TAR_GZ);
    if (Files.isRegularFile(backupFile)) {
      LOG.debug("Restoring previous server issue database from {}", backupFile);
      try {
        TarGzUtils.extractTarGz(backupFile, xodusDbDir);
      } catch (Exception e) {
        LOG.error("Unable to restore backup {}", backupFile);
      }
    }
    LOG.debug("Starting server issue database from {}", xodusDbDir);
    this.entityStore = buildEntityStore();
    entityStore.executeInTransaction(txn -> {
      entityStore.registerCustomPropertyType(txn, IssueSeverity.class, new IssueSeverityBinding());
      entityStore.registerCustomPropertyType(txn, RuleType.class, new IssueTypeBinding());
      entityStore.registerCustomPropertyType(txn, Instant.class, new InstantBinding());
      entityStore.registerCustomPropertyType(txn, HotspotReviewStatus.class, new HotspotReviewStatusBinding());
    });

    entityStore.executeInExclusiveTransaction(afterInit);
  }

  private PersistentEntityStore buildEntityStore() {
    var environment = Environments.newInstance(xodusDbDir.toAbsolutePath().toFile(), new EnvironmentConfig()
      .setLogAllowRemote(true)
      .setLogAllowRemovable(true)
      .setLogAllowRamDisk(true));
    var entityStoreImpl = PersistentEntityStores.newInstance(environment);
    entityStoreImpl.setCloseEnvironment(true);
    return entityStoreImpl;
  }

  private static ServerIssue adapt(Entity storedIssue) {
    var filePath = (String) requireNonNull(storedIssue.getLink(ISSUE_TO_FILE_LINK_NAME).getProperty(PATH_PROPERTY_NAME));
    var startLine = storedIssue.getProperty(START_LINE_PROPERTY_NAME);
    var key = (String) requireNonNull(storedIssue.getProperty(KEY_PROPERTY_NAME));
    var resolved = Boolean.TRUE.equals(storedIssue.getProperty(RESOLVED_PROPERTY_NAME));
    var ruleKey = (String) requireNonNull(storedIssue.getProperty(RULE_KEY_PROPERTY_NAME));
    var msg = requireNonNull(storedIssue.getBlobString(MESSAGE_BLOB_NAME));
    var creationDate = (Instant) requireNonNull(storedIssue.getProperty(CREATION_DATE_PROPERTY_NAME));
    var userSeverity = (IssueSeverity) storedIssue.getProperty(USER_SEVERITY_PROPERTY_NAME);
    var type = (RuleType) requireNonNull(storedIssue.getProperty(TYPE_PROPERTY_NAME));
    if (startLine == null) {
      return new FileLevelServerIssue(key, resolved, ruleKey, msg, filePath, creationDate, userSeverity, type);
    } else {
      var rangeHash = storedIssue.getBlobString(RANGE_HASH_PROPERTY_NAME);
      if (rangeHash != null) {
        var startLineOffset = (Integer) storedIssue.getProperty(START_LINE_OFFSET_PROPERTY_NAME);
        var endLine = (Integer) storedIssue.getProperty(END_LINE_PROPERTY_NAME);
        var endLineOffset = (Integer) storedIssue.getProperty(END_LINE_OFFSET_PROPERTY_NAME);
        var textRange = new TextRangeWithHash((int) startLine, startLineOffset, endLine, endLineOffset, rangeHash);
        return new RangeLevelServerIssue(
          key,
          resolved,
          ruleKey,
          msg,
          filePath,
          creationDate,
          userSeverity,
          type,
          textRange);
      } else {
        return new LineLevelServerIssue(
          key,
          resolved,
          ruleKey,
          msg,
          storedIssue.getBlobString(LINE_HASH_PROPERTY_NAME),
          filePath,
          creationDate,
          userSeverity,
          type,
          (Integer) storedIssue.getProperty(START_LINE_PROPERTY_NAME));
      }
    }
  }

  private static ServerTaintIssue adaptTaint(Entity storedIssue) {
    var filePath = (String) requireNonNull(storedIssue.getLink(ISSUE_TO_FILE_LINK_NAME).getProperty(PATH_PROPERTY_NAME));
    var startLine = (Integer) storedIssue.getProperty(START_LINE_PROPERTY_NAME);
    TextRangeWithHash textRange = null;
    if (startLine != null) {
      var startLineOffset = (Integer) storedIssue.getProperty(START_LINE_OFFSET_PROPERTY_NAME);
      var endLine = (Integer) storedIssue.getProperty(END_LINE_PROPERTY_NAME);
      var endLineOffset = (Integer) storedIssue.getProperty(END_LINE_OFFSET_PROPERTY_NAME);
      var hash = storedIssue.getBlobString(RANGE_HASH_PROPERTY_NAME);
      textRange = new TextRangeWithHash(startLine, startLineOffset, endLine, endLineOffset, hash);
    }
    return new ServerTaintIssue(
      (String) requireNonNull(storedIssue.getProperty(KEY_PROPERTY_NAME)),
      Boolean.TRUE.equals(storedIssue.getProperty(RESOLVED_PROPERTY_NAME)),
      (String) requireNonNull(storedIssue.getProperty(RULE_KEY_PROPERTY_NAME)),
      requireNonNull(storedIssue.getBlobString(MESSAGE_BLOB_NAME)),
      filePath,
      (Instant) requireNonNull(storedIssue.getProperty(CREATION_DATE_PROPERTY_NAME)),
      (IssueSeverity) requireNonNull(storedIssue.getProperty(SEVERITY_PROPERTY_NAME)),
      (RuleType) requireNonNull(storedIssue.getProperty(TYPE_PROPERTY_NAME)),
      textRange, (String) storedIssue.getProperty(RULE_DESCRIPTION_CONTEXT_KEY_PROPERTY_NAME))
        .setFlows(readFlows(storedIssue.getBlob(FLOWS_BLOB_NAME)));
  }

  private static ServerHotspot adaptHotspot(Entity storedHotspot) {
    var filePath = (String) requireNonNull(storedHotspot.getLink(ISSUE_TO_FILE_LINK_NAME).getProperty(PATH_PROPERTY_NAME));
    var startLine = (Integer) storedHotspot.getProperty(START_LINE_PROPERTY_NAME);
    var startLineOffset = (Integer) storedHotspot.getProperty(START_LINE_OFFSET_PROPERTY_NAME);
    var endLine = (Integer) storedHotspot.getProperty(END_LINE_PROPERTY_NAME);
    var endLineOffset = (Integer) storedHotspot.getProperty(END_LINE_OFFSET_PROPERTY_NAME);
    var textRange = new org.sonarsource.sonarlint.core.commons.TextRange(startLine, startLineOffset, endLine, endLineOffset);
    var vulnerabilityProbability = VulnerabilityProbability.valueOf((String) storedHotspot.getProperty(VULNERABILITY_PROBABILITY_PROPERTY_NAME));
    var status = (HotspotReviewStatus) storedHotspot.getProperty(REVIEW_STATUS_PROPERTY_NAME);
    if (status == null) {
      // backward compatibility. This should not happen as hotspots are all replaced during the sync
      var resolved = Boolean.TRUE.equals(storedHotspot.getProperty(RESOLVED_PROPERTY_NAME));
      status = resolved ? HotspotReviewStatus.SAFE : HotspotReviewStatus.TO_REVIEW;
    }
    return new ServerHotspot(
      (String) requireNonNull(storedHotspot.getProperty(KEY_PROPERTY_NAME)),
      (String) requireNonNull(storedHotspot.getProperty(RULE_KEY_PROPERTY_NAME)),
      requireNonNull(storedHotspot.getBlobString(MESSAGE_BLOB_NAME)),
      filePath,
      textRange,
      (Instant) requireNonNull(storedHotspot.getProperty(CREATION_DATE_PROPERTY_NAME)),
      status,
      vulnerabilityProbability);
  }

  private static List readFlows(@Nullable InputStream blob) {
    if (blob == null) {
      return List.of();
    }
    return ProtobufUtil.readMessages(blob, Sonarlint.Flow.parser()).stream().map(XodusServerIssueStore::toJavaFlow).collect(Collectors.toList());
  }

  @Override
  public List load(String branchName, String filePath) {
    return loadIssue(branchName, filePath, FILE_TO_ISSUES_LINK_NAME, XodusServerIssueStore::adapt);
  }

  @Override
  public List loadTaint(String branchName, String filePath) {
    return loadIssue(branchName, filePath, FILE_TO_TAINT_ISSUES_LINK_NAME, XodusServerIssueStore::adaptTaint);
  }

  @Override
  public Collection loadHotspots(String branchName, String serverFilePath) {
    return loadIssue(branchName, serverFilePath, FILE_TO_HOTSPOTS_LINK_NAME, XodusServerIssueStore::adaptHotspot);
  }

  private  List loadIssue(String branchName, String filePath, String linkName, Function adapter) {
    return entityStore.computeInReadonlyTransaction(txn -> findUnique(txn, BRANCH_ENTITY_TYPE, NAME_PROPERTY_NAME, branchName)
      .map(branch -> branch.getLinks(BRANCH_TO_FILES_LINK_NAME))
      .flatMap(files -> findUniqueAmong(files, PATH_PROPERTY_NAME, filePath))
      .map(fileToLoad -> fileToLoad.getLinks(linkName))
      .map(issueEntities -> StreamSupport.stream(issueEntities.spliterator(), false)
        .map(adapter)
        .collect(Collectors.toList()))
      .orElseGet(Collections::emptyList));
  }

  @Override
  public List loadTaint(String branchName) {
    return entityStore.computeInReadonlyTransaction(txn -> findUnique(txn, BRANCH_ENTITY_TYPE, NAME_PROPERTY_NAME, branchName)
      .map(branch -> StreamSupport.stream(branch.getLinks(BRANCH_TO_TAINT_ISSUES_LINK_NAME).spliterator(), false)
        .map(XodusServerIssueStore::adaptTaint)
        .collect(Collectors.toList()))
      .orElseGet(Collections::emptyList));
  }

  @Override
  public void replaceAllIssuesOfFile(String branchName, String serverFilePath, List issues) {
    timed("Wrote " + issues.size() + " issues in store", () -> entityStore.executeInTransaction(txn -> {
      var branch = getOrCreateBranch(branchName, txn);
      var fileEntity = getOrCreateFile(branch, serverFilePath, txn);
      replaceAllIssuesOfFile(issues, txn, fileEntity);
    }));
  }

  @Override
  public void mergeIssues(String branchName, List issuesToMerge, Set closedIssueKeysToDelete, Instant syncTimestamp) {
    var issuesByFilePath = issuesToMerge.stream().collect(Collectors.groupingBy(ServerIssue::getFilePath));
    timed("Merged " + issuesToMerge.size() + " issues in store. Closed " + closedIssueKeysToDelete.size() + ".", () -> entityStore.executeInTransaction(txn -> {
      var branch = getOrCreateBranch(branchName, txn);
      issuesByFilePath.forEach((filePath, issues) -> {
        var fileEntity = getOrCreateFile(branch, filePath, txn);
        issues.forEach(issue -> updateOrCreateIssue(fileEntity, issue, txn));
        txn.flush();
      });
      closedIssueKeysToDelete.forEach(issueKey -> remove(issueKey, txn));
      branch.setProperty(LAST_ISSUE_SYNC_PROPERTY_NAME, syncTimestamp);
    }));
  }

  @Override
  public void mergeTaintIssues(String branchName, List issuesToMerge, Set closedIssueKeysToDelete, Instant syncTimestamp) {
    var issuesByFilePath = issuesToMerge.stream().collect(Collectors.groupingBy(ServerTaintIssue::getFilePath));
    timed("Merged " + issuesToMerge.size() + " taint issues in store. Closed " + closedIssueKeysToDelete.size() + ".", () -> entityStore.executeInTransaction(txn -> {
      var branch = getOrCreateBranch(branchName, txn);
      issuesByFilePath.forEach((filePath, issues) -> {
        var fileEntity = getOrCreateFile(branch, filePath, txn);
        issues.forEach(issue -> updateOrCreateTaintIssue(branch, fileEntity, issue, txn));
        txn.flush();
      });
      closedIssueKeysToDelete.forEach(issueKey -> removeTaint(issueKey, txn));
      branch.setProperty(LAST_TAINT_SYNC_PROPERTY_NAME, syncTimestamp);
    }));
  }

  @Override
  public Optional getLastIssueSyncTimestamp(String branchName) {
    return entityStore.computeInReadonlyTransaction(txn -> findUnique(txn, BRANCH_ENTITY_TYPE, NAME_PROPERTY_NAME, branchName)
      .map(branch -> (Instant) branch.getProperty(LAST_ISSUE_SYNC_PROPERTY_NAME)));
  }

  @Override
  public Optional getLastTaintSyncTimestamp(String branchName) {
    return entityStore.computeInReadonlyTransaction(txn -> findUnique(txn, BRANCH_ENTITY_TYPE, NAME_PROPERTY_NAME, branchName)
      .map(branch -> (Instant) branch.getProperty(LAST_TAINT_SYNC_PROPERTY_NAME)));
  }

  @Override
  public void replaceAllIssuesOfBranch(String branchName, List issues) {
    var issuesByFile = issues.stream().collect(Collectors.groupingBy(ServerIssue::getFilePath));
    timed("Wrote " + issues.size() + " issues in store", () -> entityStore.executeInTransaction(txn -> {
      var branch = getOrCreateBranch(branchName, txn);
      branch.getLinks(BRANCH_TO_FILES_LINK_NAME).forEach(fileEntity -> {
        var entityFilePath = fileEntity.getProperty(PATH_PROPERTY_NAME);
        if (!issuesByFile.containsKey(entityFilePath)) {
          deleteAllIssuesOfFile(txn, fileEntity);
        }
      });
      txn.flush();
      issuesByFile.forEach((filePath, fileIssues) -> {
        var fileEntity = getOrCreateFile(branch, filePath, txn);
        replaceAllIssuesOfFile(fileIssues, txn, fileEntity);
        txn.flush();
      });
    }));
  }

  @Override
  public void replaceAllHotspotsOfBranch(String branchName, Collection serverHotspots) {
    var hotspotsByFile = serverHotspots.stream().collect(Collectors.groupingBy(ServerHotspot::getFilePath));
    timed("Wrote " + serverHotspots.size() + " hotspots in store", () -> entityStore.executeInTransaction(txn -> {
      var branch = getOrCreateBranch(branchName, txn);
      branch.getLinks(BRANCH_TO_FILES_LINK_NAME).forEach(fileEntity -> {
        var entityFilePath = fileEntity.getProperty(PATH_PROPERTY_NAME);
        if (!hotspotsByFile.containsKey(entityFilePath)) {
          deleteAllHotspotsOfFile(txn, fileEntity);
        }
      });
      txn.flush();
      hotspotsByFile.forEach((filePath, fileIssues) -> {
        var fileEntity = getOrCreateFile(branch, filePath, txn);
        replaceAllHotspotsOfFile(fileIssues, txn, fileEntity);
        txn.flush();
      });
    }));
  }

  @Override
  public void replaceAllHotspotsOfFile(String branchName, String serverFilePath, Collection serverHotspots) {
    timed("Wrote " + serverHotspots.size() + " hotspots in store", () -> entityStore.executeInTransaction(txn -> {
      var branch = getOrCreateBranch(branchName, txn);
      var fileEntity = getOrCreateFile(branch, serverFilePath, txn);
      replaceAllHotspotsOfFile(serverHotspots, txn, fileEntity);
    }));
  }

  @Override
  public boolean changeHotspotStatus(String hotspotKey, HotspotReviewStatus newStatus) {
    return entityStore.computeInTransaction(txn -> {
      var optionalEntity = findUnique(txn, HOTSPOT_ENTITY_TYPE, KEY_PROPERTY_NAME, hotspotKey);
      if (optionalEntity.isPresent()) {
        var hotspotEntity = optionalEntity.get();
        hotspotEntity.setProperty(REVIEW_STATUS_PROPERTY_NAME, newStatus);
        return true;
      }
      return false;
    });
  }

  private static void replaceAllHotspotsOfFile(Collection hotspots, @NotNull StoreTransaction txn, Entity fileEntity) {
    fileEntity.getLinks(FILE_TO_HOTSPOTS_LINK_NAME).forEach(Entity::delete);
    fileEntity.deleteLinks(FILE_TO_HOTSPOTS_LINK_NAME);

    hotspots.forEach(hotspot -> updateOrCreateHotspot(fileEntity, hotspot, txn));
  }

  private static void updateOrCreateHotspot(Entity fileEntity, ServerHotspot hotspot, StoreTransaction transaction) {
    var hotspotEntity = updateOrCreateIssueCommon(fileEntity, hotspot.getKey(), transaction, HOTSPOT_ENTITY_TYPE, FILE_TO_HOTSPOTS_LINK_NAME);
    updateHotspotEntity(hotspotEntity, hotspot);
  }

  private static void updateHotspotEntity(Entity issueEntity, ServerHotspot hotspot) {
    issueEntity.setProperty(RULE_KEY_PROPERTY_NAME, hotspot.getRuleKey());
    issueEntity.setBlobString(MESSAGE_BLOB_NAME, hotspot.getMessage());
    var textRange = hotspot.getTextRange();
    issueEntity.setProperty(START_LINE_PROPERTY_NAME, textRange.getStartLine());
    issueEntity.setProperty(START_LINE_OFFSET_PROPERTY_NAME, textRange.getStartLineOffset());
    issueEntity.setProperty(END_LINE_PROPERTY_NAME, textRange.getEndLine());
    issueEntity.setProperty(END_LINE_OFFSET_PROPERTY_NAME, textRange.getEndLineOffset());
    issueEntity.setProperty(CREATION_DATE_PROPERTY_NAME, hotspot.getCreationDate());
    issueEntity.setProperty(REVIEW_STATUS_PROPERTY_NAME, hotspot.getStatus());
    issueEntity.setProperty(VULNERABILITY_PROBABILITY_PROPERTY_NAME, hotspot.getVulnerabilityProbability().toString());
  }

  private static void deleteAllHotspotsOfFile(@NotNull StoreTransaction txn, Entity fileEntity) {
    replaceAllHotspotsOfFile(List.of(), txn, fileEntity);
  }

  private static void deleteAllIssuesOfFile(@NotNull StoreTransaction txn, Entity fileEntity) {
    replaceAllIssuesOfFile(List.of(), txn, fileEntity);
  }

  private static void timed(String msg, Runnable transaction) {
    var startTime = Instant.now();
    transaction.run();
    var duration = Duration.between(startTime, Instant.now());
    LOG.debug("{} | took {}ms", msg, duration.toMillis());
  }

  private static void replaceAllIssuesOfFile(List issues, @NotNull StoreTransaction txn, Entity fileEntity) {
    fileEntity.getLinks(FILE_TO_ISSUES_LINK_NAME).forEach(Entity::delete);
    fileEntity.deleteLinks(FILE_TO_ISSUES_LINK_NAME);

    issues.forEach(issue -> updateOrCreateIssue(fileEntity, issue, txn));
  }

  @Override
  public void replaceAllTaintOfFile(String branchName, String serverFilePath, List issues) {
    timed("Wrote " + issues.size() + " taint issues in store", () -> entityStore.executeInTransaction(txn -> {
      var branch = getOrCreateBranch(branchName, txn);
      var fileEntity = getOrCreateFile(branch, serverFilePath, txn);

      fileEntity.getLinks(FILE_TO_TAINT_ISSUES_LINK_NAME).forEach(Entity::delete);
      fileEntity.deleteLinks(FILE_TO_TAINT_ISSUES_LINK_NAME);

      issues.forEach(issue -> updateOrCreateTaintIssue(branch, fileEntity, issue, txn));
    }));
  }

  private static Entity getOrCreateBranch(String branchName, StoreTransaction txn) {
    return findUnique(txn, BRANCH_ENTITY_TYPE, NAME_PROPERTY_NAME, branchName)
      .orElseGet(() -> {
        var branch = txn.newEntity(BRANCH_ENTITY_TYPE);
        branch.setProperty(NAME_PROPERTY_NAME, branchName);
        return branch;
      });
  }

  private static Entity getOrCreateFile(Entity branchEntity, String filePath, StoreTransaction txn) {
    return findUniqueAmong(branchEntity.getLinks(BRANCH_TO_FILES_LINK_NAME), PATH_PROPERTY_NAME, filePath)
      .orElseGet(() -> {
        var file = txn.newEntity(FILE_ENTITY_TYPE);
        file.setProperty(PATH_PROPERTY_NAME, filePath);
        branchEntity.addLink(BRANCH_TO_FILES_LINK_NAME, file);
        return file;
      });
  }

  private static void updateOrCreateIssue(Entity fileEntity, ServerIssue issue, StoreTransaction transaction) {
    var issueEntity = updateOrCreateIssueCommon(fileEntity, issue.getKey(), transaction, ISSUE_ENTITY_TYPE, FILE_TO_ISSUES_LINK_NAME);
    updateIssueEntity(issueEntity, issue);
  }

  private static void updateIssueEntity(Entity issueEntity, ServerIssue issue) {
    issueEntity.setProperty(RESOLVED_PROPERTY_NAME, issue.isResolved());
    issueEntity.setProperty(RULE_KEY_PROPERTY_NAME, issue.getRuleKey());
    issueEntity.setBlobString(MESSAGE_BLOB_NAME, issue.getMessage());
    issueEntity.setProperty(CREATION_DATE_PROPERTY_NAME, issue.getCreationDate());
    var userSeverity = issue.getUserSeverity();
    if (userSeverity != null) {
      issueEntity.setProperty(USER_SEVERITY_PROPERTY_NAME, userSeverity);
    }
    issueEntity.setProperty(TYPE_PROPERTY_NAME, issue.getType());
    if (issue instanceof LineLevelServerIssue) {
      var lineIssue = (LineLevelServerIssue) issue;
      issueEntity.setBlobString(LINE_HASH_PROPERTY_NAME, lineIssue.getLineHash());
      issueEntity.setProperty(START_LINE_PROPERTY_NAME, lineIssue.getLine());
    } else if (issue instanceof RangeLevelServerIssue) {
      var rangeIssue = (RangeLevelServerIssue) issue;
      var textRange = rangeIssue.getTextRange();
      issueEntity.setProperty(START_LINE_PROPERTY_NAME, textRange.getStartLine());
      issueEntity.setProperty(START_LINE_OFFSET_PROPERTY_NAME, textRange.getStartLineOffset());
      issueEntity.setProperty(END_LINE_PROPERTY_NAME, textRange.getEndLine());
      issueEntity.setProperty(END_LINE_OFFSET_PROPERTY_NAME, textRange.getEndLineOffset());
      issueEntity.setBlobString(RANGE_HASH_PROPERTY_NAME, textRange.getHash());
    }
  }

  private static void updateOrCreateTaintIssue(Entity branchEntity, Entity fileEntity, ServerTaintIssue issue, StoreTransaction transaction) {
    var issueEntity = updateOrCreateIssueCommon(fileEntity, issue.getKey(), transaction, TAINT_ISSUE_ENTITY_TYPE, FILE_TO_TAINT_ISSUES_LINK_NAME);
    updateTaintIssueEntity(issue, issueEntity);
    branchEntity.addLink(BRANCH_TO_TAINT_ISSUES_LINK_NAME, issueEntity);
    issueEntity.setLink(TAINT_ISSUE_TO_BRANCH_LINK_NAME, branchEntity);
  }

  private static void updateTaintIssueEntity(ServerTaintIssue issue, Entity issueEntity) {
    issueEntity.setProperty(RESOLVED_PROPERTY_NAME, issue.isResolved());
    issueEntity.setProperty(RULE_KEY_PROPERTY_NAME, issue.getRuleKey());
    issueEntity.setBlobString(MESSAGE_BLOB_NAME, issue.getMessage());
    issueEntity.setProperty(CREATION_DATE_PROPERTY_NAME, issue.getCreationDate());
    issueEntity.setProperty(SEVERITY_PROPERTY_NAME, issue.getSeverity());
    issueEntity.setProperty(TYPE_PROPERTY_NAME, issue.getType());
    var textRange = issue.getTextRange();
    if (textRange != null) {
      issueEntity.setProperty(START_LINE_PROPERTY_NAME, textRange.getStartLine());
      issueEntity.setProperty(START_LINE_OFFSET_PROPERTY_NAME, textRange.getStartLineOffset());
      issueEntity.setProperty(END_LINE_PROPERTY_NAME, textRange.getEndLine());
      issueEntity.setProperty(END_LINE_OFFSET_PROPERTY_NAME, textRange.getEndLineOffset());
      issueEntity.setBlobString(RANGE_HASH_PROPERTY_NAME, textRange.getHash());
    }
    issueEntity.setBlob(FLOWS_BLOB_NAME, toProtoFlow(issue.getFlows()));
    var ruleDescriptionContextKey = issue.getRuleDescriptionContextKey();
    if (ruleDescriptionContextKey != null) {
      issueEntity.setProperty(RULE_DESCRIPTION_CONTEXT_KEY_PROPERTY_NAME, ruleDescriptionContextKey);
    }
  }

  private static InputStream toProtoFlow(List flows) {
    var buffer = new ByteArrayOutputStream();
    ProtobufUtil.writeMessages(buffer, flows.stream().map(XodusServerIssueStore::toProtoFlow).collect(Collectors.toList()));
    return new ByteArrayInputStream(buffer.toByteArray());
  }

  private static Entity updateOrCreateIssueCommon(Entity fileEntity, String issueKey, StoreTransaction transaction, String entityType, String fileToIssueLink) {
    var issueEntity = findUnique(transaction, entityType, KEY_PROPERTY_NAME, issueKey)
      .orElseGet(() -> transaction.newEntity(entityType));
    var oldFileEntity = issueEntity.getLink(ISSUE_TO_FILE_LINK_NAME);
    if (oldFileEntity != null && !fileEntity.equals(oldFileEntity)) {
      // issue might have moved file
      oldFileEntity.deleteLink(fileToIssueLink, issueEntity);
    }
    issueEntity.setLink(ISSUE_TO_FILE_LINK_NAME, fileEntity);
    fileEntity.addLink(fileToIssueLink, issueEntity);
    issueEntity.setProperty(KEY_PROPERTY_NAME, issueKey);
    return issueEntity;
  }

  private static Optional findUnique(StoreTransaction transaction, String entityType, String propertyName, String caseSensitivePropertyValue) {
    // the find is case-insensitive but we need an exact match
    var entities = transaction.find(entityType, propertyName, caseSensitivePropertyValue);
    return findUniqueAmong(entities, propertyName, caseSensitivePropertyValue);
  }

  private static Optional findUniqueAmong(EntityIterable iterable, String propertyName, String caseSensitivePropertyValue) {
    return StreamSupport.stream(iterable.spliterator(), false)
      .filter(e -> caseSensitivePropertyValue.equals(e.getProperty(propertyName)))
      .findFirst();
  }

  private static void remove(String issueKey, @NotNull StoreTransaction txn) {
    findUnique(txn, ISSUE_ENTITY_TYPE, KEY_PROPERTY_NAME, issueKey)
      .ifPresent(issueEntity -> {
        var fileEntity = issueEntity.getLink(ISSUE_TO_FILE_LINK_NAME);
        if (fileEntity != null) {
          fileEntity.deleteLink(FILE_TO_ISSUES_LINK_NAME, issueEntity);
        }
        issueEntity.deleteLinks(ISSUE_TO_FILE_LINK_NAME);
        issueEntity.delete();
      });
  }

  private static void removeTaint(String issueKey, @NotNull StoreTransaction txn) {
    findUnique(txn, TAINT_ISSUE_ENTITY_TYPE, KEY_PROPERTY_NAME, issueKey)
      .ifPresent(issueEntity -> {
        var fileEntity = issueEntity.getLink(ISSUE_TO_FILE_LINK_NAME);
        if (fileEntity != null) {
          fileEntity.deleteLink(FILE_TO_TAINT_ISSUES_LINK_NAME, issueEntity);
        }
        issueEntity.deleteLinks(ISSUE_TO_FILE_LINK_NAME);
        var branchEntity = issueEntity.getLink(TAINT_ISSUE_TO_BRANCH_LINK_NAME);
        if (branchEntity != null) {
          branchEntity.deleteLink(BRANCH_TO_TAINT_ISSUES_LINK_NAME, issueEntity);
        }
        issueEntity.deleteLinks(TAINT_ISSUE_TO_BRANCH_LINK_NAME);
        issueEntity.delete();
      });
  }

  @Override
  public boolean updateIssue(String issueKey, Consumer issueUpdater) {
    return entityStore.computeInTransaction(txn -> {
      var optionalEntity = findUnique(txn, ISSUE_ENTITY_TYPE, KEY_PROPERTY_NAME, issueKey);
      if (optionalEntity.isPresent()) {
        var issueEntity = optionalEntity.get();
        var currentIssue = adapt(issueEntity);
        issueUpdater.accept(currentIssue);
        updateIssueEntity(issueEntity, currentIssue);
        return true;
      }
      return false;
    });
  }

  @Override
  public void updateTaintIssue(String issueKey, Consumer taintIssueUpdater) {
    entityStore.executeInTransaction(txn -> findUnique(txn, TAINT_ISSUE_ENTITY_TYPE, KEY_PROPERTY_NAME, issueKey)
      .ifPresent(issueEntity -> {
        var currentIssue = adaptTaint(issueEntity);
        taintIssueUpdater.accept(currentIssue);
        updateTaintIssueEntity(currentIssue, issueEntity);
      }));
  }

  @Override
  public void insert(String branchName, ServerTaintIssue taintIssue) {
    entityStore.executeInTransaction(txn -> findUnique(txn, TAINT_ISSUE_ENTITY_TYPE, KEY_PROPERTY_NAME, taintIssue.getKey())
      .ifPresentOrElse(issueEntity -> LOG.error("Trying to store a taint vulnerability that already exists"), () -> {
        var branch = getOrCreateBranch(branchName, txn);
        var fileEntity = getOrCreateFile(branch, taintIssue.getFilePath(), txn);
        updateOrCreateTaintIssue(branch, fileEntity, taintIssue, txn);
      }));
  }

  @Override
  public void deleteTaintIssue(String issueKeyToDelete) {
    entityStore.executeInTransaction(txn -> removeTaint(issueKeyToDelete, txn));
  }

  @Override
  public void close() {
    backup();
    entityStore.close();
    FileUtils.deleteQuietly(xodusDbDir.toFile());
  }

  public void backup() {
    LOG.debug("Creating backup of server issue database in {}", backupFile);
    try {
      var backupTmp = CompressBackupUtil.backup(entityStore, backupFile.getParent().toFile(), "backup", false);
      Files.move(backupTmp.toPath(), backupFile, StandardCopyOption.ATOMIC_MOVE);
    } catch (Exception e) {
      LOG.error("Unable to backup server issue database", e);
    }
  }

  private static Flow toJavaFlow(Sonarlint.Flow flowProto) {
    return new Flow(flowProto.getLocationList().stream().map(XodusServerIssueStore::toJavaLocation).collect(Collectors.toList()));
  }

  private static ServerIssueLocation toJavaLocation(Location locationProto) {
    return new ServerIssueLocation(locationProto.hasFilePath() ? locationProto.getFilePath() : null,
      locationProto.hasTextRange() ? toTextRangeJava(locationProto.getTextRange()) : null, locationProto.getMessage());
  }

  private static TextRangeWithHash toTextRangeJava(TextRange textRange) {
    return new TextRangeWithHash(textRange.getStartLine(), textRange.getStartLineOffset(), textRange.getEndLine(), textRange.getEndLineOffset(), textRange.getHash());
  }

  private static Sonarlint.Flow toProtoFlow(Flow javaFlow) {
    var flowBuilder = Sonarlint.Flow.newBuilder();
    javaFlow.locations().forEach(l -> flowBuilder.addLocation(toProtoLocation(l)));
    return flowBuilder.build();
  }

  private static Location toProtoLocation(ServerIssueLocation l) {
    var location = Location.newBuilder();
    String filePath = l.getFilePath();
    if (filePath != null) {
      location.setFilePath(filePath);
    }
    location.setMessage(l.getMessage());
    var textRange = l.getTextRange();
    if (textRange != null) {
      location.setTextRange(TextRange.newBuilder()
        .setStartLine(textRange.getStartLine())
        .setStartLineOffset(textRange.getStartLineOffset())
        .setEndLine(textRange.getEndLine())
        .setEndLineOffset(textRange.getEndLineOffset())
        .setHash(textRange.getHash()));
    }
    return location.build();
  }

  static void checkCurrentSchemaVersion(StoreTransaction txn) {
    var currentSchemaVersion = getCurrentSchemaVersion(txn);
    if (currentSchemaVersion < CURRENT_SCHEMA_VERSION) {
      // Migrate v0 to v1: force re-sync of taint vulnerabilities
      txn.getAll(BRANCH_ENTITY_TYPE).forEach(b -> b.setProperty(LAST_TAINT_SYNC_PROPERTY_NAME, Instant.EPOCH));

      // Set schema version to current after migration(s)
      txn.getAll(SCHEMA_ENTITY_TYPE).forEach(Entity::delete);
      var newSchema = txn.newEntity(SCHEMA_ENTITY_TYPE);
      newSchema.setProperty(VERSION_PROPERTY_NAME, CURRENT_SCHEMA_VERSION);
      txn.saveEntity(newSchema);
      txn.flush();
    }
  }

  static int getCurrentSchemaVersion(StoreTransaction txn) {
    var schemaEntities = txn.getAll(SCHEMA_ENTITY_TYPE);
    var schemaEntitiesCount = schemaEntities.size();
    if (schemaEntitiesCount == 1) {
      var schemaEntity = schemaEntities.getFirst();
      var schemaVersion = schemaEntity.getProperty(VERSION_PROPERTY_NAME);
      if (schemaVersion == null) {
        return 0;
      }
      return (Integer) schemaVersion;
    } else {
      // If there are 0 or more than 1 entries, then we need to wipe
      return 0;
    }
  }

  int getCurrentSchemaVersion() {
    return entityStore.computeInTransaction(XodusServerIssueStore::getCurrentSchemaVersion);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy