From 9f85e397d42b97fe9abb8bfbda9fbd47dfa2b34e Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 23 Mar 2023 16:54:26 +0100 Subject: [PATCH] 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 --- .../http/codec/xml/Jaxb2Helper.java | 192 ++++++++++++++++++ .../http/codec/xml/Jaxb2XmlDecoder.java | 137 +------------ .../http/codec/xml/Jaxb2HelperTests.java | 56 +++++ .../http/codec/xml/Jaxb2XmlDecoderTests.java | 81 ++++---- .../http/codec/xml/Jaxb2XmlEncoderTests.java | 4 +- .../http/codec/xml/TypePojo.java | 72 +++++++ .../xml/jaxb/XmlRootElementWithName.java | 4 +- .../XmlRootElementWithNameAndNamespace.java | 4 +- .../http/codec/xml/jaxb/XmlTypeSeeAlso.java | 28 +++ .../http/codec/xml/jaxb/XmlTypeWithName.java | 4 +- .../xml/jaxb/XmlTypeWithNameAndNamespace.java | 4 +- .../http/codec/xml/jaxb/package-info.java | 2 +- 12 files changed, 411 insertions(+), 177 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2Helper.java create mode 100644 spring-web/src/test/java/org/springframework/http/codec/xml/Jaxb2HelperTests.java create mode 100644 spring-web/src/test/java/org/springframework/http/codec/xml/TypePojo.java create mode 100644 spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeSeeAlso.java diff --git a/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2Helper.java b/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2Helper.java new file mode 100644 index 00000000000..976e116c357 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2Helper.java @@ -0,0 +1,192 @@ +/* + * 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 java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; + +import javax.xml.XMLConstants; +import javax.xml.namespace.QName; +import javax.xml.stream.events.XMLEvent; + +import jakarta.xml.bind.annotation.XmlRootElement; +import jakarta.xml.bind.annotation.XmlSchema; +import jakarta.xml.bind.annotation.XmlSeeAlso; +import jakarta.xml.bind.annotation.XmlType; +import reactor.core.publisher.Flux; +import reactor.core.publisher.SynchronousSink; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Helper class for JAXB2. + * + * @author Arjen Poutsma + * @since 6.1 + */ +abstract class Jaxb2Helper { + + /** + * The default value for JAXB annotations. + * @see XmlRootElement#name() + * @see XmlRootElement#namespace() + * @see XmlType#name() + * @see XmlType#namespace() + */ + private static final String JAXB_DEFAULT_ANNOTATION_VALUE = "##default"; + + + /** + * Returns the set of qualified names for the given class, according to the + * mapping rules in the JAXB specification. + */ + public static Set toQNames(Class clazz) { + Set result = new HashSet<>(1); + findQNames(clazz, result, new HashSet<>()); + return result; + } + + private static void findQNames(Class clazz, Set qNames, Set> completedClasses) { + // safety against circular XmlSeeAlso references + if (completedClasses.contains(clazz)) { + return; + } + if (clazz.isAnnotationPresent(XmlRootElement.class)) { + XmlRootElement annotation = clazz.getAnnotation(XmlRootElement.class); + qNames.add(new QName(namespace(annotation.namespace(), clazz), + localPart(annotation.name(), clazz))); + } + else if (clazz.isAnnotationPresent(XmlType.class)) { + XmlType annotation = clazz.getAnnotation(XmlType.class); + qNames.add(new QName(namespace(annotation.namespace(), clazz), + localPart(annotation.name(), clazz))); + } + else { + throw new IllegalArgumentException("Output class [" + clazz.getName() + + "] is neither annotated with @XmlRootElement nor @XmlType"); + } + completedClasses.add(clazz); + if (clazz.isAnnotationPresent(XmlSeeAlso.class)) { + XmlSeeAlso annotation = clazz.getAnnotation(XmlSeeAlso.class); + for (Class seeAlso : annotation.value()) { + findQNames(seeAlso, qNames, completedClasses); + } + } + } + + private static String localPart(String value, Class outputClass) { + if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(value)) { + return ClassUtils.getShortNameAsProperty(outputClass); + } + else { + return value; + } + } + + private static String namespace(String value, Class outputClass) { + if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(value)) { + Package outputClassPackage = outputClass.getPackage(); + if (outputClassPackage != null && outputClassPackage.isAnnotationPresent(XmlSchema.class)) { + XmlSchema annotation = outputClassPackage.getAnnotation(XmlSchema.class); + return annotation.namespace(); + } + else { + return XMLConstants.NULL_NS_URI; + } + } + else { + return value; + } + } + + /** + * Split a flux of {@link XMLEvent XMLEvents} into a flux of XMLEvent lists, one list + * for each branch of the tree that starts with one of the given qualified names. + * That is, given the XMLEvents shown {@linkplain XmlEventDecoder here}, + * and the name "{@code child}", this method returns a flux + * of two lists, each of which containing the events of a particular branch + * of the tree that starts with "{@code child}". + *
    + *
  1. The first list, dealing with the first branch of the tree: + *
      + *
    1. {@link javax.xml.stream.events.StartElement} {@code child}
    2. + *
    3. {@link javax.xml.stream.events.Characters} {@code foo}
    4. + *
    5. {@link javax.xml.stream.events.EndElement} {@code child}
    6. + *
    + *
  2. The second list, dealing with the second branch of the tree: + *
      + *
    1. {@link javax.xml.stream.events.StartElement} {@code child}
    2. + *
    3. {@link javax.xml.stream.events.Characters} {@code bar}
    4. + *
    5. {@link javax.xml.stream.events.EndElement} {@code child}
    6. + *
    + *
  3. + *
+ */ + public static Flux> split(Flux xmlEventFlux, Set names) { + return xmlEventFlux.handle(new SplitHandler(names)); + } + + + private static class SplitHandler implements BiConsumer>> { + + private final Set names; + + @Nullable + private List events; + + private int elementDepth = 0; + + private int barrier = Integer.MAX_VALUE; + + public SplitHandler(Set names) { + this.names = names; + } + + @Override + public void accept(XMLEvent event, SynchronousSink> sink) { + if (event.isStartElement()) { + if (this.barrier == Integer.MAX_VALUE) { + QName startElementName = event.asStartElement().getName(); + if (this.names.contains(startElementName)) { + this.events = new ArrayList<>(); + this.barrier = this.elementDepth; + } + } + this.elementDepth++; + } + if (this.elementDepth > this.barrier) { + Assert.state(this.events != null, "No XMLEvent List"); + this.events.add(event); + } + if (event.isEndElement()) { + this.elementDepth--; + if (this.elementDepth == this.barrier) { + this.barrier = Integer.MAX_VALUE; + Assert.state(this.events != null, "No XMLEvent List"); + sink.next(this.events); + } + } + } + } + + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlDecoder.java index 19afce9c470..cc67c3c758b 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlDecoder.java @@ -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. @@ -21,10 +21,9 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.function.BiConsumer; +import java.util.Set; import java.util.function.Function; -import javax.xml.XMLConstants; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; @@ -36,13 +35,12 @@ import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.UnmarshalException; import jakarta.xml.bind.Unmarshaller; import jakarta.xml.bind.annotation.XmlRootElement; -import jakarta.xml.bind.annotation.XmlSchema; +import jakarta.xml.bind.annotation.XmlSeeAlso; import jakarta.xml.bind.annotation.XmlType; import org.reactivestreams.Publisher; import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.SynchronousSink; import org.springframework.core.ResolvableType; import org.springframework.core.codec.AbstractDecoder; @@ -54,9 +52,8 @@ import org.springframework.core.io.buffer.DataBufferLimitException; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.log.LogFormatUtils; import org.springframework.http.MediaType; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; import org.springframework.util.xml.StaxUtils; @@ -72,15 +69,6 @@ import org.springframework.util.xml.StaxUtils; */ public class Jaxb2XmlDecoder extends AbstractDecoder { - /** - * The default value for JAXB annotations. - * @see XmlRootElement#name() - * @see XmlRootElement#namespace() - * @see XmlType#name() - * @see XmlType#namespace() - */ - private static final String JAXB_DEFAULT_ANNOTATION_VALUE = "##default"; - private static final XMLInputFactory inputFactory = StaxUtils.createDefensiveInputFactory(); @@ -162,8 +150,8 @@ public class Jaxb2XmlDecoder extends AbstractDecoder { inputStream, ResolvableType.forClass(XMLEvent.class), mimeType, hints); Class outputClass = elementType.toClass(); - QName typeName = toQName(outputClass); - Flux> splitEvents = split(xmlEventFlux, typeName); + Set typeNames = Jaxb2Helper.toQNames(outputClass); + Flux> splitEvents = Jaxb2Helper.split(xmlEventFlux, typeNames); return splitEvents.map(events -> { Object value = unmarshal(events, outputClass); @@ -184,6 +172,7 @@ public class Jaxb2XmlDecoder extends AbstractDecoder { } @Override + @NonNull public Object decode(DataBuffer dataBuffer, ResolvableType targetType, @Nullable MimeType mimeType, @Nullable Map hints) throws DecodingException { @@ -229,7 +218,8 @@ public class Jaxb2XmlDecoder extends AbstractDecoder { try { Unmarshaller unmarshaller = initUnmarshaller(outputClass); XMLEventReader eventReader = StaxUtils.createXMLEventReader(events); - if (outputClass.isAnnotationPresent(XmlRootElement.class)) { + if (outputClass.isAnnotationPresent(XmlRootElement.class) || + outputClass.isAnnotationPresent(XmlSeeAlso.class)) { return unmarshaller.unmarshal(eventReader); } else { @@ -250,113 +240,4 @@ public class Jaxb2XmlDecoder extends AbstractDecoder { return this.unmarshallerProcessor.apply(unmarshaller); } - /** - * Returns the qualified name for the given class, according to the mapping rules - * in the JAXB specification. - */ - QName toQName(Class outputClass) { - String localPart; - String namespaceUri; - - if (outputClass.isAnnotationPresent(XmlRootElement.class)) { - XmlRootElement annotation = outputClass.getAnnotation(XmlRootElement.class); - localPart = annotation.name(); - namespaceUri = annotation.namespace(); - } - else if (outputClass.isAnnotationPresent(XmlType.class)) { - XmlType annotation = outputClass.getAnnotation(XmlType.class); - localPart = annotation.name(); - namespaceUri = annotation.namespace(); - } - else { - throw new IllegalArgumentException("Output class [" + outputClass.getName() + - "] is neither annotated with @XmlRootElement nor @XmlType"); - } - - if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(localPart)) { - localPart = ClassUtils.getShortNameAsProperty(outputClass); - } - if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(namespaceUri)) { - Package outputClassPackage = outputClass.getPackage(); - if (outputClassPackage != null && outputClassPackage.isAnnotationPresent(XmlSchema.class)) { - XmlSchema annotation = outputClassPackage.getAnnotation(XmlSchema.class); - namespaceUri = annotation.namespace(); - } - else { - namespaceUri = XMLConstants.NULL_NS_URI; - } - } - return new QName(namespaceUri, localPart); - } - - /** - * Split a flux of {@link XMLEvent XMLEvents} into a flux of XMLEvent lists, one list - * for each branch of the tree that starts with the given qualified name. - * That is, given the XMLEvents shown {@linkplain XmlEventDecoder here}, - * and the {@code desiredName} "{@code child}", this method returns a flux - * of two lists, each of which containing the events of a particular branch - * of the tree that starts with "{@code child}". - *
    - *
  1. The first list, dealing with the first branch of the tree: - *
      - *
    1. {@link javax.xml.stream.events.StartElement} {@code child}
    2. - *
    3. {@link javax.xml.stream.events.Characters} {@code foo}
    4. - *
    5. {@link javax.xml.stream.events.EndElement} {@code child}
    6. - *
    - *
  2. The second list, dealing with the second branch of the tree: - *
      - *
    1. {@link javax.xml.stream.events.StartElement} {@code child}
    2. - *
    3. {@link javax.xml.stream.events.Characters} {@code bar}
    4. - *
    5. {@link javax.xml.stream.events.EndElement} {@code child}
    6. - *
    - *
  3. - *
- */ - Flux> split(Flux xmlEventFlux, QName desiredName) { - return xmlEventFlux.handle(new SplitHandler(desiredName)); - } - - - private static class SplitHandler implements BiConsumer>> { - - private final QName desiredName; - - @Nullable - private List events; - - private int elementDepth = 0; - - private int barrier = Integer.MAX_VALUE; - - public SplitHandler(QName desiredName) { - this.desiredName = desiredName; - } - - @Override - public void accept(XMLEvent event, SynchronousSink> sink) { - if (event.isStartElement()) { - if (this.barrier == Integer.MAX_VALUE) { - QName startElementName = event.asStartElement().getName(); - if (this.desiredName.equals(startElementName)) { - this.events = new ArrayList<>(); - this.barrier = this.elementDepth; - } - } - this.elementDepth++; - } - if (this.elementDepth > this.barrier) { - Assert.state(this.events != null, "No XMLEvent List"); - this.events.add(event); - } - if (event.isEndElement()) { - this.elementDepth--; - if (this.elementDepth == this.barrier) { - this.barrier = Integer.MAX_VALUE; - Assert.state(this.events != null, "No XMLEvent List"); - sink.next(this.events); - } - } - } - } - } diff --git a/spring-web/src/test/java/org/springframework/http/codec/xml/Jaxb2HelperTests.java b/spring-web/src/test/java/org/springframework/http/codec/xml/Jaxb2HelperTests.java new file mode 100644 index 00000000000..75b0a2a9fe8 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/xml/Jaxb2HelperTests.java @@ -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")); + } + + +} diff --git a/spring-web/src/test/java/org/springframework/http/codec/xml/Jaxb2XmlDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/xml/Jaxb2XmlDecoderTests.java index fbb61728050..85ac6e62b23 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/xml/Jaxb2XmlDecoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/xml/Jaxb2XmlDecoderTests.java @@ -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 xmlEvents = this.xmlEventDecoder.decode(toDataBufferMono(POJO_ROOT), null, null, HINTS); - Flux> result = this.decoder.split(xmlEvents, new QName("pojo")); + Flux> 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 xmlEvents = this.xmlEventDecoder.decode(toDataBufferMono(POJO_CHILD), null, null, HINTS); - Flux> result = this.decoder.split(xmlEvents, new QName("pojo")); + Flux> 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 source = toDataBufferMono(POJO_CHILD); + Flux 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 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 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()); } } + } diff --git a/spring-web/src/test/java/org/springframework/http/codec/xml/Jaxb2XmlEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/xml/Jaxb2XmlEncoderTests.java index e737f5ec333..ad38d40208f 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/xml/Jaxb2XmlEncoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/xml/Jaxb2XmlEncoderTests.java @@ -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 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 diff --git a/spring-web/src/test/java/org/springframework/http/codec/xml/TypePojo.java b/spring-web/src/test/java/org/springframework/http/codec/xml/TypePojo.java new file mode 100644 index 00000000000..31c3b8ab7be --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/xml/TypePojo.java @@ -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; + } +} diff --git a/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElementWithName.java b/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElementWithName.java index 5fd82ef986e..4007db3b0ad 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElementWithName.java +++ b/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElementWithName.java @@ -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 { } diff --git a/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElementWithNameAndNamespace.java b/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElementWithNameAndNamespace.java index 3a89dd3e696..fa88c4f7148 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElementWithNameAndNamespace.java +++ b/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElementWithNameAndNamespace.java @@ -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 { } diff --git a/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeSeeAlso.java b/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeSeeAlso.java new file mode 100644 index 00000000000..f646c8c0590 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeSeeAlso.java @@ -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 { + +} diff --git a/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeWithName.java b/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeWithName.java index 744524da594..f2813671721 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeWithName.java +++ b/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeWithName.java @@ -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 { } diff --git a/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeWithNameAndNamespace.java b/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeWithNameAndNamespace.java index a299b84c4c7..bfd6dc091fb 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeWithNameAndNamespace.java +++ b/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeWithNameAndNamespace.java @@ -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 { } diff --git a/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/package-info.java b/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/package-info.java index 00c3767ca86..d8a69da0c82 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/package-info.java +++ b/spring-web/src/test/java/org/springframework/http/codec/xml/jaxb/package-info.java @@ -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;