View Javadoc
1   /*
2    * Copyright 2019 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.bremersee.xml.http.codec;
18  
19  import static java.util.Objects.isNull;
20  
21  import java.io.OutputStream;
22  import java.util.Map;
23  import java.util.Set;
24  import jakarta.xml.bind.JAXBException;
25  import jakarta.xml.bind.MarshalException;
26  import jakarta.xml.bind.Marshaller;
27  import org.bremersee.xml.JaxbContextBuilder;
28  import org.springframework.core.ResolvableType;
29  import org.springframework.core.codec.AbstractSingleValueEncoder;
30  import org.springframework.core.codec.CodecException;
31  import org.springframework.core.codec.EncodingException;
32  import org.springframework.core.codec.Hints;
33  import org.springframework.core.io.buffer.DataBuffer;
34  import org.springframework.core.io.buffer.DataBufferFactory;
35  import org.springframework.core.io.buffer.DataBufferUtils;
36  import org.springframework.core.log.LogFormatUtils;
37  import org.springframework.lang.NonNull;
38  import org.springframework.lang.Nullable;
39  import org.springframework.util.Assert;
40  import org.springframework.util.MimeType;
41  import org.springframework.util.MimeTypeUtils;
42  import reactor.core.publisher.Flux;
43  import reactor.core.publisher.Mono;
44  
45  /**
46   * Encode from single value to a byte stream containing XML elements.
47   *
48   * <p>{@link jakarta.xml.bind.annotation.XmlElements @XmlElements} and
49   * {@link jakarta.xml.bind.annotation.XmlElement @XmlElement} can be used to specify how collections
50   * should be marshalled.
51   *
52   * <p>The encoding parts are taken from {@link org.springframework.http.codec.xml.Jaxb2XmlEncoder}.
53   *
54   * @author Sebastien Deleuze
55   * @author Arjen Poutsma
56   * @author Christian Bremer
57   */
58  public class ReactiveJaxbEncoder extends AbstractSingleValueEncoder<Object> {
59  
60    private final JaxbContextBuilder jaxbContextBuilder;
61  
62    private final Set<Class<?>> ignoreWritingClasses;
63  
64    /**
65     * Instantiates a new reactive jaxb encoder.
66     *
67     * @param jaxbContextBuilder the jaxb context builder
68     */
69    public ReactiveJaxbEncoder(JaxbContextBuilder jaxbContextBuilder) {
70      this(jaxbContextBuilder, null);
71    }
72  
73    /**
74     * Instantiates a new reactive jaxb encoder.
75     *
76     * @param jaxbContextBuilder the jaxb context builder
77     * @param ignoreWritingClasses ignore writing classes
78     */
79    public ReactiveJaxbEncoder(
80        JaxbContextBuilder jaxbContextBuilder,
81        Set<Class<?>> ignoreWritingClasses) {
82  
83      super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML);
84      Assert.notNull(jaxbContextBuilder, "JaxbContextBuilder must be present.");
85      this.jaxbContextBuilder = jaxbContextBuilder;
86      this.ignoreWritingClasses = isNull(ignoreWritingClasses) ? Set.of() : ignoreWritingClasses;
87    }
88  
89    @Override
90    public boolean canEncode(
91        @NonNull ResolvableType elementType,
92        @Nullable final MimeType mimeType) {
93  
94      if (super.canEncode(elementType, mimeType)) {
95        final Class<?> outputClass = elementType.toClass();
96        return !ignoreWritingClasses.contains(outputClass)
97            && jaxbContextBuilder.canMarshal(outputClass);
98      } else {
99        return false;
100     }
101   }
102 
103   @NonNull
104   @Override
105   protected Flux<DataBuffer> encode(
106       @NonNull Object value,
107       @NonNull DataBufferFactory bufferFactory,
108       @NonNull ResolvableType valueType,
109       @Nullable MimeType mimeType,
110       @Nullable Map<String, Object> hints) {
111 
112     // we're relying on doOnDiscard in base class
113     return Mono.fromCallable(() -> encodeValue(value, bufferFactory, valueType, mimeType, hints))
114         .flux();
115   }
116 
117   @NonNull
118   @Override
119   public DataBuffer encodeValue(
120       @NonNull Object value,
121       @NonNull DataBufferFactory bufferFactory,
122       @NonNull ResolvableType valueType,
123       @Nullable MimeType mimeType,
124       @Nullable Map<String, Object> hints) {
125 
126     if (!Hints.isLoggingSuppressed(hints)) {
127       LogFormatUtils.traceDebug(logger, traceOn -> {
128         String formatted = LogFormatUtils.formatValue(value, !traceOn);
129         return Hints.getLogPrefix(hints) + "Encoding [" + formatted + "]";
130       });
131     }
132 
133     boolean release = true;
134     DataBuffer buffer = bufferFactory.allocateBuffer(1024);
135     try {
136       OutputStream outputStream = buffer.asOutputStream();
137       Marshaller marshaller = jaxbContextBuilder.buildMarshaller(value);
138       marshaller.marshal(value, outputStream);
139       release = false;
140       return buffer;
141     } catch (MarshalException ex) {
142       throw new EncodingException("Could not marshal " + value.getClass() + " to XML", ex);
143     } catch (JAXBException ex) {
144       throw new CodecException("Invalid JAXB configuration", ex);
145     } finally {
146       if (release) {
147         DataBufferUtils.release(buffer);
148       }
149     }
150   }
151 
152 }