Browse Source
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-30167pull/30364/head
12 changed files with 411 additions and 177 deletions
@ -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<QName> toQNames(Class<?> clazz) { |
||||||
|
Set<QName> result = new HashSet<>(1); |
||||||
|
findQNames(clazz, result, new HashSet<>()); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
private static void findQNames(Class<?> clazz, Set<QName> qNames, Set<Class<?>> 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}". |
||||||
|
* <ol> |
||||||
|
* <li>The first list, dealing with the first branch of the tree: |
||||||
|
* <ol> |
||||||
|
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li> |
||||||
|
* <li>{@link javax.xml.stream.events.Characters} {@code foo}</li> |
||||||
|
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li> |
||||||
|
* </ol> |
||||||
|
* <li>The second list, dealing with the second branch of the tree: |
||||||
|
* <ol> |
||||||
|
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li> |
||||||
|
* <li>{@link javax.xml.stream.events.Characters} {@code bar}</li> |
||||||
|
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li> |
||||||
|
* </ol> |
||||||
|
* </li> |
||||||
|
* </ol> |
||||||
|
*/ |
||||||
|
public static Flux<List<XMLEvent>> split(Flux<XMLEvent> xmlEventFlux, Set<QName> names) { |
||||||
|
return xmlEventFlux.handle(new SplitHandler(names)); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private static class SplitHandler implements BiConsumer<XMLEvent, SynchronousSink<List<XMLEvent>>> { |
||||||
|
|
||||||
|
private final Set<QName> names; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private List<XMLEvent> events; |
||||||
|
|
||||||
|
private int elementDepth = 0; |
||||||
|
|
||||||
|
private int barrier = Integer.MAX_VALUE; |
||||||
|
|
||||||
|
public SplitHandler(Set<QName> names) { |
||||||
|
this.names = names; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void accept(XMLEvent event, SynchronousSink<List<XMLEvent>> 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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
||||||
@ -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")); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
@ -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 { |
||||||
|
|
||||||
|
} |
||||||
@ -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; |
package org.springframework.http.codec.xml.jaxb; |
||||||
|
|||||||
Loading…
Reference in new issue