Support XmlSeeAlso in Jaxb2XmlDecoder

This commit adds support for the @XmlSeeAlso annotation in the
Jaxb2XmlDecoder. This includes

- Finding the set of possible qualified names given a class name, rather
  than a single name.
- Splitting the XMLEvent stream when coming across one of the names in
  this set.

Closes gh-30167
This commit is contained in:
Arjen Poutsma
2023-03-23 16:54:26 +01:00
parent 7df2e2a8d2
commit 9f85e397d4
12 changed files with 411 additions and 177 deletions

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2002-2023 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.http.codec.xml;
import javax.xml.namespace.QName;
import org.junit.jupiter.api.Test;
import org.springframework.http.codec.xml.jaxb.XmlRootElement;
import org.springframework.http.codec.xml.jaxb.XmlRootElementWithName;
import org.springframework.http.codec.xml.jaxb.XmlRootElementWithNameAndNamespace;
import org.springframework.http.codec.xml.jaxb.XmlType;
import org.springframework.http.codec.xml.jaxb.XmlTypeSeeAlso;
import org.springframework.http.codec.xml.jaxb.XmlTypeWithName;
import org.springframework.http.codec.xml.jaxb.XmlTypeWithNameAndNamespace;
import org.springframework.web.testfixture.xml.Pojo;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Arjen Poutsma
*/
class Jaxb2HelperTests {
@Test
public void toExpectedQName() {
assertThat(Jaxb2Helper.toQNames(Pojo.class)).containsExactly(new QName("pojo"));
assertThat(Jaxb2Helper.toQNames(TypePojo.class)).containsExactly(new QName("pojo"));
assertThat(Jaxb2Helper.toQNames(XmlRootElementWithNameAndNamespace.class)).containsExactly(new QName("namespace-type", "name-type"));
assertThat(Jaxb2Helper.toQNames(XmlRootElementWithName.class)).containsExactly(new QName("namespace-package", "name-type"));
assertThat(Jaxb2Helper.toQNames(XmlRootElement.class)).containsExactly(new QName("namespace-package", "xmlRootElement"));
assertThat(Jaxb2Helper.toQNames(XmlTypeWithNameAndNamespace.class)).containsExactly(new QName("namespace-type", "name-type"));
assertThat(Jaxb2Helper.toQNames(XmlTypeWithName.class)).containsExactly(new QName("namespace-package", "name-type"));
assertThat(Jaxb2Helper.toQNames(XmlType.class)).containsExactly(new QName("namespace-package", "xmlType"));
assertThat(Jaxb2Helper.toQNames(XmlTypeSeeAlso.class)).containsExactlyInAnyOrder(new QName("namespace-package", "xmlTypeSeeAlso"),
new QName("namespace-package", "name-type"), new QName("namespace-type", "name-type"));
}
}

View File

@@ -20,10 +20,13 @@ import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.xml.namespace.QName;
import javax.xml.stream.events.XMLEvent;
import jakarta.xml.bind.annotation.XmlSeeAlso;
import org.junit.jupiter.api.Test;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
@@ -35,13 +38,6 @@ import org.springframework.core.codec.DecodingException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
import org.springframework.http.MediaType;
import org.springframework.http.codec.xml.jaxb.XmlRootElement;
import org.springframework.http.codec.xml.jaxb.XmlRootElementWithName;
import org.springframework.http.codec.xml.jaxb.XmlRootElementWithNameAndNamespace;
import org.springframework.http.codec.xml.jaxb.XmlType;
import org.springframework.http.codec.xml.jaxb.XmlTypeWithName;
import org.springframework.http.codec.xml.jaxb.XmlTypeWithNameAndNamespace;
import org.springframework.lang.Nullable;
import org.springframework.util.MimeType;
import org.springframework.web.testfixture.xml.Pojo;
@@ -96,7 +92,7 @@ public class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
@Test
public void splitOneBranches() {
Flux<XMLEvent> xmlEvents = this.xmlEventDecoder.decode(toDataBufferMono(POJO_ROOT), null, null, HINTS);
Flux<List<XMLEvent>> result = this.decoder.split(xmlEvents, new QName("pojo"));
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")));
StepVerifier.create(result)
.consumeNextWith(events -> {
@@ -117,7 +113,7 @@ public class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
@Test
public void splitMultipleBranches() {
Flux<XMLEvent> xmlEvents = this.xmlEventDecoder.decode(toDataBufferMono(POJO_CHILD), null, null, HINTS);
Flux<List<XMLEvent>> result = this.decoder.split(xmlEvents, new QName("pojo"));
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")));
StepVerifier.create(result)
@@ -208,6 +204,19 @@ public class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
.verify();
}
@Test
public void decodeXmlSeeAlso() {
Mono<DataBuffer> source = toDataBufferMono(POJO_CHILD);
Flux<Object> output = this.decoder.decode(source, ResolvableType.forClass(Parent.class), null, HINTS);
StepVerifier.create(output)
.expectNext(new Child("foo", "bar"))
.expectNext(new Child("foofoo", "barbar"))
.expectComplete()
.verify();
}
@Test
public void decodeError() {
Flux<DataBuffer> source = Flux.concat(
@@ -252,21 +261,6 @@ public class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
.verify();
}
@Test
public void toExpectedQName() {
assertThat(this.decoder.toQName(Pojo.class)).isEqualTo(new QName("pojo"));
assertThat(this.decoder.toQName(TypePojo.class)).isEqualTo(new QName("pojo"));
assertThat(this.decoder.toQName(XmlRootElementWithNameAndNamespace.class)).isEqualTo(new QName("namespace", "name"));
assertThat(this.decoder.toQName(XmlRootElementWithName.class)).isEqualTo(new QName("namespace", "name"));
assertThat(this.decoder.toQName(XmlRootElement.class)).isEqualTo(new QName("namespace", "xmlRootElement"));
assertThat(this.decoder.toQName(XmlTypeWithNameAndNamespace.class)).isEqualTo(new QName("namespace", "name"));
assertThat(this.decoder.toQName(XmlTypeWithName.class)).isEqualTo(new QName("namespace", "name"));
assertThat(this.decoder.toQName(XmlType.class)).isEqualTo(new QName("namespace", "xmlType"));
}
private Mono<DataBuffer> toDataBufferMono(String value) {
return Mono.defer(() -> {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
@@ -276,20 +270,17 @@ public class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
});
}
@jakarta.xml.bind.annotation.XmlType(name = "pojo")
public static class TypePojo {
@jakarta.xml.bind.annotation.XmlType
@XmlSeeAlso(Child.class)
public static abstract class Parent {
private String foo;
private String bar;
public TypePojo() {
public Parent() {
}
public TypePojo(String foo, String bar) {
public Parent(String foo) {
this.foo = foo;
this.bar = bar;
}
public String getFoo() {
@@ -299,6 +290,20 @@ public class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
public void setFoo(String foo) {
this.foo = foo;
}
}
@jakarta.xml.bind.annotation.XmlRootElement(name = "pojo")
public static class Child extends Parent {
private String bar;
public Child() {
}
public Child(String foo, String bar) {
super(foo);
this.bar = bar;
}
public String getBar() {
return this.bar;
@@ -309,21 +314,21 @@ public class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
}
@Override
public boolean equals(@Nullable Object o) {
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof TypePojo other) {
return this.foo.equals(other.foo) && this.bar.equals(other.bar);
if (o instanceof Child other) {
return getBar().equals(other.getBar()) &&
getFoo().equals(other.getFoo());
}
return false;
}
@Override
public int hashCode() {
int result = this.foo.hashCode();
result = 31 * result + this.bar.hashCode();
return result;
return Objects.hash(getBar(), getFoo());
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -57,7 +57,7 @@ public class Jaxb2XmlEncoderTests extends AbstractEncoderTests<Jaxb2XmlEncoder>
assertThat(this.encoder.canEncode(forClass(Pojo.class), new MediaType("application", "foo+xml"))).isTrue();
assertThat(this.encoder.canEncode(forClass(Pojo.class), MediaType.APPLICATION_JSON)).isFalse();
assertThat(this.encoder.canEncode(forClass(Jaxb2XmlDecoderTests.TypePojo.class), MediaType.APPLICATION_XML)).isTrue();
assertThat(this.encoder.canEncode(forClass(TypePojo.class), MediaType.APPLICATION_XML)).isTrue();
assertThat(this.encoder.canEncode(forClass(getClass()), MediaType.APPLICATION_XML)).isFalse();
// SPR-15464

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2002-2023 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.http.codec.xml;
import org.springframework.lang.Nullable;
/**
* @author Arjen Poutsma
*/
@jakarta.xml.bind.annotation.XmlType(name = "pojo")
public class TypePojo {
private String foo;
private String bar;
public TypePojo() {
}
public TypePojo(String foo, String bar) {
this.foo = foo;
this.bar = bar;
}
public String getFoo() {
return this.foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
public String getBar() {
return this.bar;
}
public void setBar(String bar) {
this.bar = bar;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o instanceof TypePojo other) {
return this.foo.equals(other.foo) && this.bar.equals(other.bar);
}
return false;
}
@Override
public int hashCode() {
int result = this.foo.hashCode();
result = 31 * result + this.bar.hashCode();
return result;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2023 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.
@@ -21,7 +21,7 @@ import jakarta.xml.bind.annotation.XmlRootElement;
/**
* @author Arjen Poutsma
*/
@XmlRootElement(name = "name")
@XmlRootElement(name = "name-type")
public class XmlRootElementWithName {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2023 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.
@@ -21,7 +21,7 @@ import jakarta.xml.bind.annotation.XmlRootElement;
/**
* @author Arjen Poutsma
*/
@XmlRootElement(name = "name", namespace = "namespace")
@XmlRootElement(name = "name-type", namespace = "namespace-type")
public class XmlRootElementWithNameAndNamespace {
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2002-2023 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.http.codec.xml.jaxb;
import jakarta.xml.bind.annotation.XmlSeeAlso;
/**
* @author Arjen Poutsma
*/
@jakarta.xml.bind.annotation.XmlType
@XmlSeeAlso({XmlRootElementWithName.class, XmlRootElementWithNameAndNamespace.class})
public class XmlTypeSeeAlso {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2023 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.
@@ -21,7 +21,7 @@ import jakarta.xml.bind.annotation.XmlType;
/**
* @author Arjen Poutsma
*/
@XmlType(name = "name")
@XmlType(name = "name-type")
public class XmlTypeWithName {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2023 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.
@@ -21,7 +21,7 @@ import jakarta.xml.bind.annotation.XmlType;
/**
* @author Arjen Poutsma
*/
@XmlType(name = "name", namespace = "namespace")
@XmlType(name = "name-type", namespace = "namespace-type")
public class XmlTypeWithNameAndNamespace {
}

View File

@@ -1,2 +1,2 @@
@jakarta.xml.bind.annotation.XmlSchema(namespace = "namespace")
@jakarta.xml.bind.annotation.XmlSchema(namespace = "namespace-package")
package org.springframework.http.codec.xml.jaxb;