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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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; |
||||
|
||||
Loading…
Reference in new issue