From 0847774db59344316629a7171c3943dbfaa3f52d Mon Sep 17 00:00:00 2001 From: Duarte Meneses Date: Fri, 18 Sep 2015 11:43:24 +0200 Subject: [PATCH] SONAR-6777 Project cache sync --- .../src/test/java/batch/IssuesModeTest.java | 2 +- .../batch/bootstrap/GlobalContainer.java | 6 +- .../sonar/batch/bootstrap/ServerClient.java | 21 --- .../cache/DefaultProjectCacheStatus.java | 52 +++---- ...ava => GlobalPersistentCacheProvider.java} | 42 ++---- .../cache/NonAssociatedCacheSynchronizer.java | 13 +- .../sonar/batch/cache/ProjectCacheStatus.java | 8 +- .../batch/cache/ProjectCacheSynchronizer.java | 12 +- .../sonar/batch/cache/ProjectKeySupplier.java | 36 +++++ .../cache/ProjectPersistentCacheProvider.java | 67 +++++++++ .../batch/cache/ProjectSyncContainer.java | 14 +- .../java/org/sonar/batch/cache/WSLoader.java | 130 +++++++++++------ .../repository/DefaultServerIssuesLoader.java | 19 +-- .../repository/user/UserRepositoryLoader.java | 32 ++-- .../sonar/batch/rule/DefaultRulesLoader.java | 14 +- .../org/sonar/batch/scan/ProjectLock.java | 59 +------- .../batch/scan/ProjectScanContainer.java | 4 +- .../java/org/sonar/batch/util/BatchUtils.java | 24 ++- .../AnalysisWSLoaderProviderTest.java | 2 +- .../bootstrap/BatchPluginInstallerTest.java | 2 +- .../cache/DefaultProjectCacheStatusTest.java | 73 +++------ ...=> GlobalPersistentCacheProviderTest.java} | 53 +++---- .../NonAssociatedCacheSynchronizerTest.java | 8 +- .../cache/ProjectCacheSynchronizerTest.java | 41 ++++-- .../ProjectPersistentCacheProviderTest.java | 82 +++++++++++ .../cache/StrategyWSLoaderProviderTest.java | 2 +- .../org/sonar/batch/cache/WSLoaderTest.java | 77 +++++++--- .../batch/mediumtest/BatchMediumTester.java | 22 +-- .../DefaultServerIssuesLoaderTest.java | 15 +- .../user/UserRepositoryLoaderTest.java | 32 ++-- .../batch/rule/DefaultRulesLoaderTest.java | 19 ++- .../org/sonar/batch/scan/ProjectLockTest.java | 2 +- .../cache/DeleteFileOnCloseInputStream.java | 84 +++++++++++ .../org/sonar/home/cache/DirectoryLock.java | 106 ++++++++++++++ .../org/sonar/home/cache/PersistentCache.java | 138 ++++-------------- .../home/cache/PersistentCacheBuilder.java | 64 ++++++-- .../home/cache/UnlockOnCloseInputStream.java | 82 +++++++++++ .../sonar/home/cache/DirectoryLockTest.java | 94 ++++++++++++ .../cache/PersistentCacheBuilderTest.java | 37 +++-- .../sonar/home/cache/PersistentCacheTest.java | 107 +++++--------- .../sonar/api/batch/bootstrap/ProjectKey.java | 8 +- .../api/batch/bootstrap/ProjectReactor.java | 10 +- 42 files changed, 1108 insertions(+), 607 deletions(-) rename sonar-batch/src/main/java/org/sonar/batch/cache/{PersistentCacheProvider.java => GlobalPersistentCacheProvider.java} (60%) create mode 100644 sonar-batch/src/main/java/org/sonar/batch/cache/ProjectKeySupplier.java create mode 100644 sonar-batch/src/main/java/org/sonar/batch/cache/ProjectPersistentCacheProvider.java rename sonar-batch/src/test/java/org/sonar/batch/cache/{PersistentCacheProviderTest.java => GlobalPersistentCacheProviderTest.java} (58%) create mode 100644 sonar-batch/src/test/java/org/sonar/batch/cache/ProjectPersistentCacheProviderTest.java create mode 100644 sonar-home/src/main/java/org/sonar/home/cache/DeleteFileOnCloseInputStream.java create mode 100644 sonar-home/src/main/java/org/sonar/home/cache/DirectoryLock.java create mode 100644 sonar-home/src/main/java/org/sonar/home/cache/UnlockOnCloseInputStream.java create mode 100644 sonar-home/src/test/java/org/sonar/home/cache/DirectoryLockTest.java rename sonar-home/src/main/java/org/sonar/home/cache/PersistentCacheLoader.java => sonar-plugin-api/src/main/java/org/sonar/api/batch/bootstrap/ProjectKey.java (86%) diff --git a/it/it-tests/src/test/java/batch/IssuesModeTest.java b/it/it-tests/src/test/java/batch/IssuesModeTest.java index 0935af6c35f..7ff66112e3c 100644 --- a/it/it-tests/src/test/java/batch/IssuesModeTest.java +++ b/it/it-tests/src/test/java/batch/IssuesModeTest.java @@ -71,7 +71,7 @@ public class IssuesModeTest { restoreProfile("one-issue-per-line.xml"); orchestrator.getServer().provisionProject("sample", "xoo-sample"); orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line"); - SonarRunner runner = configureRunnerIssues("shared/xoo-sample"); + SonarRunner runner = configureRunnerIssues("shared/xoo-sample", "sonar.verbose", "true"); BuildResult result = orchestrator.executeBuild(runner); assertThat(ItUtils.countIssuesInJsonReport(result, true)).isEqualTo(17); } diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/GlobalContainer.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/GlobalContainer.java index 2a802f6be1b..30b54799632 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/GlobalContainer.java +++ b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/GlobalContainer.java @@ -28,7 +28,7 @@ import org.sonar.api.utils.System2; import org.sonar.api.utils.UriReader; import org.sonar.batch.analysis.AnalysisProperties; import org.sonar.batch.analysis.DefaultAnalysisMode; -import org.sonar.batch.cache.PersistentCacheProvider; +import org.sonar.batch.cache.GlobalPersistentCacheProvider; import org.sonar.batch.cache.ProjectSyncContainer; import org.sonar.batch.cache.StrategyWSLoaderProvider; import org.sonar.batch.cache.WSLoader.LoadStrategy; @@ -85,7 +85,7 @@ public class GlobalContainer extends ComponentContainer { BatchPluginPredicate.class, ExtensionInstaller.class, - CachesManager.class, + CachesManager.class, GlobalMode.class, GlobalSettings.class, new RulesProvider(), @@ -95,7 +95,7 @@ public class GlobalContainer extends ComponentContainer { DefaultHttpDownloader.class, UriReader.class, new FileCacheProvider(), - new PersistentCacheProvider(), + new GlobalPersistentCacheProvider(), System2.INSTANCE, new GlobalRepositoriesProvider(), UuidFactoryImpl.INSTANCE); diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/ServerClient.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/ServerClient.java index 901e2d2cac2..79a6257a504 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/ServerClient.java +++ b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/ServerClient.java @@ -19,9 +19,6 @@ */ package org.sonar.batch.bootstrap; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Strings; @@ -30,13 +27,10 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.ArrayList; @@ -62,7 +56,6 @@ import org.sonar.core.util.DefaultHttpDownloader; @BatchSide public class ServerClient { private static final String GET = "GET"; - private static final Logger LOG = LoggerFactory.getLogger(ServerClient.class); private GlobalProperties props; private DefaultHttpDownloader.BaseHttpDownloader downloader; @@ -74,20 +67,6 @@ public class ServerClient { public String getURL() { return StringUtils.removeEnd(StringUtils.defaultIfBlank(props.property("sonar.host.url"), "http://localhost:9000"), "/"); } - - public String getServerVersion() { - InputStream is = this.getClass().getClassLoader().getResourceAsStream("sq-version.txt"); - if (is == null) { - LOG.warn("Failed to get SQ version"); - return null; - } - try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { - return br.readLine(); - } catch (IOException e) { - LOG.warn("Failed to get SQ version", e); - return null; - } - } public URI getURI(String pathStartingWithSlash) { Preconditions.checkArgument(pathStartingWithSlash.startsWith("/"), "Path must start with slash /: " + pathStartingWithSlash); diff --git a/sonar-batch/src/main/java/org/sonar/batch/cache/DefaultProjectCacheStatus.java b/sonar-batch/src/main/java/org/sonar/batch/cache/DefaultProjectCacheStatus.java index 248d2b8b350..3ecf425d11a 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/cache/DefaultProjectCacheStatus.java +++ b/sonar-batch/src/main/java/org/sonar/batch/cache/DefaultProjectCacheStatus.java @@ -19,73 +19,67 @@ */ package org.sonar.batch.cache; -import javax.annotation.Nullable; +import org.apache.commons.io.FileUtils; -import org.sonar.batch.bootstrap.ServerClient; import org.sonar.home.cache.PersistentCache; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Date; public class DefaultProjectCacheStatus implements ProjectCacheStatus { - private static final String STATUS_PREFIX = "cache-sync-status-"; + private static final String STATUS_FILENAME = "cache-sync-status"; private PersistentCache cache; - private ServerClient client; - public DefaultProjectCacheStatus(PersistentCache cache, ServerClient client) { + public DefaultProjectCacheStatus(PersistentCache cache) { this.cache = cache; - this.client = client; } @Override - public void save(@Nullable String projectKey) { + public void save() { Date now = new Date(); try { - ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); - try (ObjectOutputStream objOutput = new ObjectOutputStream(byteOutput)) { + FileOutputStream fos = new FileOutputStream(getStatusFilePath().toFile()); + try (ObjectOutputStream objOutput = new ObjectOutputStream(fos)) { objOutput.writeObject(now); } - cache.put(getKey(projectKey), byteOutput.toByteArray()); + } catch (IOException e) { throw new IllegalStateException("Failed to write cache sync status", e); } } @Override - public void delete(@Nullable String projectKey) { - try { - cache.put(getKey(projectKey), new byte[0]); - } catch (IOException e) { - throw new IllegalStateException("Failed to delete cache sync status", e); - } + public void delete() { + cache.clear(); + FileUtils.deleteQuietly(getStatusFilePath().toFile()); } @Override - public Date getSyncStatus(@Nullable String projectKey) { + public Date getSyncStatus() { + Path p = getStatusFilePath(); try { - byte[] status = cache.get(getKey(projectKey), null); - if (status == null || status.length == 0) { + if (!Files.isRegularFile(p)) { return null; } - ByteArrayInputStream byteInput = new ByteArrayInputStream(status); - try (ObjectInputStream objInput = new ObjectInputStream(byteInput)) { + InputStream is = new FileInputStream(p.toFile()); + try (ObjectInputStream objInput = new ObjectInputStream(is)) { return (Date) objInput.readObject(); } } catch (IOException | ClassNotFoundException e) { + FileUtils.deleteQuietly(p.toFile()); throw new IllegalStateException("Failed to read cache sync status", e); } } - private String getKey(@Nullable String projectKey) { - if (projectKey != null) { - return STATUS_PREFIX + client.getURL() + "-" + client.getServerVersion() + "-" + projectKey; - } else { - return STATUS_PREFIX + client.getURL() + "-" + client.getServerVersion(); - } + private Path getStatusFilePath() { + return cache.getDirectory().resolve(STATUS_FILENAME); } } diff --git a/sonar-batch/src/main/java/org/sonar/batch/cache/PersistentCacheProvider.java b/sonar-batch/src/main/java/org/sonar/batch/cache/GlobalPersistentCacheProvider.java similarity index 60% rename from sonar-batch/src/main/java/org/sonar/batch/cache/PersistentCacheProvider.java rename to sonar-batch/src/main/java/org/sonar/batch/cache/GlobalPersistentCacheProvider.java index 99040ba9927..29e2f42ea38 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/cache/PersistentCacheProvider.java +++ b/sonar-batch/src/main/java/org/sonar/batch/cache/GlobalPersistentCacheProvider.java @@ -19,54 +19,38 @@ */ package org.sonar.batch.cache; +import org.apache.commons.lang.StringUtils; import org.sonar.batch.bootstrap.Slf4jLogger; -import org.sonar.batch.bootstrap.UserProperties; - -import org.sonar.api.utils.log.Logger; -import org.sonar.api.utils.log.Loggers; +import org.sonar.batch.util.BatchUtils; +import org.sonar.home.cache.PersistentCacheBuilder; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; import java.nio.file.Paths; -import org.picocontainer.injectors.ProviderAdapter; +import org.sonar.batch.bootstrap.GlobalProperties; import org.sonar.home.cache.PersistentCache; -import org.sonar.home.cache.PersistentCacheBuilder; +import org.picocontainer.injectors.ProviderAdapter; -public class PersistentCacheProvider extends ProviderAdapter { - private static final Logger LOG = Loggers.get(PersistentCacheProvider.class); +public class GlobalPersistentCacheProvider extends ProviderAdapter { private PersistentCache cache; - public PersistentCache provide(UserProperties props) { + public PersistentCache provide(GlobalProperties props) { if (cache == null) { PersistentCacheBuilder builder = new PersistentCacheBuilder(new Slf4jLogger()); - String home = props.property("sonar.userHome"); + String serverUrl = getServerUrl(props); + if (home != null) { builder.setSonarHome(Paths.get(home)); } - - builder.setVersion(getServerVersion()); + + builder.setAreaForGlobal(serverUrl, BatchUtils.getServerVersion()); cache = builder.build(); } return cache; } - private String getServerVersion() { - InputStream is = this.getClass().getClassLoader().getResourceAsStream("sq-version.txt"); - if (is == null) { - LOG.warn("Failed to get SQ version"); - return null; - } - try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { - return br.readLine(); - } catch (IOException e) { - LOG.warn("Failed to get SQ version", e); - return null; - } + private String getServerUrl(GlobalProperties props) { + return StringUtils.removeEnd(StringUtils.defaultIfBlank(props.property("sonar.host.url"), "http://localhost:9000"), "/"); } } diff --git a/sonar-batch/src/main/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizer.java b/sonar-batch/src/main/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizer.java index e21d1420694..44743f5c120 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizer.java +++ b/sonar-batch/src/main/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizer.java @@ -46,7 +46,7 @@ public class NonAssociatedCacheSynchronizer { } public void execute(boolean force) { - Date lastSync = cacheStatus.getSyncStatus(null); + Date lastSync = cacheStatus.getSyncStatus(); if (lastSync != null) { if (!force) { @@ -55,13 +55,15 @@ public class NonAssociatedCacheSynchronizer { } else { LOG.info("-- Found cache [{}], synchronizing data..", lastSync); } - cacheStatus.delete(null); + cacheStatus.delete(); } else { LOG.info("-- Cache not found, synchronizing data.."); } loadData(); - saveStatus(); + + cacheStatus.save(); + LOG.info("-- Succesfully synchronized cache"); } private static Collection getKeys(Collection qProfiles) { @@ -73,11 +75,6 @@ public class NonAssociatedCacheSynchronizer { return list; } - private void saveStatus() { - cacheStatus.save(null); - LOG.info("-- Succesfully synchronized cache"); - } - private void loadData() { Profiler profiler = Profiler.create(Loggers.get(ProjectCacheSynchronizer.class)); diff --git a/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectCacheStatus.java b/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectCacheStatus.java index a5f97349a4b..39947c64e3b 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectCacheStatus.java +++ b/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectCacheStatus.java @@ -19,14 +19,12 @@ */ package org.sonar.batch.cache; -import javax.annotation.Nullable; - import java.util.Date; public interface ProjectCacheStatus { - void save(@Nullable String projectKey); + void save(); - void delete(@Nullable String projectKey); + void delete(); - Date getSyncStatus(@Nullable String projectKey); + Date getSyncStatus(); } diff --git a/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectCacheSynchronizer.java b/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectCacheSynchronizer.java index d2d6cbcf2db..20d16fbaaad 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectCacheSynchronizer.java +++ b/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectCacheSynchronizer.java @@ -63,7 +63,7 @@ public class ProjectCacheSynchronizer { } public void load(String projectKey, boolean force) { - Date lastSync = cacheStatus.getSyncStatus(projectKey); + Date lastSync = cacheStatus.getSyncStatus(); if (lastSync != null) { if (!force) { @@ -72,17 +72,17 @@ public class ProjectCacheSynchronizer { } else { LOG.info("-- Found project [{}] cache [{}], synchronizing data..", projectKey, lastSync); } - cacheStatus.delete(projectKey); + cacheStatus.delete(); } else { LOG.info("-- Cache for project [{}] not found, synchronizing data..", projectKey); } loadData(projectKey); - saveStatus(projectKey); + saveStatus(); } - private void saveStatus(String projectKey) { - cacheStatus.save(projectKey); + private void saveStatus() { + cacheStatus.save(); LOG.info("-- Succesfully synchronized project cache"); } @@ -103,7 +103,7 @@ public class ProjectCacheSynchronizer { profiler.stopInfo(); Collection profileKeys = getKeys(qProfiles); - + profiler.startInfo("Load project active rules"); activeRulesLoader.load(profileKeys, projectKey); profiler.stopInfo(); diff --git a/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectKeySupplier.java b/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectKeySupplier.java new file mode 100644 index 00000000000..78afcee1ac3 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectKeySupplier.java @@ -0,0 +1,36 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cache; + +import org.sonar.api.batch.bootstrap.ProjectKey; + +public class ProjectKeySupplier implements ProjectKey { + private final String key; + + ProjectKeySupplier(String key) { + this.key = key; + } + + @Override + public String get() { + return key; + } + +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectPersistentCacheProvider.java b/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectPersistentCacheProvider.java new file mode 100644 index 00000000000..49684faff18 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectPersistentCacheProvider.java @@ -0,0 +1,67 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cache; + +import org.sonar.api.batch.bootstrap.ProjectKey; + +import org.sonar.batch.util.BatchUtils; +import org.apache.commons.lang.StringUtils; +import org.sonar.batch.bootstrap.GlobalProperties; +import com.google.common.base.Preconditions; +import org.sonar.batch.analysis.DefaultAnalysisMode; +import org.sonar.batch.bootstrap.Slf4jLogger; + +import java.nio.file.Paths; + +import org.picocontainer.injectors.ProviderAdapter; +import org.sonar.home.cache.PersistentCache; +import org.sonar.home.cache.PersistentCacheBuilder; + +public class ProjectPersistentCacheProvider extends ProviderAdapter { + private PersistentCache cache; + + public PersistentCache provide(GlobalProperties props, DefaultAnalysisMode mode, ProjectKey key) { + if (cache == null) { + PersistentCacheBuilder builder = new PersistentCacheBuilder(new Slf4jLogger()); + String projectKey = key.get(); + String home = props.property("sonar.userHome"); + String serverUrl = getServerUrl(props); + + if (home != null) { + builder.setSonarHome(Paths.get(home)); + } + + if (mode.isNotAssociated()) { + builder.setAreaForLocalProject(serverUrl, BatchUtils.getServerVersion()); + } else { + Preconditions.checkNotNull(projectKey); + builder.setAreaForProject(serverUrl, BatchUtils.getServerVersion(), projectKey); + } + + cache = builder.build(); + } + + return cache; + } + + private String getServerUrl(GlobalProperties props) { + return StringUtils.removeEnd(StringUtils.defaultIfBlank(props.property("sonar.host.url"), "http://localhost:9000"), "/"); + } +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectSyncContainer.java b/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectSyncContainer.java index 600a6791c3a..79be9383f10 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectSyncContainer.java +++ b/sonar-batch/src/main/java/org/sonar/batch/cache/ProjectSyncContainer.java @@ -24,8 +24,8 @@ import javax.annotation.Nullable; import org.sonar.batch.repository.ProjectRepositoriesFactoryProvider; import org.sonar.batch.analysis.DefaultAnalysisMode; import org.sonar.api.CoreProperties; -import com.google.common.collect.ImmutableMap; +import java.util.HashMap; import java.util.Map; import org.sonar.batch.analysis.AnalysisProperties; @@ -68,8 +68,12 @@ public class ProjectSyncContainer extends ComponentContainer { } } - private static DefaultAnalysisMode createIssuesAnalysisMode() { - Map props = ImmutableMap.of(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES); + private static DefaultAnalysisMode createIssuesAnalysisMode(@Nullable String projectKey) { + Map props = new HashMap<>(); + props.put(CoreProperties.ANALYSIS_MODE, CoreProperties.ANALYSIS_MODE_ISSUES); + if (projectKey != null) { + props.put(CoreProperties.PROJECT_KEY_PROPERTY, projectKey); + } GlobalProperties globalProps = new GlobalProperties(props); AnalysisProperties analysisProps = new AnalysisProperties(props); return new DefaultAnalysisMode(globalProps, analysisProps); @@ -77,10 +81,12 @@ public class ProjectSyncContainer extends ComponentContainer { private void addComponents() { add(new StrategyWSLoaderProvider(LoadStrategy.SERVER_ONLY), + new ProjectKeySupplier(projectKey), projectKey != null ? ProjectCacheSynchronizer.class : NonAssociatedCacheSynchronizer.class, UserRepositoryLoader.class, new ProjectRepositoriesFactoryProvider(projectKey), - createIssuesAnalysisMode()); + new ProjectPersistentCacheProvider(), + createIssuesAnalysisMode(projectKey)); addIfMissing(DefaultProjectCacheStatus.class, ProjectCacheStatus.class); addIfMissing(DefaultProjectRepositoriesLoader.class, ProjectRepositoriesLoader.class); diff --git a/sonar-batch/src/main/java/org/sonar/batch/cache/WSLoader.java b/sonar-batch/src/main/java/org/sonar/batch/cache/WSLoader.java index 06b38f7b607..96a83811d08 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/cache/WSLoader.java +++ b/sonar-batch/src/main/java/org/sonar/batch/cache/WSLoader.java @@ -19,18 +19,19 @@ */ package org.sonar.batch.cache; -import com.google.common.io.ByteSource; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; + import javax.annotation.Nonnull; +import javax.annotation.Nullable; + import org.apache.commons.io.IOUtils; import org.sonar.api.utils.HttpDownloader; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.batch.bootstrap.ServerClient; import org.sonar.home.cache.PersistentCache; - import static org.sonar.batch.cache.WSLoader.ServerStatus.ACCESSIBLE; import static org.sonar.batch.cache.WSLoader.ServerStatus.NOT_ACCESSIBLE; import static org.sonar.batch.cache.WSLoader.ServerStatus.UNKNOWN; @@ -63,9 +64,8 @@ public class WSLoader { } @Nonnull - public WSLoaderResult loadSource(String id) { - WSLoaderResult byteResult = load(id, defautLoadStrategy); - return new WSLoaderResult(ByteSource.wrap(byteResult.get()), byteResult.isFromCache()); + public WSLoaderResult loadStream(String id) { + return load(id, defautLoadStrategy, createStreamLoaderServer(), createStreamLoaderCache()); } @Nonnull @@ -75,31 +75,25 @@ public class WSLoader { @Nonnull public WSLoaderResult loadString(String id, WSLoader.LoadStrategy strategy) { - WSLoaderResult byteResult = load(id, strategy); - return new WSLoaderResult(new String(byteResult.get(), StandardCharsets.UTF_8), byteResult.isFromCache()); - } - - @Nonnull - public WSLoaderResult load(String id) { - return load(id, defautLoadStrategy); + return load(id, strategy, createStringLoaderServer(), createStringLoaderCache()); } @Nonnull - public WSLoaderResult load(String id, WSLoader.LoadStrategy strategy) { + private WSLoaderResult load(String id, WSLoader.LoadStrategy strategy, DataLoader serverLoader, DataLoader cacheLoader) { switch (strategy) { case CACHE_FIRST: - return loadFromCacheFirst(id, true); + return loadFromCacheFirst(id, cacheLoader, serverLoader); case CACHE_ONLY: - return loadFromCacheFirst(id, false); + return loadFromCacheFirst(id, cacheLoader, null); case SERVER_FIRST: - return loadFromServerFirst(id, true); + return loadFromServerFirst(id, serverLoader, cacheLoader); case SERVER_ONLY: default: - return loadFromServerFirst(id, false); + return loadFromServerFirst(id, serverLoader, null); } } - public LoadStrategy getStrategy() { + public LoadStrategy getDefaultStrategy() { return this.defautLoadStrategy; } @@ -116,22 +110,14 @@ public class WSLoader { return serverStatus == NOT_ACCESSIBLE; } - private void updateCache(String id, byte[] value) { - try { - cache.put(client.getURI(id).toString(), value); - } catch (IOException e) { - throw new IllegalStateException("Error saving to WS cache", e); - } - } - @Nonnull - private WSLoaderResult loadFromCacheFirst(String id, boolean fallback) { + private WSLoaderResult loadFromCacheFirst(String id, DataLoader cacheLoader, @Nullable DataLoader serverLoader) { try { - return loadFromCache(id); + return loadFromCache(id, cacheLoader); } catch (NotAvailableException cacheNotAvailable) { - if (fallback) { + if (serverLoader != null) { try { - return loadFromServer(id); + return loadFromServer(id, serverLoader); } catch (NotAvailableException serverNotAvailable) { throw new IllegalStateException(FAIL_MSG, serverNotAvailable.getCause()); } @@ -141,13 +127,13 @@ public class WSLoader { } @Nonnull - private WSLoaderResult loadFromServerFirst(String id, boolean fallback) { + private WSLoaderResult loadFromServerFirst(String id, DataLoader serverLoader, @Nullable DataLoader cacheLoader) { try { - return loadFromServer(id); + return loadFromServer(id, serverLoader); } catch (NotAvailableException serverNotAvailable) { - if (fallback) { + if (cacheLoader != null) { try { - return loadFromCache(id); + return loadFromCache(id, cacheLoader); } catch (NotAvailableException cacheNotAvailable) { throw new IllegalStateException(FAIL_MSG, serverNotAvailable.getCause()); } @@ -156,31 +142,35 @@ public class WSLoader { } } + interface DataLoader { + T load(String id) throws IOException; + } + @Nonnull - private WSLoaderResult loadFromCache(String id) throws NotAvailableException { + private WSLoaderResult loadFromCache(String id, DataLoader loader) throws NotAvailableException { + T result = null; + try { - byte[] result = cache.get(client.getURI(id).toString(), null); - if (result == null) { - throw new NotAvailableException("resource not cached"); - } - return new WSLoaderResult(result, true); + result = loader.load(id); } catch (IOException e) { // any exception on the cache should fail fast throw new IllegalStateException(e); } + if (result == null) { + throw new NotAvailableException("resource not cached"); + } + return new WSLoaderResult(result, true); } @Nonnull - private WSLoaderResult loadFromServer(String id) throws NotAvailableException { + private WSLoaderResult loadFromServer(String id, DataLoader loader) throws NotAvailableException { if (isOffline()) { throw new NotAvailableException("Server not available"); } try { - InputStream is = client.load(id, REQUEST_METHOD, true, CONNECT_TIMEOUT, READ_TIMEOUT); + T t = loader.load(id); switchToOnline(); - byte[] value = IOUtils.toByteArray(is); - updateCache(id, value); - return new WSLoaderResult(value, false); + return new WSLoaderResult(t, false); } catch (IllegalStateException e) { if (e.getCause() instanceof HttpDownloader.HttpException) { // fail fast if it could connect but there was a application-level error @@ -194,6 +184,56 @@ public class WSLoader { } } + private DataLoader createStringLoaderServer() { + return new DataLoader() { + @Override + public String load(String id) throws IOException { + InputStream is = client.load(id, REQUEST_METHOD, true, CONNECT_TIMEOUT, READ_TIMEOUT); + String str = IOUtils.toString(is, StandardCharsets.UTF_8); + try { + cache.put(id, str.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new IllegalStateException("Error saving to WS cache", e); + } + return str; + } + }; + } + + private DataLoader createStringLoaderCache() { + return new DataLoader() { + @Override + public String load(String id) throws IOException { + return cache.getString(id); + } + }; + } + + private DataLoader createStreamLoaderServer() { + return new DataLoader() { + @Override + public InputStream load(String id) throws IOException { + InputStream is = client.load(id, REQUEST_METHOD, true, CONNECT_TIMEOUT, READ_TIMEOUT); + try { + cache.put(id, is); + } catch (IOException e) { + throw new IllegalStateException("Error saving to WS cache", e); + } + is.close(); + return cache.getStream(id); + } + }; + } + + private DataLoader createStreamLoaderCache() { + return new DataLoader() { + @Override + public InputStream load(String id) throws IOException { + return cache.getStream(id); + } + }; + } + private class NotAvailableException extends Exception { private static final long serialVersionUID = 1L; diff --git a/sonar-batch/src/main/java/org/sonar/batch/repository/DefaultServerIssuesLoader.java b/sonar-batch/src/main/java/org/sonar/batch/repository/DefaultServerIssuesLoader.java index aefe0569275..0989cd5e616 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/repository/DefaultServerIssuesLoader.java +++ b/sonar-batch/src/main/java/org/sonar/batch/repository/DefaultServerIssuesLoader.java @@ -19,16 +19,15 @@ */ package org.sonar.batch.repository; -import org.sonar.batch.cache.WSLoaderResult; - -import org.sonar.batch.cache.WSLoader; -import org.sonar.batch.util.BatchUtils; -import com.google.common.io.ByteSource; import com.google.common.base.Function; -import org.sonar.batch.protocol.input.BatchInput.ServerIssue; - +import com.google.common.io.ByteSource; import java.io.IOException; import java.io.InputStream; +import org.apache.commons.io.IOUtils; +import org.sonar.batch.cache.WSLoader; +import org.sonar.batch.cache.WSLoaderResult; +import org.sonar.batch.protocol.input.BatchInput.ServerIssue; +import org.sonar.batch.util.BatchUtils; public class DefaultServerIssuesLoader implements ServerIssuesLoader { @@ -45,8 +44,8 @@ public class DefaultServerIssuesLoader implements ServerIssuesLoader { return result.isFromCache(); } - private static void parseIssues(ByteSource input, Function consumer) { - try (InputStream is = input.openBufferedStream()) { + private static void parseIssues(InputStream is, Function consumer) { + try { ServerIssue previousIssue = ServerIssue.parseDelimitedFrom(is); while (previousIssue != null) { consumer.apply(previousIssue); @@ -54,6 +53,8 @@ public class DefaultServerIssuesLoader implements ServerIssuesLoader { } } catch (IOException e) { throw new IllegalStateException("Unable to get previous issues", e); + } finally { + IOUtils.closeQuietly(is); } } } diff --git a/sonar-batch/src/main/java/org/sonar/batch/repository/user/UserRepositoryLoader.java b/sonar-batch/src/main/java/org/sonar/batch/repository/user/UserRepositoryLoader.java index d40f918f87b..a7b9dc71d4a 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/repository/user/UserRepositoryLoader.java +++ b/sonar-batch/src/main/java/org/sonar/batch/repository/user/UserRepositoryLoader.java @@ -19,8 +19,9 @@ */ package org.sonar.batch.repository.user; -import org.sonar.batch.cache.WSLoaderResult; +import org.apache.commons.io.IOUtils; +import org.sonar.batch.cache.WSLoaderResult; import org.sonar.batch.cache.WSLoader; import javax.annotation.Nullable; @@ -29,7 +30,6 @@ import org.apache.commons.lang.mutable.MutableBoolean; import com.google.common.collect.Lists; import com.google.common.base.Joiner; import org.sonar.batch.util.BatchUtils; -import com.google.common.io.ByteSource; import com.google.common.base.Function; import org.sonar.batch.protocol.input.BatchInput; @@ -50,16 +50,16 @@ public class UserRepositoryLoader { public BatchInput.User load(String userLogin) { return load(userLogin, null); } - + public BatchInput.User load(String userLogin, @Nullable MutableBoolean fromCache) { - ByteSource byteSource = loadQuery(new UserEncodingFunction().apply(userLogin), fromCache); - return parseUser(byteSource); + InputStream is = loadQuery(new UserEncodingFunction().apply(userLogin), fromCache); + return parseUser(is); } public Collection load(List userLogins) { return load(userLogins, null); } - + /** * Not cache friendly. Should not be used if a cache hit is expected. */ @@ -67,13 +67,13 @@ public class UserRepositoryLoader { if (userLogins.isEmpty()) { return Collections.emptyList(); } - ByteSource byteSource = loadQuery(Joiner.on(',').join(Lists.transform(userLogins, new UserEncodingFunction())), fromCache); + InputStream is = loadQuery(Joiner.on(',').join(Lists.transform(userLogins, new UserEncodingFunction())), fromCache); - return parseUsers(byteSource); + return parseUsers(is); } - private ByteSource loadQuery(String loginsQuery, @Nullable MutableBoolean fromCache) { - WSLoaderResult result = wsLoader.loadSource("/scanner/users?logins=" + loginsQuery); + private InputStream loadQuery(String loginsQuery, @Nullable MutableBoolean fromCache) { + WSLoaderResult result = wsLoader.loadStream("/scanner/users?logins=" + loginsQuery); if (fromCache != null) { fromCache.setValue(result.isFromCache()); } @@ -87,18 +87,20 @@ public class UserRepositoryLoader { } } - private static BatchInput.User parseUser(ByteSource input) { - try (InputStream is = input.openStream()) { + private static BatchInput.User parseUser(InputStream is) { + try { return BatchInput.User.parseDelimitedFrom(is); } catch (IOException e) { throw new IllegalStateException("Unable to get user details from server", e); + } finally { + IOUtils.closeQuietly(is); } } - private static Collection parseUsers(ByteSource input) { + private static Collection parseUsers(InputStream is) { List users = new ArrayList<>(); - try (InputStream is = input.openStream()) { + try { BatchInput.User user = BatchInput.User.parseDelimitedFrom(is); while (user != null) { users.add(user); @@ -106,6 +108,8 @@ public class UserRepositoryLoader { } } catch (IOException e) { throw new IllegalStateException("Unable to get user details from server", e); + } finally { + IOUtils.closeQuietly(is); } return users; diff --git a/sonar-batch/src/main/java/org/sonar/batch/rule/DefaultRulesLoader.java b/sonar-batch/src/main/java/org/sonar/batch/rule/DefaultRulesLoader.java index b427cfed871..315106a6179 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/rule/DefaultRulesLoader.java +++ b/sonar-batch/src/main/java/org/sonar/batch/rule/DefaultRulesLoader.java @@ -19,15 +19,15 @@ */ package org.sonar.batch.rule; -import org.sonar.batch.cache.WSLoaderResult; +import org.apache.commons.io.IOUtils; +import org.sonar.batch.cache.WSLoaderResult; import org.sonar.batch.cache.WSLoader; import javax.annotation.Nullable; import org.apache.commons.lang.mutable.MutableBoolean; import org.sonarqube.ws.Rules.ListResponse.Rule; -import com.google.common.io.ByteSource; import org.sonarqube.ws.Rules.ListResponse; import java.io.IOException; @@ -45,19 +45,21 @@ public class DefaultRulesLoader implements RulesLoader { @Override public List load(@Nullable MutableBoolean fromCache) { - WSLoaderResult result = wsLoader.loadSource(RULES_SEARCH_URL); - ListResponse list = loadFromSource(result.get()); + WSLoaderResult result = wsLoader.loadStream(RULES_SEARCH_URL); + ListResponse list = loadFromStream(result.get()); if (fromCache != null) { fromCache.setValue(result.isFromCache()); } return list.getRulesList(); } - private static ListResponse loadFromSource(ByteSource input) { - try (InputStream is = input.openStream()) { + private static ListResponse loadFromStream(InputStream is) { + try { return ListResponse.parseFrom(is); } catch (IOException e) { throw new IllegalStateException("Unable to get rules", e); + } finally { + IOUtils.closeQuietly(is); } } diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectLock.java b/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectLock.java index 6266bf44e6c..ff42abf956d 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectLock.java +++ b/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectLock.java @@ -19,46 +19,32 @@ */ package org.sonar.batch.scan; +import org.sonar.batch.bootstrap.Slf4jLogger; + +import org.sonar.home.cache.DirectoryLock; import org.picocontainer.Startable; import org.sonar.api.batch.bootstrap.ProjectReactor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; import java.nio.channels.OverlappingFileLockException; -import java.nio.file.Files; import java.nio.file.Path; public class ProjectLock implements Startable { - private static final Logger LOG = LoggerFactory.getLogger(ProjectLock.class); static final String LOCK_FILE_NAME = ".sonar_lock"; - private final Path lockFilePath; - private RandomAccessFile lockRandomAccessFile; - private FileChannel lockChannel; - private FileLock lockFile; + private DirectoryLock lock; public ProjectLock(ProjectReactor projectReactor) { Path directory = projectReactor.getRoot().getBaseDir().toPath(); - this.lockFilePath = directory.resolve(LOCK_FILE_NAME).toAbsolutePath(); + this.lock = new DirectoryLock(directory.toAbsolutePath(), new Slf4jLogger()); } public void tryLock() { try { - lockRandomAccessFile = new RandomAccessFile(lockFilePath.toFile(), "rw"); - lockChannel = lockRandomAccessFile.getChannel(); - lockFile = lockChannel.tryLock(0, 1024, false); - - if (lockFile == null) { + if (!lock.tryLock()) { failAlreadyInProgress(null); } } catch (OverlappingFileLockException e) { failAlreadyInProgress(e); - } catch (IOException e) { - throw new IllegalStateException("Failed to create project lock in " + lockFilePath.toString(), e); } } @@ -68,42 +54,11 @@ public class ProjectLock implements Startable { @Override public void stop() { - if (lockFile != null) { - try { - lockFile.release(); - lockFile = null; - } catch (IOException e) { - LOG.error("Error releasing lock", e); - } - } - if (lockChannel != null) { - try { - lockChannel.close(); - lockChannel = null; - } catch (IOException e) { - LOG.error("Error closing file channel", e); - } - } - if (lockRandomAccessFile != null) { - try { - lockRandomAccessFile.close(); - lockRandomAccessFile = null; - } catch (IOException e) { - LOG.error("Error closing file", e); - } - } - - try { - Files.delete(lockFilePath); - } catch (IOException e) { - // ignore, as an error happens if another process just started to acquire the same lock - LOG.debug("Couldn't delete lock file: " + lockFilePath.toString(), e); - } + lock.unlock(); } @Override public void start() { // nothing to do } - } diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java b/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java index 431b916ebff..5c7bbb9e026 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java +++ b/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java @@ -19,8 +19,9 @@ */ package org.sonar.batch.scan; -import org.sonar.batch.issue.tracking.LocalIssueTracking; +import org.sonar.batch.cache.ProjectPersistentCacheProvider; +import org.sonar.batch.issue.tracking.LocalIssueTracking; import org.sonar.batch.issue.tracking.IssueTransition; import org.sonar.batch.repository.DefaultProjectRepositoriesFactory; import org.sonar.batch.repository.QualityProfileProvider; @@ -151,6 +152,7 @@ public class ProjectScanContainer extends ComponentContainer { BatchComponentCache.class, DefaultIssueCallback.class, new ProjectSettingsProvider(), + new ProjectPersistentCacheProvider(), // temp new AnalysisTempFolderProvider(), diff --git a/sonar-batch/src/main/java/org/sonar/batch/util/BatchUtils.java b/sonar-batch/src/main/java/org/sonar/batch/util/BatchUtils.java index 42d53af38cc..aa5fbc35ae0 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/util/BatchUtils.java +++ b/sonar-batch/src/main/java/org/sonar/batch/util/BatchUtils.java @@ -20,12 +20,20 @@ package org.sonar.batch.util; import com.google.common.base.Strings; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import javax.annotation.Nullable; import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class BatchUtils { + private static final Logger LOG = LoggerFactory.getLogger(BatchUtils.class); private BatchUtils() { } @@ -38,7 +46,7 @@ public class BatchUtils { String cleanKey = StringUtils.deleteWhitespace(projectKey); return StringUtils.replace(cleanKey, ":", "_"); } - + public static String encodeForUrl(@Nullable String url) { try { return URLEncoder.encode(Strings.nullToEmpty(url), "UTF-8"); @@ -59,4 +67,18 @@ public class BatchUtils { return o.getClass().getName(); } + + public static String getServerVersion() { + InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("sq-version.txt"); + if (is == null) { + LOG.warn("Failed to get SQ version"); + return null; + } + try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + return br.readLine(); + } catch (IOException e) { + LOG.warn("Failed to get SQ version", e); + return null; + } + } } diff --git a/sonar-batch/src/test/java/org/sonar/batch/analysis/AnalysisWSLoaderProviderTest.java b/sonar-batch/src/test/java/org/sonar/batch/analysis/AnalysisWSLoaderProviderTest.java index 77cd2311311..9e80507094b 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/analysis/AnalysisWSLoaderProviderTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/analysis/AnalysisWSLoaderProviderTest.java @@ -63,6 +63,6 @@ public class AnalysisWSLoaderProviderTest { props = new AnalysisProperties(propMap, null); WSLoader loader = loaderProvider.provide(props, mode, cache, client); - assertThat(loader.getStrategy()).isEqualTo(LoadStrategy.SERVER_ONLY); + assertThat(loader.getDefaultStrategy()).isEqualTo(LoadStrategy.SERVER_ONLY); } } diff --git a/sonar-batch/src/test/java/org/sonar/batch/bootstrap/BatchPluginInstallerTest.java b/sonar-batch/src/test/java/org/sonar/batch/bootstrap/BatchPluginInstallerTest.java index c663dbb3ef4..654616811d5 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/bootstrap/BatchPluginInstallerTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/bootstrap/BatchPluginInstallerTest.java @@ -81,7 +81,7 @@ public class BatchPluginInstallerTest { thrown.expect(IllegalStateException.class); WSLoader wsLoader = mock(WSLoader.class); - doThrow(new IllegalStateException()).when(wsLoader).load("/deploy/plugins/index.txt"); + doThrow(new IllegalStateException()).when(wsLoader).loadString("/deploy/plugins/index.txt"); new BatchPluginInstaller(wsLoader, serverClient, fileCache, pluginPredicate).installRemotes(); } diff --git a/sonar-batch/src/test/java/org/sonar/batch/cache/DefaultProjectCacheStatusTest.java b/sonar-batch/src/test/java/org/sonar/batch/cache/DefaultProjectCacheStatusTest.java index 4239ca1c024..8d86733a01d 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/cache/DefaultProjectCacheStatusTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/cache/DefaultProjectCacheStatusTest.java @@ -22,19 +22,15 @@ package org.sonar.batch.cache; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.assertj.core.api.Assertions.assertThat; - -import org.sonar.home.cache.PersistentCacheLoader; - +import com.google.common.io.Files; import org.junit.rules.ExpectedException; +import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Date; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.any; import org.junit.Test; -import org.sonar.home.cache.Logger; import org.junit.rules.TemporaryFolder; import org.junit.Rule; import org.junit.Before; @@ -42,10 +38,9 @@ import org.sonar.batch.bootstrap.ServerClient; import org.sonar.home.cache.PersistentCache; public class DefaultProjectCacheStatusTest { - private static final String PROJ_KEY = "project1"; @Rule public TemporaryFolder tmp = new TemporaryFolder(); - + @Rule public ExpectedException exception = ExpectedException.none(); @@ -55,70 +50,44 @@ public class DefaultProjectCacheStatusTest { @Before public void setUp() { - cache = new PersistentCache(tmp.getRoot().toPath(), Long.MAX_VALUE, mock(Logger.class), null); client = mock(ServerClient.class); - when(client.getServerVersion()).thenReturn("5.2"); - when(client.getURL()).thenReturn("localhost"); - cacheStatus = new DefaultProjectCacheStatus(cache, client); - } - - @Test - public void errorDelete() throws IOException { cache = mock(PersistentCache.class); - doThrow(IOException.class).when(cache).put(anyString(), any(byte[].class)); - cacheStatus = new DefaultProjectCacheStatus(cache, client); - - exception.expect(IllegalStateException.class); - exception.expectMessage("Failed to delete cache sync status"); - cacheStatus.delete(PROJ_KEY); + when(cache.getDirectory()).thenReturn(tmp.getRoot().toPath()); + cacheStatus = new DefaultProjectCacheStatus(cache); } - + @Test public void errorSave() throws IOException { - cache = mock(PersistentCache.class); - doThrow(IOException.class).when(cache).put(anyString(), any(byte[].class)); - cacheStatus = new DefaultProjectCacheStatus(cache, client); + when(cache.getDirectory()).thenReturn(tmp.getRoot().toPath().resolve("unexistent_folder")); + cacheStatus = new DefaultProjectCacheStatus(cache); exception.expect(IllegalStateException.class); exception.expectMessage("Failed to write cache sync status"); - cacheStatus.save(PROJ_KEY); - } - - @Test - public void useServerVersionAsKey() { - cacheStatus.save(PROJ_KEY); - assertThat(cacheStatus.getSyncStatus(PROJ_KEY)).isNotNull(); - assertThat(age(cacheStatus.getSyncStatus(PROJ_KEY))).isLessThan(2000); - - when(client.getServerVersion()).thenReturn("5.1"); - - assertThat(cacheStatus.getSyncStatus(PROJ_KEY)).isNull(); + cacheStatus.save(); } - + @Test public void errorStatus() throws IOException { - cache = mock(PersistentCache.class); - doThrow(IOException.class).when(cache).get(anyString(), any(PersistentCacheLoader.class)); - cacheStatus = new DefaultProjectCacheStatus(cache, client); + Files.write("trash".getBytes(StandardCharsets.UTF_8), new File(tmp.getRoot(), "cache-sync-status")); + cacheStatus = new DefaultProjectCacheStatus(cache); exception.expect(IllegalStateException.class); exception.expectMessage("Failed to read cache sync status"); - cacheStatus.getSyncStatus(PROJ_KEY); + cacheStatus.getSyncStatus(); } - + @Test public void testSave() { - cacheStatus.save(PROJ_KEY); - assertThat(cacheStatus.getSyncStatus(PROJ_KEY)).isNotNull(); - assertThat(age(cacheStatus.getSyncStatus(PROJ_KEY))).isLessThan(2000); - assertThat(cacheStatus.getSyncStatus(PROJ_KEY + "1")).isNull(); + cacheStatus.save(); + assertThat(cacheStatus.getSyncStatus()).isNotNull(); + assertThat(age(cacheStatus.getSyncStatus())).isLessThan(2000); } @Test public void testDelete() { - cacheStatus.save(PROJ_KEY); - cacheStatus.delete(PROJ_KEY); - assertThat(cacheStatus.getSyncStatus(PROJ_KEY)).isNull(); + cacheStatus.save(); + cacheStatus.delete(); + assertThat(cacheStatus.getSyncStatus()).isNull(); } private long age(Date date) { diff --git a/sonar-batch/src/test/java/org/sonar/batch/cache/PersistentCacheProviderTest.java b/sonar-batch/src/test/java/org/sonar/batch/cache/GlobalPersistentCacheProviderTest.java similarity index 58% rename from sonar-batch/src/test/java/org/sonar/batch/cache/PersistentCacheProviderTest.java rename to sonar-batch/src/test/java/org/sonar/batch/cache/GlobalPersistentCacheProviderTest.java index 3f9c4af0e77..fe0513bfc5a 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/cache/PersistentCacheProviderTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/cache/GlobalPersistentCacheProviderTest.java @@ -19,45 +19,40 @@ */ package org.sonar.batch.cache; -import org.junit.Rule; -import org.junit.rules.TemporaryFolder; -import org.sonar.batch.bootstrap.GlobalProperties; -import org.sonar.batch.cache.PersistentCacheProvider; +import org.sonar.batch.util.BatchUtils; -import java.io.File; -import java.util.Collections; +import org.sonar.home.cache.PersistentCache; + +import java.util.HashMap; -import org.junit.Before; import static org.assertj.core.api.Assertions.assertThat; +import org.sonar.batch.bootstrap.GlobalProperties; +import org.junit.Before; import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; -public class PersistentCacheProviderTest { +public class GlobalPersistentCacheProviderTest { @Rule public TemporaryFolder temp = new TemporaryFolder(); - private PersistentCacheProvider provider = null; - private GlobalProperties props = null; - + private GlobalPersistentCacheProvider provider; + private GlobalProperties globalProperties; + @Before - public void prepare() { - props = new GlobalProperties(Collections.emptyMap()); - provider = new PersistentCacheProvider(); + public void setUp() { + HashMap map = new HashMap(); + map.put("sonar.userHome", temp.getRoot().getAbsolutePath()); + globalProperties = new GlobalProperties(map); + provider = new GlobalPersistentCacheProvider(); } - - @Test - public void test_singleton() { - assertThat(provider.provide(props)).isEqualTo(provider.provide(props)); - } - - @Test - public void test_cache_dir() { - assertThat(provider.provide(props).getBaseDirectory().toFile()).exists().isDirectory(); - } - + @Test - public void test_home() { - File f = temp.getRoot(); - props.properties().put("sonar.userHome", f.getAbsolutePath()); - assertThat(provider.provide(props).getBaseDirectory()).isEqualTo(f.toPath().resolve("ws_cache")); + public void test_path() { + PersistentCache cache = provider.provide(globalProperties); + assertThat(cache.getDirectory()).isEqualTo(temp.getRoot().toPath() + .resolve("ws_cache") + .resolve("http%3A%2F%2Flocalhost%3A9000-" + BatchUtils.getServerVersion()) + .resolve("global")); } } diff --git a/sonar-batch/src/test/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizerTest.java b/sonar-batch/src/test/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizerTest.java index d7b3bbd6710..3d58f3d3daa 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizerTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/cache/NonAssociatedCacheSynchronizerTest.java @@ -62,14 +62,14 @@ public class NonAssociatedCacheSynchronizerTest { @Test public void dont_sync_if_exists() { - when(cacheStatus.getSyncStatus(null)).thenReturn(new Date()); + when(cacheStatus.getSyncStatus()).thenReturn(new Date()); synchronizer.execute(false); verifyNoMoreInteractions(qualityProfileLoader, activeRulesLoader); } @Test public void always_sync_if_force() { - when(cacheStatus.getSyncStatus(null)).thenReturn(new Date()); + when(cacheStatus.getSyncStatus()).thenReturn(new Date()); synchronizer.execute(true); checkSync(); } @@ -81,8 +81,8 @@ public class NonAssociatedCacheSynchronizerTest { } private void checkSync() { - verify(cacheStatus).getSyncStatus(null); - verify(cacheStatus).save(null); + verify(cacheStatus).getSyncStatus(); + verify(cacheStatus).save(); verify(qualityProfileLoader).load(null, null); verify(activeRulesLoader).load(ImmutableList.of("profile"), null); diff --git a/sonar-batch/src/test/java/org/sonar/batch/cache/ProjectCacheSynchronizerTest.java b/sonar-batch/src/test/java/org/sonar/batch/cache/ProjectCacheSynchronizerTest.java index a4dade54f2d..d91f154ee79 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/cache/ProjectCacheSynchronizerTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/cache/ProjectCacheSynchronizerTest.java @@ -19,11 +19,32 @@ */ package org.sonar.batch.cache; +import static org.mockito.Mockito.when; +import org.sonar.batch.repository.ProjectRepositoriesFactory; +import org.sonar.batch.repository.DefaultProjectRepositoriesFactory; +import org.junit.Rule; +import org.junit.rules.ExpectedException; +import org.sonar.batch.repository.ProjectSettingsRepo; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; -import com.google.common.io.ByteSource; -import com.google.common.io.Resources; +import org.sonar.batch.protocol.input.ActiveRule; +import org.sonar.batch.protocol.input.QProfile; +import org.apache.commons.lang.mutable.MutableBoolean; +import org.sonar.batch.repository.DefaultProjectSettingsLoader; +import org.sonar.batch.rule.DefaultActiveRulesLoader; +import org.sonar.batch.repository.DefaultQualityProfileLoader; +import org.sonar.batch.repository.ProjectSettingsLoader; +import org.sonar.batch.rule.ActiveRulesLoader; +import org.sonar.batch.repository.QualityProfileLoader; +import org.sonar.batch.analysis.DefaultAnalysisMode; +import org.sonar.batch.analysis.AnalysisProperties; +import org.sonar.batch.protocol.input.ProjectRepositories; +import org.sonar.batch.repository.DefaultServerIssuesLoader; +import org.sonar.batch.repository.DefaultProjectRepositoriesLoader; +import org.sonar.api.batch.bootstrap.ProjectReactor; + import java.io.IOException; +import java.io.InputStream; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Date; @@ -98,10 +119,10 @@ public class ProjectCacheSynchronizerTest { MockitoAnnotations.initMocks(this); String batchProject = getResourceAsString("batch_project.json"); - ByteSource issues = getResourceAsByteSource("batch_issues.protobuf"); + InputStream issues = getResourceAsInputStream("batch_issues.protobuf"); when(ws.loadString(BATCH_PROJECT)).thenReturn(new WSLoaderResult<>(batchProject, false)); - when(ws.loadSource(ISSUES)).thenReturn(new WSLoaderResult<>(issues, false)); + when(ws.loadStream(ISSUES)).thenReturn(new WSLoaderResult<>(issues, false)); when(analysisMode.isIssues()).thenReturn(true); when(properties.properties()).thenReturn(new HashMap()); @@ -157,10 +178,10 @@ public class ProjectCacheSynchronizerTest { sync.load(PROJECT_KEY, false); verify(ws).loadString(BATCH_PROJECT); - verify(ws).loadSource(ISSUES); + verify(ws).loadStream(ISSUES); verifyNoMoreInteractions(ws); - verify(cacheStatus).save(anyString()); + verify(cacheStatus).save(); } @Test @@ -193,12 +214,12 @@ public class ProjectCacheSynchronizerTest { ProjectCacheSynchronizer sync = create(mockedProjectRepositories); sync.load(PROJECT_KEY, true); - verify(cacheStatus).save(PROJECT_KEY); + verify(cacheStatus).save(); } @Test public void testDontSyncIfNotForce() { - when(cacheStatus.getSyncStatus(PROJECT_KEY)).thenReturn(new Date()); + when(cacheStatus.getSyncStatus()).thenReturn(new Date()); ProjectCacheSynchronizer sync = create(null); sync.load(PROJECT_KEY, false); @@ -210,8 +231,8 @@ public class ProjectCacheSynchronizerTest { return Resources.toString(resource, StandardCharsets.UTF_8); } - private ByteSource getResourceAsByteSource(String name) throws IOException { + private InputStream getResourceAsInputStream(String name) throws IOException { URL resource = this.getClass().getResource(getClass().getSimpleName() + "/" + name); - return Resources.asByteSource(resource); + return Resources.asByteSource(resource).openBufferedStream(); } } diff --git a/sonar-batch/src/test/java/org/sonar/batch/cache/ProjectPersistentCacheProviderTest.java b/sonar-batch/src/test/java/org/sonar/batch/cache/ProjectPersistentCacheProviderTest.java new file mode 100644 index 00000000000..75e5b65e633 --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/cache/ProjectPersistentCacheProviderTest.java @@ -0,0 +1,82 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.cache; + +import org.sonar.api.batch.bootstrap.ProjectKey; + +import org.sonar.batch.util.BatchUtils; +import org.sonar.api.batch.bootstrap.ProjectReactor; +import org.sonar.api.batch.bootstrap.ProjectDefinition; +import org.sonar.batch.analysis.DefaultAnalysisMode; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; +import org.sonar.batch.bootstrap.GlobalProperties; +import org.sonar.batch.cache.ProjectPersistentCacheProvider; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collections; + +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.mock; +import org.junit.Before; +import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Test; + +public class ProjectPersistentCacheProviderTest { + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private ProjectPersistentCacheProvider provider = null; + private GlobalProperties props = null; + private DefaultAnalysisMode mode = null; + private ProjectKey key = null; + + @Before + public void prepare() { + key = new ProjectKeySupplier("proj"); + props = new GlobalProperties(Collections.emptyMap()); + mode = mock(DefaultAnalysisMode.class); + provider = new ProjectPersistentCacheProvider(); + } + + @Test + public void test_singleton() { + assertThat(provider.provide(props, mode, key)).isEqualTo(provider.provide(props, mode, key)); + } + + @Test + public void test_cache_dir() { + assertThat(provider.provide(props, mode, key).getDirectory().toFile()).exists().isDirectory(); + } + + @Test + public void test_home() { + File f = temp.getRoot(); + props.properties().put("sonar.userHome", f.getAbsolutePath()); + Path expected = f.toPath() + .resolve("ws_cache") + .resolve("http%3A%2F%2Flocalhost%3A9000-" + BatchUtils.getServerVersion()) + .resolve("projects") + .resolve("proj"); + + assertThat(provider.provide(props, mode, key).getDirectory()).isEqualTo(expected); + } +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/cache/StrategyWSLoaderProviderTest.java b/sonar-batch/src/test/java/org/sonar/batch/cache/StrategyWSLoaderProviderTest.java index 6ad5591a6d4..a7c613056bf 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/cache/StrategyWSLoaderProviderTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/cache/StrategyWSLoaderProviderTest.java @@ -47,7 +47,7 @@ public class StrategyWSLoaderProviderTest { StrategyWSLoaderProvider provider = new StrategyWSLoaderProvider(LoadStrategy.CACHE_FIRST); WSLoader wsLoader = provider.provide(cache, client); - assertThat(wsLoader.getStrategy()).isEqualTo(LoadStrategy.CACHE_FIRST); + assertThat(wsLoader.getDefaultStrategy()).isEqualTo(LoadStrategy.CACHE_FIRST); } @Test diff --git a/sonar-batch/src/test/java/org/sonar/batch/cache/WSLoaderTest.java b/sonar-batch/src/test/java/org/sonar/batch/cache/WSLoaderTest.java index fa812fb4988..db9fe132fa9 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/cache/WSLoaderTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/cache/WSLoaderTest.java @@ -33,6 +33,7 @@ import org.mockito.Mockito; import org.mockito.InOrder; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import static org.junit.Assert.*; @@ -53,7 +54,7 @@ import org.sonar.home.cache.PersistentCache; import org.mockito.Mock; public class WSLoaderTest { - private final static String ID = "/dummy"; + private final static String ID = "dummy"; private final static String cacheValue = "cache"; private final static String serverValue = "server"; @@ -68,7 +69,7 @@ public class WSLoaderTest { public void setUp() throws IOException { MockitoAnnotations.initMocks(this); when(client.load(anyString(), anyString(), anyBoolean(), anyInt(), anyInt())).thenReturn(IOUtils.toInputStream(serverValue)); - when(cache.get(ID, null)).thenReturn(cacheValue.getBytes()); + when(cache.getString(ID)).thenReturn(cacheValue); when(client.getURI(anyString())).thenAnswer(new Answer() { @Override public URI answer(InvocationOnMock invocation) throws Throwable { @@ -89,15 +90,46 @@ public class WSLoaderTest { assertUsedCache(2); } + @Test + public void get_stream_from_cache() throws IOException { + InputStream is = mock(InputStream.class); + when(cache.getStream(ID)).thenReturn(is); + WSLoader loader = new WSLoader(LoadStrategy.CACHE_FIRST, cache, client); + WSLoaderResult result = loader.loadStream(ID); + assertThat(result.get()).isEqualTo(is); + verify(cache).getStream(ID); + + verifyNoMoreInteractions(cache, client); + } + + @Test + public void put_stream_in_cache() throws IOException { + InputStream is1 = mock(InputStream.class); + InputStream is2 = mock(InputStream.class); + + when(client.load(anyString(), anyString(), anyBoolean(), anyInt(), anyInt())).thenReturn(is1); + when(cache.getStream(ID)).thenReturn(is2); + + WSLoader loader = new WSLoader(LoadStrategy.SERVER_FIRST, cache, client); + WSLoaderResult result = loader.loadStream(ID); + assertThat(result.get()).isEqualTo(is2); + + verify(client).load(anyString(), anyString(), anyBoolean(), anyInt(), anyInt()); + verify(cache).put(ID, is1); + verify(cache).getStream(ID); + + verifyNoMoreInteractions(cache, client); + } + @Test public void test_cache_strategy_fallback() throws IOException { turnCacheEmpty(); WSLoader loader = new WSLoader(LoadStrategy.CACHE_FIRST, cache, client); - assertResult(loader.load(ID), serverValue.getBytes(), false); + assertResult(loader.loadString(ID), serverValue, false); InOrder inOrder = Mockito.inOrder(client, cache); - inOrder.verify(cache).get(ID, null); + inOrder.verify(cache).getString(ID); inOrder.verify(client).load(eq(ID), anyString(), anyBoolean(), anyInt(), anyInt()); } @@ -110,13 +142,13 @@ public class WSLoaderTest { InOrder inOrder = Mockito.inOrder(client, cache); inOrder.verify(client).load(eq(ID), anyString(), anyBoolean(), anyInt(), anyInt()); - inOrder.verify(cache).get(ID, null); + inOrder.verify(cache).getString(ID); } @Test public void test_put_cache() throws IOException { WSLoader loader = new WSLoader(LoadStrategy.SERVER_FIRST, cache, client); - loader.load(ID); + loader.loadString(ID); verify(cache).put(ID, serverValue.getBytes()); } @@ -124,11 +156,11 @@ public class WSLoaderTest { public void test_throw_cache_exception_fallback() throws IOException { turnServerOffline(); - when(cache.get(ID, null)).thenThrow(new NullPointerException()); + when(cache.getString(ID)).thenThrow(new NullPointerException()); WSLoader loader = new WSLoader(LoadStrategy.SERVER_FIRST, cache, client); try { - loader.load(ID); + loader.loadString(ID); fail("NPE expected"); } catch (NullPointerException e) { assertUsedServer(1); @@ -138,12 +170,12 @@ public class WSLoaderTest { @Test public void test_throw_cache_exception() throws IOException { - when(cache.get(ID, null)).thenThrow(new IllegalStateException()); + when(cache.getString(ID)).thenThrow(new IllegalStateException()); WSLoader loader = new WSLoader(LoadStrategy.CACHE_FIRST, cache, client); try { - loader.load(ID); + loader.loadString(ID); fail("IllegalStateException expected"); } catch (IllegalStateException e) { assertUsedServer(0); @@ -161,7 +193,7 @@ public class WSLoaderTest { WSLoader loader = new WSLoader(LoadStrategy.SERVER_FIRST, cache, client); try { - loader.load(ID); + loader.loadString(ID); fail("IllegalStateException expected"); } catch (IllegalStateException e) { // cache should not be used @@ -177,7 +209,7 @@ public class WSLoaderTest { exception.expectMessage(Matchers.is("Server is not available")); WSLoader loader = new WSLoader(LoadStrategy.SERVER_ONLY, cache, client); - loader.load(ID); + loader.loadString(ID); } @Test @@ -189,7 +221,7 @@ public class WSLoaderTest { exception.expectMessage(Matchers.is("Server is not accessible and data is not cached")); WSLoader loader = new WSLoader(LoadStrategy.SERVER_FIRST, cache, client); - loader.load(ID); + loader.loadString(ID); } @Test @@ -200,13 +232,13 @@ public class WSLoaderTest { exception.expectMessage(Matchers.is("Data is not cached")); WSLoader loader = new WSLoader(LoadStrategy.CACHE_ONLY, cache, client); - loader.load(ID); + loader.loadString(ID); } @Test public void test_server_strategy() throws IOException { WSLoader loader = new WSLoader(LoadStrategy.SERVER_FIRST, cache, client); - assertResult(loader.load(ID), serverValue.getBytes(), false); + assertResult(loader.loadString(ID), serverValue, false); // should not fetch from cache verify(cache).put(ID, serverValue.getBytes()); @@ -217,7 +249,7 @@ public class WSLoaderTest { public void test_server_only() throws IOException { turnServerOffline(); WSLoader loader = new WSLoader(LoadStrategy.SERVER_ONLY, cache, client); - loader.load(ID); + loader.loadString(ID); } @Test @@ -227,14 +259,21 @@ public class WSLoaderTest { } private void assertUsedCache(int times) throws IOException { - verify(cache, times(times)).get(ID, null); + verify(cache, times(times)).getString(ID); } private void assertUsedServer(int times) { verify(client, times(times)).load(anyString(), anyString(), anyBoolean(), anyInt(), anyInt()); } - private void assertResult(WSLoaderResult result, T expected, boolean fromCache) { + private void assertResult(WSLoaderResult result, byte[] expected, boolean fromCache) throws IOException { + byte[] content = IOUtils.toByteArray(result.get()); + assertThat(result).isNotNull(); + assertThat(content).isEqualTo(expected); + assertThat(result.isFromCache()).isEqualTo(fromCache); + } + + private void assertResult(WSLoaderResult result, String expected, boolean fromCache) { assertThat(result).isNotNull(); assertThat(result.get()).isEqualTo(expected); assertThat(result.isFromCache()).isEqualTo(fromCache); @@ -245,6 +284,6 @@ public class WSLoaderTest { } private void turnCacheEmpty() throws IOException { - when(cache.get(ID, null)).thenReturn(null); + when(cache.getString(ID)).thenReturn(null); } } diff --git a/sonar-batch/src/test/java/org/sonar/batch/mediumtest/BatchMediumTester.java b/sonar-batch/src/test/java/org/sonar/batch/mediumtest/BatchMediumTester.java index 9f3e1c4f9c2..9016442b345 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/mediumtest/BatchMediumTester.java +++ b/sonar-batch/src/test/java/org/sonar/batch/mediumtest/BatchMediumTester.java @@ -76,25 +76,25 @@ public class BatchMediumTester { private Batch batch; private static Path workingDir = null; private static Path globalWorkingDir = null; - + private static void createWorkingDirs() throws IOException { destroyWorkingDirs(); - + workingDir = java.nio.file.Files.createTempDirectory("mediumtest-working-dir"); globalWorkingDir = java.nio.file.Files.createTempDirectory("mediumtest-global-working-dir"); } - + private static void destroyWorkingDirs() throws IOException { - if(workingDir != null) { + if (workingDir != null) { FileUtils.deleteDirectory(workingDir.toFile()); workingDir = null; } - - if(globalWorkingDir != null) { + + if (globalWorkingDir != null) { FileUtils.deleteDirectory(globalWorkingDir.toFile()); globalWorkingDir = null; } - + } public static BatchMediumTesterBuilder builder() { @@ -103,7 +103,7 @@ public class BatchMediumTester { } catch (IOException e) { e.printStackTrace(); } - + BatchMediumTesterBuilder builder = new BatchMediumTesterBuilder().registerCoreMetrics(); builder.bootstrapProperties.put(MEDIUM_TEST_ENABLED, "true"); builder.bootstrapProperties.put(ReportPublisher.KEEP_REPORT_PROP_KEY, "true"); @@ -432,15 +432,15 @@ public class BatchMediumTester { private static class FakeProjectCacheStatus implements ProjectCacheStatus { @Override - public void save(String projectKey) { + public void save() { } @Override - public void delete(String projectKey) { + public void delete() { } @Override - public Date getSyncStatus(String projectKey) { + public Date getSyncStatus() { return new Date(); } diff --git a/sonar-batch/src/test/java/org/sonar/batch/repository/DefaultServerIssuesLoaderTest.java b/sonar-batch/src/test/java/org/sonar/batch/repository/DefaultServerIssuesLoaderTest.java index 07dab6b0ab5..0bcde52323b 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/repository/DefaultServerIssuesLoaderTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/repository/DefaultServerIssuesLoaderTest.java @@ -20,9 +20,7 @@ package org.sonar.batch.repository; import org.sonar.batch.cache.WSLoaderResult; - import org.sonar.batch.cache.WSLoader; -import com.google.common.io.ByteSource; import com.google.common.base.Function; import org.junit.Before; import org.junit.Test; @@ -32,6 +30,7 @@ import org.sonar.batch.protocol.input.BatchInput.ServerIssue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -51,9 +50,6 @@ public class DefaultServerIssuesLoaderTest { @Test public void loadFromWs() throws Exception { - ByteSource bs = mock(ByteSource.class); - when(wsLoader.loadSource("/scanner/issues?key=foo")).thenReturn(new WSLoaderResult<>(bs, true)); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); ServerIssue.newBuilder().setKey("ab1").build() @@ -61,7 +57,8 @@ public class DefaultServerIssuesLoaderTest { ServerIssue.newBuilder().setKey("ab2").build() .writeDelimitedTo(bos); - when(bs.openBufferedStream()).thenReturn(new ByteArrayInputStream(bos.toByteArray())); + InputStream is = new ByteArrayInputStream(bos.toByteArray()); + when(wsLoader.loadStream("/scanner/issues?key=foo")).thenReturn(new WSLoaderResult<>(is, true)); final List result = new ArrayList<>(); loader.load("foo", new Function() { @@ -78,9 +75,15 @@ public class DefaultServerIssuesLoaderTest { @Test(expected = IllegalStateException.class) public void testError() throws IOException { +<<<<<<< HEAD ByteSource source = mock(ByteSource.class); when(source.openBufferedStream()).thenThrow(IOException.class); when(wsLoader.loadSource("/scanner/issues?key=foo")).thenReturn(new WSLoaderResult(source, true)); +======= + InputStream is = mock(InputStream.class); + when(is.read()).thenThrow(IOException.class); + when(wsLoader.loadStream("/batch/issues?key=foo")).thenReturn(new WSLoaderResult(is, true)); +>>>>>>> SONAR-6777 Project cache sync loader.load("foo", mock(Function.class)); } } diff --git a/sonar-batch/src/test/java/org/sonar/batch/repository/user/UserRepositoryLoaderTest.java b/sonar-batch/src/test/java/org/sonar/batch/repository/user/UserRepositoryLoaderTest.java index a449f57837d..16ee8a426f2 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/repository/user/UserRepositoryLoaderTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/repository/user/UserRepositoryLoaderTest.java @@ -30,7 +30,6 @@ import com.google.common.collect.ImmutableMap; import org.junit.rules.ExpectedException; import org.junit.Rule; import org.mockito.Mockito; -import com.google.common.io.ByteSource; import org.junit.Test; import org.sonar.batch.protocol.input.BatchInput; @@ -64,20 +63,20 @@ public class UserRepositoryLoaderTest { public void testLoadEmptyList() { assertThat(userRepo.load(Lists.emptyList())).isEmpty(); } - + @Test public void testLoad() throws IOException { Map userMap = ImmutableMap.of("fmallet", "Freddy Mallet", "sbrandhof", "Simon"); - WSLoaderResult res = new WSLoaderResult<>(createUsersMock(userMap), true); - when(wsLoader.loadSource("/scanner/users?logins=fmallet,sbrandhof")).thenReturn(res); + WSLoaderResult res = new WSLoaderResult<>(createUsersMock(userMap), true); + when(wsLoader.loadStream("/scanner/users?logins=fmallet,sbrandhof")).thenReturn(res); assertThat(userRepo.load(Arrays.asList("fmallet", "sbrandhof"))).extracting("login", "name").containsOnly(tuple("fmallet", "Freddy Mallet"), tuple("sbrandhof", "Simon")); } @Test public void testFromCache() throws IOException { - WSLoaderResult res = new WSLoaderResult<>(createUsersMock(ImmutableMap.of("fmallet", "Freddy Mallet")), true); - when(wsLoader.loadSource(anyString())).thenReturn(res); + WSLoaderResult res = new WSLoaderResult<>(createUsersMock(ImmutableMap.of("fmallet", "Freddy Mallet")), true); + when(wsLoader.loadStream(anyString())).thenReturn(res); MutableBoolean fromCache = new MutableBoolean(); userRepo.load("", fromCache); assertThat(fromCache.booleanValue()).isTrue(); @@ -89,35 +88,42 @@ public class UserRepositoryLoaderTest { @Test public void testLoadSingleUser() throws IOException { +<<<<<<< HEAD WSLoaderResult res = new WSLoaderResult<>(createUsersMock(ImmutableMap.of("fmallet", "Freddy Mallet")), true); when(wsLoader.loadSource("/scanner/users?logins=fmallet")).thenReturn(res); +======= + WSLoaderResult res = new WSLoaderResult<>(createUsersMock(ImmutableMap.of("fmallet", "Freddy Mallet")), true); + when(wsLoader.loadStream("/batch/users?logins=fmallet")).thenReturn(res); +>>>>>>> SONAR-6777 Project cache sync assertThat(userRepo.load("fmallet").getName()).isEqualTo("Freddy Mallet"); } - private ByteSource createUsersMock(Map users) throws IOException { + private InputStream createUsersMock(Map users) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); for (Map.Entry user : users.entrySet()) { BatchInput.User.Builder builder = BatchInput.User.newBuilder(); builder.setLogin(user.getKey()).setName(user.getValue()).build().writeDelimitedTo(out); } - ByteSource source = mock(ByteSource.class); - when(source.openStream()).thenReturn(new ByteArrayInputStream(out.toByteArray())); - return source; + return new ByteArrayInputStream(out.toByteArray()); } @Test public void testInputStreamError() throws IOException { +<<<<<<< HEAD ByteSource source = mock(ByteSource.class); WSLoaderResult res = new WSLoaderResult<>(source, true); when(wsLoader.loadSource("/scanner/users?logins=fmallet,sbrandhof")).thenReturn(res); +======= + InputStream is = mock(InputStream.class); + Mockito.doThrow(IOException.class).when(is).read(); + WSLoaderResult res = new WSLoaderResult<>(is, true); +>>>>>>> SONAR-6777 Project cache sync - InputStream errorInputStream = mock(InputStream.class); - Mockito.doThrow(IOException.class).when(errorInputStream).read(); - when(source.openStream()).thenReturn(errorInputStream); + when(wsLoader.loadStream("/batch/users?logins=fmallet,sbrandhof")).thenReturn(res); exception.expect(IllegalStateException.class); exception.expectMessage("Unable to get user details from server"); diff --git a/sonar-batch/src/test/java/org/sonar/batch/rule/DefaultRulesLoaderTest.java b/sonar-batch/src/test/java/org/sonar/batch/rule/DefaultRulesLoaderTest.java index 1c593a51361..8c4989509d2 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/rule/DefaultRulesLoaderTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/rule/DefaultRulesLoaderTest.java @@ -21,9 +21,7 @@ package org.sonar.batch.rule; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; - import org.junit.rules.ExpectedException; - import org.sonar.batch.cache.WSLoaderResult; import org.sonar.batch.cache.WSLoader; import org.apache.commons.lang.mutable.MutableBoolean; @@ -32,6 +30,7 @@ import com.google.common.io.ByteSource; import com.google.common.io.Resources; import java.io.IOException; +import java.io.InputStream; import java.util.List; import static org.mockito.Matchers.anyString; @@ -45,18 +44,18 @@ public class DefaultRulesLoaderTest { @Test public void testParseServerResponse() throws IOException { WSLoader wsLoader = mock(WSLoader.class); - ByteSource source = Resources.asByteSource(this.getClass().getResource("DefaultRulesLoader/response.protobuf")); - when(wsLoader.loadSource(anyString())).thenReturn(new WSLoaderResult<>(source, true)); + InputStream is = Resources.asByteSource(this.getClass().getResource("DefaultRulesLoader/response.protobuf")).openBufferedStream(); + when(wsLoader.loadStream(anyString())).thenReturn(new WSLoaderResult<>(is, true)); DefaultRulesLoader loader = new DefaultRulesLoader(wsLoader); List ruleList = loader.load(null); assertThat(ruleList).hasSize(318); } @Test - public void testLoadedFromCache() { + public void testLoadedFromCache() throws IOException { WSLoader wsLoader = mock(WSLoader.class); - ByteSource source = Resources.asByteSource(this.getClass().getResource("DefaultRulesLoader/response.protobuf")); - when(wsLoader.loadSource(anyString())).thenReturn(new WSLoaderResult<>(source, true)); + InputStream is = Resources.asByteSource(this.getClass().getResource("DefaultRulesLoader/response.protobuf")).openBufferedStream(); + when(wsLoader.loadStream(anyString())).thenReturn(new WSLoaderResult<>(is, true)); DefaultRulesLoader loader = new DefaultRulesLoader(wsLoader); MutableBoolean fromCache = new MutableBoolean(); loader.load(fromCache); @@ -65,10 +64,10 @@ public class DefaultRulesLoaderTest { } @Test - public void testError() { + public void testError() throws IOException { WSLoader wsLoader = mock(WSLoader.class); - ByteSource source = ByteSource.wrap(new String("trash").getBytes()); - when(wsLoader.loadSource(anyString())).thenReturn(new WSLoaderResult<>(source, true)); + InputStream is = ByteSource.wrap(new String("trash").getBytes()).openBufferedStream(); + when(wsLoader.loadStream(anyString())).thenReturn(new WSLoaderResult<>(is, true)); DefaultRulesLoader loader = new DefaultRulesLoader(wsLoader); exception.expect(IllegalStateException.class); diff --git a/sonar-batch/src/test/java/org/sonar/batch/scan/ProjectLockTest.java b/sonar-batch/src/test/java/org/sonar/batch/scan/ProjectLockTest.java index 91bcb4162b5..bf432556235 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/scan/ProjectLockTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/scan/ProjectLockTest.java @@ -89,7 +89,7 @@ public class ProjectLockTest { public void errorLock() { lock = setUpTest(Paths.get("path", "that", "wont", "exist", "ever").toFile()); exception.expect(IllegalStateException.class); - exception.expectMessage("Failed to create project lock in"); + exception.expectMessage("Failed to create lock in"); lock.tryLock(); } diff --git a/sonar-home/src/main/java/org/sonar/home/cache/DeleteFileOnCloseInputStream.java b/sonar-home/src/main/java/org/sonar/home/cache/DeleteFileOnCloseInputStream.java new file mode 100644 index 00000000000..7f4a3071bed --- /dev/null +++ b/sonar-home/src/main/java/org/sonar/home/cache/DeleteFileOnCloseInputStream.java @@ -0,0 +1,84 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.home.cache; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +public class DeleteFileOnCloseInputStream extends InputStream { + private final InputStream is; + private final Path p; + + public DeleteFileOnCloseInputStream(InputStream stream, Path p) { + this.is = stream; + this.p = p; + } + + @Override + public int read() throws IOException { + return is.read(); + } + + @Override + public int read(byte[] b) throws IOException { + return is.read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return is.read(b, off, len); + } + + @Override + public long skip(long n) throws IOException { + return is.skip(n); + } + + @Override + public synchronized void mark(int readlimit) { + is.mark(readlimit); + } + + @Override + public synchronized void reset() throws IOException { + is.reset(); + } + + @Override + public int available() throws IOException { + return is.available(); + } + + @Override + public boolean markSupported() { + return is.markSupported(); + } + + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + Files.delete(p); + } + } +} diff --git a/sonar-home/src/main/java/org/sonar/home/cache/DirectoryLock.java b/sonar-home/src/main/java/org/sonar/home/cache/DirectoryLock.java new file mode 100644 index 00000000000..4f14d215f1c --- /dev/null +++ b/sonar-home/src/main/java/org/sonar/home/cache/DirectoryLock.java @@ -0,0 +1,106 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.home.cache; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.RandomAccessFile; +import java.io.StringWriter; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.Files; +import java.nio.file.Path; + +public class DirectoryLock { + static final String LOCK_FILE_NAME = ".sonar_lock"; + private final Path lockFilePath; + private final Logger logger; + + private RandomAccessFile lockRandomAccessFile; + private FileChannel lockChannel; + private FileLock lockFile; + + public DirectoryLock(Path directory, Logger logger) { + this.logger = logger; + this.lockFilePath = directory.resolve(LOCK_FILE_NAME).toAbsolutePath(); + } + + public String getFileLockName() { + return LOCK_FILE_NAME; + } + + public void lock() { + try { + lockRandomAccessFile = new RandomAccessFile(lockFilePath.toFile(), "rw"); + lockChannel = lockRandomAccessFile.getChannel(); + lockFile = lockChannel.lock(0, 1024, false); + } catch (IOException e) { + throw new IllegalStateException("Failed to create lock in " + lockFilePath.toString(), e); + } + } + + public boolean tryLock() { + try { + lockRandomAccessFile = new RandomAccessFile(lockFilePath.toFile(), "rw"); + lockChannel = lockRandomAccessFile.getChannel(); + lockFile = lockChannel.tryLock(0, 1024, false); + + return lockFile != null; + } catch (IOException e) { + throw new IllegalStateException("Failed to create lock in " + lockFilePath.toString(), e); + } + } + + public void unlock() { + if (lockFile != null) { + try { + lockFile.release(); + lockFile = null; + } catch (IOException e) { + logger.error("Error releasing lock", e); + } + } + if (lockChannel != null) { + try { + lockChannel.close(); + lockChannel = null; + } catch (IOException e) { + logger.error("Error closing file channel", e); + } + } + if (lockRandomAccessFile != null) { + try { + lockRandomAccessFile.close(); + lockRandomAccessFile = null; + } catch (IOException e) { + logger.error("Error closing file", e); + } + } + + try { + Files.delete(lockFilePath); + } catch (IOException e) { + // ignore, as an error happens if another process just started to acquire the same lock + StringWriter errors = new StringWriter(); + e.printStackTrace(new PrintWriter(errors)); + logger.debug("Couldn't delete lock file: " + lockFilePath.toString() + " " + errors.toString()); + } + } +} diff --git a/sonar-home/src/main/java/org/sonar/home/cache/PersistentCache.java b/sonar-home/src/main/java/org/sonar/home/cache/PersistentCache.java index 3122d9a6fd0..35dfa4abfcc 100644 --- a/sonar-home/src/main/java/org/sonar/home/cache/PersistentCache.java +++ b/sonar-home/src/main/java/org/sonar/home/cache/PersistentCache.java @@ -22,9 +22,6 @@ package org.sonar.home.cache; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.io.RandomAccessFile; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; @@ -37,7 +34,6 @@ import java.security.NoSuchAlgorithmException; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import static java.nio.file.StandardOpenOption.CREATE; import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; @@ -47,41 +43,38 @@ public class PersistentCache { private static final char[] hexArray = "0123456789ABCDEF".toCharArray(); private static final Charset ENCODING = StandardCharsets.UTF_8; private static final String DIGEST_ALGO = "MD5"; - private static final String LOCK_FNAME = ".lock"; // eviction strategy is to expire entries after modification once a time duration has elapsed private final long defaultDurationToExpireMs; private final Logger logger; - private final String version; - private final Path baseDir; + private final Path dir; + private DirectoryLock lock; - public PersistentCache(Path baseDir, long defaultDurationToExpireMs, Logger logger, String version) { - this.baseDir = baseDir; + public PersistentCache(Path dir, long defaultDurationToExpireMs, Logger logger, DirectoryLock lock) { + this.dir = dir; this.defaultDurationToExpireMs = defaultDurationToExpireMs; this.logger = logger; - this.version = version; + this.lock = lock; reconfigure(); - logger.debug("cache: " + baseDir + ", default expiration time (ms): " + defaultDurationToExpireMs); + logger.debug("cache: " + dir + ", default expiration time (ms): " + defaultDurationToExpireMs); } public synchronized void reconfigure() { try { - Files.createDirectories(baseDir); + Files.createDirectories(dir); } catch (IOException e) { throw new IllegalStateException("failed to create cache dir", e); } } - public Path getBaseDirectory() { - return baseDir; + public Path getDirectory() { + return dir; } @CheckForNull - public synchronized String getString(@Nonnull String obj, @Nullable final PersistentCacheLoader valueLoader) throws IOException { - ValueLoaderDecoder decoder = valueLoader != null ? new ValueLoaderDecoder(valueLoader) : null; - - byte[] cached = get(obj, decoder); + public synchronized String getString(@Nonnull String obj) throws IOException { + byte[] cached = get(obj); if (cached == null) { return null; @@ -97,7 +90,7 @@ public class PersistentCache { try { lock(); Path path = getCacheCopy(key); - return new DeleteOnCloseInputStream(new FileInputStream(path.toFile()), path); + return new DeleteFileOnCloseInputStream(new FileInputStream(path.toFile()), path); } finally { unlock(); @@ -105,7 +98,7 @@ public class PersistentCache { } @CheckForNull - public synchronized byte[] get(@Nonnull String obj, @Nullable PersistentCacheLoader valueLoader) throws IOException { + public synchronized byte[] get(@Nonnull String obj) throws IOException { String key = getKey(obj); try { @@ -119,14 +112,6 @@ public class PersistentCache { } logger.debug("cache miss for " + obj + " -> " + key); - - if (valueLoader != null) { - byte[] value = valueLoader.get(); - if (value != null) { - putCache(key, value); - } - return value; - } } finally { unlock(); } @@ -161,7 +146,7 @@ public class PersistentCache { logger.info("cache: clearing"); try { lock(); - deleteCacheEntries(new DirectoryClearFilter()); + deleteCacheEntries(new DirectoryClearFilter(lock.getFileLockName())); } catch (IOException e) { logger.error("Error clearing cache", e); } finally { @@ -176,7 +161,7 @@ public class PersistentCache { logger.info("cache: cleaning"); try { lock(); - deleteCacheEntries(new DirectoryCleanFilter(defaultDurationToExpireMs)); + deleteCacheEntries(new DirectoryCleanFilter(defaultDurationToExpireMs, lock.getFileLockName())); } catch (IOException e) { logger.error("Error cleaning cache", e); } finally { @@ -185,49 +170,16 @@ public class PersistentCache { } private void lock() throws IOException { - lockRandomAccessFile = new RandomAccessFile(getLockPath().toFile(), "rw"); - lockChannel = lockRandomAccessFile.getChannel(); - lockFile = lockChannel.lock(); + lock.lock(); } - private RandomAccessFile lockRandomAccessFile; - private FileChannel lockChannel; - private FileLock lockFile; - private void unlock() { - if (lockFile != null) { - try { - lockFile.release(); - } catch (IOException e) { - logger.error("Error releasing lock", e); - } - } - if (lockChannel != null) { - try { - lockChannel.close(); - } catch (IOException e) { - logger.error("Error closing file channel", e); - } - } - if (lockRandomAccessFile != null) { - try { - lockRandomAccessFile.close(); - } catch (IOException e) { - logger.error("Error closing file", e); - } - } - - lockFile = null; - lockRandomAccessFile = null; - lockChannel = null; + lock.unlock(); } private String getKey(String uri) { try { String key = uri; - if (version != null) { - key += version; - } MessageDigest digest = MessageDigest.getInstance(DIGEST_ALGO); digest.update(key.getBytes(StandardCharsets.UTF_8)); return byteArrayToHex(digest.digest()); @@ -237,7 +189,7 @@ public class PersistentCache { } private void deleteCacheEntries(DirectoryStream.Filter filter) throws IOException { - try (DirectoryStream stream = Files.newDirectoryStream(baseDir, filter)) { + try (DirectoryStream stream = Files.newDirectoryStream(dir, filter)) { for (Path p : stream) { try { Files.delete(p); @@ -248,40 +200,31 @@ public class PersistentCache { } } - private static class ValueLoaderDecoder implements PersistentCacheLoader { - PersistentCacheLoader valueLoader; + private static class DirectoryClearFilter implements DirectoryStream.Filter { + private String lockFileName; - ValueLoaderDecoder(PersistentCacheLoader valueLoader) { - this.valueLoader = valueLoader; + DirectoryClearFilter(String lockFileName) { + this.lockFileName = lockFileName; } - @Override - public byte[] get() throws IOException { - String s = valueLoader.get(); - if (s != null) { - return s.getBytes(ENCODING); - } - return null; - } - } - - private static class DirectoryClearFilter implements DirectoryStream.Filter { @Override public boolean accept(Path entry) throws IOException { - return !LOCK_FNAME.equals(entry.getFileName().toString()); + return !lockFileName.equals(entry.getFileName().toString()); } } private static class DirectoryCleanFilter implements DirectoryStream.Filter { private long defaultDurationToExpireMs; + private String lockFileName; - DirectoryCleanFilter(long defaultDurationToExpireMs) { + DirectoryCleanFilter(long defaultDurationToExpireMs, String lockFileName) { this.defaultDurationToExpireMs = defaultDurationToExpireMs; + this.lockFileName = lockFileName; } @Override public boolean accept(Path entry) throws IOException { - if (LOCK_FNAME.equals(entry.getFileName().toString())) { + if (lockFileName.equals(entry.getFileName().toString())) { return false; } @@ -321,27 +264,6 @@ public class PersistentCache { return temp; } - private static class DeleteOnCloseInputStream extends InputStream { - private final InputStream stream; - private final Path p; - - private DeleteOnCloseInputStream(InputStream stream, Path p) { - this.stream = stream; - this.p = p; - } - - @Override - public int read() throws IOException { - return stream.read(); - } - - @Override - public void close() throws IOException { - stream.close(); - Files.delete(p); - } - } - private boolean validateCacheEntry(Path cacheEntryPath, long durationToExpireMs) throws IOException { if (!Files.exists(cacheEntryPath)) { return false; @@ -369,12 +291,8 @@ public class PersistentCache { return false; } - private Path getLockPath() { - return baseDir.resolve(LOCK_FNAME); - } - private Path getCacheEntryPath(String key) { - return baseDir.resolve(key); + return dir.resolve(key); } public static String byteArrayToHex(byte[] bytes) { diff --git a/sonar-home/src/main/java/org/sonar/home/cache/PersistentCacheBuilder.java b/sonar-home/src/main/java/org/sonar/home/cache/PersistentCacheBuilder.java index 055d2615ed6..2097832853b 100644 --- a/sonar-home/src/main/java/org/sonar/home/cache/PersistentCacheBuilder.java +++ b/sonar-home/src/main/java/org/sonar/home/cache/PersistentCacheBuilder.java @@ -19,43 +19,73 @@ */ package org.sonar.home.cache; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.TimeUnit; + import javax.annotation.Nullable; +/** + * Cache files will be placed in 3 areas: + *
+ *   <sonar_home>/ws_cache/<server_url>-<version>/projects/<project>/
+ *   <sonar_home>/ws_cache/<server_url>-<version>/global/
+ *   <sonar_home>/ws_cache/<server_url>-<version>/local/
+ * 
+ */ public class PersistentCacheBuilder { private static final long DEFAULT_EXPIRE_DURATION = TimeUnit.MILLISECONDS.convert(1L, TimeUnit.DAYS); private static final String DIR_NAME = "ws_cache"; - private Path cachePath; + private Path cacheBasePath; + private Path relativePath; private final Logger logger; - private String version; public PersistentCacheBuilder(Logger logger) { this.logger = logger; } - public PersistentCache build() { - if (cachePath == null) { - setSonarHome(findHome()); - } - - return new PersistentCache(cachePath, DEFAULT_EXPIRE_DURATION, logger, version); + public PersistentCacheBuilder setAreaForProject(String serverUrl, String serverVersion, String projectKey) { + relativePath = Paths.get(sanitizeFilename(serverUrl + "-" + serverVersion)) + .resolve("projects") + .resolve(sanitizeFilename(projectKey)); + return this; } - - public PersistentCacheBuilder setVersion(String version) { - this.version = version; + + public PersistentCacheBuilder setAreaForGlobal(String serverUrl, String serverVersion) { + relativePath = Paths.get(sanitizeFilename(serverUrl + "-" + serverVersion)) + .resolve("global"); return this; } - + + public PersistentCacheBuilder setAreaForLocalProject(String serverUrl, String serverVersion) { + relativePath = Paths.get(sanitizeFilename(serverUrl + "-" + serverVersion)) + .resolve("local"); + return this; + } + public PersistentCacheBuilder setSonarHome(@Nullable Path p) { if (p != null) { - this.cachePath = p.resolve(DIR_NAME); + this.cacheBasePath = p.resolve(DIR_NAME); } return this; } + public PersistentCache build() { + if(relativePath == null) { + throw new IllegalStateException("area must be set before building"); + } + if (cacheBasePath == null) { + setSonarHome(findHome()); + } + Path cachePath = cacheBasePath.resolve(relativePath); + DirectoryLock lock = new DirectoryLock(cacheBasePath, logger); + return new PersistentCache(cachePath, DEFAULT_EXPIRE_DURATION, logger, lock); + } + private static Path findHome() { String home = System.getenv("SONAR_USER_HOME"); @@ -66,4 +96,12 @@ public class PersistentCacheBuilder { home = System.getProperty("user.home"); return Paths.get(home, ".sonar"); } + + private String sanitizeFilename(String name) { + try { + return URLEncoder.encode(name, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("Couldn't sanitize filename: " + name, e); + } + } } diff --git a/sonar-home/src/main/java/org/sonar/home/cache/UnlockOnCloseInputStream.java b/sonar-home/src/main/java/org/sonar/home/cache/UnlockOnCloseInputStream.java new file mode 100644 index 00000000000..fdfac82cff1 --- /dev/null +++ b/sonar-home/src/main/java/org/sonar/home/cache/UnlockOnCloseInputStream.java @@ -0,0 +1,82 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.home.cache; + +import java.io.IOException; +import java.io.InputStream; + +public class UnlockOnCloseInputStream extends InputStream { + private final DirectoryLock lock; + private final InputStream is; + + public UnlockOnCloseInputStream(InputStream stream, DirectoryLock lock) { + this.is = stream; + this.lock = lock; + } + + @Override + public int read() throws IOException { + return is.read(); + } + + @Override + public int read(byte[] b) throws IOException { + return is.read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return is.read(b, off, len); + } + + @Override + public long skip(long n) throws IOException { + return is.skip(n); + } + + @Override + public synchronized void mark(int readlimit) { + is.mark(readlimit); + } + + @Override + public synchronized void reset() throws IOException { + is.reset(); + } + + @Override + public int available() throws IOException { + return is.available(); + } + + @Override + public boolean markSupported() { + return is.markSupported(); + } + + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + lock.unlock(); + } + } +} diff --git a/sonar-home/src/test/java/org/sonar/home/cache/DirectoryLockTest.java b/sonar-home/src/test/java/org/sonar/home/cache/DirectoryLockTest.java new file mode 100644 index 00000000000..fc1d994bd6f --- /dev/null +++ b/sonar-home/src/test/java/org/sonar/home/cache/DirectoryLockTest.java @@ -0,0 +1,94 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.home.cache; + +import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.rules.ExpectedException; + +import java.nio.channels.OverlappingFileLockException; +import java.nio.file.Paths; + +import org.junit.Test; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; + +public class DirectoryLockTest { + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + @Rule + public ExpectedException exception = ExpectedException.none(); + private DirectoryLock lock; + + @Before + public void setUp() { + lock = new DirectoryLock(temp.getRoot().toPath(), mock(Logger.class)); + } + + @Test + public void lock() { + assertThat(temp.getRoot().list()).isEmpty(); + lock.lock(); + assertThat(temp.getRoot().toPath().resolve(".sonar_lock")).exists(); + lock.unlock(); + assertThat(temp.getRoot().list()).isEmpty(); + } + + @Test + public void tryLock() { + assertThat(temp.getRoot().list()).isEmpty(); + lock.tryLock(); + assertThat(temp.getRoot().toPath().resolve(".sonar_lock")).exists(); + lock.unlock(); + assertThat(temp.getRoot().list()).isEmpty(); + } + + @Test(expected = OverlappingFileLockException.class) + public void error_2locks() { + assertThat(temp.getRoot().list()).isEmpty(); + lock.lock(); + lock.lock(); + } + + @Test + public void unlockWithoutLock() { + lock.unlock(); + } + + @Test + public void errorCreatingLock() { + lock = new DirectoryLock(Paths.get("non", "existing", "path"), mock(Logger.class)); + + exception.expect(IllegalStateException.class); + exception.expectMessage("Failed to create lock"); + lock.lock(); + } + + @Test + public void errorTryLock() { + lock = new DirectoryLock(Paths.get("non", "existing", "path"), mock(Logger.class)); + + exception.expect(IllegalStateException.class); + exception.expectMessage("Failed to create lock"); + lock.tryLock(); + } +} diff --git a/sonar-home/src/test/java/org/sonar/home/cache/PersistentCacheBuilderTest.java b/sonar-home/src/test/java/org/sonar/home/cache/PersistentCacheBuilderTest.java index 78efe696695..7a57e98d68c 100644 --- a/sonar-home/src/test/java/org/sonar/home/cache/PersistentCacheBuilderTest.java +++ b/sonar-home/src/test/java/org/sonar/home/cache/PersistentCacheBuilderTest.java @@ -20,10 +20,11 @@ package org.sonar.home.cache; import java.nio.file.Files; +import java.nio.file.Paths; + import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; - import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; @@ -35,17 +36,18 @@ public class PersistentCacheBuilderTest { @Test public void user_home_property_can_be_null() { - PersistentCache cache = new PersistentCacheBuilder(mock(Logger.class)).setSonarHome(null).build(); - assertTrue(Files.isDirectory(cache.getBaseDirectory())); - assertThat(cache.getBaseDirectory().getFileName().toString()).isEqualTo("ws_cache"); + PersistentCache cache = new PersistentCacheBuilder(mock(Logger.class)).setSonarHome(null).setAreaForGlobal("url", "0").build(); + assertTrue(Files.isDirectory(cache.getDirectory())); + assertThat(cache.getDirectory()).endsWith(Paths.get("url-0", "global")); } @Test public void set_user_home() { - PersistentCache cache = new PersistentCacheBuilder(mock(Logger.class)).setSonarHome(temp.getRoot().toPath()).build(); + PersistentCache cache = new PersistentCacheBuilder(mock(Logger.class)).setSonarHome(temp.getRoot().toPath()).setAreaForGlobal("url", "0").build(); - assertThat(cache.getBaseDirectory().getParent().toString()).isEqualTo(temp.getRoot().toPath().toString()); - assertTrue(Files.isDirectory(cache.getBaseDirectory())); + assertThat(cache.getDirectory()).isDirectory(); + assertThat(cache.getDirectory()).startsWith(temp.getRoot().toPath()); + assertTrue(Files.isDirectory(cache.getDirectory())); } @Test @@ -54,11 +56,22 @@ public class PersistentCacheBuilderTest { System.setProperty("user.home", temp.getRoot().getAbsolutePath()); - PersistentCache cache = new PersistentCacheBuilder(mock(Logger.class)).build(); - assertTrue(Files.isDirectory(cache.getBaseDirectory())); - assertThat(cache.getBaseDirectory().getFileName().toString()).isEqualTo("ws_cache"); + PersistentCache cache = new PersistentCacheBuilder(mock(Logger.class)).setAreaForGlobal("url", "0").build(); + assertTrue(Files.isDirectory(cache.getDirectory())); + assertThat(cache.getDirectory()).startsWith(temp.getRoot().toPath()); + } + + @Test + public void directories() { + System.setProperty("user.home", temp.getRoot().getAbsolutePath()); + + PersistentCache cache = new PersistentCacheBuilder(mock(Logger.class)).setAreaForProject("url", "0", "proj").build(); + assertThat(cache.getDirectory()).endsWith(Paths.get(".sonar", "ws_cache", "url-0", "projects", "proj")); + + cache = new PersistentCacheBuilder(mock(Logger.class)).setAreaForLocalProject("url", "0").build(); + assertThat(cache.getDirectory()).endsWith(Paths.get(".sonar", "ws_cache", "url-0", "local")); - String expectedSonarHome = temp.getRoot().toPath().resolve(".sonar").toString(); - assertThat(cache.getBaseDirectory().getParent().toString()).isEqualTo(expectedSonarHome); + cache = new PersistentCacheBuilder(mock(Logger.class)).setAreaForGlobal("url", "0").build(); + assertThat(cache.getDirectory()).endsWith(Paths.get(".sonar", "ws_cache", "url-0", "global")); } } diff --git a/sonar-home/src/test/java/org/sonar/home/cache/PersistentCacheTest.java b/sonar-home/src/test/java/org/sonar/home/cache/PersistentCacheTest.java index 9760b096676..77ee30908e1 100644 --- a/sonar-home/src/test/java/org/sonar/home/cache/PersistentCacheTest.java +++ b/sonar-home/src/test/java/org/sonar/home/cache/PersistentCacheTest.java @@ -20,7 +20,13 @@ package org.sonar.home.cache; import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; import org.apache.commons.io.FileUtils; import org.junit.Before; import org.junit.Rule; @@ -28,20 +34,21 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; public class PersistentCacheTest { private final static String URI = "key1"; private final static String VALUE = "cache content"; private PersistentCache cache = null; + private DirectoryLock lock = null; @Rule public TemporaryFolder tmp = new TemporaryFolder(); @Before public void setUp() { - cache = new PersistentCache(tmp.getRoot().toPath(), Long.MAX_VALUE, mock(Logger.class), null); + lock = mock(DirectoryLock.class); + when(lock.getFileLockName()).thenReturn("lock"); + cache = new PersistentCache(tmp.getRoot().toPath(), Long.MAX_VALUE, mock(Logger.class), lock); } @Test @@ -49,60 +56,44 @@ public class PersistentCacheTest { assertCacheHit(false); } - @Test - public void testNullLoader() throws Exception { - assertThat(cache.get(URI, null)).isNull(); - assertCacheHit(false); - } - - @Test - public void testNullLoaderString() throws Exception { - assertThat(cache.getString(URI, null)).isNull(); - assertCacheHit(false); - } - - @Test - public void testNullValue() throws Exception { - // mocks have their methods returning null by default - PersistentCacheLoader c = mock(PersistentCacheLoader.class); - assertThat(cache.get(URI, c)).isNull(); - verify(c).get(); - assertCacheHit(false); - } - @Test public void testClean() throws Exception { + Path lockFile = cache.getDirectory().resolve("lock"); // puts entry - assertCacheHit(false); + cache.put(URI, VALUE.getBytes(StandardCharsets.UTF_8)); + Files.write(lockFile, "test".getBytes(StandardCharsets.UTF_8)); + assertCacheHit(true); // negative time to make sure it is expired - cache = new PersistentCache(tmp.getRoot().toPath(), -100, mock(Logger.class), null); + cache = new PersistentCache(tmp.getRoot().toPath(), -100, mock(Logger.class), lock); cache.clean(); assertCacheHit(false); + // lock file should not get deleted + assertThat(new String(Files.readAllBytes(lockFile), StandardCharsets.UTF_8)).isEqualTo("test"); } @Test public void testClear() throws Exception { - assertCacheHit(false); + Path lockFile = cache.getDirectory().resolve("lock"); + cache.put(URI, VALUE.getBytes(StandardCharsets.UTF_8)); + Files.write(lockFile, "test".getBytes(StandardCharsets.UTF_8)); + assertCacheHit(true); cache.clear(); assertCacheHit(false); + // lock file should not get deleted + assertThat(new String(Files.readAllBytes(lockFile), StandardCharsets.UTF_8)).isEqualTo("test"); } @Test public void testCacheHit() throws Exception { - assertCacheHit(false); - assertCacheHit(true); - } - - @Test - public void testPut() throws Exception { - cache.put(URI, VALUE.getBytes()); + cache.put(URI, VALUE.getBytes(StandardCharsets.UTF_8)); assertCacheHit(true); } @Test public void testReconfigure() throws Exception { - cache = new PersistentCache(tmp.getRoot().toPath(), Long.MAX_VALUE, mock(Logger.class), null); + cache = new PersistentCache(tmp.getRoot().toPath(), Long.MAX_VALUE, mock(Logger.class), lock); assertCacheHit(false); + cache.put(URI, VALUE.getBytes(StandardCharsets.UTF_8)); assertCacheHit(true); File root = tmp.getRoot(); @@ -113,26 +104,16 @@ public class PersistentCacheTest { assertThat(root).exists(); assertCacheHit(false); + cache.put(URI, VALUE.getBytes(StandardCharsets.UTF_8)); assertCacheHit(true); } @Test public void testExpiration() throws Exception { - // negative time to make sure it is expired on the second call - cache = new PersistentCache(tmp.getRoot().toPath(), -100, mock(Logger.class), null); - assertCacheHit(false); - assertCacheHit(false); - } - - @Test - public void testDifferentServerVersions() throws Exception { + // negative time to make sure it is expired + cache = new PersistentCache(tmp.getRoot().toPath(), -100, mock(Logger.class), lock); + cache.put(URI, VALUE.getBytes(StandardCharsets.UTF_8)); assertCacheHit(false); - assertCacheHit(true); - - PersistentCache cache2 = new PersistentCache(tmp.getRoot().toPath(), Long.MAX_VALUE, mock(Logger.class), "5.2"); - assertCacheHit(cache2, false); - assertCacheHit(cache2, true); - } private void assertCacheHit(boolean hit) throws Exception { @@ -140,31 +121,9 @@ public class PersistentCacheTest { } private void assertCacheHit(PersistentCache pCache, boolean hit) throws Exception { - CacheFillerString c = new CacheFillerString(); - assertThat(pCache.getString(URI, c)).isEqualTo(VALUE); - assertThat(c.wasCalled).isEqualTo(!hit); - } - - private class CacheFillerString implements PersistentCacheLoader { - public boolean wasCalled = false; - - @Override - public String get() { - wasCalled = true; - return VALUE; - } - } - - /** - * WSCache should be transparent regarding exceptions: if an exception is thrown by the value loader, it should pass through - * the cache to the original caller using the cache. - * @throws Exception - */ - @Test(expected = ArithmeticException.class) - public void testExceptions() throws Exception { - PersistentCacheLoader c = mock(PersistentCacheLoader.class); - when(c.get()).thenThrow(ArithmeticException.class); - cache.get(URI, c); + String expected = hit ? VALUE : null; + assertThat(pCache.getString(URI)).isEqualTo(expected); + verify(lock, atLeast(1)).unlock(); } } diff --git a/sonar-home/src/main/java/org/sonar/home/cache/PersistentCacheLoader.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/bootstrap/ProjectKey.java similarity index 86% rename from sonar-home/src/main/java/org/sonar/home/cache/PersistentCacheLoader.java rename to sonar-plugin-api/src/main/java/org/sonar/api/batch/bootstrap/ProjectKey.java index ee906b4b67d..8238f33837f 100644 --- a/sonar-home/src/main/java/org/sonar/home/cache/PersistentCacheLoader.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/bootstrap/ProjectKey.java @@ -17,10 +17,8 @@ * 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.home.cache; +package org.sonar.api.batch.bootstrap; -import java.io.IOException; - -public interface PersistentCacheLoader { - T get() throws IOException; +public interface ProjectKey { + String get(); } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/bootstrap/ProjectReactor.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/bootstrap/ProjectReactor.java index 81e2dbe3267..ddb09d932ff 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/bootstrap/ProjectReactor.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/bootstrap/ProjectReactor.java @@ -29,7 +29,7 @@ import java.util.List; * @since 2.9 */ @BatchSide -public class ProjectReactor { +public class ProjectReactor implements ProjectKey { private ProjectDefinition root; @@ -67,4 +67,12 @@ public class ProjectReactor { } return null; } + + @Override + public String get() { + if (root != null) { + return root.getKeyWithBranch(); + } + return null; + } } -- 2.39.5