diff --git a/docker/spring-cloud-contract-docker/Dockerfile b/docker/spring-cloud-contract-docker/Dockerfile index e1016c0a3f..17fe04318b 100644 --- a/docker/spring-cloud-contract-docker/Dockerfile +++ b/docker/spring-cloud-contract-docker/Dockerfile @@ -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 diff --git a/docker/spring-cloud-contract-stub-runner-docker/Dockerfile b/docker/spring-cloud-contract-stub-runner-docker/Dockerfile index d5e34dfd8a..5bd0d979ae 100644 --- a/docker/spring-cloud-contract-stub-runner-docker/Dockerfile +++ b/docker/spring-cloud-contract-stub-runner-docker/Dockerfile @@ -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 diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverter.groovy index b89ceaadef..d283eab32c 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverter.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/wiremock/WireMockToDslConverter.groovy @@ -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) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy index 1ad1f67918..e9ffb2b481 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy @@ -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 + } + } 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 659aad8f23..eb8dfb7562 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 @@ -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 diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy index a04608c9b7..c20d35689b 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilderSpec.groovy @@ -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) } + } + } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/ContentUtilsSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/ContentUtilsSpec.groovy index 98c2229de7..3d746d448c 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/ContentUtilsSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/ContentUtilsSpec.groovy @@ -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 + } }