Skip to content

Commit

Permalink
Merge pull request #649 from DwayneJengSage/develop
Browse files Browse the repository at this point in the history
BRIDGE-3389 Upload Status API for EX3
  • Loading branch information
DwayneJengSage authored Mar 26, 2023
2 parents 35c061d + ea106ef commit 5dab4cd
Show file tree
Hide file tree
Showing 30 changed files with 1,655 additions and 123 deletions.
11 changes: 10 additions & 1 deletion src/main/java/org/sagebionetworks/bridge/AuthUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,16 @@ public class AuthUtils {
.isSelf().isEnrolledInStudy().or()
.canAccessStudy().hasAnyRole(STUDY_DESIGNER, STUDY_COORDINATOR).or()
.hasAnyRole(DEVELOPER, RESEARCHER, WORKER, ADMIN);


/**
* Can the caller read uploads? Users can read their own uploads. Study designers and study coordinators can read
* any upload in their study. Developers, researchers, workers, and admins can read any upload in the app.
*/
public static final AuthEvaluator CAN_READ_UPLOADS = new AuthEvaluator()
.isSelf().or()
.canAccessStudy().hasAnyRole(STUDY_DESIGNER, STUDY_COORDINATOR).or()
.hasAnyRole(DEVELOPER, RESEARCHER, WORKER, ADMIN);

/**
* Does the caller have the required role? Note that a few roles pass for other roles.
*/
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/org/sagebionetworks/bridge/dao/UploadDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ Upload createUpload(@Nonnull UploadRequest uploadRequest, @Nonnull String appId,
* @return upload metadata
*/
Upload getUpload(@Nonnull String uploadId);


/** Get upload. Returns null if the upload doesn't exist. */
Upload getUploadNoThrow(String uploadId);

/**
* Get the uploads for an indicated time range.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.sagebionetworks.bridge.json.DateTimeToLongDeserializer;
import org.sagebionetworks.bridge.json.DateTimeToLongSerializer;
import org.sagebionetworks.bridge.models.accounts.SharingScope;
import org.sagebionetworks.bridge.models.exporter.ExportedRecordInfo;
import org.sagebionetworks.bridge.models.healthdata.HealthDataRecordEx3;

@DynamoDBTable(tableName = "HealthDataRecordEx3")
Expand All @@ -35,6 +36,8 @@ public class DynamoHealthDataRecordEx3 implements HealthDataRecordEx3 {
private String clientInfo;
private boolean exported;
private Long exportedOn;
private ExportedRecordInfo exportedRecord;
private Map<String, ExportedRecordInfo> exportedStudyRecords;
private Map<String, String> metadata;
private SharingScope sharingScope;
private Long version;
Expand Down Expand Up @@ -173,6 +176,30 @@ public void setExportedOn(Long exportedOn) {
this.exportedOn = exportedOn;
}

@DynamoDBTypeConverted(converter = ExportedRecordInfoMarshaller.class)
@Override
public ExportedRecordInfo getExportedRecord() {
return exportedRecord;
}

@Override
public void setExportedRecord(ExportedRecordInfo exportedRecord) {
this.exportedRecord = exportedRecord;
}

@DynamoDBTypeConverted(converter = ExportedRecordInfoMapMarshaller.class)
@Override
public Map<String, ExportedRecordInfo> getExportedStudyRecords() {
return exportedStudyRecords;
}

@Override
public void setExportedStudyRecords(Map<String, ExportedRecordInfo> exportedStudyRecords) {
// Dynamo DB doesn't support empty maps.
this.exportedStudyRecords = exportedStudyRecords != null && !exportedStudyRecords.isEmpty() ?
exportedStudyRecords : null;
}

@Override
public Map<String, String> getMetadata() {
return metadata;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

Expand All @@ -56,6 +58,8 @@

@Component
public class DynamoUploadDao implements UploadDao {
private static final Logger LOG = LoggerFactory.getLogger(DynamoUploadDao.class);

static final String PAGE_SIZE_ERROR = "pageSize must be from 1-"+API_MAXIMUM_PAGE_SIZE+" records";

private DynamoDBMapper mapper;
Expand Down Expand Up @@ -118,26 +122,34 @@ public Upload createUpload(@Nonnull UploadRequest uploadRequest, @Nonnull String
/** {@inheritDoc} */
@Override
public Upload getUpload(@Nonnull String uploadId) {
Upload upload = getUploadNoThrow(uploadId);
if (upload == null) {
throw new NotFoundException(String.format("Upload ID %s not found", uploadId));
}
return upload;
}

@Override
public Upload getUploadNoThrow(String uploadId) {
// Fetch upload from DynamoUpload2
DynamoUpload2 key = new DynamoUpload2();
key.setUploadId(uploadId);
DynamoUpload2 upload = mapper.load(key);
if (upload != null) {
// Very old uploads (2+ years ago) did not have appId set; for these we must do
// Very old uploads (2+ years ago) did not have appId set; for these we must do
// a lookup in the legacy DynamoHealthCode table.
if (upload.getAppId() == null) {
if (upload.getAppId() == null) {
String appId = healthCodeDao.getAppId(upload.getHealthCode());
if (appId == null) {
throw new EntityNotFoundException(DynamoApp.class,
"App not found for upload. User may have been deleted from system.");
LOG.error("App not found for upload " + uploadId + ". User may have been deleted from system.");
return null;
}
upload.setAppId(appId);
}
return upload;
}
throw new NotFoundException(String.format("Upload ID %s not found", uploadId));
return upload;
}

/** {@inheritDoc} */
@Override
public ForwardCursorPagedResourceList<Upload> getUploads(String healthCode, DateTime startTime, DateTime endTime,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.sagebionetworks.bridge.dynamodb;

import java.util.Map;

import com.fasterxml.jackson.core.type.TypeReference;

import org.sagebionetworks.bridge.models.exporter.ExportedRecordInfo;

public class ExportedRecordInfoMapMarshaller extends StringKeyMapMarshaller<ExportedRecordInfo> {
private static final TypeReference<Map<String, ExportedRecordInfo>> REF = new TypeReference<Map<String, ExportedRecordInfo>>() {
};

@Override public TypeReference<Map<String, ExportedRecordInfo>> getTypeReference() {
return REF;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.sagebionetworks.bridge.dynamodb;

import org.sagebionetworks.bridge.models.exporter.ExportedRecordInfo;

public class ExportedRecordInfoMarshaller extends JsonMarshaller<ExportedRecordInfo> {
@Override
public Class<ExportedRecordInfo> getConvertedClass() {
return ExportedRecordInfo.class;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.sagebionetworks.bridge.dynamodb;

import java.io.IOException;

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingException;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConverter;
import com.fasterxml.jackson.core.JsonProcessingException;

import org.sagebionetworks.bridge.json.BridgeObjectMapper;

/**
* Generic JSON marshaller for DynamoDB. This converts the object into JSON for marshalling to DynamoDB. Because
* DynamoDB annotations don't work with generics, you'll need to subclass this and fill in getConvertedClass().
*/
public abstract class JsonMarshaller<T> implements DynamoDBTypeConverter<String, T> {
/**
* Returns the class for Jackson to deserialize the value from DynamoDB, because Java can't infer generic types at
* runtime.
*/
public abstract Class<T> getConvertedClass();

@Override
public String convert(T t) {
if (t == null) {
return null;
}
try {
return BridgeObjectMapper.get().writerWithDefaultPrettyPrinter().writeValueAsString(t);
} catch (JsonProcessingException ex) {
throw new DynamoDBMappingException(ex);
}
}

@Override
public T unconvert(String json) {
if (json == null) {
return null;
}
try {
return BridgeObjectMapper.get().readValue(json, getConvertedClass());
} catch (IOException ex) {
throw new DynamoDBMappingException(ex);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,63 +10,10 @@
* by the worker to trigger notifications and generate study-specific notifications.
*/
public class ExportToAppNotification implements BridgeEntity {
public static class RecordInfo {
private String parentProjectId;
private String rawFolderId;
private String fileEntityId;
private String s3Bucket;
private String s3Key;

/** Synapse project that the health data is exported to. */
public String getParentProjectId() {
return parentProjectId;
}

public void setParentProjectId(String parentProjectId) {
this.parentProjectId = parentProjectId;
}

/** Synapse folder that the health data is exported to. */
public String getRawFolderId() {
return rawFolderId;
}

public void setRawFolderId(String rawFolderId) {
this.rawFolderId = rawFolderId;
}

/** Synapse file entity of the health data that is exported. */
public String getFileEntityId() {
return fileEntityId;
}

public void setFileEntityId(String fileEntityId) {
this.fileEntityId = fileEntityId;
}

/** S3 bucket that contains the exported health data. */
public String getS3Bucket() {
return s3Bucket;
}

public void setS3Bucket(String s3Bucket) {
this.s3Bucket = s3Bucket;
}

/** S3 key that of the exported health data. */
public String getS3Key() {
return s3Key;
}

public void setS3Key(String s3Key) {
this.s3Key = s3Key;
}
}

private String appId;
private String recordId;
private RecordInfo record;
private Map<String, RecordInfo> studyRecords = new HashMap<>();
private ExportedRecordInfo record;
private Map<String, ExportedRecordInfo> studyRecords = new HashMap<>();

/** App that the health data is exported for. */
public String getAppId() {
Expand All @@ -90,23 +37,23 @@ public void setRecordId(String recordId) {
* Record that is exported to the app-wide Synapse project. May be null if there is no app-wide Synapse project
* configured.
*/
public RecordInfo getRecord() {
public ExportedRecordInfo getRecord() {
return record;
}

public void setRecord(RecordInfo record) {
public void setRecord(ExportedRecordInfo record) {
this.record = record;
}

/**
* Records that are exported to the study-specific Synapse project, keyed by study ID. May be empty if there are no
* study-specific Synapse projects configured.
*/
public Map<String, RecordInfo> getStudyRecords() {
public Map<String, ExportedRecordInfo> getStudyRecords() {
return studyRecords;
}

public void setStudyRecords(Map<String, RecordInfo> studyRecords) {
public void setStudyRecords(Map<String, ExportedRecordInfo> studyRecords) {
this.studyRecords = studyRecords != null ? studyRecords : new HashMap<>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.sagebionetworks.bridge.models.exporter;

/** Info about a health data record that was exported to Synapse. */
public class ExportedRecordInfo {
private String parentProjectId;
private String rawFolderId;
private String fileEntityId;
private String s3Bucket;
private String s3Key;

/** Synapse project that the health data is exported to. */
public String getParentProjectId() {
return parentProjectId;
}

public void setParentProjectId(String parentProjectId) {
this.parentProjectId = parentProjectId;
}

/** Synapse folder that the health data is exported to. */
public String getRawFolderId() {
return rawFolderId;
}

public void setRawFolderId(String rawFolderId) {
this.rawFolderId = rawFolderId;
}

/** Synapse file entity of the health data that is exported. */
public String getFileEntityId() {
return fileEntityId;
}

public void setFileEntityId(String fileEntityId) {
this.fileEntityId = fileEntityId;
}

/** S3 bucket that contains the exported health data. */
public String getS3Bucket() {
return s3Bucket;
}

public void setS3Bucket(String s3Bucket) {
this.s3Bucket = s3Bucket;
}

/** S3 key that of the exported health data. */
public String getS3Key() {
return s3Key;
}

public void setS3Key(String s3Key) {
this.s3Key = s3Key;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.sagebionetworks.bridge.json.BridgeTypeName;
import org.sagebionetworks.bridge.models.BridgeEntity;
import org.sagebionetworks.bridge.models.accounts.SharingScope;
import org.sagebionetworks.bridge.models.exporter.ExportedRecordInfo;
import org.sagebionetworks.bridge.models.upload.Upload;

/**
Expand Down Expand Up @@ -103,6 +104,20 @@ static HealthDataRecordEx3 createFromUpload(Upload upload) {
Long getExportedOn();
void setExportedOn(Long exportedOn);

/**
* Record that is exported to the app-wide Synapse project. May be null if there is no app-wide Synapse project
* configured.
*/
ExportedRecordInfo getExportedRecord();
void setExportedRecord(ExportedRecordInfo exportedRecord);

/**
* Records that are exported to the study-specific Synapse project, keyed by study ID. May be empty if there are no
* study-specific Synapse projects configured.
*/
Map<String, ExportedRecordInfo> getExportedStudyRecords();
void setExportedStudyRecords(Map<String, ExportedRecordInfo> exportedStudyRecords);

/** Client-submitted metadata, as a map of key-value pairs. */
Map<String, String> getMetadata();
void setMetadata(Map<String, String> metadata);
Expand Down
Loading

0 comments on commit 5dab4cd

Please sign in to comment.