/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.util.IterablePair;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;
public class DirectoryManager {
private final Logger logger = LoggerFactory.getLogger(DirectoryManager.class);
private static final byte[] DIRECTORY_KEY = {'d', 'i', 'r', 'e', 'c', 't', 'o', 'r', 'y'};
private final ObjectMapper objectMapper;
private final JedisPool redisPool;
public DirectoryManager(JedisPool redisPool) {
this.redisPool = redisPool;
this.objectMapper = new ObjectMapper();
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public void remove(String number) {
remove(Util.getContactToken(number));
}
public void remove(BatchOperationHandle handle, String number) {
remove(handle, Util.getContactToken(number));
}
public void remove(byte[] token) {
Jedis jedis = redisPool.getResource();
jedis.hdel(DIRECTORY_KEY, token);
redisPool.returnResource(jedis);
}
public void remove(BatchOperationHandle handle, byte[] token) {
Pipeline pipeline = handle.pipeline;
pipeline.hdel(DIRECTORY_KEY, token);
}
public void add(ClientContact contact) {
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms());
try (Jedis jedis = redisPool.getResource()) {
jedis.hset(DIRECTORY_KEY, contact.getToken(), objectMapper.writeValueAsBytes(tokenValue));
} catch (JsonProcessingException e) {
logger.warn("JSON Serialization", e);
}
}
public void add(BatchOperationHandle handle, ClientContact contact) {
try {
Pipeline pipeline = handle.pipeline;
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isSupportsSms());
pipeline.hset(DIRECTORY_KEY, contact.getToken(), objectMapper.writeValueAsBytes(tokenValue));
} catch (JsonProcessingException e) {
logger.warn("JSON Serialization", e);
}
}
public PendingClientContact get(BatchOperationHandle handle, byte[] token) {
Pipeline pipeline = handle.pipeline;
return new PendingClientContact(objectMapper, token, pipeline.hget(DIRECTORY_KEY, token));
}
public Optional<ClientContact> get(byte[] token) {
try (Jedis jedis = redisPool.getResource()) {
byte[] result = jedis.hget(DIRECTORY_KEY, token);
if (result == null) {
return Optional.absent();
}
TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.supportsSms));
} catch (IOException e) {
logger.warn("JSON Error", e);
return Optional.absent();
}
}
public List<ClientContact> get(List<byte[]> tokens) {
try (Jedis jedis = redisPool.getResource()) {
Pipeline pipeline = jedis.pipelined();
List<Response<byte[]>> futures = new LinkedList<>();
List<ClientContact> results = new LinkedList<>();
try {
for (byte[] token : tokens) {
futures.add(pipeline.hget(DIRECTORY_KEY, token));
}
} finally {
pipeline.sync();
}
IterablePair<byte[], Response<byte[]>> lists = new IterablePair<>(tokens, futures);
for (Pair<byte[], Response<byte[]>> pair : lists) {
try {
if (pair.second().get() != null) {
TokenValue tokenValue = objectMapper.readValue(pair.second().get(), TokenValue.class);
ClientContact clientContact = new ClientContact(pair.first(), tokenValue.relay, tokenValue.supportsSms);
results.add(clientContact);
}
} catch (IOException e) {
logger.warn("Deserialization Problem: ", e);
}
}
return results;
}
}
public BatchOperationHandle startBatchOperation() {
Jedis jedis = redisPool.getResource();
return new BatchOperationHandle(jedis, jedis.pipelined());
}
public void stopBatchOperation(BatchOperationHandle handle) {
Pipeline pipeline = handle.pipeline;
Jedis jedis = handle.jedis;
pipeline.sync();
redisPool.returnResource(jedis);
}
public static class BatchOperationHandle {
public final Pipeline pipeline;
public final Jedis jedis;
public BatchOperationHandle(Jedis jedis, Pipeline pipeline) {
this.pipeline = pipeline;
this.jedis = jedis;
}
}
private static class TokenValue {
@JsonProperty(value = "r")
private String relay;
@JsonProperty(value = "s")
private boolean supportsSms;
public TokenValue() {}
public TokenValue(String relay, boolean supportsSms) {
this.relay = relay;
this.supportsSms = supportsSms;
}
}
public static class PendingClientContact {
private final ObjectMapper objectMapper;
private final byte[] token;
private final Response<byte[]> response;
PendingClientContact(ObjectMapper objectMapper, byte[] token, Response<byte[]> response) {
this.objectMapper = objectMapper;
this.token = token;
this.response = response;
}
public Optional<ClientContact> get() throws IOException {
byte[] result = response.get();
if (result == null) {
return Optional.absent();
}
TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.supportsSms));
}
}
}