001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.http.codec.xml;
018
019import java.io.OutputStream;
020import java.nio.charset.StandardCharsets;
021import java.util.Map;
022import java.util.function.Function;
023
024import javax.xml.bind.JAXBException;
025import javax.xml.bind.MarshalException;
026import javax.xml.bind.Marshaller;
027import javax.xml.bind.annotation.XmlRootElement;
028import javax.xml.bind.annotation.XmlType;
029
030import reactor.core.publisher.Flux;
031import reactor.core.publisher.Mono;
032
033import org.springframework.core.ResolvableType;
034import org.springframework.core.codec.AbstractSingleValueEncoder;
035import org.springframework.core.codec.CodecException;
036import org.springframework.core.codec.EncodingException;
037import org.springframework.core.codec.Hints;
038import org.springframework.core.io.buffer.DataBuffer;
039import org.springframework.core.io.buffer.DataBufferFactory;
040import org.springframework.core.io.buffer.DataBufferUtils;
041import org.springframework.core.log.LogFormatUtils;
042import org.springframework.lang.Nullable;
043import org.springframework.util.ClassUtils;
044import org.springframework.util.MimeType;
045import org.springframework.util.MimeTypeUtils;
046
047/**
048 * Encode from single value to a byte stream containing XML elements.
049 *
050 * <p>{@link javax.xml.bind.annotation.XmlElements @XmlElements} and
051 * {@link javax.xml.bind.annotation.XmlElement @XmlElement} can be used
052 * to specify how collections should be marshalled.
053 *
054 * @author Sebastien Deleuze
055 * @author Arjen Poutsma
056 * @since 5.0
057 * @see Jaxb2XmlDecoder
058 */
059public class Jaxb2XmlEncoder extends AbstractSingleValueEncoder<Object> {
060
061        private final JaxbContextContainer jaxbContexts = new JaxbContextContainer();
062
063        private Function<Marshaller, Marshaller> marshallerProcessor = Function.identity();
064
065
066        public Jaxb2XmlEncoder() {
067                super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML);
068        }
069
070
071        /**
072         * Configure a processor function to customize Marshaller instances.
073         * @param processor the function to use
074         * @since 5.1.3
075         */
076        public void setMarshallerProcessor(Function<Marshaller, Marshaller> processor) {
077                this.marshallerProcessor = this.marshallerProcessor.andThen(processor);
078        }
079
080        /**
081         * Return the configured processor for customizing Marshaller instances.
082         * @since 5.1.3
083         */
084        public Function<Marshaller, Marshaller> getMarshallerProcessor() {
085                return this.marshallerProcessor;
086        }
087
088
089        @Override
090        public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) {
091                if (super.canEncode(elementType, mimeType)) {
092                        Class<?> outputClass = elementType.toClass();
093                        return (outputClass.isAnnotationPresent(XmlRootElement.class) ||
094                                        outputClass.isAnnotationPresent(XmlType.class));
095                }
096                else {
097                        return false;
098                }
099        }
100
101        @Override
102        protected Flux<DataBuffer> encode(Object value, DataBufferFactory bufferFactory,
103                        ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
104
105                // we're relying on doOnDiscard in base class
106                return Mono.fromCallable(() -> encodeValue(value, bufferFactory, valueType, mimeType, hints)).flux();
107        }
108
109        @Override
110        public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory,
111                        ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
112
113                if (!Hints.isLoggingSuppressed(hints)) {
114                        LogFormatUtils.traceDebug(logger, traceOn -> {
115                                String formatted = LogFormatUtils.formatValue(value, !traceOn);
116                                return Hints.getLogPrefix(hints) + "Encoding [" + formatted + "]";
117                        });
118                }
119
120                boolean release = true;
121                DataBuffer buffer = bufferFactory.allocateBuffer(1024);
122                try {
123                        OutputStream outputStream = buffer.asOutputStream();
124                        Class<?> clazz = ClassUtils.getUserClass(value);
125                        Marshaller marshaller = initMarshaller(clazz);
126                        marshaller.marshal(value, outputStream);
127                        release = false;
128                        return buffer;
129                }
130                catch (MarshalException ex) {
131                        throw new EncodingException("Could not marshal " + value.getClass() + " to XML", ex);
132                }
133                catch (JAXBException ex) {
134                        throw new CodecException("Invalid JAXB configuration", ex);
135                }
136                finally {
137                        if (release) {
138                                DataBufferUtils.release(buffer);
139                        }
140                }
141        }
142
143        private Marshaller initMarshaller(Class<?> clazz) throws CodecException, JAXBException {
144                Marshaller marshaller = this.jaxbContexts.createMarshaller(clazz);
145                marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name());
146                marshaller = this.marshallerProcessor.apply(marshaller);
147                return marshaller;
148        }
149
150}