Browse Source

Introduce Jackson XML codecs

See gh-35752
pull/36115/head
Sébastien Deleuze 4 weeks ago
parent
commit
a1204a405a
  1. 121
      spring-web/src/main/java/org/springframework/http/codec/xml/JacksonXmlDecoder.java
  2. 107
      spring-web/src/main/java/org/springframework/http/codec/xml/JacksonXmlEncoder.java
  3. 106
      spring-web/src/test/java/org/springframework/http/codec/xml/JacksonXmlDecoderTests.java
  4. 88
      spring-web/src/test/java/org/springframework/http/codec/xml/JacksonXmlEncoderTests.java

121
spring-web/src/main/java/org/springframework/http/codec/xml/JacksonXmlDecoder.java

@ -0,0 +1,121 @@ @@ -0,0 +1,121 @@
/*
* Copyright 2002-present 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.nio.charset.StandardCharsets;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.dataformat.xml.XmlFactory;
import tools.jackson.dataformat.xml.XmlMapper;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType;
import org.springframework.http.codec.AbstractJacksonDecoder;
import org.springframework.util.MimeType;
import org.springframework.util.xml.StaxUtils;
/**
* Decode bytes into XML and convert to Objects with Jackson 3.x.
*
* <p>Stream decoding is currently not supported.
*
* @author Sebastien Deleuze
* @since 7.0.3
* @see JacksonXmlEncoder
*/
public class JacksonXmlDecoder extends AbstractJacksonDecoder<XmlMapper> {
private static final MediaType[] DEFAULT_XML_MIME_TYPES = new MediaType[] {
new MediaType("application", "xml", StandardCharsets.UTF_8),
new MediaType("text", "xml", StandardCharsets.UTF_8),
new MediaType("application", "*+xml", StandardCharsets.UTF_8)
};
/**
* Construct a new instance with a {@link XmlMapper} customized with the
* {@link tools.jackson.databind.JacksonModule}s found by
* {@link MapperBuilder#findModules(ClassLoader)}.
*/
public JacksonXmlDecoder() {
super(XmlMapper.builder(defensiveXmlFactory()), DEFAULT_XML_MIME_TYPES);
}
/**
* Construct a new instance with the provided {@link XmlMapper.Builder}
* customized with the {@link tools.jackson.databind.JacksonModule}s
* found by {@link MapperBuilder#findModules(ClassLoader)}.
* @see XmlMapper#builder()
* @see #defensiveXmlFactory()
*/
public JacksonXmlDecoder(XmlMapper.Builder builder) {
super(builder, DEFAULT_XML_MIME_TYPES);
}
/**
* Construct a new instance with the provided {@link XmlMapper}.
* @see XmlMapper#builder()
* @see #defensiveXmlFactory()
*/
public JacksonXmlDecoder(XmlMapper mapper) {
super(mapper, DEFAULT_XML_MIME_TYPES);
}
/**
* Construct a new instance with the provided {@link XmlMapper.Builder}
* customized with the {@link tools.jackson.databind.JacksonModule}s
* found by {@link MapperBuilder#findModules(ClassLoader)}, and
* {@link MimeType}s.
* @see XmlMapper#builder()
* @see #defensiveXmlFactory()
*/
public JacksonXmlDecoder(XmlMapper.Builder builder, MimeType... mimeTypes) {
super(builder, mimeTypes);
}
/**
* Construct a new instance with the provided {@link XmlMapper} and {@link MimeType}s.
* @see XmlMapper#builder()
* @see #defensiveXmlFactory()
*/
public JacksonXmlDecoder(XmlMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
}
@Override
public Flux<Object> decode(Publisher<DataBuffer> input, ResolvableType elementType, @Nullable MimeType mimeType,
@Nullable Map<String, Object> hints) {
throw new UnsupportedOperationException("Stream decoding is currently not supported");
}
/**
* Return an {@link XmlFactory} created from {@link StaxUtils#createDefensiveInputFactory}
* with Spring's defensive setup, i.e. no support for the resolution of DTDs and external
* entities.
*/
public static XmlFactory defensiveXmlFactory() {
return new XmlFactory(StaxUtils.createDefensiveInputFactory());
}
}

107
spring-web/src/main/java/org/springframework/http/codec/xml/JacksonXmlEncoder.java

@ -0,0 +1,107 @@ @@ -0,0 +1,107 @@
/*
* Copyright 2002-present 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.nio.charset.StandardCharsets;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.dataformat.xml.XmlMapper;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.AbstractJacksonEncoder;
import org.springframework.util.MimeType;
/**
* Encode from an {@code Object} to bytes of XML objects using Jackson 3.x.
*
* <p>Stream encoding is currently not supported.
*
* @author Sebastien Deleuze
* @since 7.0.3
* @see JacksonXmlDecoder
*/
public class JacksonXmlEncoder extends AbstractJacksonEncoder<XmlMapper> {
private static final MediaType[] DEFAULT_XML_MIME_TYPES = new MediaType[] {
new MediaType("application", "xml", StandardCharsets.UTF_8),
new MediaType("text", "xml", StandardCharsets.UTF_8),
new MediaType("application", "*+xml", StandardCharsets.UTF_8)
};
/**
* Construct a new instance with a {@link XmlMapper} customized with the
* {@link tools.jackson.databind.JacksonModule}s found by
* {@link MapperBuilder#findModules(ClassLoader)}.
*/
public JacksonXmlEncoder() {
super(XmlMapper.builder(), DEFAULT_XML_MIME_TYPES);
}
/**
* Construct a new instance with the provided {@link XmlMapper.Builder}
* customized with the {@link tools.jackson.databind.JacksonModule}s
* found by {@link MapperBuilder#findModules(ClassLoader)}.
* @see XmlMapper#builder()
*/
public JacksonXmlEncoder(XmlMapper.Builder builder) {
super(builder, DEFAULT_XML_MIME_TYPES);
}
/**
* Construct a new instance with the provided {@link XmlMapper}.
* @see XmlMapper#builder()
*/
public JacksonXmlEncoder(XmlMapper mapper) {
super(mapper, DEFAULT_XML_MIME_TYPES);
}
/**
* Construct a new instance with the provided {@link XmlMapper.Builder}
* customized with the {@link tools.jackson.databind.JacksonModule}s
* found by {@link MapperBuilder#findModules(ClassLoader)}, and
* {@link MimeType}s.
* @see XmlMapper#builder()
*/
public JacksonXmlEncoder(XmlMapper.Builder builder, MimeType... mimeTypes) {
super(builder, mimeTypes);
}
/**
* Construct a new instance with the provided {@link XmlMapper} and {@link MimeType}s.
* @see XmlMapper#builder()
*/
public JacksonXmlEncoder(XmlMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
}
@Override
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
throw new UnsupportedOperationException("Stream encoding is currently not supported");
}
}

106
spring-web/src/test/java/org/springframework/http/codec/xml/JacksonXmlDecoderTests.java

@ -0,0 +1,106 @@ @@ -0,0 +1,106 @@
/*
* Copyright 2002-present 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.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import tools.jackson.core.JacksonException;
import tools.jackson.dataformat.xml.XmlMapper;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.testfixture.codec.AbstractDecoderTests;
import org.springframework.http.MediaType;
import org.springframework.web.testfixture.xml.Pojo;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link JacksonXmlDecoder}.
*
* @author Sebastien Deleuze
*/
class JacksonXmlDecoderTests extends AbstractDecoderTests<JacksonXmlDecoder> {
private final Pojo pojo1 = new Pojo("f1", "b1");
private final Pojo pojo2 = new Pojo("f2", "b2");
private final XmlMapper mapper = XmlMapper.builder().build();
public JacksonXmlDecoderTests() {
super(new JacksonXmlDecoder());
}
@Override
@Test
protected void canDecode() {
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_XML)).isTrue();
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), MediaType.TEXT_XML)).isTrue();
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), new MediaType("application", "soap+xml"))).isTrue();
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), null)).isTrue();
assertThat(decoder.canDecode(ResolvableType.forClass(String.class), null)).isTrue();
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_JSON)).isFalse();
}
@Override
@Test
protected void decode() {
Flux<DataBuffer> input = Flux.just(this.pojo1, this.pojo2)
.map(this::writeObject)
.flatMap(this::dataBuffer);
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() ->
testDecodeAll(input, Pojo.class, step -> step
.expectNext(pojo1)
.expectNext(pojo2)
.verifyComplete()));
}
private byte[] writeObject(Object o) {
try {
return this.mapper.writer().writeValueAsBytes(o);
}
catch (JacksonException e) {
throw new AssertionError(e);
}
}
@Override
@Test
protected void decodeToMono() {
List<Pojo> expected = Arrays.asList(pojo1, pojo2);
Flux<DataBuffer> input = Flux.just(expected)
.map(this::writeObject)
.flatMap(this::dataBuffer);
ResolvableType elementType = ResolvableType.forClassWithGenerics(List.class, Pojo.class);
testDecodeToMono(input, elementType, step -> step
.expectNext(expected)
.expectComplete()
.verify(), null, null);
}
}

88
spring-web/src/test/java/org/springframework/http/codec/xml/JacksonXmlEncoderTests.java

@ -0,0 +1,88 @@ @@ -0,0 +1,88 @@
/*
* Copyright 2002-present 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.function.Consumer;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import tools.jackson.dataformat.xml.XmlMapper;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
import org.springframework.core.testfixture.io.buffer.DataBufferTestUtils;
import org.springframework.http.MediaType;
import org.springframework.web.testfixture.xml.Pojo;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.springframework.core.io.buffer.DataBufferUtils.release;
/**
* Tests for {@link JacksonXmlEncoder}.
*
* @author Sebastien Deleuze
*/
class JacksonXmlEncoderTests extends AbstractLeakCheckingTests {
private final XmlMapper mapper = XmlMapper.builder().build();
private final JacksonXmlEncoder encoder = new JacksonXmlEncoder();
@Test
protected void canEncode() {
ResolvableType pojoType = ResolvableType.forClass(Pojo.class);
assertThat(this.encoder.canEncode(pojoType, MediaType.APPLICATION_XML)).isTrue();
assertThat(this.encoder.canEncode(pojoType, MediaType.TEXT_XML)).isTrue();
assertThat(this.encoder.canEncode(pojoType, new MediaType("application", "soap+xml"))).isTrue();
assertThat(this.encoder.canEncode(pojoType, null)).isTrue();
assertThat(this.encoder.canEncode(ResolvableType.forClass(String.class), null)).isTrue();
assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isTrue();
assertThat(this.encoder.canEncode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_JSON)).isFalse();
}
@Test
protected void encode() {
Pojo value = new Pojo("foo", "bar");
DataBuffer result = encoder.encodeValue(value, this.bufferFactory, ResolvableType.forClass(Pojo.class),
MediaType.APPLICATION_XML, null);
pojoConsumer(value).accept(result);
}
private Consumer<DataBuffer> pojoConsumer(Pojo expected) {
return dataBuffer -> {
Pojo actual = this.mapper.reader().forType(Pojo.class)
.readValue(DataBufferTestUtils.dumpBytes(dataBuffer));
assertThat(actual).isEqualTo(expected);
release(dataBuffer);
};
}
@Test
void encodeStream() {
Pojo pojo1 = new Pojo("foo", "bar");
Pojo pojo2 = new Pojo("foofoo", "barbar");
Pojo pojo3 = new Pojo("foofoofoo", "barbarbar");
Flux<Pojo> input = Flux.just(pojo1, pojo2, pojo3);
ResolvableType type = ResolvableType.forClass(Pojo.class);
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() ->
encoder.encode(input, this.bufferFactory, type, MediaType.APPLICATION_XML, null));
}
}
Loading…
Cancel
Save