Merge branch '2.0.x'

# Conflicts:
#	docker/spring-cloud-contract-docker/Dockerfile
#	docker/spring-cloud-contract-stub-runner-docker/Dockerfile
#	spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy
This commit is contained in:
Olga Maciaszek-Sharma
2019-02-13 13:23:31 +01:00
7 changed files with 119 additions and 34 deletions

View File

@@ -1,6 +1,6 @@
FROM ubuntu:18.10
ARG SDKMAN_JAVA_INSTALLATION=8.0.201-zulu
ARG SDKMAN_JAVA_INSTALLATION=8.0.202-zulu
MAINTAINER Marcin Grzejszczak <mgrzejszczak@pivotal.io>

View File

@@ -1,6 +1,6 @@
FROM ubuntu:18.10
ARG SDKMAN_JAVA_INSTALLATION=8.0.201-zulu
ARG SDKMAN_JAVA_INSTALLATION=8.0.202-zulu
MAINTAINER Marcin Grzejszczak <mgrzejszczak@pivotal.io>

View File

@@ -24,6 +24,7 @@ import groovy.transform.CompileDynamic
import groovy.xml.XmlUtil
import org.springframework.cloud.contract.spec.Contract
import repackaged.nl.flotsam.xeger.Xeger
import org.springframework.cloud.contract.verifier.util.ContentUtils
import java.nio.charset.StandardCharsets
@@ -33,6 +34,7 @@ import static org.apache.commons.text.StringEscapeUtils.escapeJava
* Converts WireMock stubs into the DSL format
*
* @since 1.0.0
* @author Konstantin Shevchuk
*/
@CompileDynamic
class WireMockToDslConverter {
@@ -123,7 +125,7 @@ class WireMockToDslConverter {
return wrapWithMultilineGString(JsonOutput.prettyPrint(responseBody))
} catch (Exception jsonException) {
try {
def xml = new XmlSlurper().parseText(responseBody)
def xml = ContentUtils.getXmlSlurperWithDefaultErrorHandler().parseText(responseBody)
return wrapWithMultilineGString(XmlUtil.serialize(responseBody))
} catch (Exception xmlException) {
return wrapWithMultilineGString(responseBody)

View File

@@ -1,18 +1,17 @@
/*
* Copyright 2013-2018 the original author or authors.
* Copyright 2013-2019 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
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.util
@@ -36,6 +35,7 @@ import org.springframework.cloud.contract.spec.internal.Headers
import org.springframework.cloud.contract.spec.internal.MatchingStrategy
import org.springframework.cloud.contract.spec.internal.NamedProperty
import org.springframework.cloud.contract.spec.internal.OptionalProperty
import org.xml.sax.helpers.DefaultHandler
import static org.apache.commons.text.StringEscapeUtils.escapeJava
import static org.apache.commons.text.StringEscapeUtils.escapeJson
@@ -49,8 +49,9 @@ import static org.springframework.cloud.contract.verifier.util.ContentType.UNKNO
*
* @author Marcin Grzejszczak
* @author Olga Maciaszek-Sharma
*
* @author Konstantin Shevchuk
* @since 1.0.0
*
*/
@CompileStatic
class ContentUtils {
@@ -119,7 +120,7 @@ class ContentUtils {
return JSON
} catch(JsonException e) {
try {
new XmlSlurper().parseText(extractValueForXML(bodyAsValue, GET_STUB_SIDE).toString())
getXmlSlurperWithDefaultErrorHandler().parseText(extractValueForXML(bodyAsValue, GET_STUB_SIDE).toString())
return ContentType.XML
} catch (Exception ignored) {
extractValueForGString(bodyAsValue, GET_STUB_SIDE)
@@ -134,7 +135,7 @@ class ContentUtils {
return JSON
} catch(JsonException e) {
try {
new XmlSlurper().parseText(bodyAsValue)
getXmlSlurperWithDefaultErrorHandler().parseText(bodyAsValue)
return ContentType.XML
} catch (Exception ignored) {
return UNKNOWN
@@ -234,7 +235,7 @@ class ContentUtils {
bodyAsValue.strings.clone() as String[]
)
// try to convert it to XML
new XmlSlurper().parseText(impl.toString())
getXmlSlurperWithDefaultErrorHandler().parseText(impl.toString())
return impl
}
@@ -464,7 +465,7 @@ class ContentUtils {
gString.strings.clone() as String[]
)
try {
new XmlSlurper().parseText(stringWithoutValues.toString())
getXmlSlurperWithDefaultErrorHandler().parseText(stringWithoutValues.toString())
return true
} catch (Exception ignored) {
// Not XML
@@ -547,4 +548,15 @@ class ContentUtils {
}
return contentType
}
/**
* Creates new {@link XmlSlurper} with default error handler.
*
* @return XmlSlurper with default error handler
*/
static XmlSlurper getXmlSlurperWithDefaultErrorHandler() {
XmlSlurper xmlSlurper = new XmlSlurper()
xmlSlurper.setErrorHandler(new DefaultHandler())
return xmlSlurper
}
}

View File

@@ -84,8 +84,11 @@ class JsonToJsonPathsConverter {
if (bodyMatchers?.hasMatchers()) {
bodyMatchers.matchers().each { BodyMatcher matcher ->
try {
context.delete(matcher.path())
removeTrailingContainers(matcher, context)
def entry = entry(context, matcher.path())
if (entry != null) {
context.delete(matcher.path())
removeTrailingContainers(matcher, context)
}
} catch (RuntimeException e) {
if (log.isDebugEnabled()) {
log.debug("Exception occurred while trying to delete path [${matcher.path()}]", e)
@@ -96,6 +99,17 @@ class JsonToJsonPathsConverter {
return jsonCopy
}
private static def entry(DocumentContext context, String path) {
try {
return context.read(path)
} catch (Exception ex) {
if (log.isDebugEnabled()) {
log.debug("Exception occurred while trying to retrieve element via path [${path}]", ex)
}
return null
}
}
/**
* Retrieves the value from JSON via json path
*
@@ -118,22 +132,26 @@ class JsonToJsonPathsConverter {
* defining body...
*/
private static void removeTrailingContainers(BodyMatcher matcher, DocumentContext context) {
if (matcher.path().contains(ANY_ARRAY_NOTATION_IN_JSONPATH)) {
String pathWithoutAnyArray = matcher.path().substring(0, matcher.path().lastIndexOf(ANY_ARRAY_NOTATION_IN_JSONPATH))
def object = context.read(pathWithoutAnyArray)
if (object instanceof Iterable && containsOnlyEmptyElements(object)) {
String pathToDelete = pathToDelete(pathWithoutAnyArray)
context.delete(pathToDelete)
} else {
String lastParent = matcher.path().substring(0, matcher.path().lastIndexOf("."))
def lastParentObject = context.read(lastParent)
if (lastParentObject instanceof Iterable && containsOnlyEmptyElements(lastParentObject)) {
context.delete(lastParent)
}
String pathWithoutAnyArray = matcher.path().contains(ANY_ARRAY_NOTATION_IN_JSONPATH) ? matcher.path().substring(0, matcher.path().lastIndexOf(ANY_ARRAY_NOTATION_IN_JSONPATH)) : matcher.path()
def object = entry(context, pathWithoutAnyArray)
// object got removed and it was the only element
// let's get its parent and see if it contains an empty element
if (isIterable(object) && containsOnlyEmptyElements(object)) {
String pathToDelete = pathToDelete(pathWithoutAnyArray)
context.delete(pathToDelete)
} else {
String lastParent = matcher.path().substring(0, matcher.path().lastIndexOf("."))
def lastParentObject = context.read(lastParent)
if (isIterable(lastParentObject) && containsOnlyEmptyElements(lastParentObject)) {
context.delete(lastParent)
}
}
}
private static boolean isIterable(Object object) {
return object instanceof Iterable || object instanceof Map
}
private static String pathToDelete(String pathWithoutAnyArray) {
// we can't remove root
return pathWithoutAnyArray == '$' ? '$[*]' : pathWithoutAnyArray

View File

@@ -1174,4 +1174,49 @@ DocumentContext parsedJson = JsonPath.parse(json);
"JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties, classDataForMethod) }
}
@Issue("#727")
def "should not leave empty arrays [#methodBuilderName]"() {
given:
Contract contractDsl = Contract.make {
request {
method 'GET'
url '/list'
}
response {
status 200
body(
[
content: [
one: "two",
two: "two",
three: [
six: "seven"
]
]
]
)
bodyMatchers {
jsonPath('$.content.three.six', byRegex(".*seven.*"))
jsonPath('$.content.one', byRegex(".*two.*"))
}
}
}
MethodBodyBuilder builder = methodBuilder(contractDsl)
BlockBuilder blockBuilder = new BlockBuilder(" ")
when:
builder.appendTo(blockBuilder)
then:
String test = blockBuilder.toString()
SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, test)
!test.contains('''.isEmpty()''')
and:
stubMappingIsValidWireMockStub(contractDsl)
where:
methodBuilderName | methodBuilder
"MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) }
"MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) }
"JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) }
"JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) }
}
}

View File

@@ -1,11 +1,12 @@
package org.springframework.cloud.contract.verifier.util
import org.springframework.cloud.contract.spec.internal.DslProperty
import org.springframework.cloud.contract.verifier.util.ContentUtils
import spock.lang.Specification
import org.xml.sax.helpers.DefaultHandler
/**
* @author Marcin Grzejszczak
* @author Konstantin Shevchuk
*/
class ContentUtilsSpec extends Specification {
@@ -22,4 +23,11 @@ class ContentUtilsSpec extends Specification {
expect:
"test" == ContentUtils.GET_TEST_SIDE(dslProperty)
}
def "should return XmlSlurper with default error handler"() {
given:
XmlSlurper xmlSlurper = ContentUtils.getXmlSlurperWithDefaultErrorHandler()
expect:
xmlSlurper.getErrorHandler() instanceof DefaultHandler
}
}