001/*
002 * (C) Copyright 2017 Nuxeo (http://nuxeo.com/) and others.
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 *     http://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 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.runtime.kv;
020
021import static java.nio.charset.StandardCharsets.UTF_8;
022
023import java.nio.ByteBuffer;
024import java.nio.charset.CharacterCodingException;
025import java.nio.charset.CharsetDecoder;
026import java.nio.charset.CodingErrorAction;
027import java.util.Collection;
028import java.util.HashMap;
029import java.util.Map;
030
031/**
032 * Key/Value Store common methods.
033 *
034 * @since 9.3
035 */
036public abstract class AbstractKeyValueStoreProvider implements KeyValueStoreProvider {
037
038    protected static final ThreadLocal<CharsetDecoder> UTF_8_DECODERS = ThreadLocal.withInitial(
039            () -> UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(
040                    CodingErrorAction.REPORT));
041
042    /**
043     * Converts UTF-8 bytes to a String, or throws if malformed.
044     *
045     * @throws CharacterCodingException
046     */
047    protected static String bytesToString(byte[] bytes) throws CharacterCodingException {
048        return bytes == null ? null : UTF_8_DECODERS.get().decode(ByteBuffer.wrap(bytes)).toString();
049    }
050
051    /**
052     * Converts a String to UTF-8 bytes.
053     */
054    protected static byte[] stringToBytes(String string) {
055        return string == null ? null : string.getBytes(UTF_8);
056    }
057
058    /**
059     * Converts UTF-8 bytes to a Long, or throws if malformed.
060     *
061     * @throws NumberFormatException
062     */
063    protected static Long bytesToLong(byte[] bytes) throws NumberFormatException { // NOSONAR
064        if (bytes == null) {
065            return null;
066        }
067        if (bytes.length > 20) { // Long.MIN_VALUE has 20 characters including the sign
068            throw new NumberFormatException("For input string of length " + bytes.length);
069        }
070        return Long.valueOf(new String(bytes, UTF_8));
071    }
072
073    /**
074     * Converts a long to UTF-8 bytes.
075     */
076    protected static byte[] longToBytes(Long value) {
077        return value == null ? null : value.toString().getBytes(UTF_8);
078    }
079
080    @Override
081    public void put(String key, byte[] value) {
082        put(key, value, 0);
083    }
084
085    @Override
086    public void put(String key, String value) {
087        put(key, stringToBytes(value), 0);
088    }
089
090    @Override
091    public void put(String key, String value, long ttl) {
092        put(key, stringToBytes(value), ttl);
093    }
094
095    @Override
096    public void put(String key, Long value) {
097        put(key, longToBytes(value), 0);
098    }
099
100    @Override
101    public void put(String key, Long value, long ttl) {
102        put(key, longToBytes(value), ttl);
103    }
104
105    @Override
106    public String getString(String key) {
107        byte[] bytes = get(key);
108        try {
109            return bytesToString(bytes);
110        } catch (CharacterCodingException e) {
111            throw new IllegalArgumentException("Value is not a String for key: " + key);
112        }
113    }
114
115    @Override
116    public Long getLong(String key) throws NumberFormatException { // NOSONAR
117        byte[] bytes = get(key);
118        return bytesToLong(bytes);
119    }
120
121    /*
122     * This default implementation is uninteresting. It is expected that underlying storage implementations
123     * will leverage bulk fetching to deliver significant optimizations over this simple loop.
124     */
125    @Override
126    public Map<String, byte[]> get(Collection<String> keys) {
127        Map<String, byte[]> map = new HashMap<>(keys.size());
128        for (String key : keys) {
129            byte[] value = get(key);
130            if (value != null) {
131                map.put(key, value);
132            }
133        }
134        return map;
135    }
136
137    /*
138     * This default implementation is uninteresting. It is expected that underlying storage implementations
139     * will leverage bulk fetching to deliver significant optimizations over this simple loop.
140     */
141    @Override
142    public Map<String, String> getStrings(Collection<String> keys) {
143        Map<String, String> map = new HashMap<>(keys.size());
144        for (String key : keys) {
145            String value = getString(key);
146            if (value != null) {
147                map.put(key, value);
148            }
149        }
150        return map;
151    }
152
153    /*
154     * This default implementation is uninteresting. It is expected that underlying storage implementations
155     * will leverage bulk fetching to deliver significant optimizations over this simple loop.
156     */
157    @Override
158    public Map<String, Long> getLongs(Collection<String> keys) throws NumberFormatException { // NOSONAR
159        Map<String, Long> map = new HashMap<>(keys.size());
160        for (String key : keys) {
161            Long value = getLong(key);
162            if (value != null) {
163                map.put(key, value);
164            }
165        }
166        return map;
167    }
168
169    @Override
170    public boolean compareAndSet(String key, byte[] expected, byte[] value) {
171        return compareAndSet(key, expected, value, 0);
172    }
173
174    @Override
175    public boolean compareAndSet(String key, String expected, String value) {
176        return compareAndSet(key, expected, value, 0);
177    }
178
179    @Override
180    public boolean compareAndSet(String key, String expected, String value, long ttl) {
181        return compareAndSet(key, stringToBytes(expected), stringToBytes(value), ttl);
182    }
183
184    @Override
185    public long addAndGet(String key, long delta) throws NumberFormatException { // NOSONAR
186        for (;;) {
187            String value = getString(key);
188            long base = value == null ? 0 : Long.parseLong(value);
189            long result = base + delta;
190            String newValue = Long.toString(result);
191            if (compareAndSet(key, value, newValue)) {
192                return result;
193            }
194        }
195    }
196
197}