/**
* Copyright 2013-2016 Amazon.com,
* Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Amazon Software License (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/asl/
*
* or in the "license" file accompanying this file. This file is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, express or implied. See the License
* for the specific language governing permissions and
* limitations under the License.
*/
package com.amazonaws.mobileconnectors.cognito.internal.storage;
import android.util.Log;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.auth.CognitoCachingCredentialsProvider;
import com.amazonaws.mobileconnectors.cognito.DatasetMetadata;
import com.amazonaws.mobileconnectors.cognito.Record;
import com.amazonaws.mobileconnectors.cognito.exceptions.DataAccessNotAuthorizedException;
import com.amazonaws.mobileconnectors.cognito.exceptions.DataConflictException;
import com.amazonaws.mobileconnectors.cognito.exceptions.DataLimitExceededException;
import com.amazonaws.mobileconnectors.cognito.exceptions.DataStorageException;
import com.amazonaws.mobileconnectors.cognito.exceptions.DatasetNotFoundException;
import com.amazonaws.mobileconnectors.cognito.exceptions.NetworkException;
import com.amazonaws.mobileconnectors.cognito.exceptions.SubscribeFailedException;
import com.amazonaws.mobileconnectors.cognito.exceptions.UnsubscribeFailedException;
import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.cognitosync.AmazonCognitoSync;
import com.amazonaws.services.cognitosync.AmazonCognitoSyncClient;
import com.amazonaws.services.cognitosync.model.DeleteDatasetRequest;
import com.amazonaws.services.cognitosync.model.DescribeDatasetRequest;
import com.amazonaws.services.cognitosync.model.DescribeDatasetResult;
import com.amazonaws.services.cognitosync.model.LimitExceededException;
import com.amazonaws.services.cognitosync.model.ListDatasetsRequest;
import com.amazonaws.services.cognitosync.model.ListDatasetsResult;
import com.amazonaws.services.cognitosync.model.ListRecordsRequest;
import com.amazonaws.services.cognitosync.model.ListRecordsResult;
import com.amazonaws.services.cognitosync.model.NotAuthorizedException;
import com.amazonaws.services.cognitosync.model.Operation;
import com.amazonaws.services.cognitosync.model.RecordPatch;
import com.amazonaws.services.cognitosync.model.ResourceConflictException;
import com.amazonaws.services.cognitosync.model.ResourceNotFoundException;
import com.amazonaws.services.cognitosync.model.SubscribeToDatasetRequest;
import com.amazonaws.services.cognitosync.model.UnsubscribeFromDatasetRequest;
import com.amazonaws.services.cognitosync.model.UpdateRecordsRequest;
import com.amazonaws.services.cognitosync.model.UpdateRecordsResult;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Cognito remote storage powered by AWS Cognito Sync service
*/
public class CognitoSyncStorage implements RemoteDataStorage {
private static final String TAG = "CognitoSyncStorage";
/**
* Identity pool id
*/
private final String identityPoolId;
private final AmazonCognitoSync client;
private final CognitoCachingCredentialsProvider provider;
/**
* User agent string to append to all requests
*/
private String userAgent;
public CognitoSyncStorage(String identityPoolId, AmazonCognitoSync client,
CognitoCachingCredentialsProvider provider) {
this.identityPoolId = identityPoolId;
this.client = client;
this.provider = provider;
userAgent = "";
}
@Deprecated
public CognitoSyncStorage(String identityPoolId, Regions region,
CognitoCachingCredentialsProvider provider) {
this.identityPoolId = identityPoolId;
this.provider = provider;
client = new AmazonCognitoSyncClient(provider);
client.setRegion(Region.getRegion(region));
userAgent = "";
}
/*
* (non-Javadoc)
* @see com.amazonaws.cognitov2.RemoteStorage#listDatasets()
*/
@Override
public List<DatasetMetadata> getDatasets() {
List<DatasetMetadata> datasets = new ArrayList<DatasetMetadata>();
String nextToken = null;
do {
ListDatasetsRequest request = new ListDatasetsRequest();
appendUserAgent(request, userAgent);
request.setIdentityPoolId(identityPoolId);
// a large enough number to reduce # of requests
request.setMaxResults(64);
request.setNextToken(nextToken);
ListDatasetsResult result = null;
try {
request.setIdentityId(getIdentityId());
result = client.listDatasets(request);
} catch (AmazonClientException ace) {
throw handleException(ace, "Failed to list dataset metadata");
}
for (com.amazonaws.services.cognitosync.model.Dataset dataset : result.getDatasets()) {
datasets.add(modelToDatasetMetadata(dataset));
}
nextToken = result.getNextToken();
} while (nextToken != null);
return datasets;
}
@Override
public DatasetUpdates listUpdates(String datasetName, long lastSyncCount) {
DatasetUpdatesImpl.Builder builder = new DatasetUpdatesImpl.Builder(datasetName);
String nextToken = null;
do {
ListRecordsRequest request = new ListRecordsRequest();
appendUserAgent(request, userAgent);
request.setIdentityPoolId(identityPoolId);
request.setDatasetName(datasetName);
request.setLastSyncCount(lastSyncCount);
// mark it large enough to reduce # of requests
request.setMaxResults(1024);
request.setNextToken(nextToken);
ListRecordsResult result = null;
try {
request.setIdentityId(getIdentityId());
result = client.listRecords(request);
} catch (AmazonClientException ace) {
throw handleException(ace, "Failed to list records in dataset: " + datasetName);
}
for (com.amazonaws.services.cognitosync.model.Record remoteRecord : result.getRecords()) {
builder.addRecord(modelToRecord(remoteRecord));
}
builder.syncSessionToken(result.getSyncSessionToken())
.syncCount(result.getDatasetSyncCount())
.exists(result.isDatasetExists())
.deleted(result.isDatasetDeletedAfterRequestedSyncCount())
.mergedDatasetNameList(result.getMergedDatasetNames());
// update last evaluated key
nextToken = result.getNextToken();
} while (nextToken != null);
return builder.build();
}
/*
* (non-Javadoc)
* @see com.amazonaws.cognitov2.RemoteStorage#saveRecords(java.lang.String,
* java.util.List)
*/
@Override
public List<Record> putRecords(String datasetName, List<Record> records,
String syncSessionToken, String deviceId) {
UpdateRecordsRequest request = new UpdateRecordsRequest();
appendUserAgent(request, userAgent);
request.setDatasetName(datasetName);
request.setIdentityPoolId(identityPoolId);
request.setDeviceId(deviceId);
request.setSyncSessionToken(syncSessionToken);
// create patches
List<RecordPatch> patches = new ArrayList<RecordPatch>();
for (Record record : records) {
patches.add(recordToPatch(record));
}
request.setRecordPatches(patches);
List<Record> updatedRecords = new ArrayList<Record>();
try {
request.setIdentityId(getIdentityId());
UpdateRecordsResult result = client.updateRecords(request);
for (com.amazonaws.services.cognitosync.model.Record remoteRecord : result.getRecords()) {
updatedRecords.add(modelToRecord(remoteRecord));
}
} catch (AmazonClientException ace) {
throw handleException(ace, "Failed to update records in dataset: " + datasetName);
}
return updatedRecords;
}
@Override
public void deleteDataset(String datasetName) {
DeleteDatasetRequest request = new DeleteDatasetRequest();
appendUserAgent(request, userAgent);
request.setIdentityPoolId(identityPoolId);
request.setDatasetName(datasetName);
try {
request.setIdentityId(getIdentityId());
client.deleteDataset(request);
} catch (AmazonClientException ace) {
throw handleException(ace, "Failed to delete dataset: " + datasetName);
}
}
/**
* Converts a record to a RecordPatch operation.
*
* @param record
* @return
*/
RecordPatch recordToPatch(Record record) {
RecordPatch patch = new RecordPatch();
patch.setKey(record.getKey());
patch.setValue(record.getValue());
patch.setSyncCount(record.getSyncCount());
patch.setOp(record.getValue() == null ? Operation.Remove : Operation.Replace);
if (record.getDeviceLastModifiedDate() != null) {
patch.setDeviceLastModifiedDate(record.getDeviceLastModifiedDate());
}
return patch;
}
/**
* Converts a Cognito sync service Record object to generic Record object.
*
* @param model a service model object
* @return Record object
*/
Record modelToRecord(com.amazonaws.services.cognitosync.model.Record model) {
return new Record.Builder(model.getKey())
.value(model.getValue())
.syncCount(model.getSyncCount() == null ? 0 : model.getSyncCount())
.lastModifiedBy(model.getLastModifiedBy())
.lastModifiedDate(model.getLastModifiedDate() == null
? new Date(0)
: model.getLastModifiedDate())
.deviceLastModifiedDate(model.getDeviceLastModifiedDate() == null
? new Date(0)
: model.getDeviceLastModifiedDate())
.modified(false)
.build();
}
@Override
public DatasetMetadata getDatasetMetadata(String datasetName) throws DataStorageException {
DescribeDatasetRequest request = new DescribeDatasetRequest();
appendUserAgent(request, userAgent);
request.setIdentityPoolId(identityPoolId);
DatasetMetadata dataset = null;
try {
request.setIdentityId(getIdentityId());
request.setDatasetName(datasetName);
DescribeDatasetResult result = client.describeDataset(request);
dataset = modelToDatasetMetadata(result.getDataset());
} catch (AmazonClientException ace) {
throw handleException(ace, "Failed to get metadata of dataset: " + datasetName);
}
return dataset;
}
/**
* Translate AmazonClientException to DataStorageException.
*
* @param ace an AmazonClientException
* @param message extra message to include
* @return an DataStorageException
*/
DataStorageException handleException(AmazonClientException ace, String message) {
if (ace instanceof ResourceNotFoundException) {
return new DatasetNotFoundException(message);
} else if (ace instanceof ResourceConflictException) {
return new DataConflictException(message);
} else if (ace instanceof LimitExceededException) {
return new DataLimitExceededException(message);
} else if (ace instanceof NotAuthorizedException) {
return new DataAccessNotAuthorizedException(message);
} else if (isNetworkException(ace)) {
return new NetworkException(message);
} else {
return new DataStorageException(message, ace);
}
}
String getIdentityId() throws AmazonClientException, NotAuthorizedException {
return provider.getIdentityId();
}
/**
* Test whether an AmazonClientException is caused by network problem.
*
* @param ace an AmazonClientException
* @return true if the exception is caused by network problem, false
* otherwise.
*/
boolean isNetworkException(AmazonClientException ace) {
return ace.getCause() instanceof IOException;
}
/**
* Sets the user agent string to append to all requests made by this client.
*
* @param userAgent user agent string to append
*/
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
/**
* Append user agent string to the request. The final string is what is set
* in the ClientCofniguration concatenated with the given userAgent string.
*
* @param request the request object to be appended
* @param userAgent additional user agent string to append
*/
void appendUserAgent(AmazonWebServiceRequest request, String userAgent) {
request.getRequestClientOptions().appendUserAgent(userAgent);
}
@Override
public void unsubscribeFromDataset(String datasetName, String deviceId) {
String identityId = provider.getIdentityId();
UnsubscribeFromDatasetRequest request = new UnsubscribeFromDatasetRequest()
.withIdentityPoolId(provider.getIdentityPoolId())
.withIdentityId(identityId)
.withDatasetName(datasetName)
.withDeviceId(deviceId);
try {
client.unsubscribeFromDataset(request);
} catch (AmazonClientException ace) {
Log.e(TAG, "Failed to unsubscribe from dataset", ace);
throw new UnsubscribeFailedException("Failed to unsubscribe from dataset", ace);
}
}
@Override
public void subscribeToDataset(String datasetName, String deviceId) {
String identityId = provider.getIdentityId();
SubscribeToDatasetRequest request = new SubscribeToDatasetRequest()
.withIdentityPoolId(provider.getIdentityPoolId())
.withIdentityId(identityId)
.withDatasetName(datasetName)
.withDeviceId(deviceId);
try {
client.subscribeToDataset(request);
} catch (AmazonClientException ace) {
Log.e(TAG, "Failed to subscribe to dataset", ace);
throw new SubscribeFailedException("Failed to subscribe to dataset", ace);
}
}
private DatasetMetadata modelToDatasetMetadata(
com.amazonaws.services.cognitosync.model.Dataset model) {
return new DatasetMetadata.Builder(model.getDatasetName())
.creationDate(model.getCreationDate())
.lastModifiedDate(model.getLastModifiedDate())
.lastModifiedBy(model.getLastModifiedBy())
.storageSizeBytes(model.getDataStorage())
.recordCount(model.getNumRecords())
.build();
}
static class DatasetUpdatesImpl implements DatasetUpdates {
private final String datasetName;
private final List<Record> records;
private final long syncCount;
private final String syncSessionToken;
private final boolean exists;
private final boolean deleted;
private final List<String> mergedDatasetNameList;
@Override
public String getDatasetName() {
return datasetName;
}
@Override
public List<Record> getRecords() {
return records;
}
@Override
public long getSyncCount() {
return syncCount;
}
@Override
public String getSyncSessionToken() {
return syncSessionToken;
}
@Override
public boolean isExists() {
return exists;
}
@Override
public boolean isDeleted() {
return deleted;
}
@Override
public List<String> getMergedDatasetNameList() {
return mergedDatasetNameList;
}
private DatasetUpdatesImpl(Builder builder) {
this.datasetName = builder.datasetName;
this.records = builder.records;
this.syncCount = builder.syncCount;
this.syncSessionToken = builder.syncSessionToken;
this.exists = builder.exists;
this.deleted = builder.deleted;
this.mergedDatasetNameList = builder.mergedDatasetNameList;
}
static class Builder {
private final String datasetName;
private final List<Record> records = new ArrayList<Record>();
private long syncCount = 0;
private String syncSessionToken;
private boolean exists = true;
private boolean deleted = false;
private final List<String> mergedDatasetNameList = new ArrayList<String>();
Builder(String datasetName) {
this.datasetName = datasetName;
}
Builder syncSessionToken(String syncSessionToken) {
this.syncSessionToken = syncSessionToken;
return this;
}
Builder syncCount(long syncCount) {
this.syncCount = syncCount;
return this;
}
Builder exists(boolean exists) {
this.exists = exists;
return this;
}
Builder deleted(boolean deleted) {
this.deleted = deleted;
return this;
}
Builder addRecord(Record record) {
records.add(record);
return this;
}
Builder mergedDatasetNameList(List<String> mergedDatasetNameList) {
if (mergedDatasetNameList != null) {
this.mergedDatasetNameList.addAll(mergedDatasetNameList);
}
return this;
}
DatasetUpdates build() {
return new DatasetUpdatesImpl(this);
}
}
}
}