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.ecm.core.redis.contribs;
020
021import static java.nio.charset.StandardCharsets.UTF_8;
022
023import java.io.IOException;
024import java.util.Arrays;
025import java.util.Collections;
026import java.util.List;
027import java.util.Map;
028
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031import org.nuxeo.ecm.core.api.NuxeoException;
032import org.nuxeo.ecm.core.redis.RedisAdmin;
033import org.nuxeo.ecm.core.redis.RedisExecutor;
034import org.nuxeo.ecm.core.storage.kv.KeyValueStoreProvider;
035import org.nuxeo.runtime.api.Framework;
036
037/**
038 * Redis implementation of a Key/Value Store Provider.
039 * <p>
040 * The following configuration properties are available:
041 * <ul>
042 * <li>namespace: the Redis namespace to use for keys (in addition to the global Redis namespace configured in the Redis
043 * service).
044 * </ul>
045 *
046 * @since 9.1
047 */
048public class RedisKeyValueStore implements KeyValueStoreProvider {
049
050    private static final Log log = LogFactory.getLog(RedisKeyValueStore.class);
051
052    public static final String NAMESPACE_PROP = "namespace";
053
054    protected static final Long ONE = Long.valueOf(1);
055
056    protected String namespace;
057
058    protected byte[] compareAndSetSHA;
059
060    protected byte[] compareAndDelSHA;
061
062    protected byte[] compareNullAndSetSHA;
063
064    protected static byte[] getBytes(String key) {
065        return key.getBytes(UTF_8);
066    }
067
068    @Override
069    public void initialize(Map<String, String> properties) {
070        log.debug("Initializing");
071        String name = properties.get(NAMESPACE_PROP);
072        RedisAdmin redisAdmin = Framework.getService(RedisAdmin.class);
073        namespace = redisAdmin.namespace(name == null ? new String[0] : new String[] { name });
074        try {
075            compareAndSetSHA = getBytes(redisAdmin.load("org.nuxeo.ecm.core.redis", "compare-and-set"));
076            compareAndDelSHA = getBytes(redisAdmin.load("org.nuxeo.ecm.core.redis", "compare-and-del"));
077            compareNullAndSetSHA = getBytes(redisAdmin.load("org.nuxeo.ecm.core.redis", "compare-null-and-set"));
078        } catch (IOException e) {
079            throw new NuxeoException("Cannot load Redis script", e);
080        }
081    }
082
083    @Override
084    public void close() {
085        log.debug("Closed");
086    }
087
088    @Override
089    public void clear() {
090        RedisAdmin redisAdmin = Framework.getService(RedisAdmin.class);
091        redisAdmin.clear(namespace + "*");
092    }
093
094    @Override
095    public void put(String key, byte[] value) {
096        RedisExecutor redisExecutor = Framework.getService(RedisExecutor.class);
097        redisExecutor.execute(jedis -> {
098            byte[] keyb = getBytes(namespace + key);
099            if (value == null) {
100                jedis.del(keyb);
101            } else {
102                jedis.set(keyb, value);
103            }
104            return null;
105        });
106    }
107
108    @Override
109    public byte[] get(String key) {
110        RedisExecutor redisExecutor = Framework.getService(RedisExecutor.class);
111        return redisExecutor.execute(jedis -> jedis.get(getBytes(namespace + key)));
112    }
113
114    @Override
115    public boolean compareAndSet(String key, byte[] expected, byte[] value) {
116        if (expected == null && value == null) {
117            return get(key) == null;
118        } else {
119            byte[] sha;
120            List<byte[]> keys = Collections.singletonList(getBytes(namespace + key));
121            List<byte[]> args;
122            if (expected == null) {
123                sha = compareNullAndSetSHA;
124                args = Collections.singletonList(value);
125            } else if (value == null) {
126                sha = compareAndDelSHA;
127                args = Collections.singletonList(expected);
128            } else {
129                sha = compareAndSetSHA;
130                args = Arrays.asList(expected, value);
131            }
132            RedisExecutor redisExecutor = Framework.getService(RedisExecutor.class);
133            Object result = redisExecutor.evalsha(sha, keys, args);
134            return ONE.equals(result);
135        }
136    }
137
138}