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