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.nio.charset.Charset;
021import java.nio.charset.StandardCharsets;
022import java.util.ArrayList;
023import java.util.List;
024
025import org.springframework.http.HttpHeaders;
026import org.springframework.http.HttpInputMessage;
027import org.springframework.http.HttpOutputMessage;
028import org.springframework.http.MediaType;
029import org.springframework.lang.Nullable;
030import org.springframework.util.Assert;
031import org.springframework.util.StreamUtils;
032
033/**
034 * Implementation of {@link HttpMessageConverter} that can read and write strings.
035 *
036 * <p>By default, this converter supports all media types (<code>&#42;/&#42;</code>),
037 * and writes with a {@code Content-Type} of {@code text/plain}. This can be overridden
038 * by setting the {@link #setSupportedMediaTypes supportedMediaTypes} property.
039 *
040 * @author Arjen Poutsma
041 * @author Juergen Hoeller
042 * @since 3.0
043 */
044public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {
045
046        private static final MediaType APPLICATION_PLUS_JSON = new MediaType("application", "*+json");
047
048        /**
049         * The default charset used by the converter.
050         */
051        public static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1;
052
053
054        @Nullable
055        private volatile List<Charset> availableCharsets;
056
057        private boolean writeAcceptCharset = false;
058
059
060        /**
061         * A default constructor that uses {@code "ISO-8859-1"} as the default charset.
062         * @see #StringHttpMessageConverter(Charset)
063         */
064        public StringHttpMessageConverter() {
065                this(DEFAULT_CHARSET);
066        }
067
068        /**
069         * A constructor accepting a default charset to use if the requested content
070         * type does not specify one.
071         */
072        public StringHttpMessageConverter(Charset defaultCharset) {
073                super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);
074        }
075
076
077        /**
078         * Whether the {@code Accept-Charset} header should be written to any outgoing
079         * request sourced from the value of {@link Charset#availableCharsets()}.
080         * The behavior is suppressed if the header has already been set.
081         * <p>As of 5.2, by default is set to {@code false}.
082         */
083        public void setWriteAcceptCharset(boolean writeAcceptCharset) {
084                this.writeAcceptCharset = writeAcceptCharset;
085        }
086
087
088        @Override
089        public boolean supports(Class<?> clazz) {
090                return String.class == clazz;
091        }
092
093        @Override
094        protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
095                Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
096                return StreamUtils.copyToString(inputMessage.getBody(), charset);
097        }
098
099        @Override
100        protected Long getContentLength(String str, @Nullable MediaType contentType) {
101                Charset charset = getContentTypeCharset(contentType);
102                return (long) str.getBytes(charset).length;
103        }
104
105
106        @Override
107        protected void addDefaultHeaders(HttpHeaders headers, String s, @Nullable MediaType type) throws IOException {
108                if (headers.getContentType() == null ) {
109                        if (type != null && type.isConcrete() &&
110                                        (type.isCompatibleWith(MediaType.APPLICATION_JSON) ||
111                                        type.isCompatibleWith(APPLICATION_PLUS_JSON))) {
112                                // Prevent charset parameter for JSON..
113                                headers.setContentType(type);
114                        }
115                }
116                super.addDefaultHeaders(headers, s, type);
117        }
118
119        @Override
120        protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
121                HttpHeaders headers = outputMessage.getHeaders();
122                if (this.writeAcceptCharset && headers.get(HttpHeaders.ACCEPT_CHARSET) == null) {
123                        headers.setAcceptCharset(getAcceptedCharsets());
124                }
125                Charset charset = getContentTypeCharset(headers.getContentType());
126                StreamUtils.copy(str, charset, outputMessage.getBody());
127        }
128
129
130        /**
131         * Return the list of supported {@link Charset Charsets}.
132         * <p>By default, returns {@link Charset#availableCharsets()}.
133         * Can be overridden in subclasses.
134         * @return the list of accepted charsets
135         */
136        protected List<Charset> getAcceptedCharsets() {
137                List<Charset> charsets = this.availableCharsets;
138                if (charsets == null) {
139                        charsets = new ArrayList<>(Charset.availableCharsets().values());
140                        this.availableCharsets = charsets;
141                }
142                return charsets;
143        }
144
145        private Charset getContentTypeCharset(@Nullable MediaType contentType) {
146                if (contentType != null && contentType.getCharset() != null) {
147                        return contentType.getCharset();
148                }
149                else if (contentType != null &&
150                                (contentType.isCompatibleWith(MediaType.APPLICATION_JSON) ||
151                                                contentType.isCompatibleWith(APPLICATION_PLUS_JSON))) {
152                        // Matching to AbstractJackson2HttpMessageConverter#DEFAULT_CHARSET
153                        return StandardCharsets.UTF_8;
154                }
155                else {
156                        Charset charset = getDefaultCharset();
157                        Assert.state(charset != null, "No default charset");
158                        return charset;
159                }
160        }
161
162}