package org.apereo.cas.services;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest;
import com.amazonaws.services.dynamodbv2.model.DeleteItemResult;
import com.amazonaws.services.dynamodbv2.model.DeleteTableRequest;
import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest;
import com.amazonaws.services.dynamodbv2.model.GetItemRequest;
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
import com.amazonaws.services.dynamodbv2.model.KeyType;
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
import com.amazonaws.services.dynamodbv2.model.PutItemRequest;
import com.amazonaws.services.dynamodbv2.model.PutItemResult;
import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;
import com.amazonaws.services.dynamodbv2.model.ScanRequest;
import com.amazonaws.services.dynamodbv2.model.ScanResult;
import com.amazonaws.services.dynamodbv2.model.TableDescription;
import com.amazonaws.services.dynamodbv2.util.TableUtils;
import com.google.common.base.Throwables;
import org.apereo.cas.configuration.model.support.dynamodb.DynamoDbServiceRegistryProperties;
import org.apereo.cas.util.serialization.StringSerializer;
import org.apereo.cas.util.services.RegisteredServiceJsonSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* This is {@link DynamoDbServiceRegistryFacilitator}.
*
* @author Misagh Moayyed
* @since 5.1.0
*/
public class DynamoDbServiceRegistryFacilitator {
private static final Logger LOGGER = LoggerFactory.getLogger(DynamoDbServiceRegistryFacilitator.class);
private static final String TABLE_NAME = "DynamoDbCasServices";
private final StringSerializer<RegisteredService> jsonSerializer = new RegisteredServiceJsonSerializer();
private enum ColumnNames {
ID("id"),
NAME("name"),
DESCRIPTION("description"),
SERVICE_ID("serviceId"),
ENCODED("encoded");
private final String name;
ColumnNames(final String name) {
this.name = name;
}
public String getName() {
return name;
}
}
private final DynamoDbServiceRegistryProperties dynamoDbProperties;
private final AmazonDynamoDBClient amazonDynamoDBClient;
public DynamoDbServiceRegistryFacilitator(final DynamoDbServiceRegistryProperties dynamoDbProperties,
final AmazonDynamoDBClient amazonDynamoDBClient) {
this.dynamoDbProperties = dynamoDbProperties;
this.amazonDynamoDBClient = amazonDynamoDBClient;
createServicesTable(dynamoDbProperties.isDropTablesOnStartup());
}
/**
* Delete boolean.
*
* @param service the service
* @return the boolean
*/
public boolean delete(final RegisteredService service) {
final DeleteItemRequest del = new DeleteItemRequest()
.withTableName(TABLE_NAME)
.withKey(Collections.singletonMap(ColumnNames.ID.getName(), new AttributeValue(String.valueOf(service.getId()))));
LOGGER.debug("Submitting delete request [{}] for service [{}]", del, service);
final DeleteItemResult res = amazonDynamoDBClient.deleteItem(del);
LOGGER.debug("Delete request came back with result [{}]", res);
return res != null;
}
/**
* Count long.
*
* @return the long
*/
public long count() {
final ScanRequest scan = new ScanRequest(TABLE_NAME);
LOGGER.debug("Scanning table with request [{}] to count items", scan);
final ScanResult result = this.amazonDynamoDBClient.scan(scan);
LOGGER.debug("Scanned table with result [{}]", scan);
return result.getCount();
}
/**
* Gets all.
*
* @return the all
*/
public List<RegisteredService> getAll() {
final List<RegisteredService> services = new ArrayList<>();
final ScanRequest scan = new ScanRequest(TABLE_NAME);
LOGGER.debug("Scanning table with request [{}]", scan);
final ScanResult result = this.amazonDynamoDBClient.scan(scan);
LOGGER.debug("Scanned table with result [{}]", scan);
services.addAll(result.getItems()
.stream()
.map(this::deserializeServiceFromBinaryBlob)
.sorted((o1, o2) -> Integer.valueOf(o1.getEvaluationOrder()).compareTo(o2.getEvaluationOrder()))
.collect(Collectors.toList()));
return services;
}
/**
* Get registered service.
*
* @param id the id
* @return the registered service
*/
public RegisteredService get(final String id) {
final Map<String, AttributeValue> keys = new HashMap<>();
keys.put(ColumnNames.SERVICE_ID.getName(), new AttributeValue(id));
return getRegisteredServiceByKeys(keys);
}
/**
* Get registered service.
*
* @param id the id
* @return the registered service
*/
public RegisteredService get(final long id) {
final Map<String, AttributeValue> keys = new HashMap<>();
keys.put(ColumnNames.ID.getName(), new AttributeValue(String.valueOf(id)));
return getRegisteredServiceByKeys(keys);
}
private RegisteredService deserializeServiceFromBinaryBlob(final Map<String, AttributeValue> returnItem) {
final ByteBuffer bb = returnItem.get(ColumnNames.ENCODED.getName()).getB();
LOGGER.debug("Located binary encoding of service item [{}]. Transforming item into service object", returnItem);
final ByteArrayInputStream is = new ByteArrayInputStream(bb.array());
return this.jsonSerializer.from(is);
}
private RegisteredService getRegisteredServiceByKeys(final Map<String, AttributeValue> keys) {
final GetItemRequest request = new GetItemRequest()
.withKey(keys)
.withTableName(TABLE_NAME);
LOGGER.debug("Submitting request [{}] to get service with keys [{}]", request, keys);
final Map<String, AttributeValue> returnItem = amazonDynamoDBClient.getItem(request).getItem();
if (returnItem != null) {
final RegisteredService service = deserializeServiceFromBinaryBlob(returnItem);
LOGGER.debug("Located service [{}]", service);
return service;
}
return null;
}
/**
* Put.
*
* @param service the service
*/
public void put(final RegisteredService service) {
final Map<String, AttributeValue> values = buildTableAttributeValuesMapFromService(service);
final PutItemRequest putItemRequest = new PutItemRequest(TABLE_NAME, values);
LOGGER.debug("Submitting put request [{}] for service id [{}]", putItemRequest, service.getServiceId());
final PutItemResult putItemResult = amazonDynamoDBClient.putItem(putItemRequest);
LOGGER.debug("Service added with result [{}]", putItemResult);
}
/**
* Create tables.
*
* @param deleteTables the delete tables
*/
public void createServicesTable(final boolean deleteTables) {
try {
final CreateTableRequest request = new CreateTableRequest()
.withAttributeDefinitions(new AttributeDefinition(ColumnNames.ID.getName(), ScalarAttributeType.S))
.withKeySchema(new KeySchemaElement(ColumnNames.ID.getName(), KeyType.HASH))
.withProvisionedThroughput(new ProvisionedThroughput(dynamoDbProperties.getReadCapacity(),
dynamoDbProperties.getWriteCapacity()))
.withTableName(TABLE_NAME);
if (deleteTables) {
final DeleteTableRequest delete = new DeleteTableRequest(request.getTableName());
LOGGER.debug("Sending delete request [{}] to remove table if necessary", delete);
TableUtils.deleteTableIfExists(amazonDynamoDBClient, delete);
}
LOGGER.debug("Sending delete request [{}] to create table", request);
TableUtils.createTableIfNotExists(amazonDynamoDBClient, request);
LOGGER.debug("Waiting until table [{}] becomes active...", request.getTableName());
TableUtils.waitUntilActive(amazonDynamoDBClient, request.getTableName());
final DescribeTableRequest describeTableRequest = new DescribeTableRequest().withTableName(request.getTableName());
LOGGER.debug("Sending request [{}] to obtain table description...", describeTableRequest);
final TableDescription tableDescription = amazonDynamoDBClient.describeTable(describeTableRequest).getTable();
LOGGER.debug("Located newly created table with description: [{}]", tableDescription);
} catch (final Exception e) {
throw Throwables.propagate(e);
}
}
/**
* Build table attribute values from map.
*
* @param service the service
* @return the map
*/
public Map<String, AttributeValue> buildTableAttributeValuesMapFromService(final RegisteredService service) {
final Map<String, AttributeValue> values = new HashMap<>();
values.put(ColumnNames.ID.getName(), new AttributeValue(String.valueOf(service.getId())));
values.put(ColumnNames.NAME.getName(), new AttributeValue(service.getName()));
values.put(ColumnNames.DESCRIPTION.getName(), new AttributeValue(service.getDescription()));
values.put(ColumnNames.SERVICE_ID.getName(), new AttributeValue(service.getServiceId()));
final ByteArrayOutputStream out = new ByteArrayOutputStream();
jsonSerializer.to(out, service);
values.put(ColumnNames.ENCODED.getName(), new AttributeValue().withB(ByteBuffer.wrap(out.toByteArray())));
LOGGER.debug("Created attribute values [{}] based on provided service [{}]", values, service);
return values;
}
}