/* * Copyright DataStax, Inc. * * Licensed 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 com.datastax.powertools.dcp; import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; import com.amazonaws.services.dynamodbv2.model.AttributeValue; import com.amazonaws.services.dynamodbv2.model.ComparisonOperator; import com.amazonaws.services.dynamodbv2.model.Condition; import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; import com.amazonaws.services.dynamodbv2.model.CreateTableResult; 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.DeleteTableResult; import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest; import com.amazonaws.services.dynamodbv2.model.DescribeTableResult; import com.amazonaws.services.dynamodbv2.model.GetItemRequest; import com.amazonaws.services.dynamodbv2.model.GetItemResult; import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; import com.amazonaws.services.dynamodbv2.model.KeyType; import com.amazonaws.services.dynamodbv2.model.PutItemRequest; import com.amazonaws.services.dynamodbv2.model.PutItemResult; import com.amazonaws.services.dynamodbv2.model.QueryRequest; import com.amazonaws.services.dynamodbv2.model.QueryResult; import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; import com.amazonaws.services.dynamodbv2.model.TableDescription; import com.amazonaws.services.dynamodbv2.model.TableStatus; import com.datastax.oss.driver.api.core.CqlIdentifier; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.cql.BoundStatement; import com.datastax.oss.driver.api.core.cql.ColumnDefinition; import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; import com.datastax.oss.driver.api.core.cql.PreparedStatement; import com.datastax.oss.driver.api.core.cql.ResultSet; import com.datastax.oss.driver.api.core.cql.Row; import com.datastax.powertools.dcp.api.DynamoDBResponse; import com.datastax.powertools.dcp.managed.dse.CassandraManager; import com.datastax.powertools.dcp.managed.dse.TableDef; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOError; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import static com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperFieldModel.DynamoDBAttributeType; import static com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperFieldModel.DynamoDBAttributeType.valueOf; import static com.amazonaws.services.dynamodbv2.model.KeyType.HASH; import static com.amazonaws.services.dynamodbv2.model.KeyType.RANGE; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.ASCII; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.BIGINT; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.BLOB; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.BOOLEAN; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.COUNTER; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.DATE; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.DECIMAL; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.DOUBLE; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.FLOAT; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.INET; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.INT; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.SMALLINT; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.TIME; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.TIMEUUID; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.TINYINT; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.UUID; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.VARCHAR; import static com.datastax.oss.protocol.internal.ProtocolConstants.DataType.VARINT; /** * DynamoDb translator which encodes the document in a simple C* schema of (partition key, sort key, JSON string) */ public class DynamoDSETranslatorJSONBlob extends DynamoDSETranslator { private final static Logger logger = LoggerFactory.getLogger(DynamoDSETranslatorJSONBlob.class); //private static final Pattern queryPattern = Pattern.compile(".*(:\\S+)"); private static final Pattern queryPattern = Pattern.compile("(\\S+)\\s?([?:=|<|>])\\s?:\\S+(?:\\s?\\S+\\s?(\\S+)\\s?(=|<|>)\\s?:\\S+)?"); private static final Map<String, ComparisonOperator> operatorConverter = new HashMap() {{ put("=", ComparisonOperator.EQ); put("!=", ComparisonOperator.NE); put("<=", ComparisonOperator.LE); put("<", ComparisonOperator.LT); put(">=", ComparisonOperator.GE); put(">", ComparisonOperator.GT); }}; public DynamoDSETranslatorJSONBlob(CassandraManager cassandraManager) { super(cassandraManager); } @Override public DynamoDBResponse query(QueryRequest payload) { logger.debug("query against JSON table"); ResultSet resultSet; if (payload.getKeyConditionExpression() != null) resultSet = queryByKeyExpression(payload); else if (payload.getKeyConditions() != null) resultSet = queryByKeyCondition(payload); else throw new UnsupportedOperationException("un-supported query type"); try { Collection<Map<String, AttributeValue>> items = new HashSet<Map<String, AttributeValue>>(); for (Row row : resultSet) { AttributeValue item; ColumnDefinitions colDefs = row.getColumnDefinitions(); Map<String, AttributeValue> keysSet = new HashMap<>(); for (ColumnDefinition colDef : colDefs) { if (colDef.getName().asInternal().equals("json_blob")) continue; item = rowToAV(colDef, row); keysSet.put(colDef.getName().asInternal(), item); } Map<String, AttributeValue> itemSet = blobToItemSet(row.getString("json_blob")); itemSet.putAll(keysSet); if (payload.getFilterExpression() != null){ if(!matchesFilterExpression(itemSet, payload)){ continue; } } items.add(itemSet); } QueryResult queryResult = new QueryResult(); queryResult.setItems(items); return new DynamoDBResponse(queryResult, 200); } catch (Throwable e) { logger.warn("Query error", e); DynamoDBResponse ddbResponse = new DynamoDBResponse(null, 500); String msg= String.format("query failed with error: %s", e.getMessage()); ddbResponse.setError(msg); return ddbResponse; } } private boolean matchesFilterExpression(Map<String, AttributeValue> itemSet, QueryRequest payload) { String filterExpression = payload.getFilterExpression(); Matcher matcher = queryPattern.matcher(filterExpression); if (matcher.find()) { BoundStatement boundStatement = null; for (int i =0; i < matcher.groupCount();i = i+2) { Map<String, AttributeValue> expressionAtributeValues = payload.getExpressionAttributeValues(); String attributeName = matcher.group(i + 1); AttributeValue itemAttributeValue = itemSet.get(attributeName); JsonNode valueJson = awsRequestMapper.valueToTree(itemAttributeValue); Comparable<Object> itemValue = (Comparable<Object>) getObjectFromJsonLeaf(valueJson.fields().next()); AttributeValue expressionAttributeValue = expressionAtributeValues.get(attributeName); valueJson = awsRequestMapper.valueToTree(expressionAttributeValue); Comparable<Object> expressionValue = (Comparable<Object>) getObjectFromJsonLeaf(valueJson.fields().next()); String operator = matcher.group(i + 2); switch (operator) { case "=": return itemValue.equals(expressionValue); case "!=": return !itemValue.equals(expressionValue); case "<=": return itemValue.compareTo(expressionValue) <= 0; case "<": return itemValue.compareTo(expressionValue) < 0; case ">=": return itemValue.compareTo(expressionValue) >= 0; case ">": return itemValue.compareTo(expressionValue) > 0; } } } throw new UnsupportedOperationException("Error parsing filter expression: " + filterExpression); } private ResultSet queryByKeyCondition(QueryRequest payload) { TableDef tableDef = cassandraManager.getTableDef(payload.getTableName()); BoundStatement boundStatement = null; if (payload.getKeyConditions().size() ==1){ for (Map.Entry<String, Condition> c : payload.getKeyConditions().entrySet()) { if (c.getKey().equals(tableDef.getPartitionKey().getAttributeName())) { PreparedStatement jsonPartitionStatement = tableDef.getJsonQueryPartitionStatement(); if (!c.getValue().getComparisonOperator().equals(ComparisonOperator.EQ.name())) throw new UnsupportedOperationException("Hash Key lookups only support equality conditions"); List<AttributeValue> v = c.getValue().getAttributeValueList(); JsonNode valueJson = awsRequestMapper.valueToTree(v.iterator().next()); Object value = getObjectFromJsonLeaf(valueJson.fields().next()); boundStatement = jsonPartitionStatement.bind(value); } } } if (payload.getKeyConditions().size() ==2) { PreparedStatement jsonPartitionAndClusteringStatement = null; Object partitionValue = null; Object clusteringValue = null; for (Map.Entry<String, Condition> c : payload.getKeyConditions().entrySet()) { if (c.getKey().equals(tableDef.getPartitionKey().getAttributeName())) { PreparedStatement jsonPartitionStatement = tableDef.getJsonQueryPartitionStatement(); if (!c.getValue().getComparisonOperator().equals(ComparisonOperator.EQ.name())) throw new UnsupportedOperationException("Hash Key lookups only support equality conditions"); List<AttributeValue> v = c.getValue().getAttributeValueList(); JsonNode valueJson = awsRequestMapper.valueToTree(v.iterator().next()); partitionValue = getObjectFromJsonLeaf(valueJson.fields().next()); } if (c.getKey().equals(tableDef.getClusteringKey().get().getAttributeName())) { if (c.getValue().getComparisonOperator() == null) { throw new UnsupportedOperationException("null Comparison Operator not allowed"); } List<AttributeValue> v = c.getValue().getAttributeValueList(); JsonNode valueJson = awsRequestMapper.valueToTree(v.iterator().next()); clusteringValue = getObjectFromJsonLeaf(valueJson.fields().next()); jsonPartitionAndClusteringStatement = tableDef.getLazyJsonQueryPartitionAndClusteringStatement(ComparisonOperator.valueOf(c.getValue().getComparisonOperator())); } } boundStatement = jsonPartitionAndClusteringStatement.bind(partitionValue, clusteringValue); } return session().execute(boundStatement); } private ResultSet queryByKeyExpression(QueryRequest payload) { TableDef tableDef = cassandraManager.getTableDef(payload.getTableName()); PreparedStatement jsonStatement = tableDef.getJsonQueryPartitionStatement(); Matcher matcher = queryPattern.matcher(payload.getKeyConditionExpression()); if (matcher.find()) { Map<String, AttributeValue> expressionAttributeValues = payload.getExpressionAttributeValues(); BoundStatement boundStatement = null; //Partition Key if (matcher.groupCount() == 2) { for (Map.Entry<String, AttributeValue> stringAttributeValueEntry : expressionAttributeValues.entrySet()) { if (!stringAttributeValueEntry.getKey().equals(tableDef.getPartitionKey().getAttributeName())) continue; JsonNode valueJson = awsRequestMapper.valueToTree(stringAttributeValueEntry.getValue()); Object value = getObjectFromJsonLeaf(valueJson.fields().next()); boundStatement = jsonStatement.bind(value); break; } } //Partition Key and Clustering Columns if (matcher.groupCount() == 4) { PreparedStatement jsonPartitionAndClusteringStatement = null; Object partitionValue = null; Object clusteringValue = null; AttributeValue v = expressionAttributeValues.get(tableDef.getPartitionKey().getAttributeName()); JsonNode valueJson = awsRequestMapper.valueToTree(v); partitionValue = getObjectFromJsonLeaf(valueJson.fields().next()); v = expressionAttributeValues.get(tableDef.getClusteringKey().get().getAttributeName()); valueJson = awsRequestMapper.valueToTree(v); clusteringValue = getObjectFromJsonLeaf(valueJson.fields().next()); ComparisonOperator comparisonOperator; if (tableDef.getClusteringKey().get().getAttributeName().equals(matcher.group(1))) { comparisonOperator = operatorConverter.get(matcher.group(2)); } else if (tableDef.getClusteringKey().get().getAttributeName().equals(matcher.group(3))) { comparisonOperator = operatorConverter.get(matcher.group(4)); } else{ throw new UnsupportedOperationException("Invalid Expression Values"); } jsonPartitionAndClusteringStatement = tableDef.getLazyJsonQueryPartitionAndClusteringStatement(comparisonOperator); boundStatement = jsonPartitionAndClusteringStatement.bind(partitionValue, clusteringValue); } return session().execute(boundStatement); } throw new UnsupportedOperationException("Error parsing expression: " + payload.getKeyConditionExpression()); } private Map<String, AttributeValue> blobToItemSet(String json_blob) throws IOException { JsonNode items = awsRequestMapper.readTree(json_blob); Map<String, AttributeValue> itemSet = new HashMap<>(); List<String> dynamoTypes = Arrays.asList("N", "S", "BOOL", "B", "S", "SS"); Iterator<Map.Entry<String, JsonNode>> fieldIterator = items.fields(); for (Iterator<Map.Entry<String, JsonNode>> it = fieldIterator; it.hasNext(); ) { Map.Entry<String, JsonNode> item = it.next(); Iterator<Map.Entry<String, JsonNode>> itemFieldIterator = item.getValue().fields(); for (Iterator<Map.Entry<String, JsonNode>> it2 = itemFieldIterator; it2.hasNext(); ) { Map.Entry<String, JsonNode> pair = it2.next(); if (!dynamoTypes.contains(pair.getKey())) { throw new UnsupportedOperationException("Nested not implemented"); } else { itemSet.put(item.getKey(), getAttributeFromJsonLeaf(item.getValue(), pair.getKey())); } } } return itemSet; } private AttributeValue getAttributeFromJsonLeaf(JsonNode values, String attributeType) { AttributeValue av = new AttributeValue(); Iterator<Map.Entry<String, JsonNode>> it; for (it = values.fields(); it.hasNext(); ) { Map.Entry<String, JsonNode> leaf = it.next(); ; JsonNodeType type = leaf.getValue().getNodeType(); av = new AttributeValue(); JsonNode value = leaf.getValue(); //TODO: nested switch is not maintainable, do something prettier switch (type) { case ARRAY: { Set set = new HashSet(); for (JsonNode jsonNode : value) { jsonNode.getNodeType(); if (jsonNode.getNodeType().equals(JsonNodeType.STRING)) { set.add(jsonNode.asText()); } else { try { set.add(jsonNode.binaryValue()); } catch (IOException e) { e.printStackTrace(); } } } av.setSS(set); } break; case BINARY: try { av.setB(ByteBuffer.wrap(leaf.getValue().binaryValue())); } catch (IOException e) { throw new IOError(e); } break; case BOOLEAN: av.setBOOL(value.asBoolean()); break; case MISSING: break; case NULL: break; case NUMBER: av.setN(String.valueOf(value.asDouble())); break; case OBJECT: break; case POJO: break; case STRING: switch (ScalarAttributeType.fromValue(attributeType)) { case S: av.setS(value.asText()); break; case N: av.setN(value.asText()); break; case B: try { av.setB(ByteBuffer.wrap(leaf.getValue().binaryValue())); } catch (IOException e) { throw new IOError(e); } break; } break; } ; } return av; } private AttributeValue rowToAV(ColumnDefinition columnDefinition, Row row) { AttributeValue av = new AttributeValue(); CqlIdentifier name = columnDefinition.getName(); switch (columnDefinition.getType().getProtocolCode()) { case BLOB: av.setB(row.getByteBuffer(name)); break; case BIGINT: case BOOLEAN: case COUNTER: case DECIMAL: case DOUBLE: case FLOAT: case INT: case VARINT: case TINYINT: case SMALLINT: String v = String.valueOf(row.getDouble(name)); if (v.endsWith(".0")) //Keep non doubles looking like non-doubles v = v.substring(0, v.length() - 2); av.setN(v); break; case TIMEUUID: case UUID: case INET: case DATE: case VARCHAR: case ASCII: case TIME: av.setS(row.getString(name)); break; default: throw new IllegalArgumentException("Type not supported: " + name.asInternal() + " " + columnDefinition.getType()); } return av; } private Object getObjectFromJsonLeaf(Map.Entry<String, JsonNode> leaf) { DynamoDBAttributeType key = valueOf(leaf.getKey()); JsonNode value = leaf.getValue(); try { switch (key) { case N: return value.asDouble(); case S: return value.asText(); case BOOL: return value.asBoolean(); case B: return value.binaryValue(); default: logger.error("Type not supported"); break; } return null; } catch (Exception e) { e.printStackTrace(); logger.error(e.getMessage()); return null; } } @Override public DynamoDBResponse deleteTable(DeleteTableRequest deleteTableRequest) { logger.info("deleting JSON table"); String keyspace = keyspaceName; String table = deleteTableRequest.getTableName(); String statement = String.format("DROP TABLE %s.\"%s\";\n", keyspace, table); ResultSet result = session().execute(statement); if (result.wasApplied()) { logger.info("deleted table " + table); cassandraManager.refreshSchema(); TableDescription newTableDesc = this.getTableDescription(table, null,null); DeleteTableResult createResult = (new DeleteTableResult()).withTableDescription(newTableDesc); return new DynamoDBResponse(createResult, 200); } return null; } @Override public DynamoDBResponse createTable(CreateTableRequest createTableRequest) throws IOException { logger.info("creating JSON table"); String columnPairs = createTableRequest.getAttributeDefinitions().stream().map(this::attributeToPairs).collect(Collectors.joining(", ")); columnPairs += ",json_blob text"; String keyspace = keyspaceName; String table = createTableRequest.getTableName(); String primaryKey = getPrimaryKey(createTableRequest.getKeySchema()); String statement = String.format("CREATE TABLE IF NOT EXISTS %s.\"%s\" ( %s, PRIMARY KEY %s);\n", keyspace, table, columnPairs, primaryKey); ResultSet result = session().execute(statement); if (result.wasApplied()) { logger.info("created {} as {}", table, statement); cassandraManager.refreshSchema(); TableDescription newTableDesc = this.getTableDescription(table, createTableRequest.getAttributeDefinitions(), createTableRequest.getKeySchema()); CreateTableResult createResult = (new CreateTableResult()).withTableDescription(newTableDesc); return new DynamoDBResponse(createResult, 200); } return null; } public String getPrimaryKey(List<KeySchemaElement> keySchema){ String partitionKey = null; String clusteringColumn = null; for (KeySchemaElement keySchemaElement : keySchema) { String type = keySchemaElement.getKeyType(); String name = keySchemaElement.getAttributeName(); if (type.equals(HASH.toString())) partitionKey = name; else if (type.equals(RANGE.toString())) clusteringColumn = name; } String primaryKey; if(clusteringColumn != null) primaryKey = String.format("((\"%s\"), \"%s\")", partitionKey, clusteringColumn); else primaryKey = String.format("(\"%s\")", partitionKey); return primaryKey; } private String attributeToPairs(AttributeDefinition attributeDefinition) { String name = "\"" + attributeDefinition.getAttributeName() + "\""; DynamoDBAttributeType type = valueOf(attributeDefinition.getAttributeType()); switch(type) { case N: return name + " double"; case S: return name + " text"; case BOOL: return name + " boolean"; case B: return name + " blob"; default: throw new RuntimeException("Type not supported"); } } @Override public DynamoDBResponse describeTable(DescribeTableRequest describeTableRequest) { String tableName = describeTableRequest.getTableName(); if (cassandraManager.hasTable(tableName)){ TableDef tableDef = cassandraManager.getTableDef(tableName); AttributeDefinition partitionKey = tableDef.getPartitionKey(); Optional<AttributeDefinition> maybeClusteringKey = tableDef.getClusteringKey(); TableDescription tableDescription = new TableDescription() .withTableName(tableName) .withTableStatus(TableStatus.ACTIVE) .withCreationDateTime(new Date()) .withTableArn(tableName); if (maybeClusteringKey.isPresent()) { AttributeDefinition clusteringKey = maybeClusteringKey.get(); tableDescription.setAttributeDefinitions(ImmutableList.of(partitionKey, clusteringKey)); tableDescription.setKeySchema( ImmutableList.of( new KeySchemaElement(partitionKey.getAttributeName(), KeyType.HASH), new KeySchemaElement(clusteringKey.getAttributeName(), KeyType.RANGE)) ); } else { tableDescription.setAttributeDefinitions(ImmutableList.of(partitionKey)); tableDescription.setKeySchema( ImmutableList.of(new KeySchemaElement(partitionKey.getAttributeName(), KeyType.HASH)) ); } DescribeTableResult dtr = new DescribeTableResult().withTable(tableDescription); DynamoDBResponse ddbr = new DynamoDBResponse(dtr, 200); return ddbr; }else{ DynamoDBResponse ddbr = new DynamoDBResponse(null, 500); ddbr.setError("Table not found"); return ddbr; } } private TableDescription getTableDescription(String tableName, Collection<AttributeDefinition> attributeDefinitions, Collection<KeySchemaElement> keySchema) { TableDescription tableDescription = (new TableDescription()) .withTableName(tableName) .withAttributeDefinitions(attributeDefinitions) .withKeySchema(keySchema) .withTableStatus(TableStatus.ACTIVE) .withCreationDateTime(new Date()) .withTableArn(tableName); return tableDescription; } @Override public DynamoDBResponse deleteItem(DeleteItemRequest dir) { logger.debug("delete item into JSON table"); String tableName = dir.getTableName(); TableDef tableDef = cassandraManager.getTableDef(tableName); PreparedStatement deleteStatement = tableDef.getDeleteStatement(); AttributeDefinition partitionKeyAttr = tableDef.getPartitionKey(); Optional<AttributeDefinition> maybeCusteringKeyAttr = tableDef.getClusteringKey(); Map<String, AttributeValue> keys = dir.getKey(); Object partitionKeyValue = getAttributeObject( ScalarAttributeType.fromValue(partitionKeyAttr.getAttributeType()), keys.get(partitionKeyAttr.getAttributeName()) ); BoundStatement boundStatement; if (maybeCusteringKeyAttr.isPresent()) { Object clusteringKeyValue = getAttributeObject( ScalarAttributeType.fromValue(maybeCusteringKeyAttr.get().getAttributeType()), keys.get(maybeCusteringKeyAttr.get().getAttributeName()) ); boundStatement = deleteStatement.bind(partitionKeyValue, clusteringKeyValue); } else { boundStatement = deleteStatement.bind(partitionKeyValue); } ResultSet result = session().execute(boundStatement); if (result.wasApplied()){ DeleteItemResult dres = new DeleteItemResult(); return new DynamoDBResponse(dres, 200); } else return null; } private Object getAttributeObject(ScalarAttributeType type, AttributeValue value) { //Note: only string, number, and binary are allowed for primary keys in dynamodb switch (type) { case N: return Double.parseDouble(value.getN()); case S: return value.getS(); case B: return value.getB(); default: throw new IllegalStateException("Unknown dynamo scalar type: " + type); } } @Override public DynamoDBResponse putItem(PutItemRequest putItemRequest) { PutItemResult pir = new PutItemResult(); logger.debug("put item into JSON table"); TableDef tableDef = cassandraManager.getTableDef(putItemRequest.getTableName()); PreparedStatement jsonStatement = tableDef.getJsonPutStatement(); if (jsonStatement == null) { String msg= String.format("Requested resource not found: Table: %s not found", putItemRequest.getTableName()); DynamoDBResponse ddbResponse = new DynamoDBResponse(pir, 400); ddbResponse.setError(msg); logger.error(msg); return ddbResponse; } Map<String, AttributeValue> items = putItemRequest.getItem(); //TODO: there may be a cleaner way to to this using Marshal JsonNode itemsJson = awsRequestMapper.valueToTree(items); itemsJson = organizeColumns(itemsJson, tableDef.getPartitionKey(), tableDef.getClusteringKey()); String jsonColumns = stringify(itemsJson); BoundStatement boundStatement = jsonStatement.bind(jsonColumns); try { ResultSet result = session().execute(boundStatement); if (result.wasApplied()){ DynamoDBResponse ddbResponse = new DynamoDBResponse(pir, 200); return ddbResponse; } else { DynamoDBResponse ddbResponse = new DynamoDBResponse(pir, 500); String msg = String.format("PutItem not applied", putItemRequest.getTableName()); ddbResponse.setError(msg); return ddbResponse; } }catch (Exception e){ DynamoDBResponse ddbResponse = new DynamoDBResponse(pir, 500); String msg= String.format("PutItem write failed with error: %s",e.getMessage()); ddbResponse.setError(msg); return ddbResponse; } } @Override public DynamoDBResponse getItem(GetItemRequest getItemRequest) { logger.debug("get item from JSON table"); String tableName = getItemRequest.getTableName(); TableDef tableDef = cassandraManager.getTableDef(tableName); PreparedStatement selectStatement = tableDef.getQueryRowStatement(); AttributeDefinition partitionKeyDef = tableDef.getPartitionKey(); Optional<AttributeDefinition> clusteringKeyDef = tableDef.getClusteringKey(); Map<String, AttributeValue> keys = getItemRequest.getKey(); AttributeValue partitionKey = keys.get(partitionKeyDef.getAttributeName()); AttributeValue clusteringKey = clusteringKeyDef.isPresent() ? keys.get(clusteringKeyDef.get().getAttributeName()) : null; ScalarAttributeType partitionKeyType = ScalarAttributeType.valueOf(partitionKeyDef.getAttributeType()); ScalarAttributeType clusteringKeyType = clusteringKeyDef.isPresent() ? ScalarAttributeType.valueOf(clusteringKeyDef.get().getAttributeType()) : null; BoundStatement boundStatement = clusteringKey == null ? selectStatement.bind(getAttributeObject(partitionKeyType, partitionKey)) : selectStatement.bind(getAttributeObject(partitionKeyType, partitionKey), getAttributeObject(clusteringKeyType, clusteringKey)); ResultSet result = session().execute(boundStatement); GetItemResult gir = new GetItemResult(); Map<String, AttributeValue> item = new HashMap<>(); ColumnDefinitions colDefs = result.getColumnDefinitions(); Row row = result.one(); //Case that nothing is found if (row == null) return new DynamoDBResponse(null, 200); Map<String, AttributeValue> keysSet = new HashMap<>(); for (ColumnDefinition colDef : colDefs) { if (colDef.getName().asInternal().equals("json_blob")) continue; keysSet.put(colDef.getName().asInternal(), rowToAV(colDef, row)); } try { item = blobToItemSet(row.getString("json_blob")); item.putAll(keysSet); gir.withItem(item); return new DynamoDBResponse(gir, 200); } catch (IOException e) { DynamoDBResponse ddbResponse = new DynamoDBResponse(gir, 500); String msg = String.format("GetItem failed", getItemRequest.getTableName()); ddbResponse.setError(msg); return ddbResponse; } } private CqlSession session() { return cassandraManager.getSession(); } private String stringify(JsonNode items) { ObjectNode itemsClone = (ObjectNode) items.deepCopy(); String jsonString = items.get("json_blob").toString(); itemsClone.remove("json_blob"); itemsClone.put("json_blob", jsonString); return itemsClone.toString(); } private ObjectNode organizeColumns(JsonNode items, AttributeDefinition partitionKey, Optional<AttributeDefinition> clusteringKey) { ObjectNode itemsClone = new ObjectNode(JsonNodeFactory.instance); ObjectNode jsonBlobNode = new ObjectNode(JsonNodeFactory.instance); for (Iterator<Map.Entry<String, JsonNode>> it = items.fields(); it.hasNext(); ) { Map.Entry<String, JsonNode> item = it.next(); String itemKey = item.getKey(); if ( partitionKey.getAttributeName().equals(itemKey) || (clusteringKey.isPresent() && clusteringKey.get().getAttributeName().equals(itemKey))) itemsClone.put("\"" + itemKey + "\"", stripDynamoTypes(item.getValue())); else jsonBlobNode.put(itemKey, item.getValue()); } itemsClone.put("json_blob", items); return itemsClone; } public JsonNode stripDynamoTypes(JsonNode items) { JsonNodeType type = items.getNodeType(); Set<String> dynamoTypes = Sets.newHashSet("N", "S", "BOOL", "B", "SS", "NS"); switch (type) { case OBJECT: { Iterator<Map.Entry<String, JsonNode>> fieldIterator = items.fields(); ObjectNode jsonObjectNode = new ObjectNode(JsonNodeFactory.instance); for (Iterator<Map.Entry<String, JsonNode>> it = fieldIterator; it.hasNext(); ) { Map.Entry<String, JsonNode> item = it.next(); if (!dynamoTypes.contains(item.getKey())) { jsonObjectNode.put(item.getKey(), stripDynamoTypes(item.getValue())); }else { JsonNode coerced = coerceDynamoTypes(item.getValue(), item.getKey()); return coerced; } } return jsonObjectNode; } case ARRAY: { ArrayNode item = (ArrayNode) items; ArrayNode jsonArrayNode = new ArrayNode(JsonNodeFactory.instance); for (JsonNode jsonNode : item) { jsonArrayNode.add(stripDynamoTypes(jsonNode)); } return jsonArrayNode; } default: return items; } } private JsonNode coerceDynamoTypes(JsonNode value, String key) { try { DynamoDBAttributeType keyType = valueOf(key); switch (keyType) { case N: return awsRequestMapper.valueToTree(value.asDouble()); case S: return awsRequestMapper.valueToTree(value.asText()); case BOOL: return awsRequestMapper.valueToTree(value.asBoolean()); case B: return awsRequestMapper.valueToTree(value.binaryValue()); case SS: return stripDynamoTypes(value); default: logger.error("Type not supported"); break; } return null; }catch(Exception e){ e.printStackTrace(); logger.error(e.getMessage()); return null; } } }