package com.lambdaworks.redis.codec; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import com.lambdaworks.redis.internal.LettuceAssert; import com.lambdaworks.redis.protocol.LettuceCharsets; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; /** * Optimized String codec. This {@link RedisCodec} encodes and decodes {@link String} keys and values using a specified * {@link Charset}. It accepts provided {@link ByteBuf buffers} so it does not need to allocate buffers during encoding. * * @author Mark Paluch * @since 4.3 */ public class StringCodec implements RedisCodec<String, String>, ToByteBufEncoder<String, String> { public static final StringCodec UTF8 = new StringCodec(LettuceCharsets.UTF8); public static final StringCodec ASCII = new StringCodec(LettuceCharsets.ASCII); private static final byte[] EMPTY = new byte[0]; private final Charset charset; private final boolean ascii; private final boolean utf8; /** * Creates a new {@link StringCodec} with the default {@link Charset#defaultCharset() charset}. The default is determined * from the {@code file.encoding} system property. */ public StringCodec() { this(Charset.defaultCharset()); } /** * Creates a new {@link StringCodec} for the given {@link Charset} that encodes and decodes keys and values. * * @param charset must not be {@literal null}. */ public StringCodec(Charset charset) { LettuceAssert.notNull(charset, "Charset must not be null"); this.charset = charset; if (charset.name().equals("UTF-8")) { utf8 = true; ascii = false; } else if (charset.name().contains("ASCII")) { utf8 = false; ascii = true; } else { ascii = false; utf8 = false; } } @Override public void encodeKey(String key, ByteBuf target) { encode(key, target); } public void encode(String str, ByteBuf target) { if (str == null) { return; } if (utf8) { ByteBufUtil.writeUtf8(target, str); return; } if (ascii) { ByteBufUtil.writeAscii(target, str); return; } CharsetEncoder encoder = CharsetUtil.encoder(charset); int length = (int) ((double) str.length() * encoder.maxBytesPerChar()); target.ensureWritable(length); try { final ByteBuffer dstBuf = target.nioBuffer(0, length); final int pos = dstBuf.position(); CoderResult cr = encoder.encode(CharBuffer.wrap(str), dstBuf, true); if (!cr.isUnderflow()) { cr.throwException(); } cr = encoder.flush(dstBuf); if (!cr.isUnderflow()) { cr.throwException(); } target.writerIndex(target.writerIndex() + dstBuf.position() - pos); } catch (CharacterCodingException x) { throw new IllegalStateException(x); } } @Override public int estimateSize(Object keyOrValue) { if (keyOrValue instanceof String) { CharsetEncoder encoder = CharsetUtil.encoder(charset); return (int) (encoder.averageBytesPerChar() * ((String) keyOrValue).length()); } return 0; } @Override public void encodeValue(String value, ByteBuf target) { encode(value, target); } @Override public String decodeKey(ByteBuffer bytes) { return Unpooled.wrappedBuffer(bytes).toString(charset); } @Override public String decodeValue(ByteBuffer bytes) { return Unpooled.wrappedBuffer(bytes).toString(charset); } @Override public ByteBuffer encodeKey(String key) { return encodeAndAllocateBuffer(key); } @Override public ByteBuffer encodeValue(String value) { return encodeAndAllocateBuffer(value); } /** * Compatibility implementation. * * @param key * @return */ private ByteBuffer encodeAndAllocateBuffer(String key) { if (key == null) { return ByteBuffer.wrap(EMPTY); } CharsetEncoder encoder = CharsetUtil.encoder(charset); ByteBuffer buffer = ByteBuffer.allocate((int) (encoder.maxBytesPerChar() * key.length())); ByteBuf byteBuf = Unpooled.wrappedBuffer(buffer); byteBuf.clear(); encode(key, byteBuf); buffer.limit(byteBuf.writerIndex()); return buffer; } }