| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more contributor license |
| * agreements. See the NOTICE file distributed with this work for additional information regarding |
| * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance with the License. You may obtain a |
| * copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software distributed under the License |
| * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
| * or implied. See the License for the specific language governing permissions and limitations under |
| * the License. |
| * |
| */ |
| |
| package org.apache.geode.redis.internal.data; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.apache.geode.cache.Region; |
| import org.apache.geode.redis.internal.executor.StripedExecutor; |
| import org.apache.geode.redis.internal.executor.string.RedisStringCommands; |
| import org.apache.geode.redis.internal.executor.string.RedisStringCommandsFunctionInvoker; |
| import org.apache.geode.redis.internal.executor.string.SetOptions; |
| import org.apache.geode.redis.internal.netty.Coder; |
| |
| /** |
| * Used when a RedisString instance does not exist. |
| * The code that looks for an existing instance will |
| * return this one, instead of null, when it does not |
| * find an existing instance. |
| * A canonical instance of this class will be used so |
| * care must be taken that it does not allow itself |
| * to be modified. |
| */ |
| public class NullRedisString extends RedisString { |
| public NullRedisString() { |
| super(new ByteArrayWrapper(new byte[0])); |
| } |
| |
| @Override |
| public boolean isNull() { |
| return true; |
| } |
| |
| @Override |
| protected void valueAppend(byte[] bytes) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| protected void valueSet(ByteArrayWrapper newValue) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| protected void valueSetBytes(byte[] bytes) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public ByteArrayWrapper get() { |
| return null; |
| } |
| |
| @Override |
| public int bitpos(Region<ByteArrayWrapper, RedisData> region, ByteArrayWrapper key, int bit, |
| int start, Integer end) { |
| if (bit == 0) { |
| return 0; |
| } else { |
| return -1; |
| } |
| } |
| |
| @Override |
| public int setbit( |
| Region<ByteArrayWrapper, RedisData> region, |
| ByteArrayWrapper key, int bitValue, int byteIndex, byte bitIndex) { |
| RedisString newValue; |
| if (bitValue == 1) { |
| byte[] bytes = new byte[byteIndex + 1]; |
| bytes[byteIndex] = (byte) (0x80 >> bitIndex); |
| newValue = new RedisString(new ByteArrayWrapper(bytes)); |
| } else { |
| // all bits are 0 so use an empty byte array |
| newValue = new RedisString(new ByteArrayWrapper(new byte[0])); |
| } |
| region.put(key, newValue); |
| return 0; |
| } |
| |
| @Override |
| public long incr(Region<ByteArrayWrapper, RedisData> region, ByteArrayWrapper key) |
| throws NumberFormatException, ArithmeticException { |
| byte[] newValue = {Coder.NUMBER_1_BYTE}; |
| region.put(key, new RedisString(new ByteArrayWrapper(newValue))); |
| return 1; |
| } |
| |
| @Override |
| public long incrby(Region<ByteArrayWrapper, RedisData> region, ByteArrayWrapper key, |
| long increment) throws NumberFormatException, ArithmeticException { |
| byte[] newValue = Coder.longToBytes(increment); |
| region.put(key, new RedisString(new ByteArrayWrapper(newValue))); |
| return increment; |
| } |
| |
| @Override |
| public double incrbyfloat(Region<ByteArrayWrapper, RedisData> region, ByteArrayWrapper key, |
| double increment) throws NumberFormatException, ArithmeticException { |
| byte[] newValue = Coder.doubleToBytes(increment); |
| region.put(key, new RedisString(new ByteArrayWrapper(newValue))); |
| return increment; |
| } |
| |
| @Override |
| public long decr(Region<ByteArrayWrapper, RedisData> region, ByteArrayWrapper key) |
| throws NumberFormatException, ArithmeticException { |
| region.put(key, new RedisString(new ByteArrayWrapper(Coder.stringToBytes("-1")))); |
| return -1; |
| } |
| |
| @Override |
| public long decrby(Region<ByteArrayWrapper, RedisData> region, ByteArrayWrapper key, |
| long decrement) { |
| byte[] newValue = Coder.longToBytes(-decrement); |
| region.put(key, new RedisString(new ByteArrayWrapper(newValue))); |
| return -decrement; |
| } |
| |
| @Override |
| public int append(ByteArrayWrapper appendValue, |
| Region<ByteArrayWrapper, RedisData> region, |
| ByteArrayWrapper key) { |
| region.put(key, new RedisString(appendValue)); |
| return appendValue.length(); |
| } |
| |
| @Override |
| public ByteArrayWrapper getset(Region<ByteArrayWrapper, RedisData> region, ByteArrayWrapper key, |
| ByteArrayWrapper value) { |
| region.put(key, new RedisString(value)); |
| return null; |
| } |
| |
| @Override |
| public int setrange(Region<ByteArrayWrapper, RedisData> region, ByteArrayWrapper key, int offset, |
| byte[] valueToAdd) { |
| byte[] newBytes = valueToAdd; |
| if (valueToAdd.length != 0) { |
| if (offset != 0) { |
| newBytes = new byte[offset + valueToAdd.length]; |
| System.arraycopy(valueToAdd, 0, newBytes, offset, valueToAdd.length); |
| } |
| region.put(key, new RedisString(new ByteArrayWrapper(newBytes))); |
| } |
| return newBytes.length; |
| } |
| |
| /** |
| * SET is currently mostly implemented here. It does not have an implementation on |
| * RedisString which is a bit odd. |
| */ |
| public boolean set(CommandHelper helper, ByteArrayWrapper key, |
| ByteArrayWrapper value, SetOptions options) { |
| if (options != null) { |
| if (options.isNX()) { |
| return setnx(helper, key, value, options); |
| } |
| |
| if (options.isXX() && helper.getRedisData(key).isNull()) { |
| return false; |
| } |
| } |
| |
| RedisString redisString = helper.setRedisString(key, value); |
| redisString.handleSetExpiration(options); |
| return true; |
| } |
| |
| private boolean setnx(CommandHelper helper, ByteArrayWrapper key, |
| ByteArrayWrapper value, SetOptions options) { |
| if (helper.getRedisData(key).exists()) { |
| return false; |
| } |
| RedisString redisString = new RedisString(value); |
| redisString.handleSetExpiration(options); |
| helper.getRegion().put(key, redisString); |
| return true; |
| } |
| |
| /** |
| * bitop is currently only implemented here. It does not have an implementation on |
| * RedisString which is a bit odd. This implementation only has a couple of places |
| * that care if a RedisString for "key" exists. |
| */ |
| public int bitop(CommandHelper helper, |
| String operation, |
| ByteArrayWrapper key, List<ByteArrayWrapper> sources) { |
| List<ByteArrayWrapper> sourceValues = new ArrayList<>(); |
| int selfIndex = -1; |
| // Read all the source values, except for self, before locking the stripe. |
| RedisStringCommands commander = |
| new RedisStringCommandsFunctionInvoker(helper.getRegion()); |
| for (ByteArrayWrapper sourceKey : sources) { |
| if (sourceKey.equals(key)) { |
| // get self later after the stripe is locked |
| selfIndex = sourceValues.size(); |
| sourceValues.add(null); |
| } else { |
| sourceValues.add(commander.get(sourceKey)); |
| } |
| } |
| int indexOfSelf = selfIndex; |
| StripedExecutor stripedExecutor = helper.getStripedExecutor(); |
| return stripedExecutor.execute(key, |
| () -> doBitOp(helper, operation, key, indexOfSelf, sourceValues)); |
| } |
| |
| private enum BitOp { |
| AND, OR, XOR |
| } |
| |
| private int doBitOp(CommandHelper helper, |
| String operation, |
| ByteArrayWrapper key, |
| int selfIndex, |
| List<ByteArrayWrapper> sourceValues) { |
| if (selfIndex != -1) { |
| RedisString redisString = helper.getRedisString(key); |
| if (!redisString.isNull()) { |
| sourceValues.set(selfIndex, redisString.getValue()); |
| } |
| } |
| int maxLength = 0; |
| for (ByteArrayWrapper sourceValue : sourceValues) { |
| if (sourceValue != null && maxLength < sourceValue.length()) { |
| maxLength = sourceValue.length(); |
| } |
| } |
| ByteArrayWrapper newValue; |
| switch (operation) { |
| case "AND": |
| newValue = doBitOp(BitOp.AND, sourceValues, maxLength); |
| break; |
| case "OR": |
| newValue = doBitOp(BitOp.OR, sourceValues, maxLength); |
| break; |
| case "XOR": |
| newValue = doBitOp(BitOp.XOR, sourceValues, maxLength); |
| break; |
| default: // NOT |
| newValue = not(sourceValues.get(0), maxLength); |
| break; |
| } |
| if (newValue.length() == 0) { |
| helper.getRegion().remove(key); |
| } else { |
| helper.setRedisString(key, newValue); |
| } |
| return newValue.length(); |
| } |
| |
| private ByteArrayWrapper doBitOp(BitOp bitOp, List<ByteArrayWrapper> sourceValues, int max) { |
| byte[] dest = new byte[max]; |
| for (int i = 0; i < max; i++) { |
| byte b = 0; |
| boolean firstByte = true; |
| for (ByteArrayWrapper sourceValue : sourceValues) { |
| byte sourceByte = 0; |
| if (sourceValue != null && i < sourceValue.length()) { |
| sourceByte = sourceValue.toBytes()[i]; |
| } |
| if (firstByte) { |
| b = sourceByte; |
| firstByte = false; |
| } else { |
| switch (bitOp) { |
| case AND: |
| b &= sourceByte; |
| break; |
| case OR: |
| b |= sourceByte; |
| break; |
| case XOR: |
| b ^= sourceByte; |
| break; |
| } |
| } |
| } |
| dest[i] = b; |
| } |
| return new ByteArrayWrapper(dest); |
| } |
| |
| private ByteArrayWrapper not(ByteArrayWrapper sourceValue, int max) { |
| byte[] dest = new byte[max]; |
| if (sourceValue == null) { |
| for (int i = 0; i < max; i++) { |
| dest[i] = ~0; |
| } |
| } else { |
| byte[] cA = sourceValue.toBytes(); |
| for (int i = 0; i < max; i++) { |
| dest[i] = (byte) (~cA[i] & 0xFF); |
| } |
| } |
| return new ByteArrayWrapper(dest); |
| } |
| |
| } |