View Javadoc
1   /*
2    * Copyright 2013-2020 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.cloud.contract.maven.verifier;
18  
19  import java.io.File;
20  import java.util.HashMap;
21  import java.util.Map;
22  
23  import org.apache.maven.execution.MavenSession;
24  import org.apache.maven.model.Dependency;
25  import org.apache.maven.plugin.AbstractMojo;
26  import org.apache.maven.plugin.MojoExecution;
27  import org.apache.maven.plugin.MojoExecutionException;
28  import org.apache.maven.plugins.annotations.Component;
29  import org.apache.maven.plugins.annotations.LifecyclePhase;
30  import org.apache.maven.plugins.annotations.Mojo;
31  import org.apache.maven.plugins.annotations.Parameter;
32  import org.apache.maven.project.MavenProject;
33  import org.apache.maven.shared.filtering.MavenResourcesFiltering;
34  import org.eclipse.aether.RepositorySystemSession;
35  
36  import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
37  import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties;
38  import org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter;
39  import org.springframework.cloud.contract.verifier.converter.ToYamlConverter;
40  
41  import static org.springframework.cloud.contract.maven.verifier.ChangeDetector.inputFilesChangeDetected;
42  
43  /**
44   * Convert Spring Cloud Contract Verifier contracts into stubs mappings.
45   * <p>
46   * This goal allows you to generate `stubs-jar` or execute `spring-cloud-contract:run`
47   * with generated mappings.
48   *
49   * @author Mariusz Smykula
50   */
51  @Mojo(name = "convert", requiresProject = false, defaultPhase = LifecyclePhase.PROCESS_TEST_RESOURCES)
52  public class ConvertMojo extends AbstractMojo {
53  
54  	static final String DEFAULT_STUBS_DIR = "${project.build.directory}/stubs/";
55  	static final String MAPPINGS_PATH = "/mappings";
56  	static final String CONTRACTS_PATH = "/contracts";
57  	static final String ORIGINAL_PATH = "/original";
58  
59  	@Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
60  	private RepositorySystemSession repoSession;
61  
62  	/**
63  	 * Directory containing Spring Cloud Contract Verifier contracts written using the
64  	 * GroovyDSL.
65  	 */
66  	@Parameter(property = "spring.cloud.contract.verifier.contractsDirectory",
67  			defaultValue = "${project.basedir}/src/test/resources/contracts")
68  	private File contractsDirectory;
69  
70  	/**
71  	 * Directory where the generated WireMock stubs from Groovy DSL should be placed. You
72  	 * can then mention them in your packaging task to create jar with stubs
73  	 */
74  	@Parameter(defaultValue = DEFAULT_STUBS_DIR)
75  	private File stubsDirectory;
76  
77  	/**
78  	 * Directory containing contracts written using the GroovyDSL
79  	 * <p>
80  	 * This parameter is only used when goal is executed outside of maven project.
81  	 */
82  	@Parameter(property = "contractsDirectory", defaultValue = "${basedir}")
83  	private File source;
84  
85  	@Parameter(property = "stubsDirectory", defaultValue = "${basedir}")
86  	private File destination;
87  
88  	@Parameter(property = "spring.cloud.contract.verifier.skip", defaultValue = "false")
89  	private boolean skip;
90  
91  	@Parameter(defaultValue = "${session}", readonly = true)
92  	private MavenSession mavenSession;
93  
94  	@Parameter(defaultValue = "${project}", readonly = true)
95  	private MavenProject project;
96  
97  	/**
98  	 * The URL from which a JAR containing the contracts should get downloaded. If not
99  	 * provided but artifactid / coordinates notation was provided then the current
100 	 * Maven's build repositories will be taken into consideration
101 	 */
102 	@Parameter(property = "contractsRepositoryUrl")
103 	private String contractsRepositoryUrl;
104 
105 	@Parameter(property = "contractDependency")
106 	private Dependency contractDependency;
107 
108 	/**
109 	 * The path in the JAR with all the contracts where contracts for this particular
110 	 * service lay. If not provided will be resolved to {@code groupid/artifactid}.
111 	 * Example: If {@code groupid} is {@code com.example} and {@code artifactid} is
112 	 * {@code service} then the resolved path will be {@code /com/example/artifactid}
113 	 */
114 	@Parameter(property = "contractsPath")
115 	private String contractsPath;
116 
117 	/**
118 	 * Picks the mode in which stubs will be found and registered.
119 	 */
120 	@Parameter(property = "contractsMode", defaultValue = "CLASSPATH")
121 	private StubRunnerProperties.StubsMode contractsMode;
122 
123 	/**
124 	 * If {@code true} then any file laying in a path that contains {@code build} or
125 	 * {@code target} will get excluded in further processing.
126 	 */
127 	@Parameter(property = "excludeBuildFolders", defaultValue = "false")
128 	private boolean excludeBuildFolders;
129 
130 	/**
131 	 * The user name to be used to connect to the repo with contracts.
132 	 */
133 	@Parameter(property = "contractsRepositoryUsername")
134 	private String contractsRepositoryUsername;
135 
136 	/**
137 	 * The password to be used to connect to the repo with contracts.
138 	 */
139 	@Parameter(property = "contractsRepositoryPassword")
140 	private String contractsRepositoryPassword;
141 
142 	/**
143 	 * The proxy host to be used to connect to the repo with contracts.
144 	 */
145 	@Parameter(property = "contractsRepositoryProxyHost")
146 	private String contractsRepositoryProxyHost;
147 
148 	/**
149 	 * The proxy port to be used to connect to the repo with contracts.
150 	 */
151 	@Parameter(property = "contractsRepositoryProxyPort")
152 	private Integer contractsRepositoryProxyPort;
153 
154 	/**
155 	 * If set to {@code false} will NOT delete stubs from a temporary folder after running
156 	 * tests.
157 	 */
158 	@Parameter(property = "deleteStubsAfterTest", defaultValue = "true")
159 	private boolean deleteStubsAfterTest;
160 
161 	/**
162 	 * Map of properties that can be passed to custom
163 	 * {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder}.
164 	 */
165 	@Parameter(property = "contractsProperties")
166 	private Map<String, String> contractsProperties = new HashMap<>();
167 
168 	/**
169 	 * If {@code true} then will convert contracts to a YAML representation.
170 	 */
171 	@Parameter(property = "convertToYaml", defaultValue = "false")
172 	private boolean convertToYaml;
173 
174 	@Component(role = MavenResourcesFiltering.class, hint = "default")
175 	private MavenResourcesFiltering mavenResourcesFiltering;
176 
177 	/**
178 	 * When enabled, this flag will tell stub runner to throw an exception when no stubs /
179 	 * contracts were found.
180 	 */
181 	@Parameter(property = "failOnNoContracts", defaultValue = "true")
182 	private boolean failOnNoContracts;
183 
184 	/**
185 	 * If set to true then stubs are created only when contracts have changed since last
186 	 * build.
187 	 */
188 	@Parameter(property = "incrementalContractStubs", defaultValue = "true")
189 	private boolean incrementalContractStubs = true;
190 
191 	@Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true)
192 	private MojoExecution mojoExecution;
193 
194 	@Parameter(defaultValue = "${session}", readonly = true, required = true)
195 	private MavenSession session;
196 
197 	@Override
198 	public void execute() throws MojoExecutionException {
199 		if (this.skip) {
200 			getLog().info(String.format(
201 					"Skipping Spring Cloud Contract Verifier execution: spring.cloud.contract.verifier.skip=%s",
202 					this.skip));
203 			return;
204 		}
205 		String groupId = this.project.getGroupId();
206 		String artifactId = this.project.getArtifactId();
207 		String version = this.project.getVersion();
208 		String rootPath = "META-INF/" + groupId + "/" + artifactId + "/" + version;
209 		// download contracts, unzip them and pass as output directory
210 		ContractVerifierConfigProperties config = new ContractVerifierConfigProperties();
211 		config.setExcludeBuildFolders(this.excludeBuildFolders);
212 		File contractsDirectory = locationOfContracts(config);
213 		contractsDirectory = contractSubfolderIfPresent(contractsDirectory);
214 
215 		if (this.incrementalContractStubs && !inputFilesChangeDetected(contractsDirectory, mojoExecution, session)) {
216 			getLog().info("Nothing to generate - all stubs are up to date");
217 			return;
218 		}
219 
220 		File contractsDslDir = contractsDslDir(contractsDirectory);
221 		LeftOverPrevention leftOverPrevention = new LeftOverPrevention(this.stubsDirectory, mojoExecution, session);
222 
223 		File copiedContracts = copyContracts(rootPath, config, contractsDirectory);
224 		if (this.convertToYaml) {
225 			contractsDslDir = copiedContracts;
226 			convertBackedUpDslsToYaml(rootPath, config, contractsDirectory, contractsDslDir);
227 		}
228 		config.setContractsDslDir(contractsDslDir);
229 		config.setStubsOutputDir(stubsOutputDir(rootPath));
230 		logSetup(config, contractsDslDir);
231 		RecursiveFilesConverter converter = new RecursiveFilesConverter(config.getStubsOutputDir(),
232 				config.getContractsDslDir(), config.getExcludedFiles(), config.getIncludedContracts(),
233 				config.isExcludeBuildFolders());
234 		converter.processFiles();
235 		leftOverPrevention.deleteLeftOvers();
236 	}
237 
238 	private void convertBackedUpDslsToYaml(String rootPath, ContractVerifierConfigProperties config,
239 			File contractsDirectory, File contractsDslDir) throws MojoExecutionException {
240 		copyOriginals(rootPath, config, contractsDirectory);
241 		ToYamlConverter.replaceContractWithYaml(contractsDslDir);
242 		getLog().info("Replaced DSL files with their YAML representation at [" + contractsDslDir + "]");
243 	}
244 
245 	private File copyOriginals(String rootPath, ContractVerifierConfigProperties config, File contractsDirectory)
246 			throws MojoExecutionException {
247 		File outputFolderWithOriginals = new File(this.stubsDirectory, rootPath + ORIGINAL_PATH);
248 		new CopyContracts(this.project, this.mavenSession, this.mavenResourcesFiltering, config)
249 				.copy(contractsDirectory, outputFolderWithOriginals);
250 		return outputFolderWithOriginals;
251 	}
252 
253 	private File copyContracts(String rootPath, ContractVerifierConfigProperties config, File contractsDirectory)
254 			throws MojoExecutionException {
255 		File outputFolderWithContracts = this.stubsDirectory.getPath().endsWith("contracts") ? this.stubsDirectory
256 				: new File(this.stubsDirectory, rootPath + CONTRACTS_PATH);
257 		new CopyContracts(this.project, this.mavenSession, this.mavenResourcesFiltering, config)
258 				.copy(contractsDirectory, outputFolderWithContracts);
259 		return outputFolderWithContracts;
260 	}
261 
262 	private void logSetup(ContractVerifierConfigProperties config, File contractsDslDir) {
263 		if (getLog().isDebugEnabled()) {
264 			getLog().debug("The contracts dir equals [" + contractsDslDir + "]");
265 		}
266 		getLog().info("Converting from Spring Cloud Contract Verifier contracts to WireMock stubs mappings");
267 		getLog().info(String.format("     Spring Cloud Contract Verifier contracts directory: %s",
268 				config.getContractsDslDir()));
269 		getLog().info(String.format("Stub Server stubs mappings directory: %s", config.getStubsOutputDir()));
270 	}
271 
272 	private File contractSubfolderIfPresent(File contractsDirectory) {
273 		File contractsSubFolder = new File(contractsDirectory, "contracts");
274 		if (contractsSubFolder.exists()) {
275 			if (getLog().isDebugEnabled()) {
276 				getLog().debug("The subfolder [contracts] exists, will pick it as a source of contracts");
277 			}
278 			contractsDirectory = contractsSubFolder;
279 		}
280 		return contractsDirectory;
281 	}
282 
283 	private File locationOfContracts(ContractVerifierConfigProperties config) {
284 		return new MavenContractsDownloader(this.project, this.contractDependency, this.contractsPath,
285 				this.contractsRepositoryUrl, this.contractsMode, getLog(), this.contractsRepositoryUsername,
286 				this.contractsRepositoryPassword, this.contractsRepositoryProxyHost, this.contractsRepositoryProxyPort,
287 				this.deleteStubsAfterTest, this.contractsProperties, this.failOnNoContracts)
288 						.downloadAndUnpackContractsIfRequired(config, this.contractsDirectory);
289 	}
290 
291 	private File stubsOutputDir(String rootPath) {
292 		return isInsideProject() ? new File(this.stubsDirectory, rootPath + MAPPINGS_PATH) : this.destination;
293 	}
294 
295 	private File contractsDslDir(File contractsDirectory) {
296 		return isInsideProject() ? contractsDirectory : this.source;
297 	}
298 
299 	private boolean isInsideProject() {
300 		return this.mavenSession.getRequest().isProjectPresent();
301 	}
302 
303 }