/*
* Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.amazonaws.mobileconnectors.kinesis.kinesisrecorder;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.mobileconnectors.kinesis.kinesisrecorder.FileRecordStore.RecordIterator;
import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.kinesis.AmazonKinesis;
import com.amazonaws.services.kinesis.AmazonKinesisClient;
import com.amazonaws.util.VersionInfoUtils;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.util.regex.Pattern;
/**
* The KinesisRecorder is a high level client meant for storing records on the
* users Android device. This allows developers to retain requests when the
* device is offline. It can also increase performance and battery efficiency
* since the wi-fi or cell network does not need to be woken up as frequently.
* <p/>
* Note: KinesisRecorder uses all synchronous calls regardless of the
* AmazonKinesisClient passed in. Therefore you should not call KinesisRecorder
* methods on the main thread.
* <p/>
* To use KinesisRecorder create an AmazonKinesisClient and an directory that is
* private to your application. The directory passed should be empty the first
* time you instantiate KinesisRecorder, and should only be used for
* KinesisRecorder to prevent collision. Additionally you may pass an instance
* of KinesisRecorderConfig in order to set parameters on KinesisRecorder (Such
* as the maximum amount of storage KinesisRecorder may use).
* <p/>
* Warning: You should not create multiple KinesisRecorders given the same
* directory. Doing so is an error and behavior is undefined.
* <p/>
* Note: KinesisRecorder stores the requests in plain-text, and does not perform
* additional security measures outside of what the Android OS offers by
* default. Therefore it is recommended you pass a directory that is only
* visible to your application, and additionally do not store highly sensitive
* information using Kinesis Recorder.
* <p/>
* KinesisRecorder requires an IAM policy that allows PutRecords action on the
* target stream. Here is an example:
*
* <pre>
* {
* "Version": "2012-10-17",
* "Statement": [{
* "Effect": "Allow",
* "Action": [ "kinesis:PutRecords" ],
* "Resource": [
* "arn:aws:kinesis:us-east-1:123456789012:stream/my_stream"
* ]
* }]
* }
* </pre>
*/
public class KinesisRecorder extends AbstractKinesisRecorder {
/**
* Name of local file record store.
*/
private static final String RECORD_FILE_NAME = "kinesis_stream_records";
/**
* User agent string to identify {@link KinesisRecorder}
*/
private static final String USER_AGENT = KinesisRecorder.class.getName() + "/"
+ VersionInfoUtils.getVersion();
/**
* The maximum size of a data blob (the data payload before Base64-encoding)
* is up to 1 MB.
*/
private static final int MAX_RECORD_SIZE_BYTES = 1024 * 1024;
/**
* Valid stream name pattern.
*/
private static final Pattern STREAM_NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_.-]{1,128}");
private KinesisStreamRecordSender sender;
/**
* Constructs a new Kinesis Recorder specifying a directory that Kinesis
* Recorder has exclusive access to for storing requests.
* <p>
* Note: Kinesis Recorder is synchronous, and it's methods should not be
* called on the main thread.
* <p>
* Note: Kinesis Recorder stores requests in plain-text, we recommend using
* a directory that is only readable by your application and not storing
* highly sensitive information in requests stored by Kinesis Recorder.
*
* @param credentialsProvider The credentials provider to use when making
* requests to AWS
* @param region The region of Amazon Kinesis this Recorder should save and
* send requests to.
* @param directory An empty directory KinesisRecorder can use for storing
* requests.
*/
public KinesisRecorder(File directory, Regions region,
AWSCredentialsProvider credentialsProvider) {
this(directory, region, credentialsProvider, new KinesisRecorderConfig());
}
/**
* Constructs a new Kinesis Recorder specifying a directory that Kinesis
* Recorder has exclusive access to for storing requests. Allows specifying
* various aspects of Kinesis Recorder through the KinesisRecorderConfig
* parameter. Note: Kinesis Recorder is synchronous, and it's methods should
* not be called on the main thread. Note: Kinesis Recorder stores requests
* in plain-text, we recommend using a directory that is only readable by
* your application and not storing highly sensitive information in requests
* stored by Kinesis Recorder.
*
* @param credentialsProvider The credentials provider to use when making
* requests to AWS
* @param region The region of Amazon Kinesis this Recorder should save and
* send requests to.
* @param directory An empty directory KinesisRecorder can use for storing
* requests.
* @param config Allows configuring various parameters of the recorder
*/
public KinesisRecorder(File directory, Regions region,
AWSCredentialsProvider credentialsProvider, KinesisRecorderConfig config) {
super(new FileRecordStore(directory, RECORD_FILE_NAME,
config.getMaxStorageSize()), config);
if (directory == null || credentialsProvider == null || region == null || config == null) {
throw new IllegalArgumentException(
"You must pass a non-null credentialsProvider, region, directory, and config to KinesisRecordStore");
}
AmazonKinesis client = new AmazonKinesisClient(credentialsProvider,
config.getClientConfiguration());
client.setRegion(Region.getRegion(region));
sender = new KinesisStreamRecordSender(client, USER_AGENT);
checkUpgrade(directory);
}
/**
* Constructs a {@link KinesisRecorder}. It allows you to inject
* dependencies.
*
* @param sender a {@link KinesisStreamRecordSender}
* @param recordStore record store
* @param config configuration
*/
KinesisRecorder(KinesisStreamRecordSender sender, FileRecordStore recordStore,
KinesisRecorderConfig config) {
super(recordStore, config);
this.sender = sender;
}
private void checkUpgrade(final File directory) {
File recordsDir = new File(directory, Constants.RECORDS_DIRECTORY);
File oldRecordsFile = new File(recordsDir, Constants.RECORDS_FILE_NAME);
// if the records file exists, run upgrade in a background thread
if (oldRecordsFile.isFile()) {
new Thread(new Runnable() {
@Override
public void run() {
upgrade(directory);
}
}).start();
}
}
/**
* Ports Kinesis records in old records file into the new place and in new
* format.
*
* @param directory working directory
*/
void upgrade(File directory) {
synchronized (KinesisRecorder.this) {
File recordsDir = new File(directory, Constants.RECORDS_DIRECTORY);
File oldRecordsFile = new File(recordsDir, Constants.RECORDS_FILE_NAME);
if (!oldRecordsFile.isFile()) {
return;
}
// iterate through all records in the old records file
FileRecordStore frs = new FileRecordStore(directory, Constants.RECORDS_FILE_NAME,
Long.MAX_VALUE);
RecordIterator iterator = frs.iterator();
while (iterator.hasNext()) {
try {
JSONObject json = new JSONObject(iterator.next());
saveRecord(JSONRecordAdapter.getData(json).array(),
JSONRecordAdapter.getStreamName(json));
} catch (JSONException e) {
// skip invalid json
continue;
}
}
try {
iterator.close();
} catch (IOException e) {
// ignore
}
oldRecordsFile.delete();
}
}
@Override
protected RecordSender getRecordSender() {
return sender;
}
@Override
public void saveRecord(byte[] data, String streamName) {
if (streamName == null || !STREAM_NAME_PATTERN.matcher(streamName).matches()) {
throw new IllegalArgumentException("Invalid stream name: " + streamName);
}
if (data == null || data.length == 0 || data.length > MAX_RECORD_SIZE_BYTES) {
throw new IllegalArgumentException("Invalid data size.");
}
super.saveRecord(data, streamName);
}
}