/*
* 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.nifi.processors.aws.dynamodb;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.annotation.lifecycle.OnStopped;
import org.apache.nifi.components.AllowableValue;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processor.ProcessSession;
import org.apache.nifi.processor.Relationship;
import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.processors.aws.AbstractAWSCredentialsProviderProcessor;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
/**
* Base class for NiFi dynamo db related processors
*
* @see DeleteDynamoDB
* @see PutDynamoDB
* @see GetDynamoDB
*/
public abstract class AbstractDynamoDBProcessor extends AbstractAWSCredentialsProviderProcessor<AmazonDynamoDBClient> {
public static final Relationship REL_UNPROCESSED = new Relationship.Builder().name("unprocessed")
.description("FlowFiles are routed to unprocessed relationship when DynamoDB is not able to process "
+ "all the items in the request. Typical reasons are insufficient table throughput capacity and exceeding the maximum bytes per request. "
+ "Unprocessed FlowFiles can be retried with a new request.").build();
public static final AllowableValue ALLOWABLE_VALUE_STRING = new AllowableValue("string");
public static final AllowableValue ALLOWABLE_VALUE_NUMBER = new AllowableValue("number");
public static final String DYNAMODB_KEY_ERROR_UNPROCESSED = "dynamodb.key.error.unprocessed";
public static final String DYNAMODB_RANGE_KEY_VALUE_ERROR = "dynmodb.range.key.value.error";
public static final String DYNAMODB_HASH_KEY_VALUE_ERROR = "dynmodb.hash.key.value.error";
public static final String DYNAMODB_KEY_ERROR_NOT_FOUND = "dynamodb.key.error.not.found";
public static final String DYNAMODB_ERROR_EXCEPTION_MESSAGE = "dynamodb.error.exception.message";
public static final String DYNAMODB_ERROR_CODE = "dynamodb.error.code";
public static final String DYNAMODB_ERROR_MESSAGE = "dynamodb.error.message";
public static final String DYNAMODB_ERROR_TYPE = "dynamodb.error.type";
public static final String DYNAMODB_ERROR_SERVICE = "dynamodb.error.service";
public static final String DYNAMODB_ERROR_RETRYABLE = "dynamodb.error.retryable";
public static final String DYNAMODB_ERROR_REQUEST_ID = "dynamodb.error.request.id";
public static final String DYNAMODB_ERROR_STATUS_CODE = "dynamodb.error.status.code";
public static final String DYNAMODB_ITEM_HASH_KEY_VALUE = " dynamodb.item.hash.key.value";
public static final String DYNAMODB_ITEM_RANGE_KEY_VALUE = " dynamodb.item.range.key.value";
public static final String DYNAMODB_ITEM_IO_ERROR = "dynamodb.item.io.error";
public static final String AWS_DYNAMO_DB_ITEM_SIZE_ERROR = "dynamodb.item.size.error";
protected static final String DYNAMODB_KEY_ERROR_NOT_FOUND_MESSAGE = "DynamoDB key not found : ";
public static final PropertyDescriptor TABLE = new PropertyDescriptor.Builder()
.name("Table Name")
.required(true)
.expressionLanguageSupported(true)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.description("The DynamoDB table name")
.build();
public static final PropertyDescriptor HASH_KEY_VALUE = new PropertyDescriptor.Builder()
.name("Hash Key Value")
.required(true)
.expressionLanguageSupported(true)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.description("The hash key value of the item")
.defaultValue("${dynamodb.item.hash.key.value}")
.build();
public static final PropertyDescriptor RANGE_KEY_VALUE = new PropertyDescriptor.Builder()
.name("Range Key Value")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.expressionLanguageSupported(true)
.defaultValue("${dynamodb.item.range.key.value}")
.build();
public static final PropertyDescriptor HASH_KEY_VALUE_TYPE = new PropertyDescriptor.Builder()
.name("Hash Key Value Type")
.required(true)
.description("The hash key value type of the item")
.defaultValue(ALLOWABLE_VALUE_STRING.getValue())
.allowableValues(ALLOWABLE_VALUE_STRING, ALLOWABLE_VALUE_NUMBER)
.build();
public static final PropertyDescriptor RANGE_KEY_VALUE_TYPE = new PropertyDescriptor.Builder()
.name("Range Key Value Type")
.required(true)
.description("The range key value type of the item")
.defaultValue(ALLOWABLE_VALUE_STRING.getValue())
.allowableValues(ALLOWABLE_VALUE_STRING, ALLOWABLE_VALUE_NUMBER)
.build();
public static final PropertyDescriptor HASH_KEY_NAME = new PropertyDescriptor.Builder()
.name("Hash Key Name")
.required(true)
.expressionLanguageSupported(true)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.description("The hash key name of the item")
.build();
public static final PropertyDescriptor RANGE_KEY_NAME = new PropertyDescriptor.Builder()
.name("Range Key Name")
.required(false)
.expressionLanguageSupported(true)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.description("The range key name of the item")
.build();
public static final PropertyDescriptor JSON_DOCUMENT = new PropertyDescriptor.Builder()
.name("Json Document attribute")
.required(true)
.expressionLanguageSupported(true)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.description("The Json document to be retrieved from the dynamodb item")
.build();
public static final PropertyDescriptor BATCH_SIZE = new PropertyDescriptor.Builder()
.name("Batch items for each request (between 1 and 50)")
.required(false)
.expressionLanguageSupported(true)
.addValidator(StandardValidators.createLongValidator(1, 50, true))
.defaultValue("1")
.description("The items to be retrieved in one batch")
.build();
public static final PropertyDescriptor DOCUMENT_CHARSET = new PropertyDescriptor.Builder()
.name("Character set of document")
.description("Character set of data in the document")
.addValidator(StandardValidators.CHARACTER_SET_VALIDATOR)
.required(true)
.expressionLanguageSupported(true)
.defaultValue(Charset.defaultCharset().name())
.build();
protected volatile DynamoDB dynamoDB;
public static final Set<Relationship> dynamoDBrelationships = Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(REL_SUCCESS, REL_FAILURE, REL_UNPROCESSED)));
@Override
public Set<Relationship> getRelationships() {
return dynamoDBrelationships;
}
/**
* Create client using credentials provider. This is the preferred way for creating clients
*/
@Override
protected AmazonDynamoDBClient createClient(final ProcessContext context, final AWSCredentialsProvider credentialsProvider, final ClientConfiguration config) {
getLogger().debug("Creating client with credentials provider");
final AmazonDynamoDBClient client = new AmazonDynamoDBClient(credentialsProvider, config);
return client;
}
/**
* Create client using AWSCredentials
*
* @deprecated use {@link #createClient(ProcessContext, AWSCredentialsProvider, ClientConfiguration)} instead
*/
@Override
protected AmazonDynamoDBClient createClient(final ProcessContext context, final AWSCredentials credentials, final ClientConfiguration config) {
getLogger().debug("Creating client with aws credentials");
final AmazonDynamoDBClient client = new AmazonDynamoDBClient(credentials, config);
return client;
}
protected Object getValue(ProcessContext context, PropertyDescriptor type, PropertyDescriptor value, FlowFile flowFile) {
if ( context.getProperty(type).getValue().equals(ALLOWABLE_VALUE_STRING.getValue())) {
return context.getProperty(value).evaluateAttributeExpressions(flowFile).getValue();
} else {
return new BigDecimal(context.getProperty(value).evaluateAttributeExpressions(flowFile).getValue());
}
}
protected Object getAttributeValue(ProcessContext context, PropertyDescriptor propertyType, AttributeValue value) {
if ( context.getProperty(propertyType).getValue().equals(ALLOWABLE_VALUE_STRING.getValue())) {
if ( value == null ) return null;
else return value.getS();
} else {
if ( value == null ) return null;
else return new BigDecimal(value.getN());
}
}
protected synchronized DynamoDB getDynamoDB() {
if ( dynamoDB == null )
dynamoDB = new DynamoDB(client);
return dynamoDB;
}
protected Object getValue(Map<String, AttributeValue> item, String keyName, String valueType) {
if ( ALLOWABLE_VALUE_STRING.getValue().equals(valueType)) {
AttributeValue val = item.get(keyName);
if ( val == null ) return val;
else return val.getS();
} else {
AttributeValue val = item.get(keyName);
if ( val == null ) return val;
else return val.getN();
}
}
protected List<FlowFile> processException(final ProcessSession session, List<FlowFile> flowFiles, Exception exception) {
List<FlowFile> failedFlowFiles = new ArrayList<>();
for (FlowFile flowFile : flowFiles) {
flowFile = session.putAttribute(flowFile, DYNAMODB_ERROR_EXCEPTION_MESSAGE, exception.getMessage() );
failedFlowFiles.add(flowFile);
}
return failedFlowFiles;
}
protected List<FlowFile> processClientException(final ProcessSession session, List<FlowFile> flowFiles,
AmazonClientException exception) {
List<FlowFile> failedFlowFiles = new ArrayList<>();
for (FlowFile flowFile : flowFiles) {
Map<String,String> attributes = new HashMap<>();
attributes.put(DYNAMODB_ERROR_EXCEPTION_MESSAGE, exception.getMessage() );
attributes.put(DYNAMODB_ERROR_RETRYABLE, Boolean.toString(exception.isRetryable()));
flowFile = session.putAllAttributes(flowFile, attributes);
failedFlowFiles.add(flowFile);
}
return failedFlowFiles;
}
protected List<FlowFile> processServiceException(final ProcessSession session, List<FlowFile> flowFiles,
AmazonServiceException exception) {
List<FlowFile> failedFlowFiles = new ArrayList<>();
for (FlowFile flowFile : flowFiles) {
Map<String,String> attributes = new HashMap<>();
attributes.put(DYNAMODB_ERROR_EXCEPTION_MESSAGE, exception.getMessage() );
attributes.put(DYNAMODB_ERROR_CODE, exception.getErrorCode() );
attributes.put(DYNAMODB_ERROR_MESSAGE, exception.getErrorMessage() );
attributes.put(DYNAMODB_ERROR_TYPE, exception.getErrorType().name() );
attributes.put(DYNAMODB_ERROR_SERVICE, exception.getServiceName() );
attributes.put(DYNAMODB_ERROR_RETRYABLE, Boolean.toString(exception.isRetryable()));
attributes.put(DYNAMODB_ERROR_REQUEST_ID, exception.getRequestId() );
attributes.put(DYNAMODB_ERROR_STATUS_CODE, Integer.toString(exception.getStatusCode()) );
attributes.put(DYNAMODB_ERROR_EXCEPTION_MESSAGE, exception.getMessage() );
attributes.put(DYNAMODB_ERROR_RETRYABLE, Boolean.toString(exception.isRetryable()));
flowFile = session.putAllAttributes(flowFile, attributes);
failedFlowFiles.add(flowFile);
}
return failedFlowFiles;
}
/**
* Send unhandled items to failure and remove the flow files from key to flow file map
* @param session used for sending the flow file
* @param keysToFlowFileMap - ItemKeys to flow file map
* @param hashKeyValue the items hash key value
* @param rangeKeyValue the items hash key value
*/
protected void sendUnprocessedToUnprocessedRelationship(final ProcessSession session, Map<ItemKeys, FlowFile> keysToFlowFileMap, Object hashKeyValue, Object rangeKeyValue) {
ItemKeys itemKeys = new ItemKeys(hashKeyValue, rangeKeyValue);
FlowFile flowFile = keysToFlowFileMap.get(itemKeys);
flowFile = session.putAttribute(flowFile, DYNAMODB_KEY_ERROR_UNPROCESSED, itemKeys.toString());
session.transfer(flowFile,REL_UNPROCESSED);
getLogger().error("Unprocessed key " + itemKeys + " for flow file " + flowFile);
keysToFlowFileMap.remove(itemKeys);
}
protected boolean isRangeKeyValueConsistent(String rangeKeyName, Object rangeKeyValue, ProcessSession session,
FlowFile flowFile) {
boolean isRangeNameBlank = StringUtils.isBlank(rangeKeyName);
boolean isRangeValueNull = rangeKeyValue == null;
boolean isConsistent = true;
if ( ! isRangeNameBlank && (isRangeValueNull || StringUtils.isBlank(rangeKeyValue.toString()))) {
isConsistent = false;
}
if ( isRangeNameBlank && ( ! isRangeValueNull && ! StringUtils.isBlank(rangeKeyValue.toString()))) {
isConsistent = false;
}
if ( ! isConsistent ) {
getLogger().error("Range key name '" + rangeKeyName + "' was not consistent with range value "
+ rangeKeyValue + "'" + flowFile);
flowFile = session.putAttribute(flowFile, DYNAMODB_RANGE_KEY_VALUE_ERROR, "range key '" + rangeKeyName
+ "'/value '" + rangeKeyValue + "' inconsistency error");
session.transfer(flowFile, REL_FAILURE);
}
return isConsistent;
}
protected boolean isHashKeyValueConsistent(String hashKeyName, Object hashKeyValue, ProcessSession session,
FlowFile flowFile) {
boolean isConsistent = true;
if ( hashKeyValue == null || StringUtils.isBlank(hashKeyValue.toString())) {
getLogger().error("Hash key value '" + hashKeyValue + "' is required for flow file " + flowFile);
flowFile = session.putAttribute(flowFile, DYNAMODB_HASH_KEY_VALUE_ERROR, "hash key " + hashKeyName
+ "/value '" + hashKeyValue + "' inconsistency error");
session.transfer(flowFile, REL_FAILURE);
isConsistent = false;
}
return isConsistent;
}
@OnStopped
public void onStopped() {
this.dynamoDB = null;
}
}