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 {@code true} then will not assert whether a stub / contract JAR was downloaded
156 	 * from local or remote location.
157 	 * @deprecated - with 2.1.0 this option is redundant
158 	 */
159 	@Parameter(property = "contractsSnapshotCheckSkip", defaultValue = "false")
160 	@Deprecated
161 	private boolean contractsSnapshotCheckSkip;
162 
163 	/**
164 	 * If set to {@code false} will NOT delete stubs from a temporary folder after running
165 	 * tests.
166 	 */
167 	@Parameter(property = "deleteStubsAfterTest", defaultValue = "true")
168 	private boolean deleteStubsAfterTest;
169 
170 	/**
171 	 * Map of properties that can be passed to custom
172 	 * {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder}.
173 	 */
174 	@Parameter(property = "contractsProperties")
175 	private Map<String, String> contractsProperties = new HashMap<>();
176 
177 	/**
178 	 * If {@code true} then will convert contracts to a YAML representation.
179 	 */
180 	@Parameter(property = "convertToYaml", defaultValue = "false")
181 	private boolean convertToYaml;
182 
183 	@Component(role = MavenResourcesFiltering.class, hint = "default")
184 	private MavenResourcesFiltering mavenResourcesFiltering;
185 
186 	/**
187 	 * When enabled, this flag will tell stub runner to throw an exception when no stubs /
188 	 * contracts were found.
189 	 */
190 	@Parameter(property = "failOnNoContracts", defaultValue = "true")
191 	private boolean failOnNoContracts;
192 
193 	/**
194 	 * If set to true then stubs are created only when contracts have changed since last
195 	 * build.
196 	 */
197 	@Parameter(property = "incrementalContractStubs", defaultValue = "true")
198 	private boolean incrementalContractStubs = true;
199 
200 	@Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true)
201 	private MojoExecution mojoExecution;
202 
203 	@Parameter(defaultValue = "${session}", readonly = true, required = true)
204 	private MavenSession session;
205 
206 	@Override
207 	public void execute() throws MojoExecutionException {
208 		if (this.skip) {
209 			getLog().info(String.format(
210 					"Skipping Spring Cloud Contract Verifier execution: spring.cloud.contract.verifier.skip=%s",
211 					this.skip));
212 			return;
213 		}
214 		String groupId = this.project.getGroupId();
215 		String artifactId = this.project.getArtifactId();
216 		String version = this.project.getVersion();
217 		String rootPath = "META-INF/" + groupId + "/" + artifactId + "/" + version;
218 		// download contracts, unzip them and pass as output directory
219 		ContractVerifierConfigProperties config = new ContractVerifierConfigProperties();
220 		config.setExcludeBuildFolders(this.excludeBuildFolders);
221 		File contractsDirectory = locationOfContracts(config);
222 		contractsDirectory = contractSubfolderIfPresent(contractsDirectory);
223 
224 		if (this.incrementalContractStubs && !inputFilesChangeDetected(contractsDirectory, mojoExecution, session)) {
225 			getLog().info("Nothing to generate - all stubs are up to date");
226 			return;
227 		}
228 
229 		File contractsDslDir = contractsDslDir(contractsDirectory);
230 		LeftOverPrevention leftOverPrevention = new LeftOverPrevention(this.stubsDirectory, mojoExecution, session);
231 
232 		File copiedContracts = copyContracts(rootPath, config, contractsDirectory);
233 		if (this.convertToYaml) {
234 			contractsDslDir = copiedContracts;
235 			convertBackedUpDslsToYaml(rootPath, config, contractsDirectory, contractsDslDir);
236 		}
237 		config.setContractsDslDir(contractsDslDir);
238 		config.setStubsOutputDir(stubsOutputDir(rootPath));
239 		logSetup(config, contractsDslDir);
240 		RecursiveFilesConverter converter = new RecursiveFilesConverter(config.getStubsOutputDir(),
241 				config.getContractsDslDir(), config.getExcludedFiles(), config.getIncludedContracts(),
242 				config.isExcludeBuildFolders());
243 		converter.processFiles();
244 		leftOverPrevention.deleteLeftOvers();
245 	}
246 
247 	private void convertBackedUpDslsToYaml(String rootPath, ContractVerifierConfigProperties config,
248 			File contractsDirectory, File contractsDslDir) throws MojoExecutionException {
249 		copyOriginals(rootPath, config, contractsDirectory);
250 		ToYamlConverter.replaceContractWithYaml(contractsDslDir);
251 		getLog().info("Replaced DSL files with their YAML representation at [" + contractsDslDir + "]");
252 	}
253 
254 	private File copyOriginals(String rootPath, ContractVerifierConfigProperties config, File contractsDirectory)
255 			throws MojoExecutionException {
256 		File outputFolderWithOriginals = new File(this.stubsDirectory, rootPath + ORIGINAL_PATH);
257 		new CopyContracts(this.project, this.mavenSession, this.mavenResourcesFiltering, config)
258 				.copy(contractsDirectory, outputFolderWithOriginals);
259 		return outputFolderWithOriginals;
260 	}
261 
262 	private File copyContracts(String rootPath, ContractVerifierConfigProperties config, File contractsDirectory)
263 			throws MojoExecutionException {
264 		File outputFolderWithContracts = this.stubsDirectory.getPath().endsWith("contracts") ? this.stubsDirectory
265 				: new File(this.stubsDirectory, rootPath + CONTRACTS_PATH);
266 		new CopyContracts(this.project, this.mavenSession, this.mavenResourcesFiltering, config)
267 				.copy(contractsDirectory, outputFolderWithContracts);
268 		return outputFolderWithContracts;
269 	}
270 
271 	private void logSetup(ContractVerifierConfigProperties config, File contractsDslDir) {
272 		if (getLog().isDebugEnabled()) {
273 			getLog().debug("The contracts dir equals [" + contractsDslDir + "]");
274 		}
275 		getLog().info("Converting from Spring Cloud Contract Verifier contracts to WireMock stubs mappings");
276 		getLog().info(String.format("     Spring Cloud Contract Verifier contracts directory: %s",
277 				config.getContractsDslDir()));
278 		getLog().info(String.format("Stub Server stubs mappings directory: %s", config.getStubsOutputDir()));
279 	}
280 
281 	private File contractSubfolderIfPresent(File contractsDirectory) {
282 		File contractsSubFolder = new File(contractsDirectory, "contracts");
283 		if (contractsSubFolder.exists()) {
284 			if (getLog().isDebugEnabled()) {
285 				getLog().debug("The subfolder [contracts] exists, will pick it as a source of contracts");
286 			}
287 			contractsDirectory = contractsSubFolder;
288 		}
289 		return contractsDirectory;
290 	}
291 
292 	private File locationOfContracts(ContractVerifierConfigProperties config) {
293 		return new MavenContractsDownloader(this.project, this.contractDependency, this.contractsPath,
294 				this.contractsRepositoryUrl, this.contractsMode, getLog(), this.contractsRepositoryUsername,
295 				this.contractsRepositoryPassword, this.contractsRepositoryProxyHost, this.contractsRepositoryProxyPort,
296 				this.deleteStubsAfterTest, this.contractsProperties, this.failOnNoContracts)
297 						.downloadAndUnpackContractsIfRequired(config, this.contractsDirectory);
298 	}
299 
300 	private File stubsOutputDir(String rootPath) {
301 		return isInsideProject() ? new File(this.stubsDirectory, rootPath + MAPPINGS_PATH) : this.destination;
302 	}
303 
304 	private File contractsDslDir(File contractsDirectory) {
305 		return isInsideProject() ? contractsDirectory : this.source;
306 	}
307 
308 	private boolean isInsideProject() {
309 		return this.mavenSession.getRequest().isProjectPresent();
310 	}
311 
312 }