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.core.codec;
018
019import java.nio.charset.Charset;
020import java.nio.charset.CoderMalfunctionError;
021import java.nio.charset.StandardCharsets;
022import java.util.Map;
023import java.util.concurrent.ConcurrentHashMap;
024import java.util.concurrent.ConcurrentMap;
025
026import org.reactivestreams.Publisher;
027import reactor.core.publisher.Flux;
028
029import org.springframework.core.ResolvableType;
030import org.springframework.core.io.buffer.DataBuffer;
031import org.springframework.core.io.buffer.DataBufferFactory;
032import org.springframework.core.io.buffer.DataBufferUtils;
033import org.springframework.core.log.LogFormatUtils;
034import org.springframework.lang.Nullable;
035import org.springframework.util.MimeType;
036import org.springframework.util.MimeTypeUtils;
037
038/**
039 * Encode from a {@code CharSequence} stream to a bytes stream.
040 *
041 * @author Sebastien Deleuze
042 * @author Arjen Poutsma
043 * @author Rossen Stoyanchev
044 * @since 5.0
045 * @see StringDecoder
046 */
047public final class CharSequenceEncoder extends AbstractEncoder<CharSequence> {
048
049        /**
050         * The default charset used by the encoder.
051         */
052        public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
053
054        private final ConcurrentMap<Charset, Float> charsetToMaxBytesPerChar =
055                        new ConcurrentHashMap<>(3);
056
057
058        private CharSequenceEncoder(MimeType... mimeTypes) {
059                super(mimeTypes);
060        }
061
062
063        @Override
064        public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) {
065                Class<?> clazz = elementType.toClass();
066                return super.canEncode(elementType, mimeType) && CharSequence.class.isAssignableFrom(clazz);
067        }
068
069        @Override
070        public Flux<DataBuffer> encode(Publisher<? extends CharSequence> inputStream,
071                        DataBufferFactory bufferFactory, ResolvableType elementType,
072                        @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
073
074                return Flux.from(inputStream).map(charSequence ->
075                                encodeValue(charSequence, bufferFactory, elementType, mimeType, hints));
076        }
077
078        @Override
079        public DataBuffer encodeValue(CharSequence charSequence, DataBufferFactory bufferFactory,
080                        ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
081
082                if (!Hints.isLoggingSuppressed(hints)) {
083                        LogFormatUtils.traceDebug(logger, traceOn -> {
084                                String formatted = LogFormatUtils.formatValue(charSequence, !traceOn);
085                                return Hints.getLogPrefix(hints) + "Writing " + formatted;
086                        });
087                }
088                boolean release = true;
089                Charset charset = getCharset(mimeType);
090                int capacity = calculateCapacity(charSequence, charset);
091                DataBuffer dataBuffer = bufferFactory.allocateBuffer(capacity);
092                try {
093                        dataBuffer.write(charSequence, charset);
094                        release = false;
095                }
096                catch (CoderMalfunctionError ex) {
097                        throw new EncodingException("String encoding error: " + ex.getMessage(), ex);
098                }
099                finally {
100                        if (release) {
101                                DataBufferUtils.release(dataBuffer);
102                        }
103                }
104                return dataBuffer;
105        }
106
107        int calculateCapacity(CharSequence sequence, Charset charset) {
108                float maxBytesPerChar = this.charsetToMaxBytesPerChar
109                                .computeIfAbsent(charset, cs -> cs.newEncoder().maxBytesPerChar());
110                float maxBytesForSequence = sequence.length() * maxBytesPerChar;
111                return (int) Math.ceil(maxBytesForSequence);
112        }
113
114        private Charset getCharset(@Nullable MimeType mimeType) {
115                if (mimeType != null && mimeType.getCharset() != null) {
116                        return mimeType.getCharset();
117                }
118                else {
119                        return DEFAULT_CHARSET;
120                }
121        }
122
123
124        /**
125         * Create a {@code CharSequenceEncoder} that supports only "text/plain".
126         */
127        public static CharSequenceEncoder textPlainOnly() {
128                return new CharSequenceEncoder(new MimeType("text", "plain", DEFAULT_CHARSET));
129        }
130
131        /**
132         * Create a {@code CharSequenceEncoder} that supports all MIME types.
133         */
134        public static CharSequenceEncoder allMimeTypes() {
135                return new CharSequenceEncoder(new MimeType("text", "plain", DEFAULT_CHARSET), MimeTypeUtils.ALL);
136        }
137
138}