4 changed files with 422 additions and 0 deletions
@ -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()); |
||||
} |
||||
|
||||
} |
||||
@ -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"); |
||||
} |
||||
|
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -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…
Reference in new issue