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