diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.java b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.java index 863c0be992..ec7ca2bac4 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.java @@ -36,7 +36,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.cloud.contract.spec.Contract; import org.springframework.cloud.contract.verifier.file.ContractFileScanner; -import org.springframework.cloud.contract.verifier.file.ContractFileScannerBuilder; import org.springframework.cloud.contract.verifier.file.ContractMetadata; import org.springframework.cloud.contract.verifier.util.NamesUtil; import org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter; @@ -80,7 +79,7 @@ public class RecursiveFilesConverter { } public void processFiles() { - ContractFileScanner scanner = new ContractFileScannerBuilder().baseDir(contractsDslDir) + ContractFileScanner scanner = ContractFileScanner.builder().baseDir(contractsDslDir) .excluded(new HashSet<>(excludedFiles)).ignored(new HashSet<>()).included(new HashSet<>()) .includeMatcher(includedContracts).build(); MultiValueMap contracts = scanner.findContractsRecursively(); diff --git a/spring-cloud-contract-verifier/pom.xml b/spring-cloud-contract-verifier/pom.xml index 4c66b0205d..ce2f284c5f 100644 --- a/spring-cloud-contract-verifier/pom.xml +++ b/spring-cloud-contract-verifier/pom.xml @@ -63,6 +63,10 @@ jakarta.jms-api true + + org.apache.commons + commons-text + org.apache.camel.springboot camel-spring-boot-starter diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/assertion/SpringCloudContractAssertions.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/assertion/SpringCloudContractAssertions.groovy deleted file mode 100644 index 0f25773135..0000000000 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/assertion/SpringCloudContractAssertions.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package org.springframework.cloud.contract.verifier.assertion - -import groovy.transform.CompileStatic -import org.assertj.core.api.Assertions - -@CompileStatic -public class SpringCloudContractAssertions extends Assertions { - - /** - * Creates a new instance of {@link CollectionAssert}. - * @param type to assert - * @param actual the actual value. - * @return the created assertion object. - */ - public static CollectionAssert assertThat( - Iterable actual) { - return new CollectionAssert<>(actual); - } -} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy deleted file mode 100644 index a9a06d7300..0000000000 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.groovy +++ /dev/null @@ -1,461 +0,0 @@ -/* - * Copyright 2013-2020 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 - * - * https://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.verifier.converter - -import java.util.regex.Pattern - -import groovy.transform.PackageScope - -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.spec.internal.BodyMatcher -import org.springframework.cloud.contract.spec.internal.Cookies -import org.springframework.cloud.contract.spec.internal.DslProperty -import org.springframework.cloud.contract.spec.internal.ExecutionProperty -import org.springframework.cloud.contract.spec.internal.FromFileProperty -import org.springframework.cloud.contract.spec.internal.Headers -import org.springframework.cloud.contract.spec.internal.MatchingStrategy -import org.springframework.cloud.contract.spec.internal.MatchingType -import org.springframework.cloud.contract.spec.internal.Multipart -import org.springframework.cloud.contract.spec.internal.NamedProperty -import org.springframework.cloud.contract.spec.internal.NotToEscapePattern -import org.springframework.cloud.contract.spec.internal.RegexProperty -import org.springframework.cloud.contract.spec.internal.Url -import org.springframework.cloud.contract.verifier.converter.YamlContract.RegexType -import org.springframework.cloud.contract.verifier.util.ContentType -import org.springframework.cloud.contract.verifier.util.JsonPaths -import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter -import org.springframework.cloud.contract.verifier.util.MapConverter - -import static org.springframework.cloud.contract.verifier.util.ContentType.XML -import static org.springframework.cloud.contract.verifier.util.ContentUtils.evaluateClientSideContentType - -/** - * @author Marcin Grzejszczak - * @author Olga Maciaszek-Sharma - */ -@PackageScope -class ContractsToYaml { - - List convertTo(Collection contracts) { - return contracts.collect { Contract contract -> - YamlContract yamlContract = new YamlContract() - if (contract == null) { - return yamlContract - } - yamlContract.name = contract.name - yamlContract.ignored = contract.ignored - yamlContract.inProgress = contract.inProgress - yamlContract.description = contract.description - yamlContract.label = contract.label - yamlContract.metadata = contract.metadata - request(contract, yamlContract) - response(yamlContract, contract) - input(contract, yamlContract) - output(contract, yamlContract) - return yamlContract - } - } - - protected void output(Contract contract, YamlContract yamlContract) { - if (!contract.outputMessage) { - return - } - ContentType contentType = evaluateClientSideContentType(contract.response?.headers, - contract.response?.body) - yamlContract.outputMessage = new YamlContract.OutputMessage() - yamlContract.outputMessage.sentTo = MapConverter. - getStubSideValues(contract.outputMessage.sentTo) - yamlContract.outputMessage.headers = (contract.outputMessage?.headers as Headers)?. - asStubSideMap() - yamlContract.outputMessage.body = MapConverter.getStubSideValues( - contract.outputMessage?.body) - contract.outputMessage?.bodyMatchers?.matchers()?.each { BodyMatcher matcher -> - yamlContract.outputMessage.matchers.body << new YamlContract.BodyTestMatcher( - path: matcher.path(), - type: testMatcherType(matcher.matchingType()), - value: matcher.value()?.toString(), - minOccurrence: matcher.minTypeOccurrence(), - maxOccurrence: matcher.maxTypeOccurrence() - ) - } - if (XML != contentType) { - setOutputBodyMatchers(contract.outputMessage?.body, - yamlContract.outputMessage.matchers.body) - } - setOutputHeadersMatchers(contract.outputMessage?.headers, - yamlContract.outputMessage.matchers.headers) - } - - protected void input(Contract contract, YamlContract yamlContract) { - if (!contract.input) { - return - } - ContentType contentType = evaluateClientSideContentType(contract.input?.messageHeaders, - contract.input?.messageBody) - yamlContract.input = new YamlContract.Input() - yamlContract.input.assertThat = MapConverter. - getTestSideValues(contract.input?.assertThat?.toString()) - yamlContract.input.triggeredBy = MapConverter. - getTestSideValues(contract.input?.triggeredBy?.toString()) - yamlContract.input.messageHeaders = (contract.input?.messageHeaders as Headers)?. - asTestSideMap() - yamlContract.input.messageBody = MapConverter. - getTestSideValues(contract.input?.messageBody) - yamlContract.input.messageFrom = MapConverter. - getTestSideValues(contract.input?.messageFrom) - contract.input?.bodyMatchers?.matchers()?.each { BodyMatcher matcher -> - yamlContract.input.matchers.body << new YamlContract.BodyStubMatcher( - path: matcher.path(), - type: stubMatcherType(matcher.matchingType()), - value: matcher.value()?.toString() - ) - } - if (XML != contentType) { - setInputBodyMatchers(contract.input?.messageBody, yamlContract.input.matchers.body) - } - setInputHeadersMatchers(contract.input?.messageHeaders as Headers, yamlContract.input.matchers.headers) - } - - protected void request(Contract contract, YamlContract yamlContract) { - if (!contract.request) { - return - } - ContentType requestContentType = evaluateClientSideContentType(contract.request.headers, - contract.request.body) - yamlContract.request = new YamlContract.Request() - yamlContract.request.with { YamlContract.Request request -> - request.method = contract.request?.method?.serverValue - request.url = contract.request?.url?.serverValue - request.urlPath = contract.request?.urlPath?.serverValue - request.matchers = new YamlContract.StubMatchers() - Url requestUrl = contract.request.url ?: contract.request.urlPath - if (requestUrl.queryParameters != null) { - request.queryParameters = requestUrl.queryParameters - .parameters.collectEntries { - def testSide = MapConverter.getTestSideValuesForNonBody(it) - def stubSide = it.clientValue - if (stubSide instanceof RegexProperty || stubSide instanceof Pattern) { - request.matchers.queryParameters.add(new YamlContract.QueryParameterMatcher(key: it.name, type: YamlContract.MatchingType.matching, value: new RegexProperty(stubSide).pattern())) - } - else if (stubSide instanceof MatchingStrategy) { - request.matchers.queryParameters.add(new YamlContract.QueryParameterMatcher(key: it.name, type: YamlContract.MatchingType.from(stubSide.getType().name), value: MapConverter.getStubSideValuesForNonBody(stubSide))) - } - return [(it.name): testSide] - } - } - request.headers = (contract.request?.headers as Headers)?.asMap { - String headerName, DslProperty prop -> - def testSideValue = MapConverter.getTestSideValues(prop) - if (testSideValue instanceof ExecutionProperty) { - return MapConverter.getStubSideValuesForNonBody(prop).toString() - } - return testSideValue.toString() - } - request.cookies = (contract.request?.cookies as Cookies)?.asTestSideMap() - Object body = contract.request?.body?.serverValue - if (body instanceof FromFileProperty) { - if (body.isByte()) { - request.bodyFromFileAsBytes = body.fileName() - } - if (body.isString()) { - request.bodyFromFile = body.fileName() - } - } - else { - request.body = MapConverter.getTestSideValues(contract.request?.body) - } - Multipart multipart = contract.request.multipart - if (multipart) { - request.multipart = new YamlContract.Multipart() - Map map = (Map) MapConverter. - getTestSideValues(multipart) - map.each { String key, Object value -> - if (value instanceof NamedProperty) { - Object fileName = value.name?.serverValue - Object contentType = value.contentType?.serverValue - Object fileContent = value.value?.serverValue - request.multipart.named << new YamlContract.Named(paramName: key, - fileName: fileName instanceof String ? value.name?.serverValue as String : null, - fileContent: fileContent instanceof String ? fileContent as String : null, - fileContentAsBytes: fileContent instanceof FromFileProperty ? fileContent.asBytes().toString() : null, - fileContentFromFileAsBytes: resolveFileNameAsBytes(fileContent), - contentType: contentType instanceof String ? contentType as String : null, - fileNameCommand: fileName instanceof ExecutionProperty ? fileName.toString() : null, - fileContentCommand: fileContent instanceof ExecutionProperty ? fileContent.toString() : null, - contentTypeCommand: contentType instanceof ExecutionProperty ? contentType.toString() : null) - } - else { - request.multipart.params.put(key, value != null ? value.toString() : null) - } - } - } - contract.request?.bodyMatchers?.matchers()?.each { BodyMatcher matcher -> - request.matchers.body << new YamlContract.BodyStubMatcher( - path: matcher.path(), - type: stubMatcherType(matcher.matchingType()), - value: matcher.value()?.toString(), - minOccurrence: matcher.minTypeOccurrence(), - maxOccurrence: matcher.maxTypeOccurrence(), - ) - } - Object url = contract.request.url?.clientValue - request.matchers.url = url instanceof RegexProperty ? - new YamlContract.KeyValueMatcher(regex: url.pattern()) : - url instanceof ExecutionProperty ? - new YamlContract.KeyValueMatcher(command: url.toString()) : null - Object urlPath = contract.request.urlPath?.clientValue - request.matchers.url = urlPath instanceof RegexProperty ? - new YamlContract.KeyValueMatcher(regex: urlPath.pattern()) : - urlPath instanceof ExecutionProperty ? - new YamlContract.KeyValueMatcher(command: urlPath.toString()) : null - if (multipart) { - request.matchers.multipart = new YamlContract.MultipartStubMatcher() - Map map = (Map) MapConverter. - getStubSideValues(multipart) - map.each { String key, Object value -> - if (value instanceof NamedProperty) { - Object fileName = value.name?.clientValue - Object fileContent = value.value?.clientValue - Object contentType = value.contentType?.clientValue - if (fileName instanceof RegexProperty || - fileContent instanceof RegexProperty || - contentType instanceof RegexProperty) { - request.matchers.multipart.named << new YamlContract.MultipartNamedStubMatcher( - paramName: key, - fileName: valueMatcher(fileName), - fileContent: valueMatcher(fileContent), - contentType: valueMatcher(contentType), - ) - } - } - else if (value instanceof RegexProperty || value instanceof Pattern) { - RegexProperty property = new RegexProperty(value) - request.matchers.multipart.params. - add(new YamlContract.KeyValueMatcher( - key: key, - regex: property.pattern(), - regexType: regexType(property.clazz()) - )) - } - } - } - // TODO: Cookie matchers - including absent - if (XML != requestContentType) { - setInputBodyMatchers(contract.request?.body, request.matchers.body) - } - setInputHeadersMatchers(contract.request?.headers as Headers, yamlContract.request.matchers.headers) - } - } - - protected String resolveFileNameAsBytes(Object value) { - if (!(value instanceof FromFileProperty)) { - return null - } - FromFileProperty property = (FromFileProperty) value - return property.fileName() - } - - protected YamlContract.ValueMatcher valueMatcher(Object o) { - return o instanceof RegexProperty ? new YamlContract.ValueMatcher(regex: o. - pattern()) : null - } - - protected void setInputBodyMatchers(DslProperty body, List bodyMatchers) { - def testSideValues = MapConverter.getTestSideValues(body) - JsonPaths paths = new JsonToJsonPathsConverter(). - transformToJsonPathWithStubsSideValues(body) - paths?.findAll { it.valueBeforeChecking() instanceof Pattern }?.each { - Object element = JsonToJsonPathsConverter.readElement(testSideValues, it.keyBeforeChecking()) - bodyMatchers << new YamlContract.BodyStubMatcher( - path: it.keyBeforeChecking(), - type: YamlContract.StubMatcherType.by_regex, - value: (it.valueBeforeChecking() as Pattern).pattern(), - regexType: regexType(element) - ) - } - } - - protected RegexType regexType(Object from) { - return regexType(from.class) - } - - protected RegexType regexType(Class clazz) { - switch (clazz) { - case Boolean: - return RegexType.as_boolean - case Long: - return RegexType.as_long - case Short: - return RegexType.as_short - case Integer: - return RegexType.as_integer - case Float: - return RegexType.as_float - case Double: - return RegexType.as_double - default: - return RegexType.as_string - } - } - - protected void setOutputBodyMatchers(DslProperty body, - List bodyMatchers) { - def testSideValues = MapConverter.getTestSideValues(body) - JsonPaths paths = new JsonToJsonPathsConverter(). - transformToJsonPathWithTestsSideValues(body) - paths?.findAll { it.valueBeforeChecking() instanceof Pattern }?.each { - Object element = JsonToJsonPathsConverter.readElement(testSideValues, it.keyBeforeChecking()) - bodyMatchers << new YamlContract.BodyTestMatcher( - path: it.keyBeforeChecking(), - type: YamlContract.TestMatcherType.by_regex, - value: (it.valueBeforeChecking() as Pattern).pattern(), - regexType: regexType(element) - ) - } - if (body?.serverValue instanceof Pattern) { - bodyMatchers << new YamlContract.BodyTestMatcher( - type: YamlContract.TestMatcherType.by_regex, - value: ((Pattern) body.serverValue).pattern() - ) - } - } - - protected void response(YamlContract yamlContract, Contract contract) { - if (!contract.response) { - return - } - ContentType contentType = evaluateClientSideContentType(contract.response?.headers, - contract.response?.body) - yamlContract.response = new YamlContract.Response() - yamlContract.response.with { YamlContract.Response response -> - response.async = contract.response.async - response.fixedDelayMilliseconds = contract.response?.delay?.clientValue as Integer - response.status = contract.response?.status?.clientValue as Integer - response.headers = (contract.response?.headers as Headers)?.asMap { - String headerName, DslProperty prop -> - MapConverter.getStubSideValues(prop).toString() - } - response.cookies = (contract.response?.cookies as Cookies)?.asStubSideMap() - Object body = contract.response?.body?.clientValue - if (body instanceof FromFileProperty) { - if (body.isByte()) { - response.bodyFromFileAsBytes = body.fileName() - } - if (body.isString()) { - response.bodyFromFile = body.fileName() - } - } - else { - response.body = MapConverter.getStubSideValues(contract.response?.body) - } - contract.response?.bodyMatchers?.matchers()?.each { BodyMatcher matcher -> - response.matchers.body << new YamlContract.BodyTestMatcher( - path: matcher.path(), - type: testMatcherType(matcher.matchingType()), - value: matcher.value()?.toString(), - minOccurrence: matcher.minTypeOccurrence(), - maxOccurrence: matcher.maxTypeOccurrence() - ) - } - if (XML != contentType) { - setOutputBodyMatchers(contract.response?.body, - yamlContract.response.matchers.body) - } - setOutputHeadersMatchers(contract.response?.headers, - yamlContract.response.matchers.headers) - } - } - - protected void setInputHeadersMatchers(Headers headers, List headerMatchers) { - headers?.asStubSideMap()?.each { String key, Object value -> - if (value instanceof RegexProperty || value instanceof Pattern) { - RegexProperty property = new RegexProperty(value) - headerMatchers << new YamlContract.KeyValueMatcher( - key: key, - regex: property.pattern(), - regexType: regexType(property.clazz()) - ) - } - } - } - - protected void setOutputHeadersMatchers(Headers headers, List headerMatchers) { - headers?.asTestSideMap()?.each { String key, Object value -> - if (value instanceof RegexProperty || value instanceof Pattern) { - RegexProperty property = new RegexProperty(value) - headerMatchers << new YamlContract.TestHeaderMatcher( - key: key, - regex: property.pattern(), - regexType: regexType(property.clazz()) - ) - } - else if (value instanceof ExecutionProperty) { - headerMatchers << new YamlContract.TestHeaderMatcher( - key: key, - command: value.executionCommand, - ) - } - else if (value instanceof NotToEscapePattern) { - headerMatchers << new YamlContract.TestHeaderMatcher( - key: key, - regex: ((Pattern) value.serverValue).pattern(), - ) - } - } - } - - protected YamlContract.TestMatcherType testMatcherType(MatchingType matchingType) { - switch (matchingType) { - case MatchingType.EQUALITY: - return YamlContract.TestMatcherType.by_equality - case MatchingType.TYPE: - return YamlContract.TestMatcherType.by_type - case MatchingType.COMMAND: - return YamlContract.TestMatcherType.by_command - case MatchingType.DATE: - return YamlContract.TestMatcherType.by_date - case MatchingType.TIME: - return YamlContract.TestMatcherType.by_time - case MatchingType.TIMESTAMP: - return YamlContract.TestMatcherType.by_timestamp - case MatchingType.REGEX: - return YamlContract.TestMatcherType.by_regex - case MatchingType.NULL: - return YamlContract.TestMatcherType.by_null - } - return null - } - - protected YamlContract.StubMatcherType stubMatcherType(MatchingType matchingType) { - switch (matchingType) { - case MatchingType.EQUALITY: - return YamlContract.StubMatcherType.by_equality - case MatchingType.TYPE: - case MatchingType.COMMAND: - throw new UnsupportedOperationException("No type for client side") - case MatchingType.DATE: - return YamlContract.StubMatcherType.by_date - case MatchingType.TIME: - return YamlContract.StubMatcherType.by_time - case MatchingType.TIMESTAMP: - return YamlContract.StubMatcherType.by_timestamp - case MatchingType.REGEX: - return YamlContract.StubMatcherType.by_regex - } - return null - } -} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy deleted file mode 100644 index e2201fd358..0000000000 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/converter/YamlToContracts.groovy +++ /dev/null @@ -1,785 +0,0 @@ -/* - * Copyright 2013-2020 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 - * - * https://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.verifier.converter - -import java.nio.file.Files -import java.util.regex.Pattern - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.dataformat.yaml.YAMLMapper -import groovy.transform.PackageScope -import org.yaml.snakeyaml.Yaml - -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.spec.internal.DslProperty -import org.springframework.cloud.contract.spec.internal.ExecutionProperty -import org.springframework.cloud.contract.spec.internal.Header -import org.springframework.cloud.contract.spec.internal.Headers -import org.springframework.cloud.contract.spec.internal.MatchingTypeValue -import org.springframework.cloud.contract.spec.internal.NamedProperty -import org.springframework.cloud.contract.spec.internal.RegexPatterns -import org.springframework.cloud.contract.spec.internal.Request -import org.springframework.cloud.contract.verifier.util.ContentType -import org.springframework.cloud.contract.verifier.util.NamesUtil -import org.springframework.util.StringUtils - -import static java.util.stream.Collectors.toSet -import static org.springframework.cloud.contract.verifier.util.ContentType.XML -import static org.springframework.cloud.contract.verifier.util.ContentUtils.evaluateClientSideContentType -/** - * @author Marcin Grzejszczak - * @author Olga Maciaszek-Sharma - * @author Tim Ysewyn - */ -@PackageScope -class YamlToContracts { - - Collection convertFrom(File contractFile) { - ClassLoader classLoader = YamlContractConverter.getClassLoader() - YAMLMapper mapper = new YAMLMapper() - try { - Iterable iterables = new Yaml(). - loadAll(Files.newInputStream(contractFile.toPath())) - Collection contracts = [] - int counter = 0 - for (Object document : iterables) { - List processedYaml = - processYaml(counter, document, mapper, classLoader, contractFile) - contracts.addAll(processedYaml) - counter = counter + 1 - } - return contracts - } - catch (FileNotFoundException e) { - throw new IllegalStateException(e) - } - catch (IllegalStateException ise) { - throw ise - } - catch (Exception e1) { - throw new IllegalStateException("Exception occurred while processing the file [" + contractFile + "]", e1) - } - finally { - Thread.currentThread().setContextClassLoader(classLoader) - } - } - - protected List processYaml(int counter, Object document, ObjectMapper mapper, ClassLoader classLoader, File contractFile) { - List yamlContracts = convert(mapper, document) - Thread.currentThread().setContextClassLoader( - updatedClassLoader(contractFile.getParentFile(), classLoader)) - List contracts = [] - for (YamlContract yamlContract : yamlContracts) { - Contract contract = Contract.make { - if (yamlContract.description) { - description(yamlContract.description) - } - if (yamlContract.label) { - label(yamlContract.label) - } - name(StringUtils.hasText(yamlContract.name) ? yamlContract.name - : NamesUtil.defaultContractName(contractFile, yamlContracts, counter)) - if (yamlContract.priority) { - priority(yamlContract.priority) - } - if (yamlContract.ignored) { - ignored() - } - if (yamlContract.inProgress) { - inProgress() - } - if (yamlContract.metadata) { - metadata(yamlContract.metadata) - } - if (yamlContract.request?.method) { - request { - method(yamlContract.request?.method) - if (yamlContract.request?.url) { - url(urlValue(yamlContract.request?.url, yamlContract.request?.matchers?.url)) { - if (yamlContract.request.queryParameters) { - queryParameters { - yamlContract.request.queryParameters. - each { String key, Object value -> - if (value instanceof List) { - ((List) value).each { - parameter(key, it) - } - } - else { - parameter(key, value) - } - } - } - } - } - } - if (yamlContract.request?.urlPath) { - urlPath( - urlValue(yamlContract.request?.urlPath, yamlContract.request?.matchers?.url)) { - if (yamlContract.request.queryParameters) { - queryParameters { - yamlContract.request.queryParameters. - each { String key, Object value -> - if (value instanceof List) { - ((List) value).each { - parameter(key, - queryParamValue(yamlContract, key, it)) - } - } - else { - parameter(key, - queryParamValue(yamlContract, key, value)) - } - } - } - } - } - } - if (yamlContract.request?.headers) { - headers { - yamlContract.request.headers. - each { String key, Object value -> - List matchers = - yamlContract.request.matchers.headers. - findAll { it.key == key } - matchers. - each { YamlContract.KeyValueMatcher matcher -> - if (value instanceof List) { - ((List) value).each { - header(key, - clientValue(it, matcher, key).clientValue) - } - } - else { - header(key, new DslProperty( - clientValue(value, matcher, key).clientValue, - serverValue(value, matcher))) - } - } - if (!matchers) { - header(key, value) - } - } - } - } - if (yamlContract.request?.cookies) { - cookies { - yamlContract.request?.cookies?. - each { String key, Object value -> - YamlContract.KeyValueMatcher matcher = yamlContract.request.matchers.cookies. - find { it.key == key } - cookie(key, clientValue(value, matcher, key)) - } - } - } - if (yamlContract.request.body != null) { - body(yamlContract.request.body) - } - if (yamlContract.request.bodyFromFile != null) { - body(file(yamlContract.request.bodyFromFile)) - } - if (yamlContract.request.bodyFromFileAsBytes != null) { - body(fileAsBytes(yamlContract.request.bodyFromFileAsBytes)) - } - if (yamlContract.request.multipart) { - Map multipartMap = [:] as Map - Map multiPartParams = yamlContract.request - .multipart.params. - collectEntries { String paramKey, String paramValue -> - YamlContract.KeyValueMatcher matcher = yamlContract.request.matchers - .multipart.params.find { - it.key == paramKey - } - Object value = paramValue - if (matcher) { - value = matcher.regex ? Pattern. - compile(matcher.regex) : - predefinedToPattern(matcher.predefined) - } - return [(paramKey), new DslProperty<>(value, paramValue)] - } as Map - multipartMap.putAll(multiPartParams) - yamlContract.request.multipart.named. - each { YamlContract.Named namedParam -> - YamlContract.MultipartNamedStubMatcher matcher = yamlContract.request.matchers.multipart.named. - find { - it.paramName == namedParam.paramName - } - Object fileNameValue = namedParam.fileName - Object fileContentValue = namedParam.fileContent - String fileContentAsBytes = namedParam.fileContentAsBytes - String fileContentFromFileAsBytes = namedParam.fileContentFromFileAsBytes - String contentTypeCommand = namedParam.contentTypeCommand - String fileContentCommand = namedParam.fileContentCommand - String fileNameCommand = namedParam.fileNameCommand - Object contentTypeValue = namedParam.contentType - if (matcher && matcher.fileName) { - fileNameValue = matcher.fileName.regex ? Pattern. - compile(matcher.fileName.regex) : - predefinedToPattern(matcher.fileName.predefined) - } - if (matcher && matcher.fileContent) { - fileContentValue = matcher.fileContent.regex ? Pattern. - compile(matcher.fileContent.regex) : - predefinedToPattern(matcher.fileContent.predefined) - } - if (matcher && matcher.contentType) { - contentTypeValue = matcher.contentType.regex ? Pattern. - compile(matcher.contentType.regex) : - predefinedToPattern(matcher.contentType.predefined) - } - multipartMap. - put(namedParam.paramName, new NamedProperty( - new DslProperty<>(fileNameValue, fileNameCommand ? new ExecutionProperty(fileNameCommand) - : namedParam.fileName), - new DslProperty<>(fileContentValue, namedParam.fileContent ? namedParam.fileContent : fileContentFromFileAsBytes ? - fileAsBytes(namedParam.fileContentFromFileAsBytes) : fileContentAsBytes ? fileContentAsBytes.bytes : new ExecutionProperty(fileContentCommand)), - new DslProperty(contentTypeValue, contentTypeCommand ? new ExecutionProperty(contentTypeCommand) - : namedParam.contentType))) - } - multipart(multipartMap) - } - bodyMatchers { - yamlContract.request.matchers?.body?. - each { YamlContract.BodyStubMatcher matcher -> - ContentType contentType = - evaluateClientSideContentType( - yamlHeadersToContractHeaders(yamlContract.request?.headers), - yamlContract.request?.body) - MatchingTypeValue value = null - switch (matcher.type) { - case YamlContract.StubMatcherType.by_date: - value = byDate() - break - case YamlContract.StubMatcherType.by_time: - value = byTime() - break - case YamlContract.StubMatcherType.by_timestamp: - value = byTimestamp() - break - case YamlContract.StubMatcherType.by_regex: - String regex = matcher.value - if (matcher.predefined) { - regex = - predefinedToPattern(matcher.predefined). - pattern() - } - value = byRegex(regex) - break - case YamlContract.StubMatcherType.by_equality: - value = byEquality() - break - case YamlContract.StubMatcherType.by_type: - value = byType { - if (matcher.minOccurrence != null) { - minOccurrence(matcher.minOccurrence) - } - if (matcher.maxOccurrence != null) { - maxOccurrence(matcher.maxOccurrence) - } - } - break - case YamlContract.StubMatcherType.by_null: - // do nothing - break - default: - throw new UnsupportedOperationException("The type [" + matcher.type + "] is unsupported. Hint: If you're using remember to pass ") - } - if (value) { - if (XML == contentType) { - xPath(matcher.path, value) - } - else { - jsonPath(matcher.path, value) - } - } - } - } - } - response { - status(yamlContract.response.status) - headers { - yamlContract.response?.headers?. - each { String key, Object value -> - YamlContract.TestHeaderMatcher matcher = yamlContract.response.matchers.headers. - find { it.key == key } - if (value instanceof List) { - ((List) value).each { - Object serverValue = - serverValue(it, matcher, key) - header(key, new DslProperty(it, serverValue)) - } - } - else { - Object serverValue = - serverValue(value, matcher, key) - header(key, new DslProperty(value, serverValue)) - } - } - } - if (yamlContract.response?.cookies) { - cookies { - yamlContract.response?.cookies?. - each { String key, Object value -> - YamlContract.TestCookieMatcher matcher = yamlContract.response.matchers.cookies. - find { it.key == key } - DslProperty cookieValue = - serverCookieValue(value, matcher, key) - cookie(key, cookieValue) - } - } - } - if (yamlContract.response.body != null) { - YamlContract.BodyTestMatcher bodyTestMatcher = yamlContract.response?.matchers?.body?.find { - it.path == null && (it.type == YamlContract.TestMatcherType.by_regex || - it.type == YamlContract.TestMatcherType.by_command) - } - if (bodyTestMatcher) { - body(new DslProperty(yamlContract.response.body, - bodyTestMatcher.type == YamlContract.TestMatcherType.by_regex ? - Pattern. - compile(bodyTestMatcher.value) : new ExecutionProperty(bodyTestMatcher.value))) - } - else { - body(yamlContract.response.body) - } - } - if (yamlContract.response.bodyFromFile) { - body(file(yamlContract.response.bodyFromFile)) - } - if (yamlContract.response.bodyFromFileAsBytes) { - body(fileAsBytes(yamlContract.response.bodyFromFileAsBytes)) - } - if (yamlContract.response.async) { - async() - } - if (yamlContract.response.fixedDelayMilliseconds) { - async() - fixedDelayMilliseconds(yamlContract.response.fixedDelayMilliseconds) - } - bodyMatchers { - yamlContract.response?.matchers?.body?. - each { YamlContract.BodyTestMatcher testMatcher -> - ContentType contentType = - evaluateClientSideContentType( - yamlHeadersToContractHeaders(yamlContract.response?.headers), - yamlContract.response?.body) - MatchingTypeValue value = null - switch (testMatcher.type) { - case YamlContract.TestMatcherType.by_date: - value = byDate() - break - case YamlContract.TestMatcherType.by_time: - value = byTime() - break - case YamlContract.TestMatcherType.by_timestamp: - value = byTimestamp() - break - case YamlContract.TestMatcherType.by_regex: - String regex = testMatcher.value - if (testMatcher.predefined) { - regex = - predefinedToPattern(testMatcher.predefined). - pattern() - } - value = byRegex(regex) - break - case YamlContract.TestMatcherType.by_equality: - value = byEquality() - break - case YamlContract.TestMatcherType.by_type: - value = byType() { - if (testMatcher.minOccurrence != null) { - minOccurrence(testMatcher.minOccurrence) - } - if (testMatcher.maxOccurrence != null) { - maxOccurrence(testMatcher.maxOccurrence) - } - } - break - case YamlContract.TestMatcherType.by_command: - value = byCommand(testMatcher.value) - break - case YamlContract.TestMatcherType.by_null: - value = byNull() - break - default: - throw new UnsupportedOperationException("The type [" + testMatcher.type + "] is unsupported. Hint: If you're using remember to pass ") - } - if (testMatcher.path) { - if (XML == contentType) { - xPath(testMatcher.path, value) - } - else { - jsonPath(testMatcher.path, value) - } - } - } - } - } - } - if (yamlContract.input) { - input { - if (yamlContract.input.messageFrom) { - messageFrom(yamlContract.input.messageFrom) - } - if (yamlContract.input.assertThat) { - assertThat(yamlContract.input.assertThat) - } - if (yamlContract.input.triggeredBy) { - triggeredBy(yamlContract.input.triggeredBy) - } - messageHeaders { - yamlContract.input?.messageHeaders?. - each { String key, Object value -> - YamlContract.KeyValueMatcher matcher = yamlContract.input.matchers?.headers?. - find { it.key == key } - header(key, clientValue(value, matcher, key)) - } - } - if (yamlContract.input.messageBody) { - messageBody(yamlContract.input.messageBody) - } - if (yamlContract.input.messageBodyFromFile) { - messageBody(file(yamlContract.input.messageBodyFromFile)) - } - if (yamlContract.input.messageBodyFromFileAsBytes) { - messageBody( - fileAsBytes(yamlContract.input.messageBodyFromFileAsBytes)) - } - bodyMatchers { - yamlContract.input.matchers.body?. - each { YamlContract.BodyStubMatcher matcher -> - ContentType contentType = - evaluateClientSideContentType( - yamlHeadersToContractHeaders(yamlContract.input?.messageHeaders), - yamlContract.input?.messageBody) - MatchingTypeValue value = null - switch (matcher.type) { - case YamlContract.StubMatcherType.by_date: - value = byDate() - break - case YamlContract.StubMatcherType.by_time: - value = byTime() - break - case YamlContract.StubMatcherType.by_timestamp: - value = byTimestamp() - break - case YamlContract.StubMatcherType.by_regex: - String regex = matcher.value - if (matcher.predefined) { - regex = - predefinedToPattern(matcher.predefined). - pattern() - } - value = byRegex(regex) - break - case YamlContract.StubMatcherType.by_equality: - value = byEquality() - break - default: - throw new UnsupportedOperationException("The type [" + matcher.type + "] is unsupported. Hint: If you're using remember to pass ") - } - if (XML == contentType) { - xPath(matcher.path, value) - } - else { - jsonPath(matcher.path, value) - } - } - } - } - } - YamlContract.OutputMessage outputMsg = yamlContract.outputMessage - if (outputMsg) { - outputMessage { - if (outputMsg.assertThat) { - assertThat(outputMsg.assertThat) - } - if (outputMsg.sentTo) { - sentTo(outputMsg.sentTo) - } - headers { - outputMsg.headers?.each { String key, Object value -> - YamlContract.TestHeaderMatcher matcher = outputMsg.matchers?.headers?. - find { it.key == key } - Object serverValue = serverValue(value, matcher, key) - header(key, new DslProperty(value, serverValue)) - } - } - if (outputMsg.body) { - body(outputMsg.body) - } - if (outputMsg.bodyFromFile) { - body(file(outputMsg.bodyFromFile)) - } - if (outputMsg.bodyFromFileAsBytes) { - body(fileAsBytes(outputMsg.bodyFromFileAsBytes)) - } - if (outputMsg.matchers) { - bodyMatchers { - yamlContract.outputMessage?.matchers?.body?. - each { YamlContract.BodyTestMatcher testMatcher -> - ContentType contentType = - evaluateClientSideContentType( - yamlHeadersToContractHeaders(yamlContract.outputMessage?.headers), - yamlContract.outputMessage?.body) - MatchingTypeValue value = null - switch (testMatcher.type) { - case YamlContract.TestMatcherType.by_date: - value = byDate() - break - case YamlContract.TestMatcherType.by_time: - value = byTime() - break - case YamlContract.TestMatcherType.by_timestamp: - value = byTimestamp() - break - case YamlContract.TestMatcherType.by_regex: - String regex = testMatcher.value - if (testMatcher.predefined) { - regex = - predefinedToPattern(testMatcher.predefined). - pattern() - } - value = byRegex(regex) - break - case YamlContract.TestMatcherType.by_equality: - value = byEquality() - break - case YamlContract.TestMatcherType.by_type: - value = byType() { - if (testMatcher.minOccurrence != null) { - minOccurrence(testMatcher.minOccurrence) - } - if (testMatcher.maxOccurrence != null) { - maxOccurrence(testMatcher.maxOccurrence) - } - } - break - case YamlContract.TestMatcherType.by_command: - value = byCommand(testMatcher.value) - break - case YamlContract.TestMatcherType.by_null: - value = byNull() - break - default: - throw new UnsupportedOperationException("The type [" + testMatcher.type + "] is unsupported. Hint: If you're using remember to pass ") - } - if (XML == contentType) { - xPath(testMatcher.path, value) - } - else { - jsonPath(testMatcher.path, value) - } - } - } - } - } - } - } - contracts.add(contract) - } - return contracts - } - - private Headers yamlHeadersToContractHeaders(Map headers) { - Set
convertedHeaders = headers.keySet().stream() - .map({ Header.build(it, headers.get(it)) }) - .collect(toSet()) - Headers contractHeaders = new Headers() - contractHeaders.headers(convertedHeaders) - return contractHeaders - } - - protected DslProperty urlValue(String url, YamlContract.KeyValueMatcher urlMatcher) { - if (urlMatcher) { - if (urlMatcher.command) { - return new DslProperty(url, new ExecutionProperty(urlMatcher.command)) - } - return new DslProperty(urlMatcher.regex ? Pattern.compile(urlMatcher.regex) : - urlMatcher.predefined ? - predefinedToPattern(urlMatcher.predefined) : url, url) - } - return new DslProperty(url) - } - - protected List convert(ObjectMapper mapper, Object o) { - try { - return Arrays.asList(mapper.convertValue(o, YamlContract[].class)) - } - catch (IllegalArgumentException e) { - return Collections.singletonList(mapper.convertValue(o, YamlContract.class)) - } - } - - protected Object serverValue(Object value, YamlContract.TestHeaderMatcher matcher, String key) { - Object serverValue = value - if (matcher?.regex) { - serverValue = Pattern.compile(matcher.regex) - Pattern pattern = (Pattern) serverValue - assertPatternMatched(pattern, value, key) - } - else if (matcher?.predefined) { - Pattern pattern = predefinedToPattern(matcher.predefined) - serverValue = pattern - assertPatternMatched(pattern, value, key) - } - else if (matcher?.command) { - serverValue = new ExecutionProperty(matcher.command) - } - return serverValue - } - - protected DslProperty serverCookieValue(Object value, YamlContract.TestCookieMatcher matcher, String key) { - Object serverValue = value - if (matcher?.regex) { - serverValue = Pattern.compile(matcher.regex) - Pattern pattern = (Pattern) serverValue - assertPatternMatched(pattern, value, key) - } - else if (matcher?.predefined) { - Pattern pattern = predefinedToPattern(matcher.predefined) - serverValue = pattern - assertPatternMatched(pattern, value, key) - } - else if (matcher?.command) { - return new DslProperty(new ExecutionProperty(matcher.command), value) - } - return new DslProperty(value, serverValue) - } - - protected DslProperty clientValue(Object value, YamlContract.KeyValueMatcher matcher, String key) { - Object clientValue = value instanceof DslProperty ? value.clientValue : value - if (matcher?.regex) { - clientValue = Pattern.compile(matcher.regex) - Pattern pattern = (Pattern) clientValue - assertPatternMatched(pattern, value, key) - } - else if (matcher?.predefined) { - Pattern pattern = predefinedToPattern(matcher.predefined) - clientValue = pattern - assertPatternMatched(pattern, value, key) - } - else if (matcher?.command) { - return new DslProperty(value, new ExecutionProperty(matcher.command)) - } - return new DslProperty(clientValue, value) - } - - protected Object queryParamValue(YamlContract yamlContract, String key, Object value) { - Request request = new Request() - YamlContract.QueryParameterMatcher matcher = yamlContract.request. - matchers.queryParameters.find { it.key == key } - if (!matcher) { - return value - } - switch (matcher.type) { - case YamlContract.MatchingType.equal_to: - return new DslProperty(request.equalTo(matcher.value) as Object, value) - case YamlContract.MatchingType.containing: - return new DslProperty(request.containing(matcher.value) as Object, value) - case YamlContract.MatchingType.matching: - return new DslProperty(request.matching(matcher.value) as Object, value) - case YamlContract.MatchingType.not_matching: - return new DslProperty(request.notMatching(matcher.value) as Object, value) - case YamlContract.MatchingType.equal_to_json: - return new DslProperty(request.equalToJson(matcher.value) as Object, value) - case YamlContract.MatchingType.equal_to_xml: - return new DslProperty(request.equalToXml(matcher.value) as Object, value) - case YamlContract.MatchingType.absent: - return new DslProperty(request.absent() as Object, null) - default: - throw new UnsupportedOperationException("The provided matching type [" + matcher + "] is unsupported. Use on of " - + YamlContract.MatchingType. - values()) - } - } - - protected Object serverValue(Object value, YamlContract.KeyValueMatcher matcher) { - Object serverValue = value - if (matcher?.command) { - return new ExecutionProperty(matcher.command) - } - return serverValue instanceof DslProperty ? - ((DslProperty) serverValue).serverValue : serverValue - } - - private void assertPatternMatched(Pattern pattern, value, String key) { - boolean matches = pattern.matcher(value.toString()).matches() - if (!matches) { - throw new IllegalStateException("Broken headers! A header with " - + - "key [${key}] with value [${value}] is not matched by regex [${pattern.pattern()}]") - } - } - - protected Pattern predefinedToPattern(YamlContract.PredefinedRegex predefinedRegex) { - switch (predefinedRegex) { - case YamlContract.PredefinedRegex.only_alpha_unicode: - return RegexPatterns.onlyAlphaUnicode().pattern - case YamlContract.PredefinedRegex.number: - return RegexPatterns.number().pattern - case YamlContract.PredefinedRegex.any_double: - return RegexPatterns.aDouble().pattern - case YamlContract.PredefinedRegex.any_boolean: - return RegexPatterns.anyBoolean().pattern - case YamlContract.PredefinedRegex.ip_address: - return RegexPatterns.ipAddress().pattern - case YamlContract.PredefinedRegex.hostname: - return RegexPatterns.hostname().pattern - case YamlContract.PredefinedRegex.email: - return RegexPatterns.email().pattern - case YamlContract.PredefinedRegex.url: - return RegexPatterns.url().pattern - case YamlContract.PredefinedRegex.uuid: - return RegexPatterns.uuid().pattern - case YamlContract.PredefinedRegex.iso_date: - return RegexPatterns.isoDate().pattern - case YamlContract.PredefinedRegex.iso_date_time: - return RegexPatterns.isoDateTime().pattern - case YamlContract.PredefinedRegex.iso_time: - return RegexPatterns.isoTime().pattern - case YamlContract.PredefinedRegex.iso_8601_with_offset: - return RegexPatterns.iso8601WithOffset().pattern - case YamlContract.PredefinedRegex.non_empty: - return RegexPatterns.nonEmpty().pattern - case YamlContract.PredefinedRegex.non_blank: - return RegexPatterns.nonBlank().pattern - default: - throw new UnsupportedOperationException("The predefined regex [" + predefinedRegex + "] is unsupported. Use on of " - + YamlContract.PredefinedRegex. - values()) - } - } - - protected String file(String relativePath) { - URL resource = Thread.currentThread().getContextClassLoader(). - getResource(relativePath) - if (resource == null) { - throw new IllegalStateException("File [${relativePath}] is not present") - } - return new File(resource.toURI()).text - } - - protected static ClassLoader updatedClassLoader(File rootFolder, ClassLoader classLoader) { - ClassLoader urlCl = URLClassLoader - .newInstance([rootFolder.toURI().toURL()] as URL[], classLoader) - Thread.currentThread().setContextClassLoader(urlCl) - return urlCl - } -} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy deleted file mode 100755 index 8639a69d09..0000000000 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2013-2020 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 - * - * https://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.verifier.dsl.wiremock - -import com.fasterxml.jackson.databind.ObjectMapper -import com.jayway.jsonpath.DocumentContext -import com.jayway.jsonpath.JsonPath -import groovy.transform.PackageScope -import groovy.transform.TypeChecked - -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.spec.ContractTemplate -import org.springframework.cloud.contract.spec.internal.DslProperty -import org.springframework.cloud.contract.spec.internal.FromFileProperty -import org.springframework.cloud.contract.spec.internal.Headers -import org.springframework.cloud.contract.verifier.template.HandlebarsTemplateProcessor -import org.springframework.cloud.contract.verifier.template.TemplateProcessor -import org.springframework.cloud.contract.verifier.util.ContentType -import org.springframework.cloud.contract.verifier.util.ContentUtils -import org.springframework.cloud.contract.verifier.util.MapConverter - -import static org.springframework.cloud.contract.verifier.util.ContentType.UNKNOWN -import static org.springframework.cloud.contract.verifier.util.ContentUtils.extractValue -import static org.springframework.cloud.contract.verifier.util.ContentUtils.getClientContentType -import static org.springframework.cloud.contract.verifier.util.MapConverter.transformValues -/** - * Common abstraction over WireMock Request / Response conversion implementations - * - * Do not change to {@code @CompileStatic} since it's using double dispatch. - * - * @since 1.0.0 - */ -@TypeChecked -@PackageScope -abstract class BaseWireMockStubStrategy { - - private static final String WRAPPER = "UNQUOTE_ME" - - protected final TemplateProcessor processor - protected final ContractTemplate template - protected final Contract contract - - protected BaseWireMockStubStrategy(Contract contract) { - this.processor = templateProcessor() - this.template = contractTemplate() - this.contract = contract - } - - private TemplateProcessor templateProcessor() { - return new HandlebarsTemplateProcessor() - } - - private ContractTemplate contractTemplate() { - return new HandlebarsTemplateProcessor() - } - - /** - * @return the stub side values from the object - */ - protected Object getStubSideValue(Object object) { - return MapConverter.getStubSideValues(object) - } - - private static Closure transform = { - it instanceof DslProperty ? transformValues(it.clientValue, transform) : it - } - - /** - * For the given {@link ContentType} returns the String version of the body - */ - String parseBody(Object value, ContentType contentType) { - return parseBody(value.toString(), contentType) - } - - /** - * Return body as String from file - */ - String parseBody(FromFileProperty value, ContentType contentType) { - return value.asString() - } - - /** - * For the given {@link ContentType} returns the Boolean version of the body - */ - String parseBody(Boolean value, ContentType contentType) { - return value - } - - /** - * For the given {@link ContentType} returns the String version of the body - */ - String parseBody(Map map, ContentType contentType) { - def transformedMap = MapConverter.getStubSideValues(map) - transformedMap = transformMapIfRequestPresent(transformedMap) - String json = toJson(transformedMap) - // the space is important cause at the end of the json body you also have a } - // you can't have 4 } next to each other - String unquotedJson = json.replace('"' + WRAPPER, '').replace(WRAPPER + '"', ' ') - String unescapedJson = unquotedJson.replace("\\/", "/") - return parseBody(unescapedJson, contentType) - } - - private Object transformMapIfRequestPresent(Object transformedMap) { - def requestBody = contract.request.body - if (requestBody == null) { - return transformedMap - } - String testSideBody = toJson( - MapConverter.getTestSideValues(requestBody)) - DocumentContext context = JsonPath.parse(testSideBody) - return processEntriesForTemplating(transformedMap, context) - } - - private Object processEntriesForTemplating(Object transformedMap, DocumentContext context) { - return transformValues(transformedMap, { - if (it instanceof String && processor.containsJsonPathTemplateEntry(it)) { - String jsonPath = processor.jsonPathFromTemplateEntry(it) - if (!jsonPath) { - return it - } - Object value = context.read(jsonPath) - if (value instanceof String) { - return it - } - return "${WRAPPER}${it}${WRAPPER}" - } - else if (it instanceof String && processor.containsTemplateEntry(it) - && - template.escapedBody() == it) { - return template.escapedBody() - } - return it - }) - } - - /** - * For the given {@link ContentType} returns the String version of the body - */ - String parseBody(List list, ContentType contentType) { - List result = [] - list.each { - if (it instanceof Map) { - result += MapConverter.getStubSideValues(it) - } - else { - result += parseBody(it, contentType) - } - } - return parseBody(toJson(result), contentType) - } - - /** - * For the given {@link ContentType} returns the String version of the body - */ - String parseBody(GString value, ContentType contentType) { - Object processedValue = - extractValue(value, contentType, { Object o -> o instanceof DslProperty ? o.clientValue : o }) - if (processedValue instanceof GString) { - return parseBody(processedValue.toString(), contentType) - } - return parseBody(processedValue, contentType) - } - - /** - * For the given {@link ContentType} returns the String version of the body - */ - String parseBody(String value, ContentType contentType) { - return value - } - - private static String toJson(Object value) { - if (value instanceof Map) { - Map convertedMap = MapConverter.transformValues(value) { - it instanceof GString ? it.toString() : it - } as Map - String jsonOutput = new ObjectMapper().writeValueAsString(convertedMap) - return jsonOutput.replaceAll("\\\\\\\\\\\\", "\\\\") - } - return new ObjectMapper().writeValueAsString(value) - } - - /** - * Attempts to guess the {@link ContentType} from body and headers. Returns - * {@link ContentType#UNKNOWN} if it fails to guess. - */ - protected ContentType tryToGetContentType(Object body, Headers headers) { - ContentType contentType = ContentUtils.recognizeContentTypeFromHeader(headers) - if (UNKNOWN == contentType) { - if (!body) { - return UNKNOWN - } - return getClientContentType(body) - } - return contentType - } -} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy deleted file mode 100755 index e286c301a4..0000000000 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy +++ /dev/null @@ -1,520 +0,0 @@ -/* - * Copyright 2013-2020 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 - * - * https://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.verifier.dsl.wiremock - -import java.util.regex.Pattern - -import com.github.tomakehurst.wiremock.client.WireMock -import com.github.tomakehurst.wiremock.extension.Parameters -import com.github.tomakehurst.wiremock.http.RequestMethod -import com.github.tomakehurst.wiremock.matching.ContentPattern -import com.github.tomakehurst.wiremock.matching.RequestPattern -import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder -import com.github.tomakehurst.wiremock.matching.StringValuePattern -import com.github.tomakehurst.wiremock.matching.UrlPattern -import groovy.json.JsonOutput -import groovy.json.StringEscapeUtils -import groovy.transform.PackageScope -import groovy.transform.TypeChecked -import groovy.util.logging.Commons - -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.spec.internal.Body -import org.springframework.cloud.contract.spec.internal.BodyMatcher -import org.springframework.cloud.contract.spec.internal.DslProperty -import org.springframework.cloud.contract.spec.internal.FromFileProperty -import org.springframework.cloud.contract.spec.internal.MatchingStrategy -import org.springframework.cloud.contract.spec.internal.MatchingType -import org.springframework.cloud.contract.spec.internal.NamedProperty -import org.springframework.cloud.contract.spec.internal.OptionalProperty -import org.springframework.cloud.contract.spec.internal.PathBodyMatcher -import org.springframework.cloud.contract.spec.internal.QueryParameters -import org.springframework.cloud.contract.spec.internal.RegexPatterns -import org.springframework.cloud.contract.spec.internal.RegexProperty -import org.springframework.cloud.contract.spec.internal.Request -import org.springframework.cloud.contract.verifier.converter.YamlContract -import org.springframework.cloud.contract.verifier.converter.YamlContractConverter -import org.springframework.cloud.contract.verifier.dsl.ContractVerifierMetadata -import org.springframework.cloud.contract.verifier.file.SingleContractMetadata -import org.springframework.cloud.contract.verifier.util.ContentType -import org.springframework.cloud.contract.verifier.util.ContentUtils -import org.springframework.cloud.contract.verifier.util.JsonPaths -import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter -import org.springframework.cloud.contract.verifier.util.MapConverter -import org.springframework.cloud.contract.verifier.util.xml.XmlToXPathsConverter -import org.springframework.util.StringUtils - -import static org.springframework.cloud.contract.spec.internal.MatchingStrategy.Type.BINARY_EQUAL_TO -import static org.springframework.cloud.contract.spec.internal.MatchingType.COMMAND -import static org.springframework.cloud.contract.spec.internal.MatchingType.EQUALITY -import static org.springframework.cloud.contract.spec.internal.MatchingType.NULL -import static org.springframework.cloud.contract.spec.internal.MatchingType.TYPE -import static org.springframework.cloud.contract.verifier.util.ContentType.FORM -import static org.springframework.cloud.contract.verifier.util.ContentType.JSON -import static org.springframework.cloud.contract.verifier.util.ContentUtils.getEqualsTypeFromContentType -import static org.springframework.cloud.contract.verifier.util.RegexpBuilders.buildGStringRegexpForStubSide -import static org.springframework.cloud.contract.verifier.util.RegexpBuilders.buildJSONRegexpMatch -import static org.springframework.cloud.contract.verifier.util.xml.XmlToXPathsConverter.retrieveValue - -/** - * Converts a {@link Request} into {@link RequestPattern} - * - * @author Marcin Grzejszczak - * @author Tim Ysewyn - * @author Olga Maciaszek-Sharma - * @since 1.0.0 - */ -@TypeChecked -@PackageScope -@Commons -class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { - - private final Request request - private final ContentType contentType - - WireMockRequestStubStrategy(Contract groovyDsl, SingleContractMetadata singleContractMetadata) { - super(groovyDsl) - this.request = groovyDsl.request - this.contentType = contentType(singleContractMetadata) - } - - protected ContentType contentType(SingleContractMetadata singleContractMetadata) { - return singleContractMetadata.evaluatedInputStubContentType - } - - @PackageScope - RequestPattern buildClientRequestContent() { - if (!request) { - return null - } - RequestPatternBuilder requestPatternBuilder = appendMethodAndUrl() - appendCookies(requestPatternBuilder) - appendHeaders(requestPatternBuilder) - appendQueryParameters(requestPatternBuilder) - appendBody(requestPatternBuilder) - appendMultipart(requestPatternBuilder) - return requestPatternBuilder.build() - } - - private void appendBody(RequestPatternBuilder requestPatternBuilder) { - if (contract.metadata.containsKey(ContractVerifierMetadata.METADATA_KEY)) { - ContractVerifierMetadata metadata = ContractVerifierMetadata.fromMetadata(contract.getMetadata()) - appendSpringCloudContractMatcher(metadata, requestPatternBuilder) - if (StringUtils.isEmpty(metadata.getTool())) { - doAppendBody(requestPatternBuilder) - } - } - else { - doAppendBody(requestPatternBuilder) - } - } - - private void appendSpringCloudContractMatcher(ContractVerifierMetadata metadata, RequestPatternBuilder requestPatternBuilder) { - Parameters parameters = Parameters.one("tool", metadata.getTool() ?: "unknown"); - YamlContractConverter converter = new YamlContractConverter(); - List contracts = converter.convertTo(Collections.singleton(contract)); - Map store = converter.store(contracts); - parameters.put("contract", new String(store.entrySet().iterator().next().value)) - requestPatternBuilder.andMatching(SpringCloudContractRequestMatcher.NAME, parameters) - } - - private RequestPatternBuilder appendMethodAndUrl() { - if (!request.method) { - return null - } - RequestMethod requestMethod = RequestMethod. - fromString(request.method.clientValue?.toString()) - UrlPattern urlPattern = urlPattern() - return RequestPatternBuilder.newRequestPattern(requestMethod, urlPattern) - } - - private void doAppendBody(RequestPatternBuilder requestPattern) { - if (!request.body) { - return - } - boolean bodyHasMatchingStrategy = request.body.clientValue instanceof MatchingStrategy - MatchingStrategy matchingStrategy = getMatchingStrategyFromBody(request.body) - Object clientSideBody = MapConverter.transformToClientValues(request.body) - if (contentType == ContentType.JSON) { - def originalBody = matchingStrategy?.clientValue - if (bodyHasMatchingStrategy) { - requestPattern.withRequestBody( - convertToValuePattern(matchingStrategy)) - } else if (clientSideBody instanceof Pattern || clientSideBody instanceof RegexProperty) { - requestPattern.withRequestBody( - convertToValuePattern(appendBodyRegexpMatchPattern(request.body, contentType))) - } - else { - def body = JsonToJsonPathsConverter. - removeMatchingJsonPaths(originalBody, request.bodyMatchers) - JsonPaths values = JsonToJsonPathsConverter. - transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(body) - if ((values.empty && !request.bodyMatchers?.hasMatchers()) - || - onlySizeAssertionsArePresent(values)) { - requestPattern.withRequestBody(WireMock.equalToJson(JsonOutput.toJson( - getMatchingStrategy(request.body.clientValue).clientValue), - false, false)) - } - else { - values.findAll { !it.assertsSize() }.each { - requestPattern.withRequestBody(WireMock. - matchingJsonPath(it.jsonPath().replace("\\\\", "\\"))) - } - } - } - request.bodyMatchers?.matchers()?.each { - String newPath = JsonToJsonPathsConverter. - convertJsonPathAndRegexToAJsonPath(it, originalBody) - requestPattern.withRequestBody(WireMock. - matchingJsonPath(newPath.replace("\\\\", "\\"))) - } - } - else if (contentType == ContentType.XML) { - Object originalBody = matchingStrategy?.clientValue - if (bodyHasMatchingStrategy) { - requestPattern.withRequestBody( - convertToValuePattern(matchingStrategy)) - } - else { - Object body = XmlToXPathsConverter - .removeMatchingXPaths(originalBody, request.bodyMatchers) - List byEqualityMatchersFromXml = new XmlToXPathsConverter() - .mapToMatchers(body) - byEqualityMatchersFromXml.each { - addWireMockStubMatchingSection(it, requestPattern, originalBody) - } - } - request.bodyMatchers?.matchers()?.each { - addWireMockStubMatchingSection(it, requestPattern, originalBody) - } - } - else if (containsPattern(request?.body)) { - requestPattern.withRequestBody( - convertToValuePattern(appendBodyRegexpMatchPattern(request.body))) - } - else { - requestBodyGuessedFromMatchingStrategy(requestPattern) - } - } - - private Object generateConcreteValue(Object originalBody) { - if (originalBody instanceof Pattern || originalBody instanceof RegexProperty) { - return new RegexProperty(originalBody).generate() - } - return originalBody - } - - private RequestPatternBuilder requestBodyGuessedFromMatchingStrategy(RequestPatternBuilder requestPattern) { - return requestPattern.withRequestBody(convertToValuePattern( - getMatchingStrategy(request.body.clientValue))) - } - - private static void addWireMockStubMatchingSection(BodyMatcher matcher, - RequestPatternBuilder requestPattern, - Object body) { - Set matchingTypesUnsupportedForRequest = [NULL, COMMAND, TYPE] as Set - if (!matcher instanceof PathBodyMatcher) { - throw new IllegalArgumentException("Only jsonPath and XPath matchers can be processed.") - } - String retrievedValue = Optional.ofNullable(matcher.value()).orElseGet({ - if (matchingTypesUnsupportedForRequest.contains(matcher.matchingType())) { - throw new IllegalArgumentException("Null, Command and Type matchers are not supported in requests.") - } - if (EQUALITY == matcher.matchingType()) { - return retrieveValue(matcher, body) - } - else { - return '' - } - }) - PathBodyMatcher pathMatcher = matcher as PathBodyMatcher - requestPattern.withRequestBody(WireMock.matchingXPath(pathMatcher.path(), - XPathBodyMatcherToWireMockValuePatternConverter - .mapToPattern(pathMatcher.matchingType(), - String.valueOf(retrievedValue)))) - } - - private boolean onlySizeAssertionsArePresent(JsonPaths values) { - return !values.empty && !request.bodyMatchers?.hasMatchers() && values. - every { it.assertsSize() } - } - - private void appendMultipart(RequestPatternBuilder requestPattern) { - if (!request.multipart) { - return - } - if (request.multipart.clientValue instanceof Map) { - List multipartPatterns = (request.multipart.clientValue as Map). - collect { - (it.value instanceof NamedProperty - ? WireMock.matching(RegexPatterns. - multipartFile(it.key, (it.value as NamedProperty).name.clientValue, - (it.value as NamedProperty).value.clientValue, - (it.value as NamedProperty).contentType?.clientValue)) - : WireMock. - matching(RegexPatterns.multipartParam(it.key, MapConverter.getStubSideValuesForNonBody(it.value)))) - } - multipartPatterns.each { - requestPattern.withRequestBody(it) - } - } - } - - private void appendHeaders(RequestPatternBuilder requestPattern) { - if (!request.headers) { - return - } - request.headers.entries.each { - requestPattern.withHeader(it.name, (StringValuePattern) - convertToValuePattern(it.clientValue)) - } - } - - private void appendCookies(RequestPatternBuilder requestPattern) { - if (!request.cookies) { - return - } - request.cookies.entries.each { - requestPattern.withCookie(it.key, (StringValuePattern) - convertToValuePattern(it.clientValue)) - } - } - - private UrlPattern urlPattern() { - Object urlPath = urlPathOrUrlIfQueryPresent() - if (urlPath) { - if (urlPath instanceof Pattern || urlPath instanceof RegexProperty) { - return WireMock.urlPathMatching( - getStubSideValue(new RegexProperty(urlPath).pattern()) as String) - } - else { - return WireMock. - urlPathEqualTo(getStubSideValue(urlPath.toString()) as String) - } - } - if (!request.url) { - throw new IllegalStateException("URL is required!") - } - Object url = getUrlIfGstring(request?.url?.clientValue) - if (url instanceof Pattern || url instanceof RegexProperty) { - return WireMock.urlMatching(new RegexProperty(url).pattern()) - } - return WireMock.urlEqualTo(url.toString()) - } - - private Object urlPathOrUrlIfQueryPresent() { - Object urlPath = request?.urlPath?.clientValue - Object queryParamsFromUrl = request?.url?.queryParameters?.parameters - if (urlPath) { - return urlPath - } - if (queryParamsFromUrl) { - return request?.url?.clientValue - } - return null - } - - private Object getUrlIfGstring(Object clientSide) { - if (clientSide instanceof GString) { - if (clientSide.values.any { - def value = getStubSideValue(it) - return value instanceof Pattern || value instanceof RegexProperty - }) { - String string = getStubSideValue(clientSide).toString() - return new RegexProperty(Pattern.compile(string)) - } - else { - return getStubSideValue(clientSide).toString() - } - } - return clientSide - } - - private void appendQueryParameters(RequestPatternBuilder requestPattern) { - QueryParameters queryParameters = request?.urlPath?.queryParameters ?: request?.url?.queryParameters - queryParameters?.parameters?.each { - requestPattern.withQueryParam(it.name, (StringValuePattern) - convertToValuePattern(it.clientValue)) - } - } - - protected ContentPattern convertToValuePattern(Object object) { - switch (object) { - case Pattern: - case RegexProperty: - return WireMock.matching(new RegexProperty(object).pattern()) - case OptionalProperty: - OptionalProperty value = object as OptionalProperty - return WireMock.matching(value.optionalPattern()) - case MatchingStrategy: - MatchingStrategy value = object as MatchingStrategy - switch (value.type) { - case MatchingStrategy.Type.NOT_MATCHING: - return WireMock.notMatching(value.clientValue.toString()) - case MatchingStrategy.Type.ABSENT: - return WireMock.absent() - case MatchingStrategy.Type.EQUAL_TO: - return WireMock.equalTo(clientBody(value.clientValue, contentType).toString()) - case MatchingStrategy.Type.CONTAINS: - return WireMock.containing(clientBody(value.clientValue, contentType).toString()) - case MatchingStrategy.Type.MATCHING: - return WireMock.matching(clientBody(value.clientValue, contentType).toString()) - case MatchingStrategy.Type.EQUAL_TO_JSON: - return WireMock.equalToJson(clientBody(value.clientValue, contentType).toString()) - case MatchingStrategy.Type.EQUAL_TO_XML: - return WireMock.equalToXml(clientBody(value.clientValue, contentType).toString()) - case MatchingStrategy.Type.BINARY_EQUAL_TO: - return WireMock.binaryEqualTo(clientBody(value.clientValue, contentType) as byte[]) - default: - throw new UnsupportedOperationException("Unknown matching strategy " + value.type) - } - default: - return WireMock.equalTo(clientBody(object, contentType).toString()) - } - } - - protected Object clientBody(Object bodyValue, ContentType contentType) { - if (FORM == contentType) { - if (bodyValue instanceof Map) { - // [a:3, b:4] == "a=3&b=4" - return ((Map) bodyValue).collect { - StringEscapeUtils. - unescapeJavaScript(it.key.toString() + "=" + it.value) - }.join("&") - } - else if (bodyValue instanceof List) { - // ["a=3", "b=4"] == "a=3&b=4" - return ((List) bodyValue).collect { - StringEscapeUtils.unescapeJavaScript(it.toString()) - }.join("&") - } - } - else if (bodyValue instanceof FromFileProperty) { - return bodyValue.isByte() ? bodyValue.asBytes() : bodyValue.asString() - } - else if (JSON == contentType) { - return parseBody(bodyValue, contentType) - } - return bodyValue - } - - private MatchingStrategy getMatchingStrategyFromBody(Body body) { - if (!body) { - return null - } - return getMatchingStrategy(body.clientValue) - } - - private MatchingStrategy getMatchingStrategy(MatchingStrategy matchingStrategy) { - return getMatchingStrategyIncludingContentType(matchingStrategy) - } - - private MatchingStrategy getMatchingStrategy(GString gString) { - if (!gString) { - return new MatchingStrategy("", MatchingStrategy.Type.EQUAL_TO) - } - def extractedValue = ContentUtils.extractValue(gString) { - it instanceof DslProperty ? it.clientValue : getStringFromGString(it) - } - def value = getStringFromGString(extractedValue) - return getMatchingStrategy(value) - } - - private def getStringFromGString(Object object) { - return object instanceof GString ? object.toString() : object - } - - private MatchingStrategy getMatchingStrategy(Object bodyValue) { - return tryToFindMachingStrategy(bodyValue) - } - - private MatchingStrategy getMatchingStrategy(FromFileProperty bodyValue) { - return new MatchingStrategy(bodyValue, BINARY_EQUAL_TO) - } - - private MatchingStrategy tryToFindMachingStrategy(Object bodyValue) { - return new MatchingStrategy(MapConverter.transformToClientValues(bodyValue), - getEqualsTypeFromContentType(contentType)) - } - - private MatchingStrategy getMatchingStrategyIncludingContentType(MatchingStrategy matchingStrategy) { - MatchingStrategy.Type type = matchingStrategy.type - Object value = matchingStrategy.clientValue - ContentType contentType = ContentUtils. - recognizeContentTypeFromMatchingStrategy(type) - if (contentType == ContentType.UNKNOWN && type == MatchingStrategy.Type.EQUAL_TO) { - contentType = ContentUtils.recognizeContentTypeFromContent(value) - type = getEqualsTypeFromContentType(contentType) - } - return new MatchingStrategy(parseBody(value, contentType), type) - } - - private MatchingStrategy appendBodyRegexpMatchPattern(Object value, ContentType contentType) { - Object clientValue = MapConverter.transformToClientValues(value) - switch (contentType) { - case ContentType.JSON: - return new MatchingStrategy( - buildJSONRegexpMatch(clientValue), MatchingStrategy.Type.MATCHING) - case ContentType.UNKNOWN: - return new MatchingStrategy( - buildGStringRegexpForStubSide(clientValue), MatchingStrategy.Type.MATCHING) - case ContentType.XML: - throw new IllegalStateException("XML pattern matching is not implemented yet") - } - } - - private MatchingStrategy appendBodyRegexpMatchPattern(Object value) { - return appendBodyRegexpMatchPattern(value, ContentType.UNKNOWN) - } - - private boolean containsPattern(GString bodyAsValue) { - return containsPattern(bodyAsValue.values) - } - - private boolean containsPattern(Map map) { - return containsPattern(map.entrySet()) - } - - private boolean containsPattern(Collection collection) { - return collection.collect(this.&containsPattern).inject(false) { a, b -> a || b } - } - - private boolean containsPattern(Object[] objects) { - return containsPattern(objects.toList()) - } - - private boolean containsPattern(Map.Entry entry) { - return containsPattern(entry.value) - } - - private boolean containsPattern(DslProperty dslProperty) { - return containsPattern(dslProperty.clientValue) - } - - private boolean containsPattern(Pattern pattern) { - return true - } - - private boolean containsPattern(RegexProperty pattern) { - return true - } - - private boolean containsPattern(Object o) { - return false - } -} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy deleted file mode 100755 index 3b5344c77d..0000000000 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy +++ /dev/null @@ -1,376 +0,0 @@ -/* - * Copyright 2013-2020 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 - * - * https://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.verifier.file - -import java.nio.file.FileSystem -import java.nio.file.FileSystems -import java.nio.file.Path -import java.nio.file.PathMatcher -import java.util.regex.Pattern - -import groovy.transform.CompileStatic -import groovy.util.logging.Commons -import wiremock.com.google.common.collect.HashMultiset -import wiremock.com.google.common.collect.ListMultimap -import wiremock.com.google.common.collect.Multimap -import wiremock.com.google.common.collect.Multiset - -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.spec.ContractConverter -import org.springframework.cloud.contract.verifier.converter.YamlContractConverter -import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter -import org.springframework.core.io.support.SpringFactoriesLoader -import org.springframework.util.CollectionUtils -import org.springframework.util.MultiValueMap -/** - * Scans the provided file path for the DSLs. There's a possibility to provide - * inclusion and exclusion filters. - * - * @author Jakub Kubrynski, codearte.io - * - * @since 1.0.0 - */ -@CompileStatic -@Commons -class ContractFileScanner { - - private static final String OS_NAME = System.getProperty("os.name") - private static final String OS_NAME_WINDOWS_PREFIX = "Windows" - protected static final boolean IS_OS_WINDOWS = - getOSMatchesName(OS_NAME_WINDOWS_PREFIX) - - private static final String MATCH_PREFIX = "glob:" - private static final Pattern SCENARIO_STEP_FILENAME_PATTERN = Pattern. - compile("[0-9]+_.*") - private final File baseDir - private final Set excludeMatchers - private final Set ignoreMatchers - private final Set includeMatchers - private final String includeMatcher - - ContractFileScanner(File baseDir, Set excluded, Set ignored, - Set included = [], - String includeMatcher = "") { - this.baseDir = baseDir - this.excludeMatchers = processPatterns(excluded ?: [] as Set) - this.ignoreMatchers = processPatterns(ignored ?: [] as Set) - this.includeMatchers = processPatterns(included ?: [] as Set) - this.includeMatcher = includeMatcher - } - - private Set processPatterns(Set patterns) { - FileSystem fileSystem = FileSystems.getDefault() - Set pathMatchers = new HashSet() - for (String pattern : patterns) { - String syntaxAndPattern = MATCH_PREFIX + '**' + File.separator + pattern - // FIXME: This looks strange, need to be checked on windows - if (IS_OS_WINDOWS) { - syntaxAndPattern = syntaxAndPattern.replace("\\", "\\\\") - } - pathMatchers.add(fileSystem.getPathMatcher(syntaxAndPattern)) - } - return pathMatchers - } - - /** - * @return for a map of paths for which a list of matching contracts has been found - * @deprecated use the {@link ContractFileScanner#findContractsRecursively} version - */ - @Deprecated - ListMultimap findContracts() { - MultiValueMap contracts = findContractsRecursively(); - return new ListMultimap() { - @Override - List get(Path key) { - return contracts.get(key) - } - - @Override - List removeAll(Object key) { - return contracts.remove(key) - } - - @Override - List replaceValues(Path key, Iterable values) { - return contracts.put(key, asList(values)) - } - - private static List asList(Iterable self) { - if (self instanceof List) { - return (List) self; - } else { - return toList(self.iterator()); - } - } - - private static List toList(Iterator self) { - List answer = new ArrayList<>(); - while (self.hasNext()) { - answer.add(self.next()); - } - return answer; - } - - @Override - Map> asMap() { - return contracts.collectEntries { - [(it.key): (Collection) it.value] - } as Map> - } - - @Override - int size() { - return contracts.size() - } - - @Override - boolean isEmpty() { - return contracts.isEmpty() - } - - @Override - boolean containsKey(Object key) { - return contracts.containsKey(key) - } - - @Override - boolean containsValue(Object value) { - return contracts.findResult { it.value.contains(value) } - } - - @Override - boolean containsEntry(Object key, Object value) { - return contracts.findResult { it.key == key && it.value.contains(value) } - } - - @Override - boolean put(Path key, ContractMetadata value) { - return contracts.add(key, value) - } - - @Override - boolean remove(Object key, Object value) { - return contracts.getOrDefault(key, new ArrayList()).remove(value) - } - - @Override - boolean putAll(Path key, Iterable values) { - return contracts.getOrDefault(key, new ArrayList()).addAll(values) - } - - @Override - boolean putAll(Multimap multimap) { - multimap.entries().each { - contracts.add(it.key, it.value) - } - return true - } - - @Override - void clear() { - contracts.clear() - } - - @Override - Set keySet() { - return contracts.keySet() - } - - @Override - Multiset keys() { - return HashMultiset.create(contracts.keySet()) - } - - @Override - Collection values() { - return (Collection) contracts.values().flatten() - } - - @Override - Collection> entries() { - Collection> entries = new LinkedList<>() - contracts.each { - Path path = it.key - List list = it.value - list.each { - entries.add(new AbstractMap.SimpleEntry(path, it)) - } - } - return entries - } - } - } - - - MultiValueMap findContractsRecursively() { - MultiValueMap result = CollectionUtils.toMultiValueMap(new LinkedHashMap<>()); - appendRecursively(baseDir, result) - return result - } - - /** - * We iterate over found contracts, filter out those that should be excluded - * and try to convert via pluggable Contract Converters any possible contracts - */ - private void appendRecursively(File baseDir, MultiValueMap result) { - List converters = convertersWithYml() - if (log.isTraceEnabled()) { - log.trace("Found the following contract converters ${converters}") - } - File[] files = baseDir.listFiles() - if (!files) { - return - } - File[] sortedFiles = files.sort() as File[] - for (int i = 0; i < sortedFiles.length; i++) { - File file = sortedFiles[i] - boolean excluded = matchesPattern(file, excludeMatchers) - if (!excluded) { - boolean contractFile = isContractFile(file) - boolean included = includeMatcher ? file.absolutePath. - matches(includeMatcher) : true - included = includeMatchers ? - matchesPattern(file, includeMatchers) : included - if (contractFile && included) { - addContractToTestGeneration(result, files, file, i, ContractVerifierDslConverter. - convertAsCollection(baseDir, file)) - } - if (!contractFile && included) { - addContractToTestGeneration(converters, result, files, file, i) - } - else { - appendRecursively(file, result) - if (log.isDebugEnabled()) { - log.debug("File [$file] is ignored. Is a contract file? [$contractFile]. Should be included by pattern? [$included]") - } - } - } - else { - if (log.isDebugEnabled()) { - log.debug("File [$file] is ignored. Should be excluded? [$excluded]") - } - } - } - } - - protected List convertersWithYml() { - List converters = converters() - converters.add(ContractVerifierDslConverter.INSTANCE) - converters.add(YamlContractConverter.INSTANCE) - return converters - } - - protected List converters() { - return SpringFactoriesLoader.loadFactories(ContractConverter, null) - } - - private void addContractToTestGeneration(List converters, MultiValueMap result, - File[] files, File file, int index) { - boolean converted = false - if (!file.isDirectory()) { - for (ContractConverter converter : converters) { - Collection contracts = tryConvert(converter, file) - if (contracts) { - addContractToTestGeneration(result, files, file, index, contracts) - converted = true - break - } - } - } - if (!converted) { - appendRecursively(file, result) - if (log.isDebugEnabled()) { - log.debug("File [$file] wasn't ignored but no converter was applicable. The file is a directory [${file.isDirectory()}]") - } - } - } - - private Collection tryConvert(ContractConverter converter, File file) { - boolean accepted = converter.isAccepted(file) - if (!accepted) { - return null - } - try { - return converter.convertFrom(file) - } - catch (Exception e) { - throw new IllegalStateException("Failed to convert file [" + file + "]", e) - } - } - - private void addContractToTestGeneration(MultiValueMap result, File[] files, File file, - int index, Collection convertedContract) { - Path path = file.toPath() - Integer order = null - if (hasScenarioFilenamePattern(path)) { - order = index - } - Path parent = file.parentFile.toPath() - ContractMetadata metadata = new ContractMetadata(path, - matchesPattern(file, ignoreMatchers), - files.size(), order, convertedContract) - if (log.isDebugEnabled()) { - log.debug("Creating a contract entry for path [" + path + "] and metadata [" + metadata + "]") - } - result.add(parent, metadata) - } - - private boolean hasScenarioFilenamePattern(Path path) { - return SCENARIO_STEP_FILENAME_PATTERN.matcher(path.fileName.toString()).matches() - } - - private boolean matchesPattern(File file, Set matchers) { - for (PathMatcher matcher : matchers) { - if (matcher.matches(file.toPath())) { - return true - } - log.debug("Path [${file.toPath()}] doesn't match the pattern [${matcher}]") - } - return false - } - - private boolean isContractFile(File file) { - return file.isFile() && ContractVerifierDslConverter.INSTANCE.isAccepted(file) - } - - /** - * Decides if the operating system matches. - * - * @param osNamePrefix the prefix for the os name - * @return true if matches, or false if not or can't determine - */ - private static boolean getOSMatchesName(final String osNamePrefix) { - return isOSNameMatch(OS_NAME, osNamePrefix) - } - - /** - * Decides if the operating system matches. - *

- * This method is package private instead of private to support unit test invocation. - *

- * - * @param osName the actual OS name - * @param osNamePrefix the prefix for the expected OS name - * @return true if matches, or false if not or can't determine - */ - private static boolean isOSNameMatch(final String osName, final String osNamePrefix) { - if (osName == null) { - return false - } - return osName.startsWith(osNamePrefix) - } -} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerBuilder.groovy deleted file mode 100755 index 7a93e0d4ac..0000000000 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerBuilder.groovy +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2013-2020 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 - * - * https://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.verifier.file - - -import groovy.transform.CompileStatic - -/** - * Scans the provided file path for the DSLs. There's a possibility to provide - * inclusion and exclusion filters. - * - * @author Jakub Kubrynski, codearte.io - * - * @since 2.1.0 - */ -@CompileStatic -class ContractFileScannerBuilder { - - private File baseDir - private Set excluded - private Set ignored - private Set included = [] - private String includeMatcher = "" - - ContractFileScannerBuilder baseDir(File baseDir) { - this.baseDir = baseDir - return this - } - - ContractFileScannerBuilder excluded(Set excluded) { - this.excluded = excluded - return this - } - - ContractFileScannerBuilder ignored(Set ignored) { - this.ignored = ignored - return this - } - - ContractFileScannerBuilder included(Set included) { - this.included = included - return this - } - - ContractFileScannerBuilder includeMatcher(String includeMatcher) { - this.includeMatcher = includeMatcher - return this - } - - ContractFileScanner build() { - return new ContractFileScanner(this.baseDir, - this.excluded, - this.ignored, - this.included, - this.includeMatcher) - } -} - diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverter.groovy deleted file mode 100644 index a0334aca9d..0000000000 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverter.groovy +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Copyright 2013-2020 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 - * - * https://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.verifier.util - -import java.lang.reflect.Constructor -import java.util.function.Supplier -import java.util.regex.Matcher -import java.util.regex.Pattern - -import groovy.transform.CompileStatic -import groovy.util.logging.Commons -import org.codehaus.groovy.control.CompilerConfiguration - -import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.spec.ContractConverter -import org.springframework.cloud.function.compiler.java.CompilationResult -import org.springframework.cloud.function.compiler.java.RuntimeJavaCompiler -import org.springframework.util.StringUtils - -/** - * Converts a String or a Groovy or Java file into a {@link Contract}. - * - * @author Marcin Grzejszczak - * @author Olga Maciaszek-Sharma - * - * @since 1.0.0 - */ -@CompileStatic -@Commons -class ContractVerifierDslConverter implements ContractConverter> { - - public static final ContractVerifierDslConverter INSTANCE = new ContractVerifierDslConverter() - - private static final Pattern PACKAGE_PATTERN = Pattern.compile(".*package (.+?);.+?", Pattern.DOTALL) - - private static final Pattern CLASS_PATTERN = Pattern.compile(".+?class (.+?)( |\\{).+?", Pattern.DOTALL) - - private static final RuntimeJavaCompiler COMPILER = new RuntimeJavaCompiler() - - /** - * @deprecated - use {@link ContractVerifierDslConverter#convertAsCollection(java.io.File, java.lang.String)} - */ - @Deprecated - static Collection convertAsCollection(String dsl) { - try { - Object object = groovyShell().evaluate(dsl) - return listOfContracts(object) - } - catch (DslParseException e) { - throw e - } - catch (Exception e) { - log.error("Exception occurred while trying to evaluate the contract", e) - throw new DslParseException(e) - } - } - - static Collection convertAsCollection(File rootFolder, String dsl) { - ClassLoader classLoader = ContractVerifierDslConverter.getClassLoader() - try { - ClassLoader urlCl = updatedClassLoader(rootFolder, classLoader) - Object object = groovyShell(urlCl, rootFolder).evaluate(dsl) - return listOfContracts(object) - } - catch (DslParseException e) { - throw e - } - catch (Exception e) { - log.error("Exception occurred while trying to evaluate the contract", e) - throw new DslParseException(e) - } - finally { - Thread.currentThread().setContextClassLoader(classLoader) - } - } - - static Collection convertAsCollection(File dsl) { - return convertAsCollection(dsl.parentFile, dsl) - } - - static Collection convertAsCollection(File rootFolder, File dsl) { - ClassLoader classLoader = ContractVerifierDslConverter.getClassLoader() - try { - ClassLoader urlCl = updatedClassLoader(rootFolder, classLoader) - Object object = toObject(urlCl, rootFolder, dsl) - return listOfContracts(dsl, object) - } - catch (DslParseException e) { - throw e - } - catch (Exception e) { - log.error("Exception occurred while trying to evaluate the contract at path [${dsl.path}]", e) - throw new DslParseException(e) - } - finally { - Thread.currentThread().setContextClassLoader(classLoader) - } - } - - private static ClassLoader updatedClassLoader(File rootFolder, ClassLoader classLoader) { - ClassLoader urlCl = URLClassLoader - .newInstance([rootFolder.toURI().toURL()] as URL[], classLoader) - updateTheThreadClassLoader(urlCl) - return urlCl - } - - private static void updateTheThreadClassLoader(ClassLoader urlCl) { - Thread.currentThread().setContextClassLoader(urlCl) - } - - private static GroovyShell groovyShell() { - return new GroovyShell(ContractVerifierDslConverter.classLoader, new CompilerConfiguration(sourceEncoding: 'UTF-8')) - } - - private static Object toObject(ClassLoader cl, File rootFolder, File dsl) { - if (isJava(dsl)) { - try { - return parseJavaFile(cl, dsl) - } - catch (Exception ex) { - if (log.isWarnEnabled()) { - log.warn("Exception occurred while trying to parse the file [" + dsl + "] as a contract. Will not parse it.", ex) - } - return null - } - } - return groovyShell(cl, rootFolder).evaluate(dsl) - } - - private static Object parseJavaFile(ClassLoader cl, File dsl) { - Constructor constructor = classConstructor(dsl) - Object newInstance = constructor.newInstance() - if (!newInstance instanceof Supplier) { - if (log.isDebugEnabled()) { - log.debug("The class [" + dsl + "] is not instance of Supplier. Will not parse it as a contract") - } - return null - } - Supplier supplier = (Supplier) newInstance - return supplier.get() - } - - private static Constructor classConstructor(File dsl) { - String classText = dsl.text - String fqn = fqn(classText) - CompilationResult compilationResult = COMPILER - .compile(fqn, classText) - if (!compilationResult.wasSuccessful()) { - throw new IllegalStateException("Exceptions occurred while trying to compile the file " + compilationResult.compilationMessages) - } - Class clazz = compilationResult.compiledClasses.find { it.name == fqn} - if (clazz == null) { - throw new IllegalStateException("Class with name [" + fqn + "] not found") - } - Constructor constructor = clazz.getDeclaredConstructor() - constructor.setAccessible(true) - return constructor - } - - private static boolean isJava(File dsl) { - return dsl.name.endsWith(".java") - } - - private static String fqn(String classText) { - Matcher packageMatcher = PACKAGE_PATTERN.matcher(classText) - String fqn = ""; - if (packageMatcher.matches()) { - fqn = packageMatcher.group(1) + "." - } - Matcher classMatcher = CLASS_PATTERN.matcher(classText) - if (!classMatcher.matches()) { - throw new IllegalAccessException("Can't parse the class name") - } - return fqn + classMatcher.group(1) - } - - private static GroovyShell groovyShell(ClassLoader cl, File rootFolder) { - return new GroovyShell(cl, - new CompilerConfiguration(sourceEncoding: 'UTF-8', - classpathList: [rootFolder.absolutePath])) - } - - private static Collection listOfContracts(object) { - if (object instanceof Collection) { - return object as Collection - } - else if (!object instanceof Contract) { - throw new DslParseException("Contract is not returning a Contract or list of Contracts") - } - return [object] as Collection - } - - private static Collection listOfContracts(File file, Object object) { - if (object == null) { - return Collections.emptyList() - } - else if (isACollectionOfContracts(object)) { - return withName(file, object as Collection) - } - else if (!object instanceof Contract) { - throw new DslParseException("Contract is not returning a Contract or list of Contracts") - } - return withName(file, [object] as Collection) - } - - private static boolean isACollectionOfContracts(object) { - return object instanceof Collection && ((Collection) object).every { it instanceof Contract } - } - - private static Collection withName(File file, Collection contracts) { - int counter = 0 - return contracts.collect { - if (contractNameEmpty(it)) { - it.name(NamesUtil.defaultContractName(file, contracts, counter)) - } - counter++ - return it - } - } - - private static boolean contractNameEmpty(Contract it) { - return it != null && StringUtils.isEmpty(it.name) - } - - @Override - boolean isAccepted(File file) { - return file.name.endsWith(".groovy") || file.name.endsWith(".gvy") || file.name.endsWith(".java") - } - - @Override - Collection convertFrom(File file) { - return convertAsCollection(file) - } - - @Override - Collection convertTo(Collection contract) { - return contract - } -} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy index 7782fd61a2..460edae2a8 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy @@ -16,6 +16,7 @@ package org.springframework.cloud.contract.verifier.util +import java.util.function.Function import java.util.regex.Matcher import java.util.regex.Pattern @@ -208,7 +209,9 @@ class JsonToJsonPathsConverter { while ({ matches << matcher.group() matcher.find() - }()) continue + }()) { + continue + } return matches[matches.size() - 1] } @@ -335,6 +338,10 @@ class JsonToJsonPathsConverter { } } + JsonPaths transformToJsonPathWithTestsSideValues(def json, Function parsingClosure, boolean includeEmptyCheck) { + return transformToJsonPathWithValues(json, SERVER_SIDE, { parsingClosure.apply(it) }, includeEmptyCheck) + } + JsonPaths transformToJsonPathWithTestsSideValues(def json, Closure parsingClosure = MapConverter.JSON_PARSING_CLOSURE, boolean includeEmptyCheck = false) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy deleted file mode 100644 index 192a717824..0000000000 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright 2013-2020 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 - * - * https://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.verifier.util - -import java.util.function.Function - -import groovy.json.JsonSlurper - -import org.springframework.cloud.contract.spec.internal.DslProperty -import org.springframework.cloud.contract.spec.internal.FromFileProperty -import org.springframework.cloud.contract.verifier.template.HandlebarsTemplateProcessor -import org.springframework.cloud.contract.verifier.template.TemplateProcessor - -/** - * Converts an object into either client or server side representation. - * Iterates over the structure of an object (depending on whether it's an - * iterable or a primitive type etc.), converts the {@link DslProperty} into their - * client / server representation and returns the result - * - * @author Marcin Grzejszczak - * - * @since 1.1.0 - */ -class MapConverter { - - public static final boolean STUB_SIDE = true - public static final boolean TEST_SIDE = false - public static final Closure JSON_PARSING_CLOSURE = { String value -> - new JsonSlurper().parseText(value) - } - public static final Function JSON_PARSING_FUNCTION = { String value -> - new JsonSlurper().parseText(value) - } as Function - - private final TemplateProcessor templateProcessor - - MapConverter() { - this.templateProcessor = processor() - } - - private TemplateProcessor processor() { - return new HandlebarsTemplateProcessor() - } - - /** - * @return the object with client side values of {@link org.springframework.cloud.contract.spec.internal.DslProperty} - */ - static def transformToClientValues(def value) { - return transformValues(value) { - it instanceof DslProperty ? it.clientValue : it - } - } - - static Closure fromFunction(Function function) { - return { - function.apply(it) - } - } - - /** - * Iterates over the structure of the object and executes the closure - * on each element of that structure. - * - * @return the transformed structure - */ - static def transformValues(def value, Closure closure, - Closure parsingClosure = JSON_PARSING_CLOSURE) { - if (value instanceof String && value) { - try { - def parsed = parsingClosure(value) - if (parsed instanceof Map) { - return convert(parsed, closure, parsingClosure) - } - else if (parsed instanceof List) { - return transformValues(parsed, closure, parsingClosure) - } - } - catch (Exception ignore) { - } - return extractValue(value, closure) - } - else if (value instanceof Map) { - return convert(value as Map, closure, parsingClosure) - } - else if (value instanceof List) { - return value.collect({ transformValues(it, closure, parsingClosure) }) - } - return transformValue(closure, value, parsingClosure) - } - - /** - * Transforms a value with the given closure. Needs to be protected, otherwise - * method access exception will occur at runtime. - */ - protected static Object transformValue(Closure closure, Object value, Closure parsingClosure) { - return extractValue(value, { Object val -> - Object newValue = closure(val) - if (newValue instanceof Map || newValue instanceof List || newValue instanceof String && value) { - return transformValues(newValue, closure, parsingClosure) - } - return newValue - }) - } - - private static extractValue(Object value, Closure closure) { - try { - return closure(value) - } - catch (Exception ignore) { - return value - } - } - - private static Map convert(Map map, Closure closure, Closure parsingClosure) { - return map.collectEntries { - key, value -> - [key, transformValues(value, closure, parsingClosure)] - } - } - - /** - * If {@code clientSide} is {@code true} returns the client side value for the - * provided object - */ - static Object getClientOrServerSideValues(json, boolean clientSide, - Closure parsingClosure = JSON_PARSING_CLOSURE) { - return transformValues(json, { - if (it instanceof DslProperty) { - DslProperty dslProperty = ((DslProperty) it) - return clientSide ? - getClientOrServerSideValues(dslProperty.clientValue, clientSide, parsingClosure) : - getClientOrServerSideValues(dslProperty.serverValue, clientSide, parsingClosure) - } - else if (it instanceof GString) { - ContentType type = new MapConverter().templateProcessor. - containsJsonPathTemplateEntry( - ContentUtils. - extractValueForGString(it, ContentUtils.GET_TEST_SIDE). - toString() - ) ? ContentType.TEXT : null - return ContentUtils.extractValue(it, type, { - if (it instanceof DslProperty) { - return clientSide ? - getClientOrServerSideValues((it as DslProperty).clientValue, clientSide, parsingClosure) : - getClientOrServerSideValues((it as DslProperty).serverValue, clientSide, parsingClosure) - } - return it - }) - } - else if (it instanceof FromFileProperty) { - return it.isByte() ? it.asBytes() : it.asString() - } - return it - }, parsingClosure) - } - - static Object getStubSideValues(json, Closure parsingClosure = JSON_PARSING_CLOSURE) { - return getClientOrServerSideValues(json, STUB_SIDE, parsingClosure) - } - - static Object getTestSideValues(json, Function function) { - return getClientOrServerSideValues(json, TEST_SIDE, { function.apply(it) }) - } - - static Object getTestSideValues(json, Closure parsingClosure = JSON_PARSING_CLOSURE) { - return getClientOrServerSideValues(json, TEST_SIDE, parsingClosure) - } - - static Object getTestSideValuesForText(json) { - return getClientOrServerSideValues(json, TEST_SIDE, Closure.IDENTITY) - } - - static Object getStubSideValuesForNonBody(object) { - return getClientOrServerSideValues(object, STUB_SIDE, Closure.IDENTITY) - } - - static Object getTestSideValuesForNonBody(object) { - return getClientOrServerSideValues(object, TEST_SIDE, Closure.IDENTITY) - } -} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/RegexpBuilders.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/RegexpBuilders.groovy deleted file mode 100644 index 97ed6f1dde..0000000000 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/RegexpBuilders.groovy +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2013-2020 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 - * - * https://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.verifier.util - -import java.util.regex.Pattern - -import groovy.transform.TypeChecked -import org.codehaus.groovy.runtime.GStringImpl - -import org.springframework.cloud.contract.spec.internal.DslProperty -import org.springframework.cloud.contract.spec.util.RegexpUtils - -import static ContentUtils.extractValue -import static org.apache.commons.text.StringEscapeUtils.escapeJson -import static org.springframework.cloud.contract.verifier.util.ContentType.JSON - -/** - * Useful utility methods to work with regular expressions - * - * Do not change to {@code @CompileStatic} since it's using double dispatch. - * - * @since 1.0.0 - */ -@TypeChecked -class RegexpBuilders { - - /** - * Converts the {@link GString} passed values into their stub side String representations - */ - static String buildGStringRegexpForStubSide(GString gString) { - new GStringImpl( - gString.values.collect(this.&buildGStringRegexpForStubSide) as Object[], - gString.strings.collect(this.&escapeSpecialRegexChars) as String[] - ) - } - - /** - * Converts the {@link Pattern} passed values into their stub side String representations - */ - static String buildGStringRegexpForStubSide(Pattern pattern) { - return pattern.pattern() - } - - /** - * Converts the {@link org.springframework.cloud.contract.spec.internal.DslProperty} passed values into their stub side String representations - */ - static String buildGStringRegexpForStubSide(DslProperty dslProperty) { - return buildGStringRegexpForStubSide(dslProperty.clientValue) - } - - /** - * Converts the {@link Object} passed values into their stub side String representations - */ - static String buildGStringRegexpForStubSide(Object o) { - if (o instanceof DslProperty) { - return buildGStringRegexpForStubSide((DslProperty) o) - } - else if (o instanceof Pattern) { - return buildGStringRegexpForStubSide((Pattern) o) - } - else if (o instanceof GString) { - return buildGStringRegexpForStubSide((GString) o) - } - return escapeSpecialRegexChars(o.toString()) - } - - /** - * Converts the {@link GString} passed values into their test side String representations - */ - static String buildGStringRegexpForTestSide(GString gString) { - new GStringImpl( - gString.values.collect(this.&buildGStringRegexpForTestSide) as Object[], - gString.strings.collect(this.&escapeSpecialRegexChars) as String[] - ) - } - - /** - * Converts the {@link Pattern} passed values into their test side String representations - */ - static String buildGStringRegexpForTestSide(Pattern pattern) { - return pattern.pattern() - } - - /** - * Converts the {@link DslProperty} passed values into their test side String representations - */ - static String buildGStringRegexpForTestSide(DslProperty dslProperty) { - return buildGStringRegexpForTestSide(dslProperty.clientValue) - } - - /** - * Converts the {@link Object} passed values into their test side String representations - */ - static String buildGStringRegexpForTestSide(Object o) { - return o.toString().replaceAll('\\\\', '\\\\\\\\') - } - - static String escapeSpecialRegexChars(String str) { - return RegexpUtils.escapeSpecialRegexChars(str) - } - - private final static String WS = /\s*/ - - static String buildJSONRegexpMatch(GString gString) { - return buildJSONRegexpMatch( - extractValue(gString, JSON, { DslProperty dslProperty -> dslProperty.clientValue })) - } - - static String buildJSONRegexpMatch(Map jsonMap) { - return WS + "\\{" + jsonMap.collect(this.&buildJSONRegexpMatch).join(",") + "\\}" + WS - } - - static String buildJSONRegexpMatch(List jsonList) { - return WS + "\\[" + jsonList.collect(this.&buildJSONRegexpMatch).join(",") + "\\]" + WS - } - - /** - * Converts the map into String representation of regular expressions - */ - static String buildJSONRegexpMatch(Map.Entry entry) { - return buildJSONRegexpMatchString(escapeJson(entry.key)) + ":" + - buildJSONRegexpMatch(entry.value) - } - - /** - * Converts the object into String representation of regular expressions - */ - static String buildJSONRegexpMatch(Object value) { - return buildJSONRegexpMatchStringOptionalQuotes(escapeJson(value.toString())) - } - - /** - * Converts the pattern into String representation of regular expressions - */ - static String buildJSONRegexpMatch(Pattern pattern) { - return buildJSONRegexpMatchStringOptionalQuotes(pattern.pattern()) - } - - /** - * Converts the String into String representation of regular expressions - */ - static String buildJSONRegexpMatchString(String value) { - return WS + '"' + value + '"' + WS - } - - /** - * Converts the String into an optional String representation of regular expressions - */ - static String buildJSONRegexpMatchStringOptionalQuotes(String value) { - return WS + '"?' + value + '"?' + WS - } - -} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/TestGenerator.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/TestGenerator.java index 29bb026154..45082ebe64 100755 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/TestGenerator.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/TestGenerator.java @@ -37,7 +37,6 @@ import org.springframework.cloud.contract.verifier.builder.JavaTestGenerator; import org.springframework.cloud.contract.verifier.builder.SingleTestGenerator; import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties; import org.springframework.cloud.contract.verifier.file.ContractFileScanner; -import org.springframework.cloud.contract.verifier.file.ContractFileScannerBuilder; import org.springframework.cloud.contract.verifier.file.ContractMetadata; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.util.MultiValueMap; @@ -85,7 +84,7 @@ public class TestGenerator { public TestGenerator(ContractVerifierConfigProperties configProperties, SingleTestGenerator generator, FileSaver saver) { - this(configProperties, generator, saver, new ContractFileScannerBuilder() + this(configProperties, generator, saver, ContractFileScanner.builder() .baseDir(configProperties.getContractsDslDir()).excluded(toSet(configProperties.getExcludedFiles())) .ignored(toSet(configProperties.getIgnoredFiles())).included(toSet(configProperties.getIncludedFiles())) .includeMatcher(configProperties.getIncludedContracts()).build()); diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/assertion/SpringCloudContractAssertions.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/assertion/SpringCloudContractAssertions.java new file mode 100644 index 0000000000..0fc3e205eb --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/assertion/SpringCloudContractAssertions.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2020 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 + * + * https://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.verifier.assertion; + +import org.assertj.core.api.Assertions; + +public class SpringCloudContractAssertions extends Assertions { + + /** + * Creates a new instance of {@link CollectionAssert}. + * @param type to assert + * @param actual the actual value. + * @return the created assertion object. + */ + public static CollectionAssert assertThat(Iterable actual) { + return new CollectionAssert<>(actual); + } + +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/BodyParser.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/BodyParser.java index e7a502a65d..3312294b3e 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/BodyParser.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/BodyParser.java @@ -18,10 +18,10 @@ package org.springframework.cloud.contract.verifier.builder; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; import groovy.json.JsonOutput; -import groovy.lang.Closure; import groovy.lang.GString; import org.apache.commons.text.StringEscapeUtils; @@ -117,14 +117,15 @@ interface BodyParser extends BodyThen { */ default Object extractServerValueFromBody(ContentType contentType, Object bodyValue) { if (bodyValue instanceof GString) { - return extractValue((GString) bodyValue, contentType, ContentUtils.GET_TEST_SIDE); + return extractValue((GString) bodyValue, contentType, ContentUtils.GET_TEST_SIDE_FUNCTION); } else if (bodyValue instanceof FromFileProperty) { - return MapConverter.transformValues(bodyValue, ContentUtils.GET_TEST_SIDE); + return MapConverter.transformValues(bodyValue, ContentUtils.GET_TEST_SIDE_FUNCTION); } else if (TEXT != contentType && FORM != contentType && DEFINED != contentType) { boolean dontParseStrings = contentType == JSON && bodyValue instanceof Map; - Closure parsingClosure = dontParseStrings ? Closure.IDENTITY : MapConverter.JSON_PARSING_CLOSURE; + Function parsingClosure = dontParseStrings ? MapConverter.IDENTITY + : MapConverter.JSON_PARSING_FUNCTION; return MapConverter.getTestSideValues(bodyValue, parsingClosure); } return bodyValue; diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.java index acafd4aa54..1868fb4858 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/JsonBodyVerificationBuilder.java @@ -26,7 +26,6 @@ import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.PathNotFoundException; import groovy.json.JsonOutput; -import groovy.lang.Closure; import org.apache.commons.beanutils.PropertyUtilsBean; import org.springframework.cloud.contract.spec.Contract; @@ -86,10 +85,11 @@ class JsonBodyVerificationBuilder implements BodyMethodGeneration, ClassVerifier appendJsonPath(bb, responseString); DocumentContext parsedRequestBody = null; boolean dontParseStrings = convertedResponseBody instanceof Map; - Closure parsingClosure = dontParseStrings ? Closure.IDENTITY : MapConverter.JSON_PARSING_CLOSURE; + Function parsingFunction = dontParseStrings ? MapConverter.IDENTITY + : MapConverter.JSON_PARSING_FUNCTION; if (hasRequestBody()) { Object testSideRequestBody = MapConverter.getTestSideValues(contract.getRequest().getBody(), - parsingClosure); + parsingFunction); parsedRequestBody = JsonPath.parse(testSideRequestBody); if (convertedResponseBody instanceof String && !textContainsJsonPathTemplate(convertedResponseBody.toString())) { @@ -108,9 +108,9 @@ class JsonBodyVerificationBuilder implements BodyMethodGeneration, ClassVerifier TestSideRequestTemplateModel templateModel = hasRequestBody() ? TestSideRequestTemplateModel.from(contract.getRequest()) : null; convertedResponseBody = MapConverter.transformValues(convertedResponseBody, - returnReferencedEntries(templateModel), parsingClosure); + returnReferencedEntries(templateModel), parsingFunction); JsonPaths jsonPaths = new JsonToJsonPathsConverter(assertJsonSize) - .transformToJsonPathWithTestsSideValues(convertedResponseBody, parsingClosure, includeEmptyCheck); + .transformToJsonPathWithTestsSideValues(convertedResponseBody, parsingFunction, includeEmptyCheck); DocumentContext finalParsedRequestBody = parsedRequestBody; jsonPaths.forEach(it -> { @@ -280,8 +280,8 @@ class JsonBodyVerificationBuilder implements BodyMethodGeneration, ClassVerifier } } - private Closure returnReferencedEntries(TestSideRequestTemplateModel templateModel) { - return MapConverter.fromFunction(entry -> { + private Function returnReferencedEntries(TestSideRequestTemplateModel templateModel) { + return entry -> { if (!(entry instanceof String) || templateModel == null) { return entry; } @@ -313,7 +313,7 @@ class JsonBodyVerificationBuilder implements BodyMethodGeneration, ClassVerifier } } return entry; - }); + }; } private static String minus(CharSequence self, Object target) { diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/TestSideRequestTemplateModel.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/TestSideRequestTemplateModel.java index 667e332b31..c7489c8dac 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/TestSideRequestTemplateModel.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/TestSideRequestTemplateModel.java @@ -193,7 +193,7 @@ public class TestSideRequestTemplateModel { bodyValue = ContentUtils.extractValue((GString) bodyValue, ContentUtils.GET_TEST_SIDE_FUNCTION); } else { - bodyValue = MapConverter.transformValues(bodyValue, ContentUtils.GET_TEST_SIDE); + bodyValue = MapConverter.transformValues(bodyValue, ContentUtils.GET_TEST_SIDE_FUNCTION); } return bodyValue; } diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.java new file mode 100644 index 0000000000..f1419f41b2 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/converter/ContractsToYaml.java @@ -0,0 +1,585 @@ +/* + * Copyright 2013-2020 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 + * + * https://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.verifier.converter; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.internal.BodyMatchers; +import org.springframework.cloud.contract.spec.internal.Cookies; +import org.springframework.cloud.contract.spec.internal.DslProperty; +import org.springframework.cloud.contract.spec.internal.ExecutionProperty; +import org.springframework.cloud.contract.spec.internal.FromFileProperty; +import org.springframework.cloud.contract.spec.internal.Headers; +import org.springframework.cloud.contract.spec.internal.Input; +import org.springframework.cloud.contract.spec.internal.MatchingStrategy; +import org.springframework.cloud.contract.spec.internal.MatchingType; +import org.springframework.cloud.contract.spec.internal.Multipart; +import org.springframework.cloud.contract.spec.internal.NamedProperty; +import org.springframework.cloud.contract.spec.internal.NotToEscapePattern; +import org.springframework.cloud.contract.spec.internal.OutputMessage; +import org.springframework.cloud.contract.spec.internal.QueryParameter; +import org.springframework.cloud.contract.spec.internal.RegexProperty; +import org.springframework.cloud.contract.spec.internal.Request; +import org.springframework.cloud.contract.spec.internal.Response; +import org.springframework.cloud.contract.spec.internal.Url; +import org.springframework.cloud.contract.verifier.util.ContentType; +import org.springframework.cloud.contract.verifier.util.JsonPaths; +import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter; +import org.springframework.cloud.contract.verifier.util.MapConverter; + +import static org.springframework.cloud.contract.verifier.util.ContentType.XML; +import static org.springframework.cloud.contract.verifier.util.ContentUtils.evaluateClientSideContentType; + +/** + * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma + * @author Stessy Delcroix + */ +class ContractsToYaml { + + private static final Map, YamlContract.RegexType> PRIMITIVE_WRAPPER_TO_REGEX_TYPE = new HashMap<>(); + + private static final Map TEST_MATCHER_TYPE = new HashMap<>(); + + private static final Map STUB_MATCHER_TYPE = new HashMap<>(); + + static { + PRIMITIVE_WRAPPER_TO_REGEX_TYPE.put(Boolean.class, YamlContract.RegexType.as_boolean); + PRIMITIVE_WRAPPER_TO_REGEX_TYPE.put(Long.class, YamlContract.RegexType.as_long); + PRIMITIVE_WRAPPER_TO_REGEX_TYPE.put(Short.class, YamlContract.RegexType.as_short); + PRIMITIVE_WRAPPER_TO_REGEX_TYPE.put(Integer.class, YamlContract.RegexType.as_integer); + PRIMITIVE_WRAPPER_TO_REGEX_TYPE.put(Float.class, YamlContract.RegexType.as_float); + PRIMITIVE_WRAPPER_TO_REGEX_TYPE.put(Double.class, YamlContract.RegexType.as_double); + PRIMITIVE_WRAPPER_TO_REGEX_TYPE.put(String.class, YamlContract.RegexType.as_string); + TEST_MATCHER_TYPE.put(MatchingType.EQUALITY, YamlContract.TestMatcherType.by_equality); + TEST_MATCHER_TYPE.put(MatchingType.TYPE, YamlContract.TestMatcherType.by_type); + TEST_MATCHER_TYPE.put(MatchingType.COMMAND, YamlContract.TestMatcherType.by_command); + TEST_MATCHER_TYPE.put(MatchingType.DATE, YamlContract.TestMatcherType.by_date); + TEST_MATCHER_TYPE.put(MatchingType.TIME, YamlContract.TestMatcherType.by_time); + TEST_MATCHER_TYPE.put(MatchingType.TIMESTAMP, YamlContract.TestMatcherType.by_timestamp); + TEST_MATCHER_TYPE.put(MatchingType.REGEX, YamlContract.TestMatcherType.by_regex); + TEST_MATCHER_TYPE.put(MatchingType.NULL, YamlContract.TestMatcherType.by_null); + STUB_MATCHER_TYPE.put(MatchingType.EQUALITY, YamlContract.StubMatcherType.by_equality); + STUB_MATCHER_TYPE.put(MatchingType.DATE, YamlContract.StubMatcherType.by_date); + STUB_MATCHER_TYPE.put(MatchingType.TIME, YamlContract.StubMatcherType.by_time); + STUB_MATCHER_TYPE.put(MatchingType.TIMESTAMP, YamlContract.StubMatcherType.by_timestamp); + STUB_MATCHER_TYPE.put(MatchingType.REGEX, YamlContract.StubMatcherType.by_regex); + } + + List convertTo(Collection contracts) { + + return contracts.stream().map(contract -> { + YamlContract yamlContract = new YamlContract(); + if (contract == null) { + return yamlContract; + } + yamlContract.name = contract.getName(); + yamlContract.ignored = contract.getIgnored(); + yamlContract.inProgress = contract.getInProgress(); + yamlContract.description = contract.getDescription(); + yamlContract.label = contract.getLabel(); + yamlContract.metadata = contract.getMetadata(); + request(contract, yamlContract); + response(yamlContract, contract); + input(contract, yamlContract); + output(contract, yamlContract); + return yamlContract; + }).collect(Collectors.toList()); + } + + protected void request(Contract contract, YamlContract yamlContract) { + Request request = contract.getRequest(); + if (request != null) { + ContentType requestContentType = evaluateClientSideContentType(request.getHeaders(), request.getBody()); + yamlContract.request = new YamlContract.Request(); + mapRequestMethod(yamlContract.request, request); + mapRequestUrl(yamlContract.request, request); + mapRequestUrlPath(yamlContract.request, request); + mapRequestMatchers(yamlContract.request); + Url requestUrl = Optional.ofNullable(request.getUrl()).orElse(request.getUrlPath()); + if (requestUrl.getQueryParameters() != null) { + mapRequestQueryParameters(yamlContract.request, requestUrl); + mapRequestMatchersQueryParameters(yamlContract.request, requestUrl); + } + mapRequestHeaders(yamlContract.request, request); + mapRequestCookies(yamlContract.request, request); + mapRequestBody(yamlContract.request, request); + mapRequestMultipart(yamlContract.request, request); + mapRequestMatchersBody(yamlContract.request, request); + mapRequestMatchersUrl(yamlContract.request, request); + mapRequestMatchersMultipart(yamlContract.request, request); + + // TODO: Cookie matchers - including absent + if (XML != requestContentType) { + setInputBodyMatchers(request.getBody(), yamlContract.request.matchers.body); + } + setInputHeadersMatchers(request.getHeaders(), yamlContract.request.matchers.headers); + } + } + + private void mapRequestMatchersMultipart(YamlContract.Request yamlContractRequest, Request request) { + Multipart multipart = request.getMultipart(); + if (multipart != null) { + yamlContractRequest.matchers.multipart = new YamlContract.MultipartStubMatcher(); + Map map = (Map) MapConverter.getStubSideValues(multipart); + map.forEach((key, value) -> { + if (value instanceof NamedProperty) { + Object fileName = Optional.ofNullable(((NamedProperty) value).getName()) + .map(DslProperty::getClientValue).orElse(null); + Object fileContent = Optional.ofNullable(((NamedProperty) value).getValue()) + .map(DslProperty::getClientValue).orElse(null); + Object contentType = Optional.ofNullable(((NamedProperty) value).getContentType()) + .map(DslProperty::getClientValue).orElse(null); + if (fileName instanceof RegexProperty || fileContent instanceof RegexProperty + || contentType instanceof RegexProperty) { + YamlContract.MultipartNamedStubMatcher multipartNamedStubMatcher = new YamlContract.MultipartNamedStubMatcher(); + multipartNamedStubMatcher.paramName = key; + multipartNamedStubMatcher.fileName = valueMatcher(fileName); + multipartNamedStubMatcher.fileContent = valueMatcher(fileContent); + multipartNamedStubMatcher.contentType = valueMatcher(contentType); + yamlContractRequest.matchers.multipart.named.add(multipartNamedStubMatcher); + } + } + else if (value instanceof RegexProperty || value instanceof Pattern) { + RegexProperty property = new RegexProperty(value); + YamlContract.KeyValueMatcher keyValueMatcher = new YamlContract.KeyValueMatcher(); + keyValueMatcher.key = key; + keyValueMatcher.regex = property.pattern(); + keyValueMatcher.regexType = regexType(property.clazz()); + yamlContractRequest.matchers.multipart.params.add(keyValueMatcher); + } + }); + } + } + + private void mapRequestMatchersUrl(YamlContract.Request yamlContractRequest, Request request) { + Object url = Optional.ofNullable(request.getUrl()).map(Url::getClientValue).orElse(null); + YamlContract.KeyValueMatcher keyValueMatcher = new YamlContract.KeyValueMatcher(); + if (url instanceof RegexProperty) { + keyValueMatcher.regex = ((RegexProperty) url).pattern(); + yamlContractRequest.matchers.url = keyValueMatcher; + } + else if (url instanceof ExecutionProperty) { + keyValueMatcher.command = url.toString(); + yamlContractRequest.matchers.url = keyValueMatcher; + } + else { + yamlContractRequest.matchers.url = null; + } + + Object urlPath = Optional.ofNullable(request.getUrlPath()).map(Url::getClientValue).orElse(null); + if (urlPath instanceof RegexProperty) { + keyValueMatcher.regex = ((RegexProperty) urlPath).pattern(); + yamlContractRequest.matchers.url = keyValueMatcher; + } + else if (urlPath instanceof ExecutionProperty) { + keyValueMatcher.command = urlPath.toString(); + yamlContractRequest.matchers.url = keyValueMatcher; + } + else { + yamlContractRequest.matchers.url = null; + } + } + + private void mapRequestMatchersBody(YamlContract.Request yamlContractRequest, Request request) { + Optional.ofNullable(request.getBodyMatchers()).map(BodyMatchers::matchers) + .ifPresent(bodyMatchers -> bodyMatchers.forEach(bodyMatcher -> { + YamlContract.BodyStubMatcher bodyStubMatcher = new YamlContract.BodyStubMatcher(); + bodyStubMatcher.path = bodyMatcher.path(); + bodyStubMatcher.type = stubMatcherType(bodyMatcher.matchingType()); + bodyStubMatcher.value = Optional.ofNullable(bodyMatcher.value()).map(Object::toString).orElse(null); + bodyStubMatcher.minOccurrence = bodyMatcher.minTypeOccurrence(); + bodyStubMatcher.maxOccurrence = bodyMatcher.maxTypeOccurrence(); + yamlContractRequest.matchers.body.add(bodyStubMatcher); + })); + } + + private void mapRequestMultipart(YamlContract.Request yamlContractRequest, Request request) { + Multipart multipart = request.getMultipart(); + if (multipart != null) { + yamlContractRequest.multipart = new YamlContract.Multipart(); + Map map = (Map) MapConverter.getTestSideValues(multipart); + map.forEach((key, value) -> { + if (value instanceof NamedProperty) { + Object fileName = Optional.ofNullable(((NamedProperty) value).getName()) + .map(DslProperty::getServerValue).orElse(null); + Object contentType = Optional.ofNullable(((NamedProperty) value).getContentType()) + .map(DslProperty::getServerValue).orElse(null); + Object fileContent = Optional.ofNullable(((NamedProperty) value).getValue()) + .map(DslProperty::getServerValue).orElse(null); + YamlContract.Named named = new YamlContract.Named(); + named.paramName = key; + named.fileName = fileName instanceof String ? Optional.ofNullable(((NamedProperty) value).getName()) + .map(DslProperty::getServerValue).map(Object::toString).orElse(null) : null; + named.fileContent = (String) Optional.ofNullable(fileContent).filter(f -> f instanceof String) + .orElse(null); + named.fileContentAsBytes = fileContent instanceof FromFileProperty + ? new String(((FromFileProperty) fileContent).asBytes()) : null; + named.fileContentFromFileAsBytes = resolveFileNameAsBytes(fileContent); + named.contentType = (String) Optional.ofNullable(contentType).filter(f -> f instanceof String) + .orElse(null); + named.fileNameCommand = fileName instanceof ExecutionProperty ? fileName.toString() : null; + named.fileContentCommand = fileContent instanceof ExecutionProperty ? fileContent.toString() : null; + named.contentTypeCommand = contentType instanceof ExecutionProperty ? contentType.toString() : null; + yamlContractRequest.multipart.named.add(named); + } + else { + yamlContractRequest.multipart.params.put(key, value != null ? value.toString() : null); + } + }); + } + } + + private void mapRequestBody(YamlContract.Request yamlContractRequest, Request request) { + Object body = Optional.ofNullable(request.getBody()).map(DslProperty::getServerValue).orElse(null); + if (body instanceof FromFileProperty) { + FromFileProperty fromFileProperty = (FromFileProperty) body; + if (fromFileProperty.isByte()) { + yamlContractRequest.bodyFromFileAsBytes = fromFileProperty.fileName(); + } + if (fromFileProperty.isString()) { + yamlContractRequest.bodyFromFile = fromFileProperty.fileName(); + } + } + else { + yamlContractRequest.body = MapConverter.getTestSideValues(request.getBody()); + } + } + + private void mapRequestCookies(YamlContract.Request yamlContractRequest, Request request) { + yamlContractRequest.cookies = Optional.ofNullable(request.getCookies()).map(Cookies::asTestSideMap) + .orElse(null); + } + + private void mapRequestHeaders(YamlContract.Request yamlContractRequest, Request request) { + yamlContractRequest.headers = request.getHeaders().asMap((headerName, prop) -> { + Object testSideValue = MapConverter.getTestSideValues(prop); + if (testSideValue instanceof ExecutionProperty) { + return MapConverter.getStubSideValuesForNonBody(prop).toString(); + } + return testSideValue.toString(); + }); + } + + private void mapRequestMatchersQueryParameters(YamlContract.Request yamlContractRequest, Url requestUrl) { + yamlContractRequest.matchers.queryParameters + .addAll(requestUrl.getQueryParameters().getParameters().stream().map(parameter -> { + Object stubSide = parameter.getClientValue(); + if (stubSide instanceof RegexProperty || stubSide instanceof Pattern) { + YamlContract.QueryParameterMatcher queryParameterMatcher = new YamlContract.QueryParameterMatcher(); + queryParameterMatcher.key = parameter.getName(); + queryParameterMatcher.type = YamlContract.MatchingType.matching; + queryParameterMatcher.value = new RegexProperty(stubSide).pattern(); + return queryParameterMatcher; + } + else if (stubSide instanceof MatchingStrategy) { + YamlContract.QueryParameterMatcher queryParameterMatcher = new YamlContract.QueryParameterMatcher(); + queryParameterMatcher.key = parameter.getName(); + queryParameterMatcher.type = YamlContract.MatchingType + .from(((MatchingStrategy) stubSide).getType().getName()); + queryParameterMatcher.value = MapConverter.getStubSideValuesForNonBody(stubSide); + return queryParameterMatcher; + } + else { + return null; + } + }).filter(Objects::nonNull).collect(Collectors.toList())); + } + + private void mapRequestQueryParameters(YamlContract.Request yamlContractRequest, Url requestUrl) { + yamlContractRequest.queryParameters = requestUrl.getQueryParameters().getParameters().stream() + .collect(Collectors.toMap(QueryParameter::getName, MapConverter::getTestSideValuesForNonBody)); + } + + private void mapRequestMatchers(YamlContract.Request yamlContractRequest) { + yamlContractRequest.matchers = new YamlContract.StubMatchers(); + } + + private void mapRequestUrlPath(YamlContract.Request yamlContractRequest, Request request) { + yamlContractRequest.urlPath = Optional.ofNullable(request.getUrlPath()).map(m -> m.getServerValue().toString()) + .orElse(null); + } + + private void mapRequestUrl(YamlContract.Request yamlContractRequest, Request request) { + yamlContractRequest.url = Optional.ofNullable(request.getUrl()).map(m -> m.getServerValue().toString()) + .orElse(null); + } + + private void mapRequestMethod(YamlContract.Request yamlContractRequest, Request request) { + yamlContractRequest.method = Optional.ofNullable(request.getMethod()).map(m -> m.getServerValue().toString()) + .orElse(null); + } + + protected void output(Contract contract, YamlContract yamlContract) { + OutputMessage outputMessage = contract.getOutputMessage(); + if (outputMessage != null) { + Optional optionalResponse = Optional.ofNullable(contract.getResponse()); + ContentType contentType = evaluateClientSideContentType( + optionalResponse.map(Response::getHeaders).orElse(null), + optionalResponse.map(Response::getBody).orElse(null)); + yamlContract.outputMessage = new YamlContract.OutputMessage(); + yamlContract.outputMessage.sentTo = MapConverter.getStubSideValues(outputMessage.getSentTo()).toString(); + yamlContract.outputMessage.headers = Optional.ofNullable(outputMessage.getHeaders()) + .map(Headers::asStubSideMap).orElse(null); + yamlContract.outputMessage.body = MapConverter.getStubSideValues(outputMessage.getBody()); + Optional.ofNullable(outputMessage.getBodyMatchers()).map(BodyMatchers::matchers) + .ifPresent(bodyMatchers -> bodyMatchers.forEach(bodyMatcher -> { + YamlContract.BodyTestMatcher bodyTestMatcher = new YamlContract.BodyTestMatcher(); + bodyTestMatcher.path = bodyMatcher.path(); + bodyTestMatcher.type = testMatcherType(bodyMatcher.matchingType()); + bodyTestMatcher.value = Optional.ofNullable(bodyMatcher.value()).map(Object::toString) + .orElse(null); + bodyTestMatcher.minOccurrence = bodyMatcher.minTypeOccurrence(); + bodyTestMatcher.maxOccurrence = bodyMatcher.maxTypeOccurrence(); + yamlContract.outputMessage.matchers.body.add(bodyTestMatcher); + + })); + if (XML != contentType) { + setOutputBodyMatchers(outputMessage.getBody(), yamlContract.outputMessage.matchers.body); + } + setOutputHeadersMatchers(outputMessage.getHeaders(), yamlContract.outputMessage.matchers.headers); + } + } + + protected void input(Contract contract, YamlContract yamlContract) { + Input input = contract.getInput(); + if (input != null) { + ContentType contentType = evaluateClientSideContentType(input.getMessageHeaders(), input.getMessageBody()); + yamlContract.input = new YamlContract.Input(); + yamlContract.input.assertThat = Optional.ofNullable(input.getAssertThat()) + .map(assertThat -> MapConverter + .getTestSideValues(assertThat.toString(), MapConverter.JSON_PARSING_FUNCTION).toString()) + .orElse(null); + yamlContract.input.triggeredBy = Optional.ofNullable(input.getTriggeredBy()) + .map(triggeredBy -> MapConverter + .getTestSideValues(triggeredBy.toString(), MapConverter.JSON_PARSING_FUNCTION).toString()) + .orElse(null); + yamlContract.input.messageHeaders = input.getMessageHeaders().asTestSideMap(); + yamlContract.input.messageBody = MapConverter.getTestSideValues(input.getMessageBody(), + MapConverter.JSON_PARSING_FUNCTION); + yamlContract.input.messageFrom = Optional + .ofNullable(input.getMessageFrom()).map(messageFrom -> MapConverter + .getTestSideValues(messageFrom, MapConverter.JSON_PARSING_FUNCTION).toString()) + .orElse(null); + Optional.ofNullable(input.getBodyMatchers()).map(BodyMatchers::matchers) + .ifPresent(bodyMatchers -> bodyMatchers.forEach(bodyMatcher -> { + YamlContract.BodyStubMatcher bodyStubMatcher = new YamlContract.BodyStubMatcher(); + bodyStubMatcher.path = bodyMatcher.path(); + bodyStubMatcher.type = stubMatcherType(bodyMatcher.matchingType()); + bodyStubMatcher.value = Optional.ofNullable(bodyMatcher.value()).map(Object::toString) + .orElse(null); + yamlContract.input.matchers.body.add(bodyStubMatcher); + })); + if (XML != contentType) { + setInputBodyMatchers(input.getMessageBody(), yamlContract.input.matchers.body); + } + setInputHeadersMatchers(input.getMessageHeaders(), yamlContract.input.matchers.headers); + } + } + + protected String resolveFileNameAsBytes(Object value) { + if (!(value instanceof FromFileProperty)) { + return null; + } + FromFileProperty property = (FromFileProperty) value; + return property.fileName(); + } + + protected YamlContract.ValueMatcher valueMatcher(Object o) { + return Optional.ofNullable(o).filter(object -> object instanceof RegexProperty) + .map(object -> (RegexProperty) object).map(regexProperty -> { + YamlContract.ValueMatcher valueMatcher = new YamlContract.ValueMatcher(); + valueMatcher.regex = regexProperty.pattern(); + return valueMatcher; + }).orElse(null); + } + + protected void setInputBodyMatchers(DslProperty body, List bodyMatchers) { + Object testSideValues = MapConverter.getTestSideValues(body); + JsonPaths paths = new JsonToJsonPathsConverter().transformToJsonPathWithStubsSideValues(body); + paths.stream().filter((path) -> path.valueBeforeChecking() instanceof Pattern).forEach((path) -> { + Object element = JsonToJsonPathsConverter.readElement(testSideValues, path.keyBeforeChecking()); + YamlContract.BodyStubMatcher bodyStubMatcher = new YamlContract.BodyStubMatcher(); + bodyStubMatcher.path = path.keyBeforeChecking(); + bodyStubMatcher.type = YamlContract.StubMatcherType.by_regex; + bodyStubMatcher.value = ((Pattern) path.valueBeforeChecking()).pattern(); + bodyStubMatcher.regexType = regexType(element); + bodyMatchers.add(bodyStubMatcher); + }); + } + + protected YamlContract.RegexType regexType(Object from) { + return regexType(from.getClass()); + } + + protected YamlContract.RegexType regexType(Class clazz) { + return PRIMITIVE_WRAPPER_TO_REGEX_TYPE.getOrDefault(clazz, PRIMITIVE_WRAPPER_TO_REGEX_TYPE.get(String.class)); + } + + protected void response(YamlContract yamlContract, Contract contract) { + if (contract.getResponse() != null) { + Response contractResponse = contract.getResponse(); + ContentType contentType = evaluateClientSideContentType(contractResponse.getHeaders(), + contractResponse.getBody()); + YamlContract.Response response = new YamlContract.Response(); + yamlContract.response = response; + mapResponseAsync(contractResponse, response); + mapResponseFixedDelayMilliseconds(contractResponse, response); + mapResponseStatus(contractResponse, response); + mapResponseHeaders(contractResponse, response); + mapResponseCookies(contractResponse, response); + mapResponseBody(contractResponse, response); + mapResponseBodyMatchers(contractResponse, response); + if (XML != contentType) { + setOutputBodyMatchers(contractResponse.getBody(), yamlContract.response.matchers.body); + } + setOutputHeadersMatchers(contractResponse.getHeaders(), yamlContract.response.matchers.headers); + } + } + + private void mapResponseBodyMatchers(Response contractResponse, YamlContract.Response response) { + Optional.ofNullable(contractResponse.getBodyMatchers()).map(BodyMatchers::matchers) + .ifPresent(bodyMatchers -> bodyMatchers.forEach((bodyMatcher) -> { + YamlContract.BodyTestMatcher bodyTestMatcher = new YamlContract.BodyTestMatcher(); + bodyTestMatcher.path = bodyMatcher.path(); + bodyTestMatcher.type = testMatcherType(bodyMatcher.matchingType()); + bodyTestMatcher.value = Optional.ofNullable(bodyMatcher.value()).map(Object::toString).orElse(null); + bodyTestMatcher.minOccurrence = bodyMatcher.minTypeOccurrence(); + bodyTestMatcher.maxOccurrence = bodyMatcher.maxTypeOccurrence(); + response.matchers.body.add(bodyTestMatcher); + })); + } + + private void mapResponseBody(Response contractResponse, YamlContract.Response response) { + Object body = Optional.ofNullable(contractResponse.getBody()).map(DslProperty::getClientValue).orElse(null); + if (body instanceof FromFileProperty) { + if (((FromFileProperty) body).isByte()) { + response.bodyFromFileAsBytes = ((FromFileProperty) body).fileName(); + } + if (((FromFileProperty) body).isString()) { + response.bodyFromFile = ((FromFileProperty) body).fileName(); + } + } + else { + response.body = MapConverter.getStubSideValues(contractResponse.getBody()); + } + } + + private void mapResponseCookies(Response contractResponse, YamlContract.Response response) { + response.cookies = Optional.ofNullable(contractResponse.getCookies()).map(Cookies::asStubSideMap).orElse(null); + } + + private void mapResponseHeaders(Response contractResponse, YamlContract.Response response) { + response.headers = Optional.ofNullable(contractResponse.getHeaders()) + .map(headers -> headers.asMap((headerName, dslProperty) -> MapConverter.getStubSideValues(dslProperty))) + .orElse(null); + } + + private void mapResponseStatus(Response contractResponse, YamlContract.Response response) { + response.status = Optional.ofNullable(contractResponse.getStatus()).map(DslProperty::getClientValue) + .map(clientValue -> (Integer) clientValue).orElse(null); + } + + private void mapResponseFixedDelayMilliseconds(Response contractResponse, YamlContract.Response response) { + response.fixedDelayMilliseconds = (Integer) Optional.ofNullable(contractResponse.getDelay()) + .map(DslProperty::getClientValue).orElse(null); + } + + private void mapResponseAsync(Response contractResponse, YamlContract.Response response) { + response.async = contractResponse.getAsync(); + } + + protected void setOutputBodyMatchers(DslProperty body, List bodyMatchers) { + Object testSideValues = MapConverter.getTestSideValues(body); + JsonPaths paths = new JsonToJsonPathsConverter().transformToJsonPathWithTestsSideValues(body); + paths.stream().filter(m -> m.valueBeforeChecking() instanceof Pattern).forEach((m) -> { + Object element = JsonToJsonPathsConverter.readElement(testSideValues, m.keyBeforeChecking()); + YamlContract.BodyTestMatcher bodyTestMatcher = new YamlContract.BodyTestMatcher(); + bodyTestMatcher.path = m.keyBeforeChecking(); + bodyTestMatcher.type = YamlContract.TestMatcherType.by_regex; + bodyTestMatcher.value = ((Pattern) m.valueBeforeChecking()).pattern(); + bodyTestMatcher.regexType = regexType(element); + bodyMatchers.add(bodyTestMatcher); + }); + Optional.ofNullable(body).filter(b -> b.getServerValue() instanceof Pattern).ifPresent((b) -> { + YamlContract.BodyTestMatcher bodyTestMatcher = new YamlContract.BodyTestMatcher(); + bodyTestMatcher.type = YamlContract.TestMatcherType.by_regex; + bodyTestMatcher.value = ((Pattern) b.getServerValue()).pattern(); + bodyMatchers.add(bodyTestMatcher); + }); + } + + protected void setInputHeadersMatchers(Headers headers, List headerMatchers) { + Optional.ofNullable(headers).map(Headers::asStubSideMap) + .ifPresent(stubSideMap -> stubSideMap.forEach((key, value) -> { + if (value instanceof RegexProperty || value instanceof Pattern) { + RegexProperty property = new RegexProperty(value); + YamlContract.KeyValueMatcher keyValueMatcher = new YamlContract.KeyValueMatcher(); + keyValueMatcher.key = key; + keyValueMatcher.regex = property.pattern(); + keyValueMatcher.regexType = regexType(property.clazz()); + headerMatchers.add(keyValueMatcher); + } + })); + } + + protected void setOutputHeadersMatchers(Headers headers, List headerMatchers) { + Optional.ofNullable(headers).map(Headers::asTestSideMap) + .ifPresent(testSideMap -> testSideMap.forEach((key, value) -> { + if (value instanceof RegexProperty || value instanceof Pattern) { + RegexProperty property = new RegexProperty(value); + YamlContract.TestHeaderMatcher testHeaderMatcher = new YamlContract.TestHeaderMatcher(); + testHeaderMatcher.key = key; + testHeaderMatcher.regex = property.pattern(); + testHeaderMatcher.regexType = regexType(property.clazz()); + headerMatchers.add(testHeaderMatcher); + } + else if (value instanceof ExecutionProperty) { + YamlContract.TestHeaderMatcher testHeaderMatcher = new YamlContract.TestHeaderMatcher(); + testHeaderMatcher.key = key; + testHeaderMatcher.command = ((ExecutionProperty) value).getExecutionCommand(); + headerMatchers.add(testHeaderMatcher); + } + else if (value instanceof NotToEscapePattern) { + YamlContract.TestHeaderMatcher testHeaderMatcher = new YamlContract.TestHeaderMatcher(); + testHeaderMatcher.key = key; + testHeaderMatcher.regex = (((NotToEscapePattern) value).getServerValue()).pattern(); + headerMatchers.add(testHeaderMatcher); + } + })); + } + + protected YamlContract.TestMatcherType testMatcherType(MatchingType matchingType) { + return TEST_MATCHER_TYPE.getOrDefault(matchingType, null); + } + + protected YamlContract.StubMatcherType stubMatcherType(MatchingType matchingType) { + if (matchingType == MatchingType.COMMAND || matchingType == MatchingType.TYPE) { + throw new UnsupportedOperationException("No type or command for client side"); + } + return STUB_MATCHER_TYPE.getOrDefault(matchingType, null); + } + +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/converter/YamlToContracts.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/converter/YamlToContracts.java new file mode 100644 index 0000000000..3ffe7df1b3 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/converter/YamlToContracts.java @@ -0,0 +1,961 @@ +/* + * Copyright 2013-2020 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 + * + * https://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.verifier.converter; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import org.apache.commons.collections.MapUtils; +import org.yaml.snakeyaml.Yaml; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.internal.DslProperty; +import org.springframework.cloud.contract.spec.internal.ExecutionProperty; +import org.springframework.cloud.contract.spec.internal.Header; +import org.springframework.cloud.contract.spec.internal.Headers; +import org.springframework.cloud.contract.spec.internal.Input; +import org.springframework.cloud.contract.spec.internal.MatchingTypeValue; +import org.springframework.cloud.contract.spec.internal.NamedProperty; +import org.springframework.cloud.contract.spec.internal.OutputMessage; +import org.springframework.cloud.contract.spec.internal.RegexPatterns; +import org.springframework.cloud.contract.spec.internal.Request; +import org.springframework.cloud.contract.spec.internal.Response; +import org.springframework.cloud.contract.verifier.util.ContentType; +import org.springframework.cloud.contract.verifier.util.NamesUtil; +import org.springframework.util.StringUtils; + +import static java.util.stream.Collectors.toSet; +import static org.springframework.cloud.contract.verifier.util.ContentType.XML; +import static org.springframework.cloud.contract.verifier.util.ContentUtils.evaluateClientSideContentType; + +/** + * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma + * @author Tim Ysewyn + * @author Stessy Delcroix + */ +class YamlToContracts { + + Collection convertFrom(File contractFile) { + ClassLoader classLoader = YamlContractConverter.class.getClassLoader(); + YAMLMapper mapper = new YAMLMapper(); + try { + Iterable iterables = new Yaml().loadAll(Files.newInputStream(contractFile.toPath())); + Collection contracts = new ArrayList<>(); + int counter = 0; + for (Object document : iterables) { + List processedYaml = processYaml(counter, document, mapper, classLoader, contractFile); + contracts.addAll(processedYaml); + counter = counter + 1; + } + return contracts; + } + catch (FileNotFoundException e) { + throw new IllegalStateException(e); + } + catch (IllegalStateException ise) { + throw ise; + } + catch (Exception e1) { + throw new IllegalStateException("Exception occurred while processing the file [" + contractFile + "]", e1); + } + finally { + Thread.currentThread().setContextClassLoader(classLoader); + } + } + + protected List processYaml(int counter, Object document, ObjectMapper mapper, ClassLoader classLoader, + File contractFile) { + List yamlContracts = convert(mapper, document); + Thread.currentThread().setContextClassLoader(updatedClassLoader(contractFile.getParentFile(), classLoader)); + List contracts = new ArrayList<>(); + for (YamlContract yamlContract : yamlContracts) { + Contract contract = Contract.make((dslContract) -> { + mapDescription(yamlContract, dslContract); + mapLabel(yamlContract, dslContract); + mapName(counter, contractFile, yamlContracts, yamlContract, dslContract); + mapPriority(yamlContract, dslContract); + mapIgnored(yamlContract, dslContract); + mapInProgress(yamlContract, dslContract); + mapMetadata(yamlContract, dslContract); + mapRequest(yamlContract, dslContract); + mapResponse(yamlContract, dslContract); + mapInput(yamlContract, dslContract); + mapOutput(yamlContract, dslContract); + }); + contracts.add(contract); + } + return contracts; + } + + private void mapMetadata(YamlContract yamlContract, Contract dslContract) { + if (yamlContract.metadata != null) { + dslContract.metadata(yamlContract.metadata); + } + } + + private void mapInProgress(YamlContract yamlContract, Contract dslContract) { + if (yamlContract.inProgress) { + dslContract.inProgress(); + } + } + + private void mapIgnored(YamlContract yamlContract, Contract dslContract) { + if (yamlContract.ignored) { + dslContract.ignored(); + } + } + + private void mapPriority(YamlContract yamlContract, Contract dslContract) { + if (yamlContract.priority != null) { + dslContract.priority(yamlContract.priority); + } + } + + private void mapName(int counter, File contractFile, List yamlContracts, YamlContract yamlContract, + Contract dslContract) { + dslContract.name(StringUtils.hasText(yamlContract.name) ? yamlContract.name + : NamesUtil.defaultContractName(contractFile, yamlContracts, counter)); + } + + private void mapLabel(YamlContract yamlContract, Contract dslContract) { + if (yamlContract.label != null) { + dslContract.label(yamlContract.label); + } + } + + private void mapDescription(YamlContract yamlContract, Contract dslContract) { + if (yamlContract.description != null) { + dslContract.description(yamlContract.description); + } + } + + private void mapRequest(YamlContract yamlContract, Contract dslContract) { + YamlContract.Request yamlContractRequest = yamlContract.request; + if (yamlContractRequest != null) { + dslContract.request((dslContractRequest) -> { + mapRequestMethod(yamlContractRequest, dslContractRequest); + mapRequestUrl(yamlContractRequest, dslContractRequest); + mapRequestUrlPath(yamlContract, dslContractRequest); + mapRequestHeaders(yamlContractRequest, dslContractRequest); + mapRequestCookies(yamlContractRequest, dslContractRequest); + mapRequestBody(yamlContractRequest, dslContractRequest); + mapRequestMultiPart(yamlContractRequest, dslContractRequest); + mapRequestBodyMatchers(yamlContractRequest, dslContractRequest); + }); + } + } + + private void mapRequestMethod(YamlContract.Request yamlContractRequest, Request dslContractRequest) { + if (yamlContractRequest.method != null) { + dslContractRequest.method(yamlContractRequest.method); + } + } + + private void mapRequestUrl(YamlContract.Request yamlContractRequest, Request dslContractRequest) { + String yamlContractRequestUrl = yamlContractRequest.url; + if (yamlContractRequestUrl != null) { + YamlContract.KeyValueMatcher yamlContractRequestMatchersUrl = Optional + .ofNullable(yamlContractRequest.matchers).map(matchers -> matchers.url).orElse(null); + dslContractRequest.url(urlValue(yamlContractRequestUrl, yamlContractRequestMatchersUrl), (url) -> { + if (yamlContractRequest.queryParameters != null) { + url.queryParameters( + (queryParameters -> yamlContractRequest.queryParameters.forEach((key, value) -> { + if (value instanceof List) { + ((List) value).forEach(v -> queryParameters.parameter(key, v)); + } + else { + queryParameters.parameter(key, value); + } + }))); + } + }); + } + } + + private void mapRequestUrlPath(YamlContract yamlContract, Request dslContractRequest) { + String yamlContractRequestUrlPath = yamlContract.request.urlPath; + if (yamlContractRequestUrlPath != null) { + YamlContract.KeyValueMatcher yamlContractRequestMatchersUrl = Optional + .ofNullable(yamlContract.request.matchers).map(matchers -> matchers.url).orElse(null); + dslContractRequest.urlPath(urlValue(yamlContractRequestUrlPath, yamlContractRequestMatchersUrl), (url) -> { + if (yamlContract.request.queryParameters != null) { + url.queryParameters( + (queryParameters -> yamlContract.request.queryParameters.forEach((key, value) -> { + if (value instanceof List) { + ((List) value).forEach( + v -> queryParameters.parameter(key, queryParamValue(yamlContract, key, v))); + } + else { + queryParameters.parameter(key, queryParamValue(yamlContract, key, value)); + } + }))); + } + }); + } + } + + private void mapRequestHeaders(YamlContract.Request yamlContractRequest, Request dslContractRequest) { + Map yamlContractRequestHeaders = yamlContractRequest.headers; + if (MapUtils.isNotEmpty(yamlContractRequestHeaders)) { + dslContractRequest.headers((headers) -> yamlContractRequestHeaders.forEach((key, value) -> { + List matchers = yamlContractRequest.matchers.headers.stream() + .filter((header) -> header.key.equals(key)).collect(Collectors.toList()); + matchers.forEach(matcher -> { + if (value instanceof List) { + ((List) value) + .forEach(v -> headers.header(key, clientValue(v, matcher, key).getClientValue())); + } + else { + headers.header(key, new DslProperty<>(clientValue(value, matcher, key).getClientValue(), + serverValue(value, matcher))); + } + }); + if (matchers != null) { + headers.header(key, value); + } + })); + } + } + + private void mapRequestCookies(YamlContract.Request yamlContractRequest, Request dslContractRequest) { + Map yamlContractRequestCookies = yamlContractRequest.cookies; + if (MapUtils.isNotEmpty(yamlContractRequestCookies)) { + dslContractRequest.cookies((cookies) -> yamlContractRequestCookies.forEach((key, value) -> { + YamlContract.KeyValueMatcher matcher = yamlContractRequest.matchers.cookies.stream() + .filter(cookie -> cookie.key.equals(key)).findFirst().orElse(null); + cookies.cookie(key, clientValue(value, matcher, key)); + })); + } + } + + private void mapRequestBody(YamlContract.Request yamlContractRequest, Request dslContractRequest) { + if (yamlContractRequest.body != null) { + dslContractRequest.body(yamlContractRequest.body); + } + if (yamlContractRequest.bodyFromFile != null) { + dslContractRequest.body(file(yamlContractRequest.bodyFromFile)); + } + if (yamlContractRequest.bodyFromFileAsBytes != null) { + dslContractRequest.body(dslContractRequest.fileAsBytes(yamlContractRequest.bodyFromFileAsBytes)); + } + } + + private void mapRequestMultiPart(YamlContract.Request yamlContractRequest, Request dslContractRequest) { + if (yamlContractRequest.multipart != null) { + Map multipartMap = new HashMap<>(); + yamlContractRequest.multipart.params.forEach((paramKey, paramValue) -> { + YamlContract.KeyValueMatcher matcher = yamlContractRequest.matchers.multipart.params.stream() + .filter((param) -> param.key.equals(paramKey)).findFirst().orElse(null); + Object value = paramValue; + if (matcher != null) { + value = matcher.regex != null ? Pattern.compile(matcher.regex) + : predefinedToPattern(matcher.predefined); + } + multipartMap.put(paramKey, new DslProperty<>(value, paramValue)); + }); + yamlContractRequest.multipart.named.forEach(namedParam -> { + YamlContract.MultipartNamedStubMatcher matcher = yamlContractRequest.matchers.multipart.named.stream() + .filter((stubMatcher) -> stubMatcher.paramName.equals(namedParam.paramName)).findFirst() + .orElse(null); + Object fileNameValue = namedParam.fileName; + Object fileContentValue = namedParam.fileContent; + String fileContentAsBytes = namedParam.fileContentAsBytes; + String fileContentFromFileAsBytes = namedParam.fileContentFromFileAsBytes; + String contentTypeCommand = namedParam.contentTypeCommand; + String fileContentCommand = namedParam.fileContentCommand; + String fileNameCommand = namedParam.fileNameCommand; + Object contentTypeValue = namedParam.contentType; + if (matcher != null && matcher.fileName != null) { + fileNameValue = matcher.fileName.regex != null ? Pattern.compile(matcher.fileName.regex) + : predefinedToPattern(matcher.fileName.predefined); + } + if (matcher != null && matcher.fileContent != null) { + fileContentValue = matcher.fileContent.regex != null ? Pattern.compile(matcher.fileContent.regex) + : predefinedToPattern(matcher.fileContent.predefined); + } + if (matcher != null && matcher.contentType != null) { + contentTypeValue = matcher.contentType.regex != null ? Pattern.compile(matcher.contentType.regex) + : predefinedToPattern(matcher.contentType.predefined); + } + multipartMap.put(namedParam.paramName, new NamedProperty( + new DslProperty<>(fileNameValue, + fileNameCommand != null ? new ExecutionProperty(fileNameCommand) : namedParam.fileName), + new DslProperty<>(fileContentValue, + namedParam.fileContent != null ? namedParam.fileContent + : fileContentFromFileAsBytes != null + ? dslContractRequest.fileAsBytes(namedParam.fileContentFromFileAsBytes) + : fileContentAsBytes != null ? fileContentAsBytes.getBytes() + : new ExecutionProperty(fileContentCommand)), + new DslProperty<>(contentTypeValue, contentTypeCommand != null + ? new ExecutionProperty(contentTypeCommand) : namedParam.contentType))); + }); + dslContractRequest.multipart(multipartMap); + } + + } + + private void mapRequestBodyMatchers(YamlContract.Request yamlContractRequest, Request dslContractRequest) { + dslContractRequest.bodyMatchers((bodyMatchers) -> Optional.ofNullable(yamlContractRequest.matchers) + .map(stubMatchers -> stubMatchers.body).ifPresent(stubMatchers -> stubMatchers.forEach(stubMatcher -> { + ContentType contentType = evaluateClientSideContentType( + yamlHeadersToContractHeaders( + Optional.ofNullable(yamlContractRequest.headers).orElse(new HashMap<>())), + Optional.ofNullable(yamlContractRequest.body).orElse(null)); + MatchingTypeValue value = null; + switch (stubMatcher.type) { + case by_date: + value = bodyMatchers.byDate(); + break; + case by_time: + value = bodyMatchers.byTime(); + break; + case by_timestamp: + value = bodyMatchers.byTimestamp(); + break; + case by_regex: + String regex = stubMatcher.value; + if (stubMatcher.predefined != null) { + regex = predefinedToPattern(stubMatcher.predefined).pattern(); + } + value = bodyMatchers.byRegex(regex); + break; + case by_equality: + value = bodyMatchers.byEquality(); + break; + case by_type: + value = bodyMatchers.byType(matchingTypeValueHolder -> { + if (stubMatcher.minOccurrence != null) { + matchingTypeValueHolder.minOccurrence(stubMatcher.minOccurrence); + } + if (stubMatcher.maxOccurrence != null) { + matchingTypeValueHolder.maxOccurrence(stubMatcher.maxOccurrence); + } + }); + break; + case by_null: + // do nothing + break; + default: + throw new UnsupportedOperationException("The type [" + stubMatcher.type + "] is" + + " unsupported.Hint:If you 're using remember to pass "); + } + if (value != null) { + if (XML == contentType) { + bodyMatchers.xPath(stubMatcher.path, value); + } + else { + bodyMatchers.jsonPath(stubMatcher.path, value); + } + } + }))); + } + + private void mapResponse(YamlContract yamlContract, Contract dslContract) { + YamlContract.Response yamlContractResponse = yamlContract.response; + if (yamlContractResponse != null) { + dslContract.response(dslContractResponse -> { + mapResponseStatus(yamlContractResponse, dslContractResponse); + mapResponseHeaders(yamlContractResponse, dslContractResponse); + mapResponseCookies(yamlContractResponse, dslContractResponse); + mapResponseBody(yamlContractResponse, dslContractResponse); + mapResponseAsync(yamlContractResponse, dslContractResponse); + mapResponseFixedDelayMilliseconds(yamlContractResponse, dslContractResponse); + mapResponseBodyMatchers(yamlContractResponse, dslContractResponse); + }); + } + + } + + private void mapResponseStatus(YamlContract.Response yamlContractResponse, Response dslContractResponse) { + dslContractResponse.status(yamlContractResponse.status); + } + + private void mapResponseHeaders(YamlContract.Response yamlContractResponse, Response dslContractResponse) { + dslContractResponse.headers(headers -> Optional.ofNullable(yamlContractResponse.headers) + .ifPresent(yamlContractResponseHeaders -> yamlContractResponseHeaders.forEach((key, value) -> { + YamlContract.TestHeaderMatcher matcher = yamlContractResponse.matchers.headers.stream() + .filter(h -> h.key.equals(key)).findFirst().orElse(null); + + if (value instanceof List) { + ((List) value).forEach(v -> { + Object serverValue = serverValue(v, matcher, key); + headers.header(key, new DslProperty<>(v, serverValue)); + }); + } + else { + Object serverValue = serverValue(value, matcher, key); + headers.header(key, new DslProperty<>(value, serverValue)); + } + }))); + } + + private void mapResponseCookies(YamlContract.Response yamlContractResponse, Response dslContractResponse) { + if (yamlContractResponse.cookies != null) { + dslContractResponse.cookies(cookies -> yamlContractResponse.cookies.forEach((key, value) -> { + YamlContract.TestCookieMatcher matcher = yamlContractResponse.matchers.cookies.stream() + .filter(testCookieMatcher -> testCookieMatcher.key.equals(key)).findFirst().orElse(null); + DslProperty cookieValue = serverCookieValue(value, matcher, key); + cookies.cookie(key, cookieValue); + })); + } + } + + private void mapResponseBody(YamlContract.Response yamlContractResponse, Response dslContractResponse) { + if (yamlContractResponse.body != null) { + YamlContract.BodyTestMatcher bodyTestMatcher = Optional.ofNullable(yamlContractResponse.matchers) + .map(testMatchers -> testMatchers.body) + .flatMap( + bodyTestMatchers -> bodyTestMatchers.stream() + .filter(m -> m.path == null && (m.type == YamlContract.TestMatcherType.by_regex + || m.type == YamlContract.TestMatcherType.by_command)) + .findFirst()) + .orElse(null); + if (bodyTestMatcher != null) { + dslContractResponse.body(new DslProperty<>(yamlContractResponse.body, + bodyTestMatcher.type == YamlContract.TestMatcherType.by_regex + ? Pattern.compile(bodyTestMatcher.value) + : new ExecutionProperty(bodyTestMatcher.value))); + } + else { + dslContractResponse.body(yamlContractResponse.body); + } + } + if (yamlContractResponse.bodyFromFile != null) { + dslContractResponse.body(file(yamlContractResponse.bodyFromFile)); + } + if (yamlContractResponse.bodyFromFileAsBytes != null) { + dslContractResponse.body(dslContractResponse.fileAsBytes(yamlContractResponse.bodyFromFileAsBytes)); + } + } + + private void mapResponseAsync(YamlContract.Response yamlContractResponse, Response dslContractResponse) { + if (yamlContractResponse.async != null && yamlContractResponse.async) { + dslContractResponse.async(); + } + } + + private void mapResponseFixedDelayMilliseconds(YamlContract.Response yamlContractResponse, + Response dslContractResponse) { + if (yamlContractResponse.fixedDelayMilliseconds != null) { + dslContractResponse.async(); + dslContractResponse.fixedDelayMilliseconds(yamlContractResponse.fixedDelayMilliseconds); + } + } + + private void mapResponseBodyMatchers(YamlContract.Response yamlContractResponse, Response dslContractResponse) { + dslContractResponse.bodyMatchers(bodyMatchers -> Optional.ofNullable(yamlContractResponse.matchers) + .map(yamlContractResponseTestMatchers -> yamlContractResponseTestMatchers.body) + .ifPresent(yamlContractBodyTestMatchers -> yamlContractBodyTestMatchers + .forEach(yamlContractBodyTestMatcher -> { + ContentType contentType = evaluateClientSideContentType( + yamlHeadersToContractHeaders(yamlContractResponse.headers), + yamlContractResponse.body); + MatchingTypeValue value; + switch (yamlContractBodyTestMatcher.type) { + case by_date: + value = bodyMatchers.byDate(); + break; + case by_time: + value = bodyMatchers.byTime(); + break; + case by_timestamp: + value = bodyMatchers.byTimestamp(); + break; + case by_regex: + String regex = yamlContractBodyTestMatcher.value; + if (yamlContractBodyTestMatcher.predefined != null) { + regex = predefinedToPattern(yamlContractBodyTestMatcher.predefined).pattern(); + } + value = bodyMatchers.byRegex(regex); + break; + case by_equality: + value = bodyMatchers.byEquality(); + break; + case by_type: + value = bodyMatchers.byType(v -> { + if (yamlContractBodyTestMatcher.minOccurrence != null) { + v.minOccurrence(yamlContractBodyTestMatcher.minOccurrence); + } + if (yamlContractBodyTestMatcher.maxOccurrence != null) { + v.maxOccurrence(yamlContractBodyTestMatcher.maxOccurrence); + } + }); + break; + case by_command: + value = bodyMatchers.byCommand(yamlContractBodyTestMatcher.value); + break; + case by_null: + value = bodyMatchers.byNull(); + break; + default: + throw new UnsupportedOperationException("The type [" + yamlContractBodyTestMatcher.type + + "] is unsupported. " + + "Hint: If you're using remember to pass < type:by_regex > "); + } + if (yamlContractBodyTestMatcher.path != null) { + if (XML == contentType) { + bodyMatchers.xPath(yamlContractBodyTestMatcher.path, value); + } + else { + bodyMatchers.jsonPath(yamlContractBodyTestMatcher.path, value); + } + } + }))); + } + + private void mapOutput(YamlContract yamlContract, Contract dslContract) { + YamlContract.OutputMessage yamlContractOutputMessage = yamlContract.outputMessage; + if (yamlContract.outputMessage != null) { + dslContract.outputMessage((dslContractOutputMessage) -> { + mapOutputAssertThat(yamlContractOutputMessage, dslContractOutputMessage); + mapOutputSentTo(yamlContractOutputMessage, dslContractOutputMessage); + mapOutputMessageHeaders(yamlContractOutputMessage, dslContractOutputMessage); + mapOutputBody(yamlContractOutputMessage, dslContractOutputMessage); + mapOutputBodyMatchers(yamlContractOutputMessage, dslContractOutputMessage); + }); + } + } + + private void mapOutputBodyMatchers(YamlContract.OutputMessage yamlContractOutputMessage, + OutputMessage dslContractOutputMessage) { + if (yamlContractOutputMessage.matchers != null) { + dslContractOutputMessage.bodyMatchers( + dslContractOutputMessageBodyMatchers -> Optional.ofNullable(yamlContractOutputMessage.matchers.body) + .ifPresent(yamlContractBodyTestMatchers -> yamlContractBodyTestMatchers + .forEach(yamlContractBodyTestMatcher -> { + ContentType contentType = evaluateClientSideContentType( + yamlHeadersToContractHeaders(yamlContractOutputMessage.headers), + yamlContractOutputMessage.body); + MatchingTypeValue value; + switch (yamlContractBodyTestMatcher.type) { + case by_date: + value = dslContractOutputMessageBodyMatchers.byDate(); + break; + case by_time: + value = dslContractOutputMessageBodyMatchers.byTime(); + break; + case by_timestamp: + value = dslContractOutputMessageBodyMatchers.byTimestamp(); + break; + case by_regex: + String regex = yamlContractBodyTestMatcher.value; + if (yamlContractBodyTestMatcher.predefined != null) { + regex = predefinedToPattern(yamlContractBodyTestMatcher.predefined) + .pattern(); + } + value = dslContractOutputMessageBodyMatchers.byRegex(regex); + break; + case by_equality: + value = dslContractOutputMessageBodyMatchers.byEquality(); + break; + case by_type: + value = dslContractOutputMessageBodyMatchers.byType(v -> { + if (yamlContractBodyTestMatcher.minOccurrence != null) { + v.minOccurrence(yamlContractBodyTestMatcher.minOccurrence); + } + if (yamlContractBodyTestMatcher.maxOccurrence != null) { + v.maxOccurrence(yamlContractBodyTestMatcher.maxOccurrence); + } + }); + break; + case by_command: + value = dslContractOutputMessageBodyMatchers + .byCommand(yamlContractBodyTestMatcher.value); + break; + case by_null: + value = dslContractOutputMessageBodyMatchers.byNull(); + break; + default: + throw new UnsupportedOperationException("The type " + "[" + + yamlContractBodyTestMatcher.type + "] is unsupported. Hint: If " + + "you're using remember to pass < type:by_regex > "); + } + if (XML == contentType) { + dslContractOutputMessageBodyMatchers.xPath(yamlContractBodyTestMatcher.path, + value); + } + else { + dslContractOutputMessageBodyMatchers + .jsonPath(yamlContractBodyTestMatcher.path, value); + } + }))); + } + } + + private void mapOutputMessageHeaders(YamlContract.OutputMessage yamlContractOutputMessage, + OutputMessage dslContractOutputMessage) { + dslContractOutputMessage.headers(dslContractOutputMessageHeaders -> Optional + .ofNullable(yamlContractOutputMessage).map(yamlContractOutput -> yamlContractOutput.headers).ifPresent( + yamlContractOutputMessageHeaders -> yamlContractOutputMessageHeaders.forEach((key, value) -> { + YamlContract.TestHeaderMatcher matcher = Optional + .ofNullable(yamlContractOutputMessage.matchers) + .map(yamlContractOutputMatchers -> yamlContractOutputMatchers.headers) + .flatMap(yamlContractOutputMatchersHeaders -> yamlContractOutputMatchersHeaders + .stream() + .filter(yamlContractOutputMatchersHeader -> yamlContractOutputMatchersHeader.key + .equals(key)) + .findFirst()) + .orElse(null); + Object serverValue = serverValue(value, matcher, key); + dslContractOutputMessageHeaders.header(key, new DslProperty<>(value, serverValue)); + }))); + } + + private void mapOutputBody(YamlContract.OutputMessage yamlContractOutputMessage, + OutputMessage dslContractOutputMessage) { + if (yamlContractOutputMessage.body != null) { + dslContractOutputMessage.body(yamlContractOutputMessage.body); + } + if (yamlContractOutputMessage.bodyFromFile != null) { + dslContractOutputMessage.body(file(yamlContractOutputMessage.bodyFromFile)); + } + if (yamlContractOutputMessage.bodyFromFileAsBytes != null) { + dslContractOutputMessage + .body(dslContractOutputMessage.fileAsBytes(yamlContractOutputMessage.bodyFromFileAsBytes)); + } + } + + private void mapOutputSentTo(YamlContract.OutputMessage yamlContractOutputMessage, + OutputMessage dslContractOutputMessage) { + if (yamlContractOutputMessage.sentTo != null) { + dslContractOutputMessage.sentTo(yamlContractOutputMessage.sentTo); + } + } + + private void mapOutputAssertThat(YamlContract.OutputMessage yamlContractOutputMessage, + OutputMessage dslContractOutputMessage) { + if (yamlContractOutputMessage.assertThat != null) { + dslContractOutputMessage.assertThat(yamlContractOutputMessage.assertThat); + } + } + + private void mapInput(YamlContract yamlContract, Contract dslContract) { + YamlContract.Input yamlContractInput = yamlContract.input; + if (yamlContractInput != null) { + dslContract.input(dslContractInput -> { + mapInputMessageFrom(yamlContractInput, dslContractInput); + mapInputAssertThat(yamlContractInput, dslContractInput); + mapInputTriggeredBy(yamlContractInput, dslContractInput); + mapInputMessageHeaders(yamlContractInput, dslContractInput); + mapInputMessageBody(yamlContractInput, dslContractInput); + mapInputBodyMatchers(yamlContractInput, dslContractInput); + }); + } + + } + + private void mapInputMessageFrom(YamlContract.Input yamlContractInput, Input dslContractInput) { + if (yamlContractInput.messageFrom != null) { + dslContractInput.messageFrom(yamlContractInput.messageFrom); + } + } + + private void mapInputAssertThat(YamlContract.Input yamlContractInput, Input dslContractInput) { + if (yamlContractInput.assertThat != null) { + dslContractInput.assertThat(yamlContractInput.assertThat); + } + } + + private void mapInputTriggeredBy(YamlContract.Input yamlContractInput, Input dslContractInput) { + if (yamlContractInput.triggeredBy != null) { + dslContractInput.triggeredBy(yamlContractInput.triggeredBy); + } + } + + private void mapInputMessageHeaders(YamlContract.Input yamlContractInput, Input dslContractInput) { + dslContractInput + .messageHeaders(dslContractMessageHeaders -> Optional.ofNullable(yamlContractInput.messageHeaders) + .ifPresent(yamlContractMessageHeaders -> yamlContractMessageHeaders.forEach((key, value) -> { + YamlContract.KeyValueMatcher matcher = Optional.ofNullable(yamlContractInput.matchers) + .map(yamlContractInputMatchers -> yamlContractInputMatchers.headers) + .flatMap(yamlContractInputMatchersHeaders -> yamlContractInputMatchersHeaders + .stream() + .filter(yamlContractInputMatchersHeader -> yamlContractInputMatchersHeader.key + .equals(key)) + .findFirst()) + .orElse(null); + dslContractMessageHeaders.header(key, clientValue(value, matcher, key)); + }))); + } + + private void mapInputMessageBody(YamlContract.Input yamlContractInput, Input dslContractInput) { + if (yamlContractInput.messageBody != null) { + dslContractInput.messageBody(yamlContractInput.messageBody); + } + if (yamlContractInput.messageBodyFromFile != null) { + dslContractInput.messageBody(file(yamlContractInput.messageBodyFromFile)); + } + if (yamlContractInput.messageBodyFromFileAsBytes != null) { + dslContractInput.messageBody(dslContractInput.fileAsBytes(yamlContractInput.messageBodyFromFileAsBytes)); + } + } + + private void mapInputBodyMatchers(YamlContract.Input yamlContractInput, Input dslContractInput) { + dslContractInput + .bodyMatchers(dslContractInputBodyMatchers -> Optional.ofNullable(yamlContractInput.matchers.body) + .ifPresent(yamlContractBodyStubMatchers -> yamlContractBodyStubMatchers + .forEach(yamlContractBodyStubMatcher -> { + ContentType contentType = evaluateClientSideContentType( + yamlHeadersToContractHeaders( + Optional.ofNullable(yamlContractInput.messageHeaders) + .orElse(new HashMap<>())), + Optional.ofNullable(yamlContractInput.messageBody).orElse(null)); + MatchingTypeValue value; + switch (yamlContractBodyStubMatcher.type) { + case by_date: + value = dslContractInputBodyMatchers.byDate(); + break; + case by_time: + value = dslContractInputBodyMatchers.byTime(); + break; + case by_timestamp: + value = dslContractInputBodyMatchers.byTimestamp(); + break; + case by_regex: + String regex = yamlContractBodyStubMatcher.value; + if (yamlContractBodyStubMatcher.predefined != null) { + regex = predefinedToPattern(yamlContractBodyStubMatcher.predefined) + .pattern(); + } + value = dslContractInputBodyMatchers.byRegex(regex); + break; + case by_equality: + value = dslContractInputBodyMatchers.byEquality(); + break; + default: + throw new UnsupportedOperationException("The type " + "[" + + yamlContractBodyStubMatcher.type + "] is unsupported. " + + "Hint: If you're using remember to pass < type:by_regex > "); + } + if (XML == contentType) { + dslContractInputBodyMatchers.xPath(yamlContractBodyStubMatcher.path, value); + } + else { + dslContractInputBodyMatchers.jsonPath(yamlContractBodyStubMatcher.path, value); + } + }))); + } + + private Headers yamlHeadersToContractHeaders(Map headers) { + Set
convertedHeaders = headers.keySet().stream() + .map(header -> Header.build(header, headers.get(header))).collect(toSet()); + Headers contractHeaders = new Headers(); + contractHeaders.headers(convertedHeaders); + return contractHeaders; + } + + protected DslProperty urlValue(String url, YamlContract.KeyValueMatcher urlMatcher) { + if (urlMatcher != null) { + if (urlMatcher.command != null) { + return new DslProperty(url, new ExecutionProperty(urlMatcher.command)); + } + return new DslProperty<>(urlMatcher.regex != null ? Pattern.compile(urlMatcher.regex) + : urlMatcher.predefined != null ? predefinedToPattern(urlMatcher.predefined) : url, url); + } + return new DslProperty<>(url); + } + + protected List convert(ObjectMapper mapper, Object o) { + try { + return Arrays.asList(mapper.convertValue(o, YamlContract[].class)); + } + catch (IllegalArgumentException e) { + return Collections.singletonList(mapper.convertValue(o, YamlContract.class)); + } + } + + protected Object serverValue(Object value, YamlContract.TestHeaderMatcher matcher, String key) { + Object serverValue = value; + if (matcher != null && matcher.regex != null) { + serverValue = Pattern.compile(matcher.regex); + Pattern pattern = (Pattern) serverValue; + assertPatternMatched(pattern, value, key); + } + else if (matcher != null && matcher.predefined != null) { + Pattern pattern = predefinedToPattern(matcher.predefined); + serverValue = pattern; + assertPatternMatched(pattern, value, key); + } + else if (matcher != null && matcher.command != null) { + serverValue = new ExecutionProperty(matcher.command); + } + return serverValue; + } + + protected DslProperty serverCookieValue(Object value, YamlContract.TestCookieMatcher matcher, String key) { + Object serverValue = value; + if (matcher != null && matcher.regex != null) { + serverValue = Pattern.compile(matcher.regex); + Pattern pattern = (Pattern) serverValue; + assertPatternMatched(pattern, value, key); + } + else if (matcher != null && matcher.predefined != null) { + Pattern pattern = predefinedToPattern(matcher.predefined); + serverValue = pattern; + assertPatternMatched(pattern, value, key); + } + else if (matcher != null && matcher.command != null) { + return new DslProperty<>(new ExecutionProperty(matcher.command), value); + } + return new DslProperty<>(value, serverValue); + } + + protected DslProperty clientValue(Object value, YamlContract.KeyValueMatcher matcher, String key) { + Object clientValue = value instanceof DslProperty ? ((DslProperty) value).getClientValue() : value; + if (matcher != null && matcher.regex != null) { + clientValue = Pattern.compile(matcher.regex); + Pattern pattern = (Pattern) clientValue; + assertPatternMatched(pattern, value, key); + } + else if (matcher != null && matcher.predefined != null) { + Pattern pattern = predefinedToPattern(matcher.predefined); + clientValue = pattern; + assertPatternMatched(pattern, value, key); + } + else if (matcher != null && matcher.command != null) { + return new DslProperty<>(value, new ExecutionProperty(matcher.command)); + } + return new DslProperty<>(clientValue, value); + } + + protected Object queryParamValue(YamlContract yamlContract, String key, Object value) { + Request request = new Request(); + YamlContract.QueryParameterMatcher matcher = yamlContract.request.matchers.queryParameters.stream() + .filter(queryParameter -> queryParameter.key.equals(key)).findFirst().orElse(null); + if (matcher == null) { + return value; + } + switch (matcher.type) { + case equal_to: + return new DslProperty<>(request.equalTo(matcher.value), value); + case containing: + return new DslProperty<>(request.containing(matcher.value), value); + case matching: + return new DslProperty<>(request.matching(matcher.value), value); + case not_matching: + return new DslProperty<>(request.notMatching(matcher.value), value); + case equal_to_json: + return new DslProperty<>(request.equalToJson(matcher.value), value); + case equal_to_xml: + return new DslProperty<>(request.equalToXml(matcher.value), value); + case absent: + return new DslProperty(request.absent(), null); + default: + throw new UnsupportedOperationException("The provided matching type [" + matcher + + "] is unsupported. Use on of " + Arrays.toString(YamlContract.MatchingType.values())); + } + } + + protected Object serverValue(Object value, YamlContract.KeyValueMatcher matcher) { + if (matcher != null && matcher.command != null) { + return new ExecutionProperty(matcher.command); + } + return value instanceof DslProperty ? ((DslProperty) value).getServerValue() : value; + } + + private void assertPatternMatched(Pattern pattern, Object value, String key) { + boolean matches = pattern.matcher(value.toString()).matches(); + if (!matches) { + throw new IllegalStateException("Broken headers! A header with " + "key [" + key + "] with value [" + value + + "] is not matched by regex [" + pattern.pattern() + "]"); + } + } + + protected Pattern predefinedToPattern(YamlContract.PredefinedRegex predefinedRegex) { + switch (predefinedRegex) { + case only_alpha_unicode: + return RegexPatterns.onlyAlphaUnicode().getPattern(); + case number: + return RegexPatterns.number().getPattern(); + case any_double: + return RegexPatterns.aDouble().getPattern(); + case any_boolean: + return RegexPatterns.anyBoolean().getPattern(); + case ip_address: + return RegexPatterns.ipAddress().getPattern(); + case hostname: + return RegexPatterns.hostname().getPattern(); + case email: + return RegexPatterns.email().getPattern(); + case url: + return RegexPatterns.url().getPattern(); + case uuid: + return RegexPatterns.uuid().getPattern(); + case iso_date: + return RegexPatterns.isoDate().getPattern(); + case iso_date_time: + return RegexPatterns.isoDateTime().getPattern(); + case iso_time: + return RegexPatterns.isoTime().getPattern(); + case iso_8601_with_offset: + return RegexPatterns.iso8601WithOffset().getPattern(); + case non_empty: + return RegexPatterns.nonEmpty().getPattern(); + case non_blank: + return RegexPatterns.nonBlank().getPattern(); + default: + throw new UnsupportedOperationException("The predefined regex [" + predefinedRegex + + "] is unsupported. Use one of " + Arrays.toString(YamlContract.PredefinedRegex.values())); + } + } + + protected String file(String relativePath) { + URL resource = Thread.currentThread().getContextClassLoader().getResource(relativePath); + if (resource == null) { + throw new IllegalStateException("File [\"+relativePath+\"] is not present"); + } + try { + return String.join("\n", Files.readAllLines(Paths.get(resource.toURI()))); + } + catch (URISyntaxException | IOException e) { + throw new IllegalStateException("File [" + relativePath + "] syntax is incorrect"); + } + } + + protected static ClassLoader updatedClassLoader(File rootFolder, ClassLoader classLoader) { + try { + ClassLoader urlCl = URLClassLoader.newInstance(new URL[] { rootFolder.toURI().toURL() }, classLoader); + Thread.currentThread().setContextClassLoader(urlCl); + return urlCl; + } + catch (MalformedURLException e) { + throw new IllegalStateException("Root folder [" + rootFolder + "] URL is incorrect"); + } + } + +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.java new file mode 100644 index 0000000000..0b986debda --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.java @@ -0,0 +1,215 @@ +/* + * Copyright 2013-2020 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 + * + * https://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.verifier.dsl.wiremock; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; +import groovy.lang.GString; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.ContractTemplate; +import org.springframework.cloud.contract.spec.internal.DslProperty; +import org.springframework.cloud.contract.spec.internal.FromFileProperty; +import org.springframework.cloud.contract.spec.internal.Headers; +import org.springframework.cloud.contract.verifier.template.HandlebarsTemplateProcessor; +import org.springframework.cloud.contract.verifier.template.TemplateProcessor; +import org.springframework.cloud.contract.verifier.util.ContentType; +import org.springframework.cloud.contract.verifier.util.ContentUtils; +import org.springframework.cloud.contract.verifier.util.MapConverter; + +import static org.springframework.cloud.contract.verifier.util.ContentType.UNKNOWN; +import static org.springframework.cloud.contract.verifier.util.ContentUtils.extractValue; +import static org.springframework.cloud.contract.verifier.util.ContentUtils.getClientContentType; +import static org.springframework.cloud.contract.verifier.util.MapConverter.transformValues; + +/** + * Common abstraction over WireMock Request / Response conversion implementations. + * + * @since 1.0.0 + */ +abstract class BaseWireMockStubStrategy { + + private static final String WRAPPER = "UNQUOTE_ME"; + + protected final TemplateProcessor processor; + + protected final ContractTemplate template; + + protected final Contract contract; + + protected BaseWireMockStubStrategy(Contract contract) { + this.processor = templateProcessor(); + this.template = contractTemplate(); + this.contract = contract; + } + + private TemplateProcessor templateProcessor() { + return new HandlebarsTemplateProcessor(); + } + + private ContractTemplate contractTemplate() { + return new HandlebarsTemplateProcessor(); + } + + /** + * @return the stub side values from the object + */ + protected Object getStubSideValue(Object object) { + return MapConverter.getStubSideValues(object); + } + + /** + * For the given {@link ContentType} returns the String version of the body. + */ + String parseBody(Object value, ContentType contentType) { + return parseBody(value.toString(), contentType); + } + + /** + * Return body as String from file. + */ + String parseBody(FromFileProperty value, ContentType contentType) { + return value.asString(); + } + + /** + * For the given {@link ContentType} returns the Boolean version of the body. + */ + String parseBody(Boolean value, ContentType contentType) { + return value.toString(); + } + + /** + * For the given {@link ContentType} returns the String version of the body. + */ + String parseBody(Map map, ContentType contentType) { + Object transformedMap = MapConverter.getStubSideValues(map); + transformedMap = transformMapIfRequestPresent(transformedMap); + String json = toJson(transformedMap); + // the space is important cause at the end of the json body you also have a } + // you can't have 4 } next to each other + String unquotedJson = json.replace('"' + WRAPPER, "").replace(WRAPPER + '"', " "); + String unescapedJson = unquotedJson.replace("\\/", "/"); + return parseBody(unescapedJson, contentType); + } + + private Object transformMapIfRequestPresent(Object transformedMap) { + Object requestBody = contract.getRequest().getBody(); + if (requestBody == null) { + return transformedMap; + } + String testSideBody = toJson(MapConverter.getTestSideValues(requestBody)); + DocumentContext context = JsonPath.parse(testSideBody); + return processEntriesForTemplating(transformedMap, context); + } + + private Object processEntriesForTemplating(Object transformedMap, DocumentContext context) { + return transformValues(transformedMap, (val) -> { + if (val instanceof String && processor.containsJsonPathTemplateEntry((String) val)) { + String jsonPath = processor.jsonPathFromTemplateEntry((String) val); + if (jsonPath == null) { + return val; + } + Object value = context.read(jsonPath); + if (value instanceof String) { + return val; + } + return WRAPPER + val + WRAPPER; + } + else if (val instanceof String && processor.containsTemplateEntry((String) val) + && template.escapedBody().equals(val)) { + return template.escapedBody(); + } + return val; + }); + } + + /** + * For the given {@link ContentType} returns the String version of the body. + */ + String parseBody(List list, ContentType contentType) { + final List result = new ArrayList<>(); + list.forEach(l -> { + if (l instanceof Map) { + result.add(MapConverter.getStubSideValues(l)); + } + else if (l instanceof List) { + result.add(parseBody((List) l, contentType)); + } + else { + result.add(parseBody(l, contentType)); + } + }); + return parseBody(toJson(result), contentType); + } + + /** + * For the given {@link ContentType} returns the String version of the body. + */ + String parseBody(GString value, ContentType contentType) { + Object processedValue = extractValue(value, contentType, + (o) -> o instanceof DslProperty ? ((DslProperty) o).getClientValue() : o); + if (processedValue instanceof GString) { + return parseBody(processedValue.toString(), contentType); + } + return parseBody(processedValue, contentType); + } + + /** + * For the given {@link ContentType} returns the String version of the body. + */ + String parseBody(String value, ContentType contentType) { + return value; + } + + private static String toJson(Object value) { + try { + if (value instanceof Map) { + Object convertedMap = MapConverter.transformValues(value, + (v) -> v instanceof GString ? ((GString) v).toString() : v); + String jsonOutput = new ObjectMapper().writeValueAsString(convertedMap); + return jsonOutput.replaceAll("\\\\\\\\\\\\", "\\\\"); + } + return new ObjectMapper().writeValueAsString(value); + } + catch (JsonProcessingException e) { + throw new IllegalArgumentException("The current object [" + value + "] could not be serialized"); + } + } + + /** + * Attempts to guess the {@link ContentType} from body and headers. Returns + * {@link ContentType#UNKNOWN} if it fails to guess. + */ + protected ContentType tryToGetContentType(Object body, Headers headers) { + ContentType contentType = ContentUtils.recognizeContentTypeFromHeader(headers); + if (UNKNOWN == contentType) { + if (body == null) { + return UNKNOWN; + } + return getClientContentType(body); + } + return contentType; + } + +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/dsl/wiremock/SpringCloudContractRequestMatcher.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/dsl/wiremock/SpringCloudContractRequestMatcher.java index 61e3c44ddf..1242a2b927 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/dsl/wiremock/SpringCloudContractRequestMatcher.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/dsl/wiremock/SpringCloudContractRequestMatcher.java @@ -26,10 +26,10 @@ import com.github.tomakehurst.wiremock.http.Request; import com.github.tomakehurst.wiremock.matching.MatchResult; import com.github.tomakehurst.wiremock.matching.RequestMatcherExtension; import net.javacrumbs.jsonunit.assertj.JsonAssertions; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.assertj.core.api.Assertions; -import org.codehaus.plexus.util.StringUtils; import org.springframework.cloud.contract.verifier.converter.YamlContract; import org.springframework.cloud.contract.verifier.converter.YamlContractConverter; diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.java new file mode 100644 index 0000000000..386f4b0760 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.java @@ -0,0 +1,549 @@ +/* + * Copyright 2013-2020 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 + * + * https://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.verifier.dsl.wiremock; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.extension.Parameters; +import com.github.tomakehurst.wiremock.http.RequestMethod; +import com.github.tomakehurst.wiremock.matching.ContentPattern; +import com.github.tomakehurst.wiremock.matching.RequestPattern; +import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder; +import com.github.tomakehurst.wiremock.matching.StringValuePattern; +import com.github.tomakehurst.wiremock.matching.UrlPattern; +import groovy.lang.GString; +import org.apache.commons.text.StringEscapeUtils; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.internal.Body; +import org.springframework.cloud.contract.spec.internal.BodyMatcher; +import org.springframework.cloud.contract.spec.internal.BodyMatchers; +import org.springframework.cloud.contract.spec.internal.DslProperty; +import org.springframework.cloud.contract.spec.internal.FromFileProperty; +import org.springframework.cloud.contract.spec.internal.MatchingStrategy; +import org.springframework.cloud.contract.spec.internal.MatchingType; +import org.springframework.cloud.contract.spec.internal.NamedProperty; +import org.springframework.cloud.contract.spec.internal.OptionalProperty; +import org.springframework.cloud.contract.spec.internal.PathBodyMatcher; +import org.springframework.cloud.contract.spec.internal.QueryParameters; +import org.springframework.cloud.contract.spec.internal.RegexPatterns; +import org.springframework.cloud.contract.spec.internal.RegexProperty; +import org.springframework.cloud.contract.spec.internal.Request; +import org.springframework.cloud.contract.spec.internal.Url; +import org.springframework.cloud.contract.verifier.converter.YamlContract; +import org.springframework.cloud.contract.verifier.converter.YamlContractConverter; +import org.springframework.cloud.contract.verifier.dsl.ContractVerifierMetadata; +import org.springframework.cloud.contract.verifier.file.SingleContractMetadata; +import org.springframework.cloud.contract.verifier.util.ContentType; +import org.springframework.cloud.contract.verifier.util.ContentUtils; +import org.springframework.cloud.contract.verifier.util.JsonPaths; +import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter; +import org.springframework.cloud.contract.verifier.util.MapConverter; +import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable; +import org.springframework.cloud.contract.verifier.util.xml.XmlToXPathsConverter; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import static org.springframework.cloud.contract.spec.internal.MatchingStrategy.Type.BINARY_EQUAL_TO; +import static org.springframework.cloud.contract.spec.internal.MatchingType.EQUALITY; +import static org.springframework.cloud.contract.verifier.util.ContentType.FORM; +import static org.springframework.cloud.contract.verifier.util.ContentType.JSON; +import static org.springframework.cloud.contract.verifier.util.ContentUtils.getEqualsTypeFromContentType; +import static org.springframework.cloud.contract.verifier.util.RegexpBuilders.buildGStringRegexpForStubSide; +import static org.springframework.cloud.contract.verifier.util.RegexpBuilders.buildJSONRegexpMatch; +import static org.springframework.cloud.contract.verifier.util.xml.XmlToXPathsConverter.retrieveValue; + +/** + * Converts a {@link Request} into {@link RequestPattern}. + * + * @author Marcin Grzejszczak + * @author Tim Ysewyn + * @author Olga Maciaszek-Sharma + * @since 1.0.0 + */ +class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { + + private final Request request; + + private final ContentType contentType; + + WireMockRequestStubStrategy(Contract groovyDsl, SingleContractMetadata singleContractMetadata) { + super(groovyDsl); + this.request = groovyDsl.getRequest(); + this.contentType = contentType(singleContractMetadata); + } + + protected ContentType contentType(SingleContractMetadata singleContractMetadata) { + return singleContractMetadata.getEvaluatedInputStubContentType(); + } + + RequestPattern buildClientRequestContent() { + if (request == null) { + return null; + } + RequestPatternBuilder requestPatternBuilder = appendMethodAndUrl(); + appendCookies(requestPatternBuilder); + appendHeaders(requestPatternBuilder); + appendQueryParameters(requestPatternBuilder); + appendBody(requestPatternBuilder); + appendMultipart(requestPatternBuilder); + return requestPatternBuilder.build(); + } + + private void appendBody(RequestPatternBuilder requestPatternBuilder) { + if (contract.getMetadata().containsKey(ContractVerifierMetadata.METADATA_KEY)) { + ContractVerifierMetadata metadata = ContractVerifierMetadata.fromMetadata(contract.getMetadata()); + appendSpringCloudContractMatcher(metadata, requestPatternBuilder); + if (!StringUtils.hasLength(metadata.getTool())) { + doAppendBody(requestPatternBuilder); + } + } + else { + doAppendBody(requestPatternBuilder); + } + } + + private void appendSpringCloudContractMatcher(ContractVerifierMetadata metadata, + RequestPatternBuilder requestPatternBuilder) { + Parameters parameters = Parameters.one("tool", metadata.getTool() != null ? metadata.getTool() : "unknown"); + YamlContractConverter converter = new YamlContractConverter(); + List contracts = converter.convertTo(Collections.singleton(contract)); + Map store = converter.store(contracts); + parameters.put("contract", new String(store.entrySet().iterator().next().getValue())); + requestPatternBuilder.andMatching(SpringCloudContractRequestMatcher.NAME, parameters); + } + + private RequestPatternBuilder appendMethodAndUrl() { + if (request.getMethod() == null) { + return null; + } + RequestMethod requestMethod = RequestMethod.fromString( + Optional.ofNullable(request.getMethod().getClientValue()).map(c -> c.toString()).orElse(null)); + UrlPattern urlPattern = urlPattern(); + return RequestPatternBuilder.newRequestPattern(requestMethod, urlPattern); + } + + private void doAppendBody(RequestPatternBuilder requestPattern) { + if (request.getBody() == null) { + return; + } + boolean bodyHasMatchingStrategy = request.getBody().getClientValue() instanceof MatchingStrategy; + MatchingStrategy matchingStrategy = getMatchingStrategyFromBody(request.getBody()); + if (contentType == ContentType.JSON) { + Object clientSideBody = MapConverter.transformToClientValues(request.getBody()); + Object originalBody = Optional.ofNullable(matchingStrategy).map(DslProperty::getClientValue).orElse(null); + if (bodyHasMatchingStrategy) { + requestPattern.withRequestBody(convertToValuePattern(matchingStrategy)); + } + else if (clientSideBody instanceof Pattern || clientSideBody instanceof RegexProperty) { + requestPattern.withRequestBody( + convertToValuePattern(appendBodyRegexpMatchPattern(request.getBody(), contentType))); + } + else { + Object body = JsonToJsonPathsConverter.removeMatchingJsonPaths(originalBody, request.getBodyMatchers()); + JsonPaths values = JsonToJsonPathsConverter + .transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(body); + if ((values.isEmpty() && request.getBodyMatchers() != null && !request.getBodyMatchers().hasMatchers()) + || onlySizeAssertionsArePresent(values)) { + try { + requestPattern.withRequestBody(WireMock.equalToJson( + new ObjectMapper().writeValueAsString( + getMatchingStrategy(request.getBody().getClientValue()).getClientValue()), + false, false)); + } + catch (JsonProcessingException e) { + throw new IllegalArgumentException("The MatchingStrategy could not be serialized", e); + } + } + else { + values.stream().filter(v -> !v.assertsSize()).forEach(it -> requestPattern + .withRequestBody(WireMock.matchingJsonPath(it.jsonPath().replace("\\\\", "\\")))); + } + } + Optional.ofNullable(request.getBodyMatchers()).map(BodyMatchers::matchers) + .ifPresent(bodyMatchers -> bodyMatchers.forEach(bodyMatcher -> { + String newPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(bodyMatcher, + originalBody); + requestPattern.withRequestBody(WireMock.matchingJsonPath(newPath.replace("\\\\", "\\"))); + })); + } + else if (contentType == ContentType.XML) { + Object originalBody = Optional.ofNullable(matchingStrategy).map(DslProperty::getClientValue).orElse(null); + if (bodyHasMatchingStrategy) { + requestPattern.withRequestBody(convertToValuePattern(matchingStrategy)); + } + else { + Object body = XmlToXPathsConverter.removeMatchingXPaths(originalBody, request.getBodyMatchers()); + List byEqualityMatchersFromXml = XmlToXPathsConverter.mapToMatchers(body); + byEqualityMatchersFromXml.forEach( + bodyMatcher -> addWireMockStubMatchingSection(bodyMatcher, requestPattern, originalBody)); + } + Optional.ofNullable(request.getBodyMatchers()).map(BodyMatchers::matchers) + .ifPresent(bodyMatchers -> bodyMatchers.forEach( + bodyMatcher -> addWireMockStubMatchingSection(bodyMatcher, requestPattern, originalBody))); + } + else if (containsPattern(request.getBody())) { + requestPattern.withRequestBody(convertToValuePattern(appendBodyRegexpMatchPattern(request.getBody()))); + } + else { + requestBodyGuessedFromMatchingStrategy(requestPattern); + } + } + + private Object generateConcreteValue(Object originalBody) { + if (originalBody instanceof Pattern || originalBody instanceof RegexProperty) { + return new RegexProperty(originalBody).generate(); + } + return originalBody; + } + + private RequestPatternBuilder requestBodyGuessedFromMatchingStrategy(RequestPatternBuilder requestPattern) { + return requestPattern + .withRequestBody(convertToValuePattern(getMatchingStrategy(request.getBody().getClientValue()))); + } + + private static void addWireMockStubMatchingSection(BodyMatcher matcher, RequestPatternBuilder requestPattern, + Object body) { + Set matchingTypesUnsupportedForRequest = new HashSet<>( + Arrays.asList(MatchingType.NULL, MatchingType.COMMAND, MatchingType.TYPE)); + if (!(matcher instanceof PathBodyMatcher)) { + throw new IllegalArgumentException("Only jsonPath and XPath matchers can be processed."); + } + String retrievedValue = Optional.ofNullable(matcher.value()).map(Object::toString).orElseGet(() -> { + if (matchingTypesUnsupportedForRequest.contains(matcher.matchingType())) { + throw new IllegalArgumentException("Null, Command and Type matchers are not supported in requests."); + } + if (EQUALITY == matcher.matchingType()) { + return retrieveValue(matcher, body); + } + else { + return ""; + } + }); + PathBodyMatcher pathMatcher = (PathBodyMatcher) matcher; + requestPattern.withRequestBody( + WireMock.matchingXPath(pathMatcher.path(), XPathBodyMatcherToWireMockValuePatternConverter + .mapToPattern(pathMatcher.matchingType(), String.valueOf(retrievedValue)))); + } + + private boolean onlySizeAssertionsArePresent(JsonPaths values) { + return !CollectionUtils.isEmpty(values) + && (request.getBodyMatchers() == null || !request.getBodyMatchers().hasMatchers()) + && this.every(values.iterator(), MethodBufferingJsonVerifiable::assertsSize); + } + + private boolean every(Iterator self, Function function) { + while (self.hasNext()) { + if (!function.apply(self.next())) { + return false; + } + } + return true; + } + + private void appendMultipart(RequestPatternBuilder requestPattern) { + if (request.getMultipart() == null) { + return; + } + if (request.getMultipart().getClientValue() instanceof Map) { + List multipartPattern = ((Map) request.getMultipart() + .getClientValue()) + .entrySet().stream().map( + it -> it.getValue() instanceof NamedProperty + ? WireMock.matching(RegexPatterns.multipartFile(it.getKey(), + ((NamedProperty) it.getValue()).getName().getClientValue(), + ((NamedProperty) it.getValue()).getValue().getClientValue(), + Optional.ofNullable( + ((NamedProperty) it.getValue()).getContentType()) + .map(DslProperty::getClientValue).orElse(null))) + : WireMock.matching(RegexPatterns.multipartParam(it.getKey(), + MapConverter.getStubSideValuesForNonBody(it.getValue())))) + .collect(Collectors.toList()); + multipartPattern.forEach(requestPattern::withRequestBody); + + } + + } + + private void appendHeaders(RequestPatternBuilder requestPattern) { + if (request.getHeaders() != null) { + request.getHeaders().getEntries().forEach(header -> requestPattern.withHeader(header.getName(), + (StringValuePattern) convertToValuePattern(header.getClientValue()))); + } + } + + private void appendCookies(RequestPatternBuilder requestPattern) { + if (request.getCookies() == null) { + return; + } + request.getCookies().getEntries().forEach(cookie -> requestPattern.withCookie(cookie.getKey(), + (StringValuePattern) convertToValuePattern(cookie.getClientValue()))); + } + + private UrlPattern urlPattern() { + Object urlPath = urlPathOrUrlIfQueryPresent(); + if (urlPath != null) { + if (urlPath instanceof Pattern || urlPath instanceof RegexProperty) { + return WireMock.urlPathMatching((String) getStubSideValue(new RegexProperty(urlPath).pattern())); + } + else { + return WireMock.urlPathEqualTo((String) getStubSideValue(urlPath.toString())); + } + } + if (request.getUrl() == null) { + throw new IllegalStateException("URL is required!"); + } + Object url = getUrlIfGstring(request.getUrl().getClientValue()); + if (url instanceof Pattern || url instanceof RegexProperty) { + return WireMock.urlMatching(new RegexProperty(url).pattern()); + } + return WireMock.urlEqualTo(url.toString()); + } + + private Object urlPathOrUrlIfQueryPresent() { + Object urlPath = Optional.ofNullable(request).map(Request::getUrlPath).map(DslProperty::getClientValue) + .orElse(null); + Object queryParamsFromUrl = Optional.ofNullable(request).map(Request::getUrl).map(Url::getQueryParameters) + .map(QueryParameters::getParameters).orElse(null); + if (urlPath != null) { + return urlPath; + } + if (queryParamsFromUrl != null) { + return Optional.ofNullable(request).map(Request::getUrl).map(Url::getClientValue).orElse(null); + } + return null; + } + + private Object getUrlIfGstring(Object clientSide) { + if (clientSide instanceof GString) { + if (Arrays.stream(((GString) clientSide).getValues()).anyMatch(it -> { + Object value = getStubSideValue(it); + return value instanceof Pattern || value instanceof RegexProperty; + })) { + String string = getStubSideValue(clientSide).toString(); + return new RegexProperty(Pattern.compile(string)); + } + else { + return getStubSideValue(clientSide).toString(); + } + } + return clientSide; + } + + private void appendQueryParameters(RequestPatternBuilder requestPattern) { + QueryParameters queryParameters = Optional.ofNullable(request).map(Request::getUrlPath) + .map(Url::getQueryParameters).orElseGet(() -> Optional.ofNullable(request).map(Request::getUrl) + .map(Url::getQueryParameters).orElse(null)); + + Optional.ofNullable(queryParameters).map(QueryParameters::getParameters).ifPresent( + parameters -> parameters.forEach(parameter -> requestPattern.withQueryParam(parameter.getName(), + (StringValuePattern) convertToValuePattern(parameter.getClientValue())))); + } + + protected ContentPattern convertToValuePattern(Object object) { + if (object instanceof Pattern || object instanceof RegexProperty) { + return WireMock.matching(new RegexProperty(object).pattern()); + } + else if (object instanceof OptionalProperty) { + return WireMock.matching(((OptionalProperty) object).optionalPattern()); + } + else if (object instanceof MatchingStrategy) { + MatchingStrategy value = (MatchingStrategy) object; + switch (value.getType()) { + case NOT_MATCHING: + return WireMock.notMatching(value.getClientValue().toString()); + case ABSENT: + return WireMock.absent(); + case EQUAL_TO: + return WireMock.equalTo(clientBody(value.getClientValue(), contentType).toString()); + case CONTAINS: + return WireMock.containing(clientBody(value.getClientValue(), contentType).toString()); + case MATCHING: + return WireMock.matching(clientBody(value.getClientValue(), contentType).toString()); + case EQUAL_TO_JSON: + return WireMock.equalToJson(clientBody(value.getClientValue(), contentType).toString()); + case EQUAL_TO_XML: + return WireMock.equalToXml(clientBody(value.getClientValue(), contentType).toString()); + case BINARY_EQUAL_TO: + return WireMock.binaryEqualTo((byte[]) clientBody(value.getClientValue(), contentType)); + default: + throw new UnsupportedOperationException("Unknown matching strategy " + value.getType()); + } + } + else { + return WireMock.equalTo(clientBody(object, contentType).toString()); + } + } + + protected Object clientBody(Object bodyValue, ContentType contentType) { + if (FORM == contentType) { + if (bodyValue instanceof Map) { + // [a:3, b:4] == "a=3&b=4" + return ((Map) bodyValue).entrySet().stream() + .map(e -> StringEscapeUtils.unescapeEcmaScript(e.getKey().toString() + "=" + e.getValue())) + .collect(Collectors.joining("&")); + } + else if (bodyValue instanceof List) { + // ["a=3", "b=4"] == "a=3&b=4" + return ((List) bodyValue).stream().map(it -> StringEscapeUtils.unescapeEcmaScript(it.toString())) + .collect(Collectors.joining("&")); + } + } + else if (bodyValue instanceof FromFileProperty) { + return ((FromFileProperty) bodyValue).isByte() ? ((FromFileProperty) bodyValue).asBytes() + : ((FromFileProperty) bodyValue).asString(); + } + else if (JSON == contentType) { + return parseBody(bodyValue, contentType); + } + return bodyValue; + } + + private MatchingStrategy getMatchingStrategyFromBody(Body body) { + if (body == null) { + return null; + } + return getMatchingStrategy(body.getClientValue()); + } + + private MatchingStrategy getMatchingStrategy(Object bodyValue) { + if (bodyValue instanceof GString) { + return this.getMatchingStrategy((GString) bodyValue); + } + else if (bodyValue instanceof MatchingStrategy) { + return this.getMatchingStrategy((MatchingStrategy) bodyValue); + } + else if (bodyValue instanceof FromFileProperty) { + return this.getMatchingStrategy((FromFileProperty) bodyValue); + } + else { + return tryToFindMachingStrategy(bodyValue); + } + } + + private MatchingStrategy getMatchingStrategy(FromFileProperty bodyValue) { + return new MatchingStrategy(bodyValue, BINARY_EQUAL_TO); + } + + private MatchingStrategy getMatchingStrategy(MatchingStrategy matchingStrategy) { + return getMatchingStrategyIncludingContentType(matchingStrategy); + } + + private MatchingStrategy getMatchingStrategy(GString gString) { + if (gString == null) { + return new MatchingStrategy("", MatchingStrategy.Type.EQUAL_TO); + } + Object extractedValue = ContentUtils.extractValue(gString, + it -> it instanceof DslProperty ? ((DslProperty) it).getClientValue() : getStringFromGString(it)); + + Object value = getStringFromGString(extractedValue); + return getMatchingStrategy(value); + } + + private Object getStringFromGString(Object object) { + return object instanceof GString ? object.toString() : object; + } + + private MatchingStrategy tryToFindMachingStrategy(Object bodyValue) { + return new MatchingStrategy(MapConverter.transformToClientValues(bodyValue), + getEqualsTypeFromContentType(contentType)); + } + + private MatchingStrategy getMatchingStrategyIncludingContentType(MatchingStrategy matchingStrategy) { + MatchingStrategy.Type type = matchingStrategy.getType(); + Object value = matchingStrategy.getClientValue(); + ContentType contentType = ContentUtils.recognizeContentTypeFromMatchingStrategy(type); + if (contentType == ContentType.UNKNOWN && type == MatchingStrategy.Type.EQUAL_TO) { + contentType = ContentUtils.recognizeContentTypeFromContent(value); + type = getEqualsTypeFromContentType(contentType); + } + MatchingStrategy newMatchingStrategy; + if (value instanceof Map) { + newMatchingStrategy = new MatchingStrategy(parseBody((Map) value, contentType), type); + } + else if (value instanceof List) { + newMatchingStrategy = new MatchingStrategy(parseBody((List) value, contentType), type); + } + else if (value instanceof GString) { + newMatchingStrategy = new MatchingStrategy(parseBody((GString) value, contentType), type); + } + else { + newMatchingStrategy = new MatchingStrategy(parseBody(value, contentType), type); + } + return newMatchingStrategy; + } + + private MatchingStrategy appendBodyRegexpMatchPattern(Object value, ContentType contentType) { + Object clientValue = MapConverter.transformToClientValues(value); + switch (contentType) { + case JSON: + return new MatchingStrategy(buildJSONRegexpMatch(clientValue), MatchingStrategy.Type.MATCHING); + case UNKNOWN: + return new MatchingStrategy(buildGStringRegexpForStubSide(clientValue), MatchingStrategy.Type.MATCHING); + default: + throw new IllegalStateException(contentType.name() + " pattern matching is not implemented yet"); + } + } + + private MatchingStrategy appendBodyRegexpMatchPattern(Object value) { + return appendBodyRegexpMatchPattern(value, ContentType.UNKNOWN); + } + + private boolean containsPattern(Object o) { + if (o instanceof GString) { + return containsPattern(((GString) o).getValues()); + } + else if (o instanceof Map) { + return containsPattern(((Map) o).entrySet()); + } + else if (o instanceof Collection) { + List result = (List) ((Collection) o).stream().map(this::containsPattern) + .collect(Collectors.toList()); + return result.stream().reduce(false, (a, b) -> a || b); + } + else if (o instanceof Object[]) { + return containsPattern(Arrays.asList((Object[]) o)); + } + else if (o instanceof Map.Entry) { + return containsPattern(((Map.Entry) o).getValue()); + } + else if (o instanceof RegexProperty) { + return true; + } + else if (o instanceof DslProperty) { + return containsPattern(((DslProperty) o).getClientValue()); + } + else { + return o instanceof Pattern; + } + } + +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.java index ad811294a5..2d803f769b 100755 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.java @@ -19,13 +19,13 @@ package org.springframework.cloud.contract.verifier.dsl.wiremock; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.function.Function; import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; import com.github.tomakehurst.wiremock.extension.Extension; import com.github.tomakehurst.wiremock.http.HttpHeader; import com.github.tomakehurst.wiremock.http.HttpHeaders; import com.github.tomakehurst.wiremock.http.ResponseDefinition; -import groovy.lang.Closure; import groovy.lang.GString; import org.springframework.cloud.contract.spec.Contract; @@ -122,9 +122,9 @@ class WireMockResponseStubStrategy extends BaseWireMockStubStrategy { } } - Closure parsingClosureForContentType() { - return contractMetadata.getDefinedOutputStubContentType().contains("/stream") ? Closure.IDENTITY - : MapConverter.JSON_PARSING_CLOSURE; + Function parsingClosureForContentType() { + return contractMetadata.getDefinedOutputStubContentType().contains("/stream") ? MapConverter.IDENTITY + : MapConverter.JSON_PARSING_FUNCTION; } private void appendResponseDelayTime(ResponseDefinitionBuilder builder) { diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/file/ContractFileScanner.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/file/ContractFileScanner.java new file mode 100644 index 0000000000..44bdaad5a5 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/file/ContractFileScanner.java @@ -0,0 +1,442 @@ +/* + * Copyright 2013-2020 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 + * + * https://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.verifier.file; + +import java.io.File; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.codehaus.plexus.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import wiremock.com.google.common.collect.HashMultiset; +import wiremock.com.google.common.collect.ListMultimap; +import wiremock.com.google.common.collect.Multimap; +import wiremock.com.google.common.collect.Multiset; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.ContractConverter; +import org.springframework.cloud.contract.verifier.converter.YamlContractConverter; +import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; + +/** + * Scans the provided file path for the DSLs. There's a possibility to provide inclusion + * and exclusion filters. + * + * @author Jakub Kubrynski, codearte.io + * @author Stessy Delcroix + * @since 1.0.0 + */ +public class ContractFileScanner { + + private static final Logger LOG = LoggerFactory.getLogger(ContractFileScanner.class); + + private static final String OS_NAME = System.getProperty("os.name"); + + private static final String OS_NAME_WINDOWS_PREFIX = "Windows"; + + protected static final boolean IS_OS_WINDOWS = getOSMatchesName(OS_NAME_WINDOWS_PREFIX); + + private static final String MATCH_PREFIX = "glob:"; + + private static final Pattern SCENARIO_STEP_FILENAME_PATTERN = Pattern.compile("[0-9]+_.*"); + + private final File baseDir; + + private final Set excludeMatchers; + + private final Set ignoreMatchers; + + private final Set includeMatchers; + + private final String includeMatcher; + + public ContractFileScanner(File baseDir, Set excluded, Set ignored, Set included, + String includeMatcher) { + this.baseDir = baseDir; + this.excludeMatchers = processPatterns(excluded != null ? excluded : Collections.emptySet()); + this.ignoreMatchers = processPatterns(ignored != null ? ignored : Collections.emptySet()); + this.includeMatchers = processPatterns(included != null ? included : Collections.emptySet()); + this.includeMatcher = includeMatcher != null ? includeMatcher : ""; + } + + private Set processPatterns(Set patterns) { + FileSystem fileSystem = FileSystems.getDefault(); + Set pathMatchers = new HashSet<>(); + for (String pattern : patterns) { + String syntaxAndPattern = MATCH_PREFIX + "**" + File.separator + pattern; + // FIXME: This looks strange, need to be checked on windows + if (IS_OS_WINDOWS) { + syntaxAndPattern = syntaxAndPattern.replace("\\", "\\\\"); + } + pathMatchers.add(fileSystem.getPathMatcher(syntaxAndPattern)); + } + return pathMatchers; + } + + /** + * @return for a map of paths for which a list of matching contracts has been found + * @deprecated use the {@link ContractFileScanner#findContractsRecursively} version + */ + @Deprecated + public ListMultimap findContracts() { + MultiValueMap contracts = findContractsRecursively(); + return new ListMultimap() { + @Override + public List get(Path key) { + return contracts.get(key); + } + + @Override + public List removeAll(Object key) { + return contracts.remove(key); + } + + @Override + public List replaceValues(Path key, Iterable values) { + return contracts.put(key, asList(values)); + } + + List asList(Iterable self) { + if (self instanceof List) { + return (List) self; + } + else { + return toList(self.iterator()); + } + } + + private List toList(Iterator self) { + List answer = new ArrayList<>(); + while (self.hasNext()) { + answer.add(self.next()); + } + return answer; + } + + @Override + public Map> asMap() { + return contracts.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + public int size() { + return contracts.size(); + } + + @Override + public boolean isEmpty() { + return contracts.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return contracts.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return contracts.entrySet() + .stream() + .anyMatch(it -> it.getValue().contains(value)); + } + + @Override + public boolean containsEntry(Object key, Object value) { + return contracts.entrySet() + .stream() + .anyMatch(it -> it.getKey().equals(key) && it.getValue().contains(value)); + } + + @Override + public boolean put(Path key, ContractMetadata value) { + contracts.add(key, value); + return true; + } + + @Override + public boolean remove(Object key, Object value) { + return contracts.getOrDefault(key, new ArrayList<>()).remove(value); + } + + @Override + public boolean putAll(Path key, Iterable values) { + return contracts.getOrDefault(key, new ArrayList<>()) + .addAll(StreamSupport.stream(values.spliterator(), false) + .collect(Collectors.toList())); + } + + @Override + public boolean putAll(Multimap multimap) { + multimap.entries().forEach(it -> contracts.add(it.getKey(), it.getValue())); + return true; + } + + @Override + public void clear() { + contracts.clear(); + } + + @Override + public Set keySet() { + return contracts.keySet(); + } + + @Override + public Multiset keys() { + return HashMultiset.create(contracts.keySet()); + } + + @Override + public Collection values() { + return contracts.values().stream().flatMap(Collection::stream).collect(Collectors.toList()); + } + + @Override + public Collection> entries() { + Collection> entries = new LinkedList<>(); + contracts.forEach((path, list) -> list + .forEach(c -> entries.add(new AbstractMap.SimpleEntry<>(path, c)))); + return entries; + } + }; + } + + public MultiValueMap findContractsRecursively() { + MultiValueMap result = CollectionUtils.toMultiValueMap(new LinkedHashMap<>()); + appendRecursively(baseDir, result); + return result; + } + + /** + * We iterate over found contracts, filter out those that should be excluded and try + * to convert via pluggable Contract Converters any possible contracts. + */ + private void appendRecursively(File baseDir, MultiValueMap result) { + List converters = convertersWithYml(); + if (LOG.isTraceEnabled()) { + LOG.trace("Found the following contract converters ${converters}"); + } + File[] files = baseDir.listFiles(); + if (files == null) { + return; + } + Arrays.sort(files); + for (int i = 0; i < files.length; i++) { + File file = files[i]; + boolean excluded = matchesPattern(file, excludeMatchers); + if (!excluded) { + boolean contractFile = isContractFile(file); + boolean included = StringUtils.isEmpty(includeMatcher) || file.getAbsolutePath() + .matches(includeMatcher); + included = !CollectionUtils.isEmpty(includeMatchers) ? matchesPattern(file, includeMatchers) : included; + if (contractFile && included) { + addContractToTestGeneration(result, files, file, i, + ContractVerifierDslConverter.convertAsCollection(baseDir, file)); + } + if (!contractFile && included) { + addContractToTestGeneration(converters, result, files, file, i); + } + else { + appendRecursively(file, result); + if (LOG.isDebugEnabled()) { + LOG.debug( + "File [$file] is ignored. Is a contract file? [$contractFile]. Should be included by pattern? [$included]"); + } + } + } + else { + if (LOG.isDebugEnabled()) { + LOG.debug("File [$file] is ignored. Should be excluded? [$excluded]"); + } + } + } + } + + protected List convertersWithYml() { + List converters = converters(); + converters.add(ContractVerifierDslConverter.INSTANCE); + converters.add(YamlContractConverter.INSTANCE); + return converters; + } + + protected List converters() { + return SpringFactoriesLoader.loadFactories(ContractConverter.class, null); + } + + private void addContractToTestGeneration(List converters, + MultiValueMap result, File[] files, File file, int index) { + boolean converted = false; + if (!file.isDirectory()) { + for (ContractConverter converter : converters) { + Collection contracts = tryConvert(converter, file); + if (contracts != null) { + addContractToTestGeneration(result, files, file, index, contracts); + converted = true; + break; + } + } + } + if (!converted) { + appendRecursively(file, result); + if (LOG.isDebugEnabled()) { + LOG.debug( + "File [$file] wasn't ignored but no converter was applicable. The file is a directory [${file.isDirectory()}]"); + } + } + } + + private Collection tryConvert(ContractConverter converter, File file) { + boolean accepted = converter.isAccepted(file); + if (!accepted) { + return null; + } + try { + return converter.convertFrom(file); + } + catch (Exception e) { + throw new IllegalStateException("Failed to convert file [" + file + "]", e); + } + } + + private void addContractToTestGeneration(MultiValueMap result, File[] files, File file, + int index, Collection convertedContract) { + Path path = file.toPath(); + Integer order = null; + if (hasScenarioFilenamePattern(path)) { + order = index; + } + Path parent = file.getParentFile().toPath(); + ContractMetadata metadata = new ContractMetadata(path, matchesPattern(file, ignoreMatchers), files.length, + order, convertedContract); + if (LOG.isDebugEnabled()) { + LOG.debug("Creating a contract entry for path [" + path + "] and metadata [" + metadata + "]"); + } + result.add(parent, metadata); + } + + private boolean hasScenarioFilenamePattern(Path path) { + return SCENARIO_STEP_FILENAME_PATTERN.matcher(path.getFileName().toString()).matches(); + } + + private boolean matchesPattern(File file, Set matchers) { + for (PathMatcher matcher : matchers) { + if (matcher.matches(file.toPath())) { + return true; + } + LOG.debug("Path [{}] doesn't match the pattern [{}]", file.toPath(), matcher); + } + return false; + } + + private boolean isContractFile(File file) { + return file.isFile() && ContractVerifierDslConverter.INSTANCE.isAccepted(file); + } + + /** + * Decides if the operating system matches. + * @param osNamePrefix the prefix for the os name + * @return true if matches, or false if not or can't determine + */ + private static boolean getOSMatchesName(final String osNamePrefix) { + return isOSNameMatch(OS_NAME, osNamePrefix); + } + + /** + * Decides if the operating system matches. + *

+ * This method is package private instead of private to support unit test invocation. + *

+ * @param osName the actual OS name + * @param osNamePrefix the prefix for the expected OS name + * @return true if matches, or false if not or can't determine + */ + private static boolean isOSNameMatch(final String osName, final String osNamePrefix) { + if (osName == null) { + return false; + } + return osName.startsWith(osNamePrefix); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private File baseDir; + private Set excluded; + private Set ignored; + private Set included = Collections.emptySet(); + private String includeMatcher = ""; + + public Builder baseDir(File baseDir) { + this.baseDir = baseDir; + return this; + } + + public Builder excluded(Set excluded) { + this.excluded = excluded; + return this; + } + + public Builder ignored(Set ignored) { + this.ignored = ignored; + return this; + } + + public Builder included(Set included) { + this.included = included; + return this; + } + + public Builder includeMatcher(String includeMatcher) { + this.includeMatcher = includeMatcher; + return this; + } + + public ContractFileScanner build() { + return new ContractFileScanner(this.baseDir, + this.excluded, + this.ignored, + this.included, + this.includeMatcher); + } + } + +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverter.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverter.java new file mode 100644 index 0000000000..f483eb3091 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/ContractVerifierDslConverter.java @@ -0,0 +1,283 @@ +/* + * Copyright 2013-2020 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 + * + * https://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.verifier.util; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import groovy.lang.GroovyShell; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.cloud.contract.spec.Contract; +import org.springframework.cloud.contract.spec.ContractConverter; +import org.springframework.cloud.function.compiler.java.CompilationResult; +import org.springframework.cloud.function.compiler.java.RuntimeJavaCompiler; +import org.springframework.util.ObjectUtils; + +/** + * Converts a String or a Groovy or Java file into a {@link Contract}. + * + * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma + * @author Stessy Delcroix + * @since 1.0.0 + */ +public class ContractVerifierDslConverter implements ContractConverter> { + + private static final Logger LOG = LoggerFactory.getLogger(ContractVerifierDslConverter.class); + + /** + * {@link ContractVerifierDslConverter} instance. + */ + public static final ContractVerifierDslConverter INSTANCE = new ContractVerifierDslConverter(); + + private static final Pattern PACKAGE_PATTERN = Pattern.compile(".*package (.+?);.+?", Pattern.DOTALL); + + private static final Pattern CLASS_PATTERN = Pattern.compile(".+?class (.+?)( |\\{).+?", Pattern.DOTALL); + + private static final RuntimeJavaCompiler COMPILER = new RuntimeJavaCompiler(); + + /** + * @deprecated - use + * {@link ContractVerifierDslConverter#convertAsCollection(java.io.File, java.lang.String)} + */ + @Deprecated + public static Collection convertAsCollection(String dsl) { + try { + Object object = groovyShell().evaluate(dsl); + return listOfContracts(object); + } + catch (DslParseException e) { + throw e; + } + catch (Exception e) { + LOG.error("Exception occurred while trying to evaluate the contract", e); + throw new DslParseException(e); + } + } + + public static Collection convertAsCollection(File rootFolder, String dsl) { + ClassLoader classLoader = ContractVerifierDslConverter.class.getClassLoader(); + try { + ClassLoader urlCl = updatedClassLoader(rootFolder, classLoader); + Object object = groovyShell(urlCl, rootFolder).evaluate(dsl); + return listOfContracts(object); + } + catch (DslParseException e) { + throw e; + } + catch (Exception e) { + LOG.error("Exception occurred while trying to evaluate the contract", e); + throw new DslParseException(e); + } + finally { + Thread.currentThread().setContextClassLoader(classLoader); + } + } + + public static Collection convertAsCollection(File dsl) { + return convertAsCollection(dsl.getParentFile(), dsl); + } + + public static Collection convertAsCollection(File rootFolder, File dsl) { + ClassLoader classLoader = ContractVerifierDslConverter.class.getClassLoader(); + try { + ClassLoader urlCl = updatedClassLoader(rootFolder, classLoader); + Object object = toObject(urlCl, rootFolder, dsl); + return listOfContracts(dsl, object); + } + catch (DslParseException e) { + throw e; + } + catch (Exception e) { + LOG.error("Exception occurred while trying to evaluate the contract at path [" + dsl.getPath() + "]", e); + throw new DslParseException(e); + } + finally { + Thread.currentThread().setContextClassLoader(classLoader); + } + } + + private static ClassLoader updatedClassLoader(File rootFolder, ClassLoader classLoader) { + ClassLoader urlCl; + try { + urlCl = URLClassLoader.newInstance( + Collections.singletonList(rootFolder.toURI().toURL()).toArray(new URL[0]), classLoader); + } + catch (MalformedURLException e) { + LOG.error("Exception occurred while trying to construct the URL from the root folder at path [" + + rootFolder.getPath() + "]", e); + throw new DslParseException(e); + } + updateTheThreadClassLoader(urlCl); + return urlCl; + } + + private static void updateTheThreadClassLoader(ClassLoader urlCl) { + Thread.currentThread().setContextClassLoader(urlCl); + } + + private static GroovyShell groovyShell() { + CompilerConfiguration compilerConfiguration = new CompilerConfiguration(); + compilerConfiguration.setSourceEncoding("UTF-8"); + return new GroovyShell(ContractVerifierDslConverter.class.getClassLoader(), compilerConfiguration); + } + + private static Object toObject(ClassLoader cl, File rootFolder, File dsl) throws IOException { + if (isJava(dsl)) { + try { + return parseJavaFile(dsl); + } + catch (Exception ex) { + if (LOG.isWarnEnabled()) { + LOG.warn("Exception occurred while trying to parse the file [" + dsl + + "] as a contract. Will not parse it.", ex); + } + return null; + } + } + return groovyShell(cl, rootFolder).evaluate(dsl); + } + + private static Object parseJavaFile(File dsl) throws IllegalAccessException, InvocationTargetException, + InstantiationException, IOException, NoSuchMethodException { + Constructor constructor = classConstructor(dsl); + Object newInstance = constructor.newInstance(); + if (!(newInstance instanceof Supplier)) { + if (LOG.isDebugEnabled()) { + LOG.debug("The class [" + dsl + "] is not instance of Supplier. Will not parse it as a contract"); + } + return null; + } + Supplier supplier = (Supplier) newInstance; + return supplier.get(); + } + + private static Constructor classConstructor(File dsl) + throws IllegalAccessException, IOException, NoSuchMethodException { + String classText = Files.lines(Paths.get(dsl.getAbsolutePath())).collect(Collectors.joining("\n")); + String fqn = fqn(classText); + CompilationResult compilationResult = COMPILER.compile(fqn, classText); + if (!compilationResult.wasSuccessful()) { + throw new IllegalStateException("Exceptions occurred while trying to compile the file " + + compilationResult.getCompilationMessages()); + } + Class clazz = compilationResult.getCompiledClasses().stream().filter(it -> it.getName().equals(fqn)) + .findFirst().orElseThrow(() -> new IllegalStateException("Class with name [" + fqn + "] not found")); + Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor; + } + + private static boolean isJava(File dsl) { + return dsl.getName().endsWith(".java"); + } + + private static String fqn(String classText) throws IllegalAccessException { + Matcher packageMatcher = PACKAGE_PATTERN.matcher(classText); + String fqn = ""; + if (packageMatcher.matches()) { + fqn = packageMatcher.group(1) + "."; + } + Matcher classMatcher = CLASS_PATTERN.matcher(classText); + if (!classMatcher.matches()) { + throw new IllegalAccessException("Can't parse the class name"); + } + return fqn + classMatcher.group(1); + } + + private static GroovyShell groovyShell(ClassLoader cl, File rootFolder) { + CompilerConfiguration compilerConfiguration = new CompilerConfiguration(); + compilerConfiguration.setSourceEncoding("UTF-8"); + compilerConfiguration.setClasspathList(Collections.singletonList(rootFolder.getAbsolutePath())); + return new GroovyShell(cl, compilerConfiguration); + } + + private static Collection listOfContracts(Object object) { + if (object instanceof Collection) { + return (Collection) object; + } + else if (!(object instanceof Contract)) { + throw new DslParseException("Contract is not returning a Contract or list of Contracts"); + } + return Collections.singletonList((Contract) object); + } + + private static Collection listOfContracts(File file, Object object) { + if (object == null) { + return Collections.emptyList(); + } + else if (isACollectionOfContracts(object)) { + return withName(file, (Collection) object); + } + else if (!(object instanceof Contract)) { + throw new DslParseException("Contract is not returning a Contract or list of Contracts"); + } + return withName(file, Collections.singletonList((Contract) object)); + } + + private static boolean isACollectionOfContracts(Object object) { + return object instanceof Collection && ((Collection) object).stream().allMatch(it -> it instanceof Contract); + } + + private static Collection withName(File file, Collection contracts) { + AtomicInteger counter = new AtomicInteger(0); + return contracts.stream().peek(it -> { + if (contractNameEmpty(it)) { + it.name(NamesUtil.defaultContractName(file, contracts, counter.get())); + } + counter.getAndIncrement(); + }).collect(Collectors.toList()); + } + + private static boolean contractNameEmpty(Contract it) { + return it != null && ObjectUtils.isEmpty(it.getName()); + } + + @Override + public boolean isAccepted(File file) { + return file.getName().endsWith(".groovy") || file.getName().endsWith(".gvy") + || file.getName().endsWith(".java"); + } + + @Override + public Collection convertFrom(File file) { + return convertAsCollection(file); + } + + @Override + public Collection convertTo(Collection contract) { + return contract; + } + +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/MapConverter.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/MapConverter.java new file mode 100644 index 0000000000..8298b6277f --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/MapConverter.java @@ -0,0 +1,234 @@ +/* + * Copyright 2013-2020 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 + * + * https://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.verifier.util; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import groovy.json.JsonSlurper; +import groovy.lang.Closure; +import groovy.lang.GString; + +import org.springframework.cloud.contract.spec.internal.DslProperty; +import org.springframework.cloud.contract.spec.internal.FromFileProperty; +import org.springframework.cloud.contract.verifier.template.HandlebarsTemplateProcessor; +import org.springframework.cloud.contract.verifier.template.TemplateProcessor; + +/** + * Converts an object into either client or server side representation. Iterates over the + * structure of an object (depending on whether it's an iterable or a primitive type + * etc.), converts the {@link DslProperty} into their client / server representation and + * returns the result + * + * @author Marcin Grzejszczak + * @author Stessy Delcroix + * @since 1.1.0 + */ +public class MapConverter { + + private static final boolean STUB_SIDE = true; + + private static final boolean TEST_SIDE = false; + + /** + * Generic {@link Function} used to deserialize a json file. + */ + public static final Function JSON_PARSING_FUNCTION = (value) -> { + try { + return new ObjectMapper().readValue(value, Object.class); + } + catch (JsonProcessingException e) { + throw new IllegalArgumentException("The current json [" + value + "] could not be deserialized"); + } + }; + + /** + * Generic {@link Closure} used to deserialize a json file. + */ + public static final Closure JSON_PARSING_CLOSURE = new Closure(null) { + public Object doCall(Object it) { + return new JsonSlurper().parseText((String) it); + } + }; + + /** + * Function used to return its input argument. {@link Function#identity()} cannot be + * used as the return type is Function<T,T>, whilst this function return type is + * Function<T,R> + */ + public static final Function IDENTITY = (value) -> value; + + private final TemplateProcessor templateProcessor; + + MapConverter() { + this.templateProcessor = processor(); + } + + private TemplateProcessor processor() { + return new HandlebarsTemplateProcessor(); + } + + /** + * @return the object with client side values of + * {@link org.springframework.cloud.contract.spec.internal.DslProperty} + */ + public static Object transformToClientValues(Object value) { + return transformValues(value, (v) -> v instanceof DslProperty ? ((DslProperty) v).getClientValue() : v); + } + + public static Object transformValues(Object value, Function function) { + return transformValues(value, function, JSON_PARSING_FUNCTION); + } + + /** + * Iterates over the structure of the object and executes the function on each element + * of that structure. + * @return the transformed structure + */ + public static Object transformValues(Object value, Function function, + Function parsingFunction) { + if (value instanceof String) { + try { + Object parsed = parsingFunction.apply((String) value); + if (parsed instanceof Map) { + return convert((Map) parsed, function, parsingFunction); + } + else if (parsed instanceof List) { + return transformValues(parsed, function, parsingFunction); + } + } + catch (Exception ignore) { + } + return extractValue(value, function); + } + else if (value instanceof Map) { + return convert((Map) value, function, parsingFunction); + } + else if (value instanceof List) { + return ((List) value).stream().map((v) -> transformValues(v, function, parsingFunction)) + .collect(Collectors.toList()); + } + return transformValue(function, value, parsingFunction); + } + + /** + * Transforms a value with the given function. Needs to be protected, otherwise method + * access exception will occur at runtime. + */ + protected static Object transformValue(Function function, Object value, + Function parsingFunction) { + return extractValue(value, (val) -> { + Object newValue = function.apply(val); + if (newValue instanceof Map || newValue instanceof List || newValue instanceof String && val != null) { + return transformValues(newValue, function, parsingFunction); + } + return newValue; + }); + } + + private static Object extractValue(Object value, Function function) { + try { + return function.apply(value); + } + catch (Exception ignore) { + return value; + } + } + + private static Map convert(Map map, Function function, + Function parsingFunction) { + Map convertedMap = new LinkedHashMap<>(); + for (Map.Entry entry : map.entrySet()) { + convertedMap.put(entry.getKey(), transformValues(entry.getValue(), function, parsingFunction)); + } + return convertedMap; + } + + public static Object getClientOrServerSideValues(Object json, boolean clientSide) { + return getClientOrServerSideValues(json, clientSide, JSON_PARSING_FUNCTION); + } + + /** + * If {@code clientSide} is {@code true} returns the client side value for the + * provided object. + */ + public static Object getClientOrServerSideValues(Object json, boolean clientSide, + Function parsingFunction) { + return transformValues(json, val -> { + if (val instanceof DslProperty) { + DslProperty dslProperty = ((DslProperty) val); + return clientSide + ? getClientOrServerSideValues(dslProperty.getClientValue(), clientSide, parsingFunction) + : getClientOrServerSideValues(dslProperty.getServerValue(), clientSide, parsingFunction); + } + else if (val instanceof GString) { + ContentType type = new MapConverter().templateProcessor.containsJsonPathTemplateEntry( + ContentUtils.extractValueForGString((GString) val, ContentUtils.GET_TEST_SIDE).toString()) + ? ContentType.TEXT : null; + return ContentUtils.extractValue((GString) val, type, (v) -> { + if (v instanceof DslProperty) { + return clientSide + ? getClientOrServerSideValues(((DslProperty) v).getClientValue(), clientSide, + parsingFunction) + : getClientOrServerSideValues(((DslProperty) v).getServerValue(), clientSide, + parsingFunction); + } + return v; + }); + } + else if (val instanceof FromFileProperty) { + return ((FromFileProperty) val).isByte() ? ((FromFileProperty) val).asBytes() + : ((FromFileProperty) val).asString(); + } + return val; + }, parsingFunction); + } + + public static Object getStubSideValues(Object json) { + return getClientOrServerSideValues(json, STUB_SIDE, JSON_PARSING_FUNCTION); + } + + public static Object getStubSideValues(Object json, Function parsingClosure) { + return getClientOrServerSideValues(json, STUB_SIDE, parsingClosure); + } + + public static Object getTestSideValues(Object json) { + return getTestSideValues(json, JSON_PARSING_FUNCTION); + } + + public static Object getTestSideValues(Object json, Function parsingClosure) { + return getClientOrServerSideValues(json, TEST_SIDE, parsingClosure); + } + + public static Object getTestSideValuesForText(Object json) { + return getClientOrServerSideValues(json, TEST_SIDE, IDENTITY); + } + + public static Object getStubSideValuesForNonBody(Object object) { + return getClientOrServerSideValues(object, STUB_SIDE, IDENTITY); + } + + public static Object getTestSideValuesForNonBody(Object object) { + return getClientOrServerSideValues(object, TEST_SIDE, IDENTITY); + } + +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/RegexpBuilders.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/RegexpBuilders.java new file mode 100644 index 0000000000..e4190d1148 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/util/RegexpBuilders.java @@ -0,0 +1,169 @@ +/* + * Copyright 2013-2020 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 + * + * https://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.verifier.util; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import groovy.lang.GString; +import org.codehaus.groovy.runtime.GStringImpl; + +import org.springframework.cloud.contract.spec.internal.DslProperty; +import org.springframework.cloud.contract.spec.util.RegexpUtils; + +import static org.apache.commons.text.StringEscapeUtils.escapeJson; +import static org.springframework.cloud.contract.verifier.util.ContentType.JSON; +import static org.springframework.cloud.contract.verifier.util.ContentUtils.extractValue; + +/** + * Useful utility methods to work with regular expressions. + * + * @since 1.0.0 + */ +public final class RegexpBuilders { + + private final static String WS = "/\\s*/"; + + private static final Function, Object> CLIENT_VALUE_EXTRACTOR = DslProperty::getClientValue; + + private RegexpBuilders() { + } + + /** + * Converts the {@link Object} passed values into their stub side String + * representations. + */ + public static String buildGStringRegexpForStubSide(Object o) { + if (o instanceof DslProperty) { + return buildGStringRegexpForStubSide((DslProperty) o); + } + else if (o instanceof Pattern) { + return buildGStringRegexpForStubSide((Pattern) o); + } + else if (o instanceof GString) { + return buildGStringRegexpForStubSide((GString) o); + } + return escapeSpecialRegexChars(o.toString()); + } + + /** + * Converts the {@link GString} passed values into their stub side String + * representations. + */ + static String buildGStringRegexpForStubSide(GString gString) { + return new GStringImpl( + Stream.of(gString.getValues()).map(RegexpBuilders::buildGStringRegexpForStubSide).map(s -> (Object) s) + .toArray(), + Stream.of(gString.getStrings()).map(RegexpBuilders::escapeSpecialRegexChars).toArray(String[]::new)) + .toString(); + } + + /** + * Converts the {@link Pattern} passed values into their stub side String + * representations. + */ + static String buildGStringRegexpForStubSide(Pattern pattern) { + return pattern.pattern(); + } + + /** + * Converts the {@link org.springframework.cloud.contract.spec.internal.DslProperty} + * passed values into their stub side String representations. + */ + static String buildGStringRegexpForStubSide(DslProperty dslProperty) { + return buildGStringRegexpForStubSide(dslProperty.getClientValue()); + } + + /** + * Converts the {@link GString} passed values into their test side String + * representations. + */ + public static String buildGStringRegexpForTestSide(GString gString) { + return new GStringImpl( + Stream.of(gString.getValues()).map(RegexpBuilders::buildGStringRegexpForTestSide).map(s -> (Object) s) + .toArray(), + Stream.of(gString.getStrings()).map(RegexpBuilders::escapeSpecialRegexChars).toArray(String[]::new)) + .toString(); + } + + /** + * Converts the {@link Object} passed values into their test side String + * representations. + */ + public static String buildGStringRegexpForTestSide(Object o) { + return o.toString().replaceAll("\\\\", "\\\\\\\\"); + } + + public static String escapeSpecialRegexChars(String str) { + return RegexpUtils.escapeSpecialRegexChars(str); + } + + public static String buildJSONRegexpMatch(GString gString) { + return buildJSONRegexpMatch(extractValue(gString, JSON, CLIENT_VALUE_EXTRACTOR)); + } + + public static String buildJSONRegexpMatch(Map jsonMap) { + return WS + "\\{" + + jsonMap.entrySet().stream().map(RegexpBuilders::buildJSONRegexpMatch).collect(Collectors.joining(",")) + + "\\}" + WS; + } + + public static String buildJSONRegexpMatch(List jsonList) { + return WS + "\\[" + jsonList.stream().map(RegexpBuilders::buildJSONRegexpMatch).collect(Collectors.joining(",")) + + "\\]" + WS; + } + + /** + * Converts the map into String representation of regular expressions. + */ + public static String buildJSONRegexpMatch(Map.Entry entry) { + return buildJSONRegexpMatchString(escapeJson(entry.getKey())) + ":" + buildJSONRegexpMatch(entry.getValue()); + } + + /** + * Converts the object into String representation of regular expressions. + */ + public static String buildJSONRegexpMatch(Object value) { + return buildJSONRegexpMatchStringOptionalQuotes(escapeJson(value.toString())); + } + + /** + * Converts the pattern into String representation of regular expressions. + */ + public static String buildJSONRegexpMatch(Pattern pattern) { + return buildJSONRegexpMatchStringOptionalQuotes(pattern.pattern()); + } + + /** + * Converts the String into String representation of regular expressions. + */ + public static String buildJSONRegexpMatchString(String value) { + return WS + '"' + value + '"' + WS; + } + + /** + * Converts the String into an optional String representation of regular expressions. + */ + public static String buildJSONRegexpMatchStringOptionalQuotes(String value) { + return WS + "\"?" + value + "\"?" + WS; + } + +} diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/TestGeneratorTests.java b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/TestGeneratorTests.java index bed4de46aa..6cb3741a54 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/TestGeneratorTests.java +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/TestGeneratorTests.java @@ -55,7 +55,7 @@ public class TestGeneratorTests { r.status(r.OK()); }); }))); - ContractFileScanner scanner = new ContractFileScanner(null, null, null) { + ContractFileScanner scanner = new ContractFileScanner(null, null, null, null, null) { @Override public MultiValueMap findContractsRecursively() { return multimap; @@ -92,7 +92,7 @@ public class TestGeneratorTests { r.status(r.OK()); }); }))); - ContractFileScanner scanner = new ContractFileScanner(null, null, null) { + ContractFileScanner scanner = new ContractFileScanner(null, null, null, null, null) { @Override public MultiValueMap findContractsRecursively() { return multimap; diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/assertion/CollectionAssertSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/assertion/CollectionAssertSpec.groovy deleted file mode 100644 index 910b8c3721..0000000000 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/assertion/CollectionAssertSpec.groovy +++ /dev/null @@ -1,376 +0,0 @@ -/* - * Copyright 2013-2020 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 - * - * https://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.verifier.assertion - -import org.assertj.core.api.Assertions -import spock.lang.Specification - -/** - * @author Marcin Grzejszczak, Artem Ptushkin - */ -class CollectionAssertSpec extends Specification { - - def should_not_throw_an_exception_when_all_elements_match_regex() { - setup: - Collection collection = collection() - - when: - SpringCloudContractAssertions.assertThat(collection).allElementsMatch("[a-z]") - - then: - noExceptionThrown() - } - - def should_throw_an_exception_when_at_least_one_element_doesnt_match_regex() { - setup: - Collection collection = collection() - - when: - SpringCloudContractAssertions.assertThat(collection) - .allElementsMatch("[0-9]") - - then: - AssertionError e = thrown() - Assertions.assertThat(e).hasMessageContaining( - "The value doesn't match the regex <[0-9]>") - } - - def should_throw_an_exception_when_element_is_null() { - setup: - Collection collection = collectionWithNulls() - - when: - SpringCloudContractAssertions.assertThat(collection) - .allElementsMatch("[0-9]") - - then: - AssertionError e = thrown() - Assertions.assertThat(e).hasMessageContaining( - "The value doesn't match the regex <[0-9]>") - - } - - def should_throw_an_exception_when_collection_is_null() { - setup: - Collection collection = null - - when: - SpringCloudContractAssertions.assertThat(collection).allElementsMatch("foo") - - then: - AssertionError e = thrown() - Assertions.assertThat(e) - .hasMessageContaining("Expecting actual not to be null") - } - - def should_throw_an_exception_when_collection_is_empty() { - setup: - Collection collection = new ArrayList() - - when: - SpringCloudContractAssertions.assertThat(collection).allElementsMatch("foo") - - then: - AssertionError e = thrown() - Assertions.assertThat(e) - .hasMessageContaining("Expecting actual not to be empty") - } - - def should_not_throw_an_exception_when_flattened_size_is_greater_than_or_equal_to_provided_size() { - setup: - Collection collection = nestedCollection() - - when: - SpringCloudContractAssertions.assertThat(collection) - .hasFlattenedSizeGreaterThanOrEqualTo(0) - .hasFlattenedSizeGreaterThanOrEqualTo(4) - - then: - noExceptionThrown() - } - - def should_throw_an_exception_when_flattened_size_is_not_greater_than_or_equal_to_provided_size() { - setup: - Collection collection = nestedCollection() - - when: - SpringCloudContractAssertions.assertThat(collection) - .hasFlattenedSizeGreaterThanOrEqualTo(5) - - then: - AssertionError e = thrown() - Assertions.assertThat(e).hasMessageContaining( - "The flattened size <4> is not greater or equal to <5>") - } - - def should_throw_an_exception_when_collection_is_null_for_flattened_greater_than_or_equal() { - setup: - Collection collection = null - - when: - SpringCloudContractAssertions.assertThat(collection) - .hasFlattenedSizeGreaterThanOrEqualTo(1) - then: - AssertionError e = thrown() - Assertions.assertThat(e) - .hasMessageContaining("Expecting actual not to be null") - } - - def should_not_throw_an_exception_when_flattened_size_is_less_than_or_equal_to_provided_size() { - setup: - Collection collection = nestedCollection() - - when: - SpringCloudContractAssertions.assertThat(collection) - .hasFlattenedSizeLessThanOrEqualTo(5) - .hasFlattenedSizeLessThanOrEqualTo(4) - - then: - noExceptionThrown() - } - - def should_throw_an_exception_when_flattened_size_is_not_less_than_or_equal_to_provided_size() { - setup: - Collection collection = nestedCollection() - - when: - SpringCloudContractAssertions.assertThat(collection) - .hasFlattenedSizeLessThanOrEqualTo(1) - - then: - AssertionError e = thrown() - Assertions.assertThat(e).hasMessageContaining( - "The flattened size <4> is not less or equal to <1>") - } - - def should_throw_an_exception_when_collection_is_null_for_flattened_less_than_or_equal() { - setup: - Collection collection = null - - when: - SpringCloudContractAssertions.assertThat(collection) - .hasFlattenedSizeLessThanOrEqualTo(1) - - then: - AssertionError e = thrown() - Assertions.assertThat(e) - .hasMessageContaining("Expecting actual not to be null") - } - - def should_not_throw_an_exception_when_flattened_size_is_between_the_provided_sizes() { - setup: - Collection collection = nestedCollection() - - when: - SpringCloudContractAssertions.assertThat(collection).hasFlattenedSizeBetween(1, 5) - .hasFlattenedSizeBetween(4, 4) - - then: - noExceptionThrown() - } - - def should_throw_an_exception_when_flattened_size_is_not_between_the_provided_sizes() { - setup: - Collection collection = nestedCollection() - - when: - SpringCloudContractAssertions.assertThat(collection) - .hasFlattenedSizeBetween(5, 7) - - then: - AssertionError e = thrown() - Assertions.assertThat(e).hasMessageContaining( - "The flattened size <4> is not between <5> and <7>") - } - - def should_throw_an_exception_when_collection_is_null_for_flattened_between() { - setup: - Collection collection = null - - when: - SpringCloudContractAssertions.assertThat(collection) - .hasFlattenedSizeBetween(1, 2) - - then: - AssertionError e = thrown() - Assertions.assertThat(e) - .hasMessageContaining("Expecting actual not to be null") - } - - def should_not_throw_an_exception_when_size_is_greater_than_or_equal_to_provided_size() { - setup: - Collection collection = collection() - - when: - SpringCloudContractAssertions.assertThat(collection) - .hasSizeGreaterThanOrEqualTo(0).hasSizeGreaterThanOrEqualTo(3) - - then: - noExceptionThrown() - } - - def should_throw_an_exception_when_size_is_not_greater_than_or_equal_to_provided_size() { - setup: - Collection collection = collection() - - when: - SpringCloudContractAssertions.assertThat(collection) - .hasSizeGreaterThanOrEqualTo(5) - - then: - AssertionError e = thrown() - Assertions.assertThat(e) - .hasMessageContaining("The size <3> is not greater or equal to <5>") - } - - def should_throw_an_exception_when_collection_is_null_for_greater_than_or_equal() { - setup: - Collection collection = null - - when: - SpringCloudContractAssertions.assertThat(collection) - .hasSizeGreaterThanOrEqualTo(1) - then: - AssertionError e = thrown() - Assertions.assertThat(e) - .hasMessageContaining("Expecting actual not to be null") - } - - def should_not_throw_an_exception_when_size_is_less_than_or_equal_to_provided_size() { - setup: - Collection collection = collection() - - when: - SpringCloudContractAssertions.assertThat(collection).hasSizeLessThanOrEqualTo(4) - .hasSizeLessThanOrEqualTo(3) - - then: - noExceptionThrown() - } - - def should_throw_an_exception_when_size_is_not_less_than_or_equal_to_provided_size() { - setup: - Collection collection = collection() - - when: - SpringCloudContractAssertions.assertThat(collection) - .hasSizeLessThanOrEqualTo(1) - - then: - AssertionError e = thrown() - Assertions.assertThat(e) - .hasMessageContaining("The size <3> is not less or equal to <1>") - } - - def should_throw_an_exception_when_collection_is_null_for_less_than_or_equal() { - setup: - Collection collection = null - - when: - SpringCloudContractAssertions.assertThat(collection) - .hasSizeLessThanOrEqualTo(1) - - then: - AssertionError e = thrown() - Assertions.assertThat(e) - .hasMessageContaining("Expecting actual not to be null") - } - - def should_not_throw_an_exception_when_size_is_between_the_provided_sizes() { - setup: - Collection collection = collection() - - when: - SpringCloudContractAssertions.assertThat(collection).hasSizeBetween(1, 4) - .hasSizeBetween(3, 3) - - then: - noExceptionThrown() - } - - def should_throw_an_exception_when_size_is_not_between_the_provided_sizes() { - setup: - Collection collection = collection() - - when: - SpringCloudContractAssertions.assertThat(collection).hasSizeBetween(5, 7) - - then: - AssertionError e = thrown() - Assertions.assertThat(e) - .hasMessageContaining("The size <3> is not between <5> and <7>") - } - - def should_not_break_compilation_when_using_as() { - setup: - Collection collection = collection() - - when: - SpringCloudContractAssertions.assertThat(collection).as("for jsonpath x.y.z") - .hasSizeBetween(5, 7) - - then: - AssertionError e = thrown() - Assertions.assertThat(e).hasMessageContaining( - "[for jsonpath x.y.z] The size <3> is not between <5> and <7>") - } - - def should_throw_an_exception_when_collection_is_null_for_between() { - setup: - Collection collection = null - - when: - SpringCloudContractAssertions.assertThat(collection).hasSizeBetween( 1, 2) - - then: - AssertionError e = thrown() - Assertions.assertThat(e) - .hasMessageContaining("Expecting actual not to be null") - } - - Collection collection() { - List list = new ArrayList<>() - list.add("a") - list.add("b") - list.add("c") - return list - } - - Collection collectionWithNulls() { - List list = new ArrayList<>() - list.add(null) - return list - } - - Collection nestedCollection() { - List list = new ArrayList<>() - List list1 = new ArrayList<>() - Map map1 = new HashMap<>() - map1.put("a", "1") - map1.put("b", "2") - map1.put("c", "3") - List list2 = new ArrayList<>() - Map map2 = new HashMap<>() - map2.put("d", "4") - list.add(list1) - list.add(list2) - list1.add(map1) - list2.add(map2) - return list - } -} - diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy index 78e48d20e6..1492e62527 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategySpec.groovy @@ -16,6 +16,8 @@ package org.springframework.cloud.contract.verifier.dsl.wiremock +import java.util.function.Function + import groovy.json.JsonSlurper import spock.lang.Issue import spock.lang.Specification @@ -47,8 +49,8 @@ class WireMockResponseStubStrategySpec extends Specification { metadata.evaluatedOutputStubContentType >> ContentType.JSON def subject = new WireMockResponseStubStrategy(contract, metadata) { @Override - protected Closure parsingClosureForContentType() { - return MapConverter.JSON_PARSING_CLOSURE + Function parsingClosureForContentType() { + return MapConverter.JSON_PARSING_FUNCTION } } def content = subject.buildClientResponseContent() @@ -80,8 +82,8 @@ class WireMockResponseStubStrategySpec extends Specification { metadata.evaluatedOutputStubContentType >> ContentType.JSON def subject = new WireMockResponseStubStrategy(contract, metadata) { @Override - protected Closure parsingClosureForContentType() { - return MapConverter.JSON_PARSING_CLOSURE + Function parsingClosureForContentType() { + return MapConverter.JSON_PARSING_FUNCTION } } def content = subject.buildClientResponseContent() @@ -260,8 +262,8 @@ class WireMockResponseStubStrategySpec extends Specification { } @Override - Closure parsingClosureForContentType() { - return MapConverter.JSON_PARSING_CLOSURE + Function parsingClosureForContentType() { + return MapConverter.JSON_PARSING_FUNCTION } } response.buildClientResponseContent() diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerNewApiSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerNewApiSpec.groovy index 3ea0dd3af3..9a185af3d9 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerNewApiSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerNewApiSpec.groovy @@ -50,7 +50,7 @@ class ContractFileScannerNewApiSpec extends Specification { File baseDir = tmpFolder Set excluded = ["package/**"] as Set Set ignored = ["other/different/**"] as Set - ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored, [] as Set) + ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored, [] as Set, null) when: MultiValueMap result = scanner.findContractsRecursively() then: @@ -68,7 +68,7 @@ class ContractFileScannerNewApiSpec extends Specification { File baseDir = new File(this.getClass().getResource("/strange_[3.3.3]_directory").toURI()) Set excluded = ["foo/**"] as Set Set ignored = ["bar/**"] as Set - ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored, [] as Set) + ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored, [] as Set, null) when: MultiValueMap result = scanner.findContractsRecursively() then: @@ -82,7 +82,7 @@ class ContractFileScannerNewApiSpec extends Specification { def "should find contracts group in scenario"() { given: File baseDir = new File(this.getClass().getResource("/directory/with/scenario").toURI()) - ContractFileScanner scanner = new ContractFileScanner(baseDir, [] as Set, [] as Set, [] as Set) + ContractFileScanner scanner = new ContractFileScanner(baseDir, [] as Set, [] as Set, [] as Set, null) when: MultiValueMap contracts = scanner.findContractsRecursively() then: @@ -106,7 +106,7 @@ class ContractFileScannerNewApiSpec extends Specification { def "should find contract files with converters"() { given: File baseDir = new File(this.getClass().getResource("/directory/with/mixed").toURI()) - ContractFileScanner scanner = new ContractFileScanner(baseDir, null, null, null) { + ContractFileScanner scanner = new ContractFileScanner(baseDir, null, null, null, null) { @Override protected List converters() { return [new ContractConverter() { @@ -138,7 +138,7 @@ class ContractFileScannerNewApiSpec extends Specification { def "should prefer custom yaml converter over standard yaml converter"() { given: File baseDir = new File(this.getClass().getResource("/directory/with/custom/yml").toURI()) - ContractFileScanner scanner = new ContractFileScanner(baseDir, null, null) { + ContractFileScanner scanner = new ContractFileScanner(baseDir, null, null, null, null) { @Override protected List converters() { return [new ContractConverter() { @@ -181,7 +181,7 @@ class ContractFileScannerNewApiSpec extends Specification { and: File baseDir = tmpFolder Set included = ["social-service/**", "**/coupon-collected/**/*V1*"] as Set - ContractFileScanner scanner = new ContractFileScanner(baseDir, [] as Set, [] as Set, included) + ContractFileScanner scanner = new ContractFileScanner(baseDir, [] as Set, [] as Set, included, null) when: MultiValueMap result = scanner.findContractsRecursively() then: diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerSpec.groovy index 3d65e13280..ddc7670524 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerSpec.groovy @@ -50,7 +50,7 @@ class ContractFileScannerSpec extends Specification { File baseDir = tmpFolder Set excluded = ["package/**"] as Set Set ignored = ["other/different/**"] as Set - ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored, [] as Set) + ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored, [] as Set, "") when: ListMultimap result = scanner.findContracts() then: @@ -68,7 +68,7 @@ class ContractFileScannerSpec extends Specification { File baseDir = new File(this.getClass().getResource("/strange_[3.3.3]_directory").toURI()) Set excluded = ["foo/**"] as Set Set ignored = ["bar/**"] as Set - ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored, [] as Set) + ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored, [] as Set, "") when: ListMultimap result = scanner.findContracts() then: @@ -82,7 +82,7 @@ class ContractFileScannerSpec extends Specification { def "should find contracts group in scenario"() { given: File baseDir = new File(this.getClass().getResource("/directory/with/scenario").toURI()) - ContractFileScanner scanner = new ContractFileScanner(baseDir, [] as Set, [] as Set, [] as Set) + ContractFileScanner scanner = new ContractFileScanner(baseDir, [] as Set, [] as Set, [] as Set, "") when: ListMultimap contracts = scanner.findContracts() then: @@ -104,7 +104,7 @@ class ContractFileScannerSpec extends Specification { def "should find contract files with converters"() { given: File baseDir = new File(this.getClass().getResource("/directory/with/mixed").toURI()) - ContractFileScanner scanner = new ContractFileScanner(baseDir, null, null, null) { + ContractFileScanner scanner = new ContractFileScanner(baseDir, null, null, null, null) { @Override protected List converters() { return [new ContractConverter() { @@ -136,7 +136,7 @@ class ContractFileScannerSpec extends Specification { def "should prefer custom yaml converter over standard yaml converter"() { given: File baseDir = new File(this.getClass().getResource("/directory/with/custom/yml").toURI()) - ContractFileScanner scanner = new ContractFileScanner(baseDir, null, null) { + ContractFileScanner scanner = new ContractFileScanner(baseDir, null, null, null, null) { @Override protected List converters() { return [new ContractConverter() { @@ -179,7 +179,7 @@ class ContractFileScannerSpec extends Specification { and: File baseDir = tmpFolder Set included = ["social-service/**", "**/coupon-collected/**/*V1*"] as Set - ContractFileScanner scanner = new ContractFileScanner(baseDir, [] as Set, [] as Set, included) + ContractFileScanner scanner = new ContractFileScanner(baseDir, [] as Set, [] as Set, included, null) when: ListMultimap result = scanner.findContracts() then: