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}