001/*
002 * Copyright 2002-2020 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.converter;
018
019import java.io.IOException;
020import java.io.OutputStream;
021import java.nio.charset.Charset;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.List;
026
027import org.apache.commons.logging.Log;
028
029import org.springframework.http.HttpHeaders;
030import org.springframework.http.HttpInputMessage;
031import org.springframework.http.HttpLogging;
032import org.springframework.http.HttpOutputMessage;
033import org.springframework.http.MediaType;
034import org.springframework.http.StreamingHttpOutputMessage;
035import org.springframework.lang.Nullable;
036import org.springframework.util.Assert;
037
038/**
039 * Abstract base class for most {@link HttpMessageConverter} implementations.
040 *
041 * <p>This base class adds support for setting supported {@code MediaTypes}, through the
042 * {@link #setSupportedMediaTypes(List) supportedMediaTypes} bean property. It also adds
043 * support for {@code Content-Type} and {@code Content-Length} when writing to output messages.
044 *
045 * @author Arjen Poutsma
046 * @author Juergen Hoeller
047 * @author Sebastien Deleuze
048 * @since 3.0
049 * @param <T> the converted object type
050 */
051public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConverter<T> {
052
053        /** Logger available to subclasses. */
054        protected final Log logger = HttpLogging.forLogName(getClass());
055
056        private List<MediaType> supportedMediaTypes = Collections.emptyList();
057
058        @Nullable
059        private Charset defaultCharset;
060
061
062        /**
063         * Construct an {@code AbstractHttpMessageConverter} with no supported media types.
064         * @see #setSupportedMediaTypes
065         */
066        protected AbstractHttpMessageConverter() {
067        }
068
069        /**
070         * Construct an {@code AbstractHttpMessageConverter} with one supported media type.
071         * @param supportedMediaType the supported media type
072         */
073        protected AbstractHttpMessageConverter(MediaType supportedMediaType) {
074                setSupportedMediaTypes(Collections.singletonList(supportedMediaType));
075        }
076
077        /**
078         * Construct an {@code AbstractHttpMessageConverter} with multiple supported media types.
079         * @param supportedMediaTypes the supported media types
080         */
081        protected AbstractHttpMessageConverter(MediaType... supportedMediaTypes) {
082                setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
083        }
084
085        /**
086         * Construct an {@code AbstractHttpMessageConverter} with a default charset and
087         * multiple supported media types.
088         * @param defaultCharset the default character set
089         * @param supportedMediaTypes the supported media types
090         * @since 4.3
091         */
092        protected AbstractHttpMessageConverter(Charset defaultCharset, MediaType... supportedMediaTypes) {
093                this.defaultCharset = defaultCharset;
094                setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
095        }
096
097
098        /**
099         * Set the list of {@link MediaType} objects supported by this converter.
100         */
101        public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
102                Assert.notEmpty(supportedMediaTypes, "MediaType List must not be empty");
103                this.supportedMediaTypes = new ArrayList<>(supportedMediaTypes);
104        }
105
106        @Override
107        public List<MediaType> getSupportedMediaTypes() {
108                return Collections.unmodifiableList(this.supportedMediaTypes);
109        }
110
111        /**
112         * Set the default character set, if any.
113         * @since 4.3
114         */
115        public void setDefaultCharset(@Nullable Charset defaultCharset) {
116                this.defaultCharset = defaultCharset;
117        }
118
119        /**
120         * Return the default character set, if any.
121         * @since 4.3
122         */
123        @Nullable
124        public Charset getDefaultCharset() {
125                return this.defaultCharset;
126        }
127
128
129        /**
130         * This implementation checks if the given class is {@linkplain #supports(Class) supported},
131         * and if the {@linkplain #getSupportedMediaTypes() supported media types}
132         * {@linkplain MediaType#includes(MediaType) include} the given media type.
133         */
134        @Override
135        public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
136                return supports(clazz) && canRead(mediaType);
137        }
138
139        /**
140         * Returns {@code true} if any of the {@linkplain #setSupportedMediaTypes(List)
141         * supported} media types {@link MediaType#includes(MediaType) include} the
142         * given media type.
143         * @param mediaType the media type to read, can be {@code null} if not specified.
144         * Typically the value of a {@code Content-Type} header.
145         * @return {@code true} if the supported media types include the media type,
146         * or if the media type is {@code null}
147         */
148        protected boolean canRead(@Nullable MediaType mediaType) {
149                if (mediaType == null) {
150                        return true;
151                }
152                for (MediaType supportedMediaType : getSupportedMediaTypes()) {
153                        if (supportedMediaType.includes(mediaType)) {
154                                return true;
155                        }
156                }
157                return false;
158        }
159
160        /**
161         * This implementation checks if the given class is
162         * {@linkplain #supports(Class) supported}, and if the
163         * {@linkplain #getSupportedMediaTypes() supported} media types
164         * {@linkplain MediaType#includes(MediaType) include} the given media type.
165         */
166        @Override
167        public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
168                return supports(clazz) && canWrite(mediaType);
169        }
170
171        /**
172         * Returns {@code true} if the given media type includes any of the
173         * {@linkplain #setSupportedMediaTypes(List) supported media types}.
174         * @param mediaType the media type to write, can be {@code null} if not specified.
175         * Typically the value of an {@code Accept} header.
176         * @return {@code true} if the supported media types are compatible with the media type,
177         * or if the media type is {@code null}
178         */
179        protected boolean canWrite(@Nullable MediaType mediaType) {
180                if (mediaType == null || MediaType.ALL.equalsTypeAndSubtype(mediaType)) {
181                        return true;
182                }
183                for (MediaType supportedMediaType : getSupportedMediaTypes()) {
184                        if (supportedMediaType.isCompatibleWith(mediaType)) {
185                                return true;
186                        }
187                }
188                return false;
189        }
190
191        /**
192         * This implementation simple delegates to {@link #readInternal(Class, HttpInputMessage)}.
193         * Future implementations might add some default behavior, however.
194         */
195        @Override
196        public final T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
197                        throws IOException, HttpMessageNotReadableException {
198
199                return readInternal(clazz, inputMessage);
200        }
201
202        /**
203         * This implementation sets the default headers by calling {@link #addDefaultHeaders},
204         * and then calls {@link #writeInternal}.
205         */
206        @Override
207        public final void write(final T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
208                        throws IOException, HttpMessageNotWritableException {
209
210                final HttpHeaders headers = outputMessage.getHeaders();
211                addDefaultHeaders(headers, t, contentType);
212
213                if (outputMessage instanceof StreamingHttpOutputMessage) {
214                        StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
215                        streamingOutputMessage.setBody(outputStream -> writeInternal(t, new HttpOutputMessage() {
216                                @Override
217                                public OutputStream getBody() {
218                                        return outputStream;
219                                }
220                                @Override
221                                public HttpHeaders getHeaders() {
222                                        return headers;
223                                }
224                        }));
225                }
226                else {
227                        writeInternal(t, outputMessage);
228                        outputMessage.getBody().flush();
229                }
230        }
231
232        /**
233         * Add default headers to the output message.
234         * <p>This implementation delegates to {@link #getDefaultContentType(Object)} if a
235         * content type was not provided, set if necessary the default character set, calls
236         * {@link #getContentLength}, and sets the corresponding headers.
237         * @since 4.2
238         */
239        protected void addDefaultHeaders(HttpHeaders headers, T t, @Nullable MediaType contentType) throws IOException {
240                if (headers.getContentType() == null) {
241                        MediaType contentTypeToUse = contentType;
242                        if (contentType == null || !contentType.isConcrete()) {
243                                contentTypeToUse = getDefaultContentType(t);
244                        }
245                        else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
246                                MediaType mediaType = getDefaultContentType(t);
247                                contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
248                        }
249                        if (contentTypeToUse != null) {
250                                if (contentTypeToUse.getCharset() == null) {
251                                        Charset defaultCharset = getDefaultCharset();
252                                        if (defaultCharset != null) {
253                                                contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
254                                        }
255                                }
256                                headers.setContentType(contentTypeToUse);
257                        }
258                }
259                if (headers.getContentLength() < 0 && !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
260                        Long contentLength = getContentLength(t, headers.getContentType());
261                        if (contentLength != null) {
262                                headers.setContentLength(contentLength);
263                        }
264                }
265        }
266
267        /**
268         * Returns the default content type for the given type. Called when {@link #write}
269         * is invoked without a specified content type parameter.
270         * <p>By default, this returns the first element of the
271         * {@link #setSupportedMediaTypes(List) supportedMediaTypes} property, if any.
272         * Can be overridden in subclasses.
273         * @param t the type to return the content type for
274         * @return the content type, or {@code null} if not known
275         */
276        @Nullable
277        protected MediaType getDefaultContentType(T t) throws IOException {
278                List<MediaType> mediaTypes = getSupportedMediaTypes();
279                return (!mediaTypes.isEmpty() ? mediaTypes.get(0) : null);
280        }
281
282        /**
283         * Returns the content length for the given type.
284         * <p>By default, this returns {@code null}, meaning that the content length is unknown.
285         * Can be overridden in subclasses.
286         * @param t the type to return the content length for
287         * @return the content length, or {@code null} if not known
288         */
289        @Nullable
290        protected Long getContentLength(T t, @Nullable MediaType contentType) throws IOException {
291                return null;
292        }
293
294
295        /**
296         * Indicates whether the given class is supported by this converter.
297         * @param clazz the class to test for support
298         * @return {@code true} if supported; {@code false} otherwise
299         */
300        protected abstract boolean supports(Class<?> clazz);
301
302        /**
303         * Abstract template method that reads the actual object. Invoked from {@link #read}.
304         * @param clazz the type of object to return
305         * @param inputMessage the HTTP input message to read from
306         * @return the converted object
307         * @throws IOException in case of I/O errors
308         * @throws HttpMessageNotReadableException in case of conversion errors
309         */
310        protected abstract T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage)
311                        throws IOException, HttpMessageNotReadableException;
312
313        /**
314         * Abstract template method that writes the actual body. Invoked from {@link #write}.
315         * @param t the object to write to the output message
316         * @param outputMessage the HTTP output message to write to
317         * @throws IOException in case of I/O errors
318         * @throws HttpMessageNotWritableException in case of conversion errors
319         */
320        protected abstract void writeInternal(T t, HttpOutputMessage outputMessage)
321                        throws IOException, HttpMessageNotWritableException;
322
323}