Made stub downloading pluggable (#165)

* Added implementation for pluggable stub downloading
* Added docs and test for pluggable sub dowloader

fixes #165
This commit is contained in:
Marcin Grzejszczak
2016-12-13 09:32:32 +01:00
committed by GitHub
parent a649ade37b
commit faa18a69b1
21 changed files with 297 additions and 88 deletions

View File

@@ -602,10 +602,35 @@ and just register it in your `spring.factories` file
[source]
----
include::{tests_path}/spring-cloud-contract-stub-runner-moco/src/test/resources/META-INF/spring.factories[indent=0]
include::{tests_path}/spring-cloud-contract-stub-runner-moco/src/test/resources/META-INF/spring.factories[indent=0,lines=1,4]
----
that way you'll be able to run stubs using Moco.
IMPORTANT: If you don't provide any implementation then the default one - WireMock based
will be picked. If you provide more than one then the first one on the list will be picked.
==== Custom Stub Downloader
You can customize the way your stubs are downloaded. If you don't want to download the JARs
from Nexus / Artifactory in the way we do by default you can set your own implementation.
Below you can find an example of a Stub Downloader Provider that takes `json` files from the test resources
from classpath, copies them to a temp file and then passes that temporary folder
as a root for the stubs.
[source,java]
----
include::{tests_path}/spring-cloud-contract-stub-runner-moco/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/moco/ClasspathStubProvider.groovy[indent=0]
----
and just register it in your `spring.factories` file
[source]
----
include::{tests_path}/spring-cloud-contract-stub-runner-moco/src/test/resources/META-INF/spring.factories[indent=0,lines=5..-1]
----
that way you'll be able to pick a folder with the source of your stubs.
IMPORTANT: If you don't provide any implementation then the default one - Aether based that will download stubs from a remote repo
will be picked. If you provide more than one then the first one on the list will be picked.

View File

@@ -32,7 +32,12 @@ public class BatchStubRunnerFactory {
private final MessageVerifier<?> contractVerifierMessaging;
public BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions) {
this(stubRunnerOptions, new AetherStubDownloader(stubRunnerOptions), new NoOpStubMessages());
this(stubRunnerOptions, aetherStubDownloader(stubRunnerOptions), new NoOpStubMessages());
}
private static StubDownloader aetherStubDownloader(StubRunnerOptions stubRunnerOptions) {
StubDownloaderBuilderProvider provider = new StubDownloaderBuilderProvider();
return provider.hasBuilder() ? provider.get().build(stubRunnerOptions) : new AetherStubDownloader(stubRunnerOptions);
}
public BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions, StubDownloader stubDownloader) {

View File

@@ -19,6 +19,16 @@ package org.springframework.cloud.contract.stubrunner;
import java.io.File;
import java.util.Map;
/**
* Contract for providing a tuple containing configuration of a downloaded
* and unpacked stub, together with the file location of that extracted artifact.
*
* Note: Actually the artifact doesn't have to be a JAR. method name contains
* that suffix for historical reasons.
*
* @author Marcin Grzejszczak
* @since 1.0.0
*/
public interface StubDownloader {
/**

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner;
/**
* Builder for a {@link StubDownloader}. Can't allow direct usage
* of {@link StubDownloader} cause in order to register instances
* of this interface in {@link org.springframework.core.io.support.SpringFactoriesLoader}
* one needs a default constructor whereas the {@link StubDownloader}
* instances need to be constructed from stub related options.
*
* @author Marcin Grzejszczak
* @since 1.1.0
*/
public interface StubDownloaderBuilder {
StubDownloader build(StubRunnerOptions stubRunnerOptions);
}

View File

@@ -0,0 +1,30 @@
package org.springframework.cloud.contract.stubrunner;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.io.support.SpringFactoriesLoader;
/**
* Provider for {@link StubDownloaderBuilder}
*
* @author Marcin Grzejszczak
* @since 1.1.0
*/
public class StubDownloaderBuilderProvider {
private final List<StubDownloaderBuilder> builders = new ArrayList<>();
public StubDownloaderBuilderProvider() {
this.builders.addAll(
SpringFactoriesLoader.loadFactories(StubDownloaderBuilder.class, null));
}
public StubDownloaderBuilder get() {
return this.builders.isEmpty() ? null : this.builders.get(0);
}
public boolean hasBuilder() {
return get() != null;
}
}

View File

@@ -117,6 +117,30 @@ public class StubRunnerOptions {
return this.stubIdsToPortMapping;
}
public String getStubRepositoryRoot() {
return this.stubRepositoryRoot;
}
public boolean isWorkOffline() {
return this.workOffline;
}
public String getStubsClassifier() {
return this.stubsClassifier;
}
public String getUsername() {
return this.username;
}
public String getPassword() {
return this.password;
}
public StubRunnerProxyOptions getStubRunnerProxyOptions() {
return this.stubRunnerProxyOptions;
}
public StubRunnerProxyOptions getProxyOptions() {
return this.stubRunnerProxyOptions;
}

View File

@@ -28,7 +28,7 @@ import org.springframework.cloud.contract.stubrunner.BatchStubRunner;
import org.springframework.cloud.contract.stubrunner.BatchStubRunnerFactory;
import org.springframework.cloud.contract.stubrunner.RunningStubs;
import org.springframework.cloud.contract.stubrunner.StubConfiguration;
import org.springframework.cloud.contract.stubrunner.StubDownloader;
import org.springframework.cloud.contract.stubrunner.StubDownloaderBuilderProvider;
import org.springframework.cloud.contract.stubrunner.StubRunnerOptions;
import org.springframework.cloud.contract.stubrunner.StubRunnerOptionsBuilder;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
@@ -53,8 +53,7 @@ public class StubRunnerConfiguration {
@Autowired(required = false)
private MessageVerifier<?> contractVerifierMessaging;
@Autowired(required = false)
private StubDownloader stubDownloader;
private StubDownloaderBuilderProvider provider = new StubDownloaderBuilderProvider();
@Autowired
private StubRunnerProperties props;
@Autowired
@@ -73,7 +72,7 @@ public class StubRunnerConfiguration {
}
StubRunnerOptions stubRunnerOptions = builder.build();
BatchStubRunner batchStubRunner = new BatchStubRunnerFactory(stubRunnerOptions,
this.stubDownloader != null ? this.stubDownloader
this.provider.hasBuilder() ? this.provider.get().build(stubRunnerOptions)
: new AetherStubDownloader(stubRunnerOptions),
this.contractVerifierMessaging != null ? this.contractVerifierMessaging
: new NoOpStubMessages()).buildBatchStubRunner();

View File

@@ -39,20 +39,20 @@ import java.nio.file.Paths
@CompileStatic
class RecursiveFilesConverter {
private final StubGeneratorHolder holder
private final StubGeneratorProvider holder
private final ContractVerifierConfigProperties properties
private final File outMappingsDir
RecursiveFilesConverter(ContractVerifierConfigProperties properties, StubGeneratorHolder holder = null) {
RecursiveFilesConverter(ContractVerifierConfigProperties properties, StubGeneratorProvider holder = null) {
this.properties = properties
this.outMappingsDir = properties.stubsOutputDir
this.holder = holder ?: new StubGeneratorHolder()
this.holder = holder ?: new StubGeneratorProvider()
}
RecursiveFilesConverter(ContractVerifierConfigProperties properties, File outMappingsDir, StubGeneratorHolder holder = null) {
RecursiveFilesConverter(ContractVerifierConfigProperties properties, File outMappingsDir, StubGeneratorProvider holder = null) {
this.properties = properties
this.outMappingsDir = outMappingsDir
this.holder = holder ?: new StubGeneratorHolder()
this.holder = holder ?: new StubGeneratorProvider()
}
void processFiles() {

View File

@@ -10,15 +10,15 @@ import org.springframework.core.io.support.SpringFactoriesLoader
* @since 1.1.0
*/
@CompileStatic
class StubGeneratorHolder {
class StubGeneratorProvider {
private final List<StubGenerator> converters = []
StubGeneratorHolder() {
StubGeneratorProvider() {
this.converters.addAll(SpringFactoriesLoader.loadFactories(StubGenerator, null))
}
StubGeneratorHolder(List<StubGenerator> converters) {
StubGeneratorProvider(List<StubGenerator> converters) {
this.converters.addAll(converters)
}

View File

@@ -24,7 +24,7 @@ import org.springframework.cloud.contract.verifier.config.ContractVerifierConfig
import org.springframework.cloud.contract.verifier.converter.ConversionContractVerifierException
import org.springframework.cloud.contract.verifier.converter.SingleFileConverter
import org.springframework.cloud.contract.verifier.converter.StubGenerator
import org.springframework.cloud.contract.verifier.converter.StubGeneratorHolder
import org.springframework.cloud.contract.verifier.converter.StubGeneratorProvider
import org.springframework.cloud.contract.verifier.file.ContractFileScanner
import org.springframework.cloud.contract.verifier.file.ContractMetadata
import org.springframework.cloud.contract.verifier.util.NamesUtil
@@ -45,34 +45,34 @@ import java.nio.file.Paths
@Deprecated
class RecursiveFilesConverter {
private final StubGeneratorHolder holder
private final StubGeneratorProvider holder
private final ContractVerifierConfigProperties properties
private final File outMappingsDir
RecursiveFilesConverter(ContractVerifierConfigProperties properties, StubGeneratorHolder holder = null) {
RecursiveFilesConverter(ContractVerifierConfigProperties properties, StubGeneratorProvider holder = null) {
this.properties = properties
this.outMappingsDir = properties.stubsOutputDir
this.holder = holder ?: new StubGeneratorHolder()
this.holder = holder ?: new StubGeneratorProvider()
}
RecursiveFilesConverter(ContractVerifierConfigProperties properties, File outMappingsDir, StubGeneratorHolder holder = null) {
RecursiveFilesConverter(ContractVerifierConfigProperties properties, File outMappingsDir, StubGeneratorProvider holder = null) {
this.properties = properties
this.outMappingsDir = outMappingsDir
this.holder = holder ?: new StubGeneratorHolder()
this.holder = holder ?: new StubGeneratorProvider()
}
@Deprecated
RecursiveFilesConverter(SingleFileConverter singleFileConverter, ContractVerifierConfigProperties properties) {
this.properties = properties
this.outMappingsDir = properties.stubsOutputDir
this.holder = new StubGeneratorHolder()
this.holder = new StubGeneratorProvider()
}
@Deprecated
RecursiveFilesConverter(SingleFileConverter singleFileConverter, ContractVerifierConfigProperties properties, File outMappingsDir) {
this.properties = properties
this.outMappingsDir = outMappingsDir
this.holder = new StubGeneratorHolder()
this.holder = new StubGeneratorProvider()
}
void processFiles() {

View File

@@ -89,7 +89,7 @@ class RecursiveFilesConverterSpec extends Specification {
ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties()
properties.contractsDslDir = tmpFolder.root
properties.stubsOutputDir = tmpFolder.root
RecursiveFilesConverter recursiveFilesConverter = new RecursiveFilesConverter(properties, new StubGeneratorHolder([stubGenerator]))
RecursiveFilesConverter recursiveFilesConverter = new RecursiveFilesConverter(properties, new StubGeneratorProvider([stubGenerator]))
when:
recursiveFilesConverter.processFiles()
then:

View File

@@ -10,6 +10,10 @@ import org.springframework.cloud.contract.maven.verifier.stubrunner.AetherStubDo
import org.springframework.cloud.contract.stubrunner.AetherStubDownloader;
import org.springframework.cloud.contract.stubrunner.ContractDownloader;
import org.springframework.cloud.contract.stubrunner.StubConfiguration;
import org.springframework.cloud.contract.stubrunner.StubDownloader;
import org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder;
import org.springframework.cloud.contract.stubrunner.StubDownloaderBuilderProvider;
import org.springframework.cloud.contract.stubrunner.StubRunnerOptions;
import org.springframework.cloud.contract.stubrunner.StubRunnerOptionsBuilder;
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties;
import org.springframework.util.StringUtils;
@@ -33,6 +37,7 @@ class MavenContractsDownloader {
private final Log log;
private final AetherStubDownloaderFactory aetherStubDownloaderFactory;
private final RepositorySystemSession repoSession;
private final StubDownloaderBuilderProvider stubDownloaderBuilderProvider;
MavenContractsDownloader(MavenProject project, Dependency contractDependency,
String contractsPath, String contractsRepositoryUrl,
@@ -47,6 +52,7 @@ class MavenContractsDownloader {
this.log = log;
this.aetherStubDownloaderFactory = aetherStubDownloaderFactory;
this.repoSession = repoSession;
this.stubDownloaderBuilderProvider = new StubDownloaderBuilderProvider();
}
File downloadAndUnpackContractsIfRequired(ContractVerifierConfigProperties config, File defaultContractsDir) {
@@ -77,20 +83,36 @@ class MavenContractsDownloader {
this.contractsPath, this.project.getGroupId(), this.project.getArtifactId());
}
private AetherStubDownloader stubDownloader() {
private StubDownloader stubDownloader() {
StubDownloaderBuilder builder = this.stubDownloaderBuilderProvider.get();
if (StringUtils.hasText(this.contractsRepositoryUrl) || this.contractsWorkOffline) {
this.log.info("Will download contracts from [" + this.contractsRepositoryUrl + "]. "
+ "Work offline switch equals to [" + this.contractsWorkOffline + "]");
return new AetherStubDownloader(
new StubRunnerOptionsBuilder()
.withStubRepositoryRoot(this.contractsRepositoryUrl)
.withWorkOffline(this.contractsWorkOffline)
.build());
if (builder != null) {
logStubDownloader(builder);
return builder.build(buildOptions());
}
return new AetherStubDownloader(buildOptions());
}
this.log.info("Will download contracts using current build's Maven repository setup");
if (builder != null) {
logStubDownloader(builder);
return builder.build(buildOptions());
}
return this.aetherStubDownloaderFactory.build(this.repoSession);
}
private void logStubDownloader(StubDownloaderBuilder builder) {
this.log.info("A custom stub downloader [" + builder + "] was provided");
}
StubRunnerOptions buildOptions() {
return new StubRunnerOptionsBuilder()
.withStubRepositoryRoot(this.contractsRepositoryUrl)
.withWorkOffline(this.contractsWorkOffline)
.build();
}
private StubConfiguration stubConfiguration() {
String groupId = this.contractDependency.getGroupId();
String artifactId = this.contractDependency.getArtifactId();

View File

@@ -23,6 +23,7 @@ import org.apache.maven.project.MavenProject;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.springframework.cloud.contract.stubrunner.AetherStubDownloader;
import org.springframework.cloud.contract.stubrunner.StubDownloader;
@Named
@Singleton
@@ -37,7 +38,7 @@ public class AetherStubDownloaderFactory {
this.project = project;
}
public AetherStubDownloader build(RepositorySystemSession repoSession) {
public StubDownloader build(RepositorySystemSession repoSession) {
return new AetherStubDownloader(this.repoSystem,
this.project.getRemoteProjectRepositories(), repoSession);
}

View File

@@ -22,10 +22,10 @@ import javax.inject.Named;
import org.eclipse.aether.RepositorySystemSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.contract.stubrunner.AetherStubDownloader;
import org.springframework.cloud.contract.stubrunner.BatchStubRunner;
import org.springframework.cloud.contract.stubrunner.BatchStubRunnerFactory;
import org.springframework.cloud.contract.stubrunner.RunningStubs;
import org.springframework.cloud.contract.stubrunner.StubDownloader;
import org.springframework.cloud.contract.stubrunner.StubRunnerOptions;
@Named
@@ -39,7 +39,7 @@ public class RemoteStubRunner {
}
public BatchStubRunner run(StubRunnerOptions options, RepositorySystemSession repositorySystemSession) {
AetherStubDownloader stubDownloader = this.aetherStubDownloaderFactory.build(repositorySystemSession);
StubDownloader stubDownloader = this.aetherStubDownloaderFactory.build(repositorySystemSession);
try {
if (log.isDebugEnabled()) {
log.debug("Launching StubRunner with args: " + options);

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2013-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.provider.moco
import org.springframework.cloud.contract.stubrunner.StubConfiguration
import org.springframework.cloud.contract.stubrunner.StubDownloader
import org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder
import org.springframework.cloud.contract.stubrunner.StubRunnerOptions
import org.springframework.core.io.DefaultResourceLoader
import org.springframework.core.io.Resource
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
import java.nio.file.Files
/**
* Poor man's version of taking stubs from classpath. It needs much more
* love and attention to go to the main sources.
*
* @author Marcin Grzejszczak
*/
class ClasspathStubProvider implements StubDownloaderBuilder {
private static final int TEMP_DIR_ATTEMPTS = 10000
@Override
public StubDownloader build(StubRunnerOptions stubRunnerOptions) {
final StubConfiguration configuration = stubRunnerOptions.getDependencies().first()
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(
new DefaultResourceLoader())
try {
String rootFolder = repoRoot(stubRunnerOptions) ?: "**/" + separatedArtifact(configuration) + "/**/*.json"
Resource[] resources = resolver.getResources(rootFolder)
final File tmp = createTempDir()
tmp.deleteOnExit()
// you'd have to write an impl to maintain the folder structure
// this is just for demo
resources.each { Resource resource ->
Files.copy(resource.getInputStream(), new File(tmp, resource.getFile().getName()).toPath())
}
return new StubDownloader() {
@Override
public Map.Entry<StubConfiguration, File> downloadAndUnpackStubJar(
StubConfiguration stubConfiguration) {
return new AbstractMap.SimpleEntry(configuration, tmp)
}
}
} catch (IOException e) {
throw new IllegalStateException(e)
}
}
private String repoRoot(StubRunnerOptions stubRunnerOptions) {
switch (stubRunnerOptions.stubRepositoryRoot) {
case { !it }:
return ""
case { String root -> root.endsWith("**/*.json") }:
return stubRunnerOptions.stubRepositoryRoot
default:
return stubRunnerOptions.stubRepositoryRoot + "/**/*.json"
}
}
private String separatedArtifact(StubConfiguration configuration) {
return configuration.getGroupId().replace(".", File.separator) +
File.separator + configuration.getArtifactId()
}
// Taken from Guava
private File createTempDir() {
File baseDir = new File(System.getProperty("java.io.tmpdir"))
String baseName = System.currentTimeMillis() + "-"
for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
File tempDir = new File(baseDir, baseName + counter)
if (tempDir.mkdir()) {
return tempDir
}
}
throw new IllegalStateException(
"Failed to create directory within " + TEMP_DIR_ATTEMPTS + " attempts (tried " + baseName + "0 to " + baseName + (
TEMP_DIR_ATTEMPTS - 1) + ")")
}
}

View File

@@ -33,8 +33,8 @@ import spock.lang.Specification
@ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@AutoConfigureStubRunner( ids =
["org.springframework.cloud.contract.verifier.stubs:fraudDetectionServerMoco"],
repositoryRoot = "classpath:m2repo/repository/")
["com.example:fraudDetectionServerMoco"],
repositoryRoot = "classpath:unpacked/")
@DirtiesContext
class MocoHttpServerStubSpec extends Specification {

View File

@@ -1,2 +1,8 @@
# Example of a custom HTTP Server Stub
org.springframework.cloud.contract.stubrunner.HttpServerStub=\
org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub
org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub
# Example of a custom Stub Downloader Provider
org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\
org.springframework.cloud.contract.stubrunner.provider.moco.ClasspathStubProvider

View File

@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2013-2016 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.cloud.contract.verifier.stubs</groupId>
<artifactId>fraudDetectionServerMoco</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
</project>

View File

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2013-2016 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<metadata>
<groupId>org.springframework.cloud.contract.verifier.stubs</groupId>
<artifactId>fraudDetectionServerMoco</artifactId>
<version>0.0.1-SNAPSHOT</version>
<versioning>
<versions>
<version>0.0.1-SNAPSHOT</version>
</versions>
<lastUpdated>20160409062112</lastUpdated>
</versioning>
</metadata>

View File

@@ -0,0 +1,12 @@
[
{
"request": {
"method" : "get",
"uri": "/name"
},
"response": {
"text" : "fraudDetectionServerMoco",
"status": 200
}
}
]