/*
* Copyright (c) 2013, the authors.
*
* This file is part of 'DXFS'.
*
* DXFS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DXFS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DXFS. If not, see <http://www.gnu.org/licenses/>.
*/
package nextflow.fs.dx.api;
import static nextflow.fs.dx.DxHelper.jsonToObj;
import static nextflow.fs.dx.DxHelper.objToJson;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import nextflow.fs.dx.DxHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A wrapper over the {@code DXAPI} class
*
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
* @author Beatriz San Juan <bmsanjuan@gmail.com>
*
*/
public class DxApi {
private static Logger log = LoggerFactory.getLogger(DxApi.class);
static class Holder {
private static final DxApi INSTANCE = new DxApi( DxHttpClient.getInstance() );
}
private final DxHttpClient client;
private final JsonNode EMPTY_JSON = objToJson(new HashMap<>(0));
// keep track of the last method abject for testing purpose
private DxMethod lastMethodObj;
/*
* Access to the last method object
* NOT thread safe -- ONLY FOR TESTING
*/
DxMethod getMethodObj() { return lastMethodObj; }
Map getMethodMap() {
Map result = new HashMap(2);
result.put("action", lastMethodObj.action );
result.put("input", DxHelper.jsonToObj(lastMethodObj.input));
return result;
}
/**
* Wrap the API method to be invoked
*/
public class DxMethod {
final String action;
final JsonNode input;
public DxMethod( String m, JsonNode args) {
this.action = m; this.input = args;
}
/**
* Invoke the target method
* @param <T>
* @return The object as returned by the target API call
*/
@SuppressWarnings("unchecked")
public <T> T call() throws IOException {
if( log.isDebugEnabled() ) {
log.debug("Call API method '" + action + "' with params: " + input.toString());
}
T result = (T) client.request(action, input);
if( log.isDebugEnabled() ) {
log.debug(String.format("[result of method '%s' ==> %s]", action, String.valueOf(result)));
}
return result;
}
}
/**
* Used only internally, access through the singleton access method
*
* @param client
*/
protected DxApi(DxHttpClient client) {
this.client = client;
}
/**
* @return {@code DxApi} instance object
*/
static public DxApi getInstance() {
return Holder.INSTANCE;
}
/**
* Get a method reference to the specified api *verb* and arguments
*
* @param verb
* @param args
* @return
*/
DxMethod api( String verb, Object... args) {
if( args.length == 0 ) {
args = new Object[] { EMPTY_JSON };
}
else if( args.length>1 ) {
throw new IllegalArgumentException("DxApi api accepts at most one argument");
}
if( !(args[0] instanceof JsonNode) ) {
throw new IllegalArgumentException("DxApi api argument must be a JsonNode");
}
return lastMethodObj = new DxMethod(verb, (JsonNode)args[0]);
}
// ------------------------ PUBLIC API START HERE -------------------------
/**
* See https://wiki.dnanexus.com/API-Specification-v1.0.0/Projects#API-method:-/project/new
*
* @param name
* @return
*/
public String projectNew(String name) throws IOException {
Objects.requireNonNull(name);
Map inputs = new HashMap();
inputs.put("name",name);
JsonNode node = objToJson(inputs);
JsonNode result = api("/project/new", node).call();
return result.get("id").textValue();
}
/**
* See https://wiki.dnanexus.com/API-Specification-v1.0.0/Projects#API-method:-/project-xxxx/describe
* @param projectId
* @return
*/
public Map projectDescribe( String projectId, String... fields ) throws IOException {
Objects.requireNonNull(projectId);
Map inputs = new HashMap();
for( String item : fields ) {
inputs.put(item, true);
}
JsonNode result = api(String.format("/%s/describe",projectId), objToJson(inputs)).call();
return jsonToObj(result);
}
/**
* See https://wiki.dnanexus.com/API-Specification-v1.0.0/Projects#API-method:-/project-xxxx/destroy
* @param projectId
* @return
*/
public void projectDestroy(String projectId) throws IOException {
api(String.format("/%s/destroy",projectId)).call();
}
/**
* Invoke DnaNexus API
*
* See https://wiki.dnanexus.com/API-Specification-v1.0.0/Search#API-method:-/system/findDataObjects
*
* @param args
* @return
*/
public List systemFindDataObjects( Map args ) throws IOException {
DxApi.DxMethod method = api("/system/findDataObjects", objToJson(args));
JsonNode result = method.call();
Map map = jsonToObj(result);
return (List) map.get("results") ;
}
/**
* Find a file in remote DnaNexus cloud storage
*
* @param projectId
* @param fullyQualifiedFileName
* @return
*/
public List fileFind(String projectId, String fullyQualifiedFileName) throws IOException {
int p = fullyQualifiedFileName.lastIndexOf('/');
String folder;
String fileName;
if( p == -1 ) {
folder = "/";
fileName = fullyQualifiedFileName;
}
else {
fileName = fullyQualifiedFileName.substring(p+1);
folder = fullyQualifiedFileName.substring(0,p);
}
Map<String,Object> scope = new HashMap<>();
scope.put("folder", folder);
scope.put("project", projectId);
Map<String,Object> args = new HashMap<>();
args.put("class","file");
args.put("name", fileName);
args.put("scope", scope);
return systemFindDataObjects(args);
}
/**
* https://wiki.dnanexus.com/API-Specification-v1.0.0/Search#API-method:-/system/findProjects
*/
@SuppressWarnings("unchecked")
public List<Map<String,Object>> projectFind(String name) throws IOException {
Map<String,String> request = new HashMap<>(1);
if( name != null && !"".equals(name)) {
request.put("name", name);
}
JsonNode result = api("/system/findProjects", objToJson(request)).call();
return (List<Map<String, Object>>) ((Map) jsonToObj(result)).get("results");
}
/**
* ListCmd the content of a folder as define by the following API
*
* https://wiki.dnanexus.com/API-Specification-v1.0.0/Folders-and-Deletion#API-method:-/class-xxxx/listFolder
*
* @param contextId
* @param path The folder to be listed. Note" must start with a / (slash) character
* @param describe
* @return
*/
public Map<String,Object> folderList(String contextId, String path, boolean describe) throws IOException {
Map<String,Object> input = new HashMap<>();
input.put("folder", path);
input.put("describe", describe);
JsonNode result = api(String.format("/%s/listFolder",contextId), objToJson(input)).call() ;
return jsonToObj(result);
}
/**
* Wrap DnaNexus *newFolder* operation as define by
* https://wiki.dnanexus.com/API-Specification-v1.0.0/Folders-and-Deletion#API-method:-/class-xxxx/newFolder
*
* @param path
* @return the entity ID of the manipulated data container
*/
public String folderCreate(String containerId, String path, boolean create) throws IOException {
/*
* An example input JSON object
*
* '{"folder":"/alpha/beta/delta", "parents":true}'
*/
JsonNode obj = DxJson.getObjectBuilder()
.put("folder", path)
.put("parents", true)
.build();
JsonNode result = api(String.format("/%s/newFolder", containerId), obj).call();
return result.get("id").textValue();
}
/**
* Create the {@code JsonNode} object to delete a folder.
* <p>
* Example object format:
*
* <pre>
* '{"folder":"/folderName", "recurse": true}'
* </pre>
*
* <p>
* Read more https://wiki.dnanexus.com/API-Specification-v1.0.0/Folders-and-Deletion#API-method:-/class-xxxx/removeFolder
* </p>
*
* @param path
*
* @return The entity ID of the manipulated data container
*/
public String folderDelete(String contextId, String path, boolean recurse) throws IOException {
JsonNode node = DxJson.getObjectBuilder()
.put("folder", path)
.put("recurse", recurse)
.build();
JsonNode result = api(String.format("/%s/removeFolder", contextId), node).call();
return result.get("id").textValue();
}
/**
* Creater the {@code JsonNode} input object required to delete one, or more, files
*
* <p>
* Example JSON object format:
* <pre>
* '{"objects":["file-B86Q5B80j58B67zVXG3Q01K8"]}'
* </pre>
* </p>
*
*
*
* <p>
* http://wiki.dnanexus.com/API-Specification-v1.0.0/Folders-and-Deletion#API-method:-/class-xxxx/removeObjects
* </p>
*
* @param fileIds an array of strings representing IDs of the objects to be removed from the data
* @return @return The entity ID of the manipulated data container
*/
public String fileDelete(String contextId, String... fileIds) throws IOException {
ArrayNode container = new ArrayNode(JsonNodeFactory.instance);
for( String item : fileIds ) {
container.add( item );
}
JsonNode input = DxJson.getObjectBuilder().put("objects", container).build();
JsonNode result = api(String.format("/%s/removeObjects", contextId), input).call();
return result.get("id").textValue();
}
/**
* Describe a file
*
* Read more https://wiki.dnanexus.com/API-Specification-v1.0.0/Files#API-method%3A-%2Ffile-xxxx%2Fdescribe
*
*
* @param fileId
* @return
*/
public Map<String,Object> fileDescribe(String fileId) throws IOException {
JsonNode node = api(String.format("/%s/describe", fileId)).call();
return jsonToObj(node);
}
/**
* https://wiki.dnanexus.com/API-Specification-v1.0.0/Name#API-method%3A-%2Fclass-xxxx%2Frename
*
* @param contextId
* @param fileId
* @param name
* @return
*/
public String rename (String contextId, String fileId, String name) throws IOException {
JsonNode input = DxJson.getObjectBuilder()
.put("project", contextId)
.put("name", name)
.build();
JsonNode result = api(String.format("/%s/rename", fileId), input).call();
return result.get("id").textValue();
}
/**
* https://wiki.dnanexus.com/API-Specification-v1.0.0/Tags#API-method%3A-%2Fclass-xxxx%2FaddTags
*
* @param contextId
* @param fileId
* @param tags
* @return
*/
public String addFileTags(String contextId, String fileId, String[] tags) throws IOException {
ArrayNode container = new ArrayNode(JsonNodeFactory.instance);
for( String item : tags ) {
container.add( item );
}
JsonNode input = DxJson.getObjectBuilder()
.put("project", contextId)
.put("tags", container)
.build();
JsonNode result = api(String.format("/%s/addTags",fileId), input).call();
return result.get("id").textValue();
}
/**
* https://wiki.dnanexus.com/API-Specification-v1.0.0/Types#API-method%3A-%2Fclass-xxxx%2FaddTypes
*
* @param contextId
* @param fileId
* @param types
* @return
*/
public String addFileTypes(String contextId, String fileId, String[] types) throws IOException {
ArrayNode container = new ArrayNode(JsonNodeFactory.instance);
for( String item : types ) {
container.add( item );
}
JsonNode input = DxJson.getObjectBuilder()
.put("project", contextId)
.put("types", container)
.build();
JsonNode result = api(String.format("/%s/addTypes",fileId), input).call();
return result.get("id").textValue();
}
/**
* Given a file-id return the a URL and the authorization code to download it.
*
* See https://wiki.dnanexus.com/API-Specification-v1.0.0/Files#API-method%3A-%2Ffile-xxxx%2Fdownload
*
* @param fileId
* @return A map object holding the
*/
public Map<String,Object> fileDownload(String fileId) throws IOException {
JsonNode result = api(String.format("/%s/download",fileId)).call();
return jsonToObj(result);
}
/**
* Create a new (empty) file ready
*
* Read more
* https://wiki.dnanexus.com/API-Specification-v1.0.0/Files#API-method%3A-%2Ffile%2Fnew
*
* @param contextId
* @param path
* @return
*/
public String fileNew( String contextId, String path ) throws IOException {
String name;
String folder = null;
int pos = path.lastIndexOf('/');
if( pos == -1 ) {
name = path;
}
else {
name = path.substring(pos+1);
folder = path.substring(0,pos);
}
Map<String,Object> uploadInfo = new HashMap<>();
uploadInfo.put("project", contextId);
uploadInfo.put("name", name);
if( folder != null && folder.length()>0 ) {
// set the parent folder where it must be stored
uploadInfo.put("folder", folder);
uploadInfo.put("parents", true);
}
return fileNew(uploadInfo);
}
/**
* Create a new (empty) file ready
*
* Read more
* https://wiki.dnanexus.com/API-Specification-v1.0.0/Files#API-method%3A-%2Ffile%2Fnew
*
* @param uploadInfo
* @return
*/
public String fileNew( Map<String,Object> uploadInfo ) throws IOException {
JsonNode result = api("/file/new", objToJson(uploadInfo)).call();
return result.get("id").textValue();
}
/**
* Upload a file (part) to the remote container
*
* See https://wiki.dnanexus.com/API-Specification-v1.0.0/Files#API-method:-/file-xxxx/upload
*
* @param fileId The file-id of the file being uploaded
* @param index The chunk index of the filed being uploaded (1-based)
* @return
*/
public Map fileUpload( String fileId, int index ) throws IOException {
JsonNode input = DxJson.getObjectBuilder().put("index", index).build();
JsonNode result = api(String.format("/%s/upload",fileId), input).call();
return jsonToObj(result);
}
/**
* Close the file when upload is complete
*
* See https://wiki.dnanexus.com/API-Specification-v1.0.0/Files#API-method:-/file-xxxx/close
*
* @param fileId The file-id of the file being uploaded
* @return
*/
public Map fileClose( String fileId ) throws IOException {
JsonNode result = api(String.format("/%s/close",fileId)).call();
return jsonToObj(result);
}
/**
* Clone a file to the specified path
*
* @param sourceContainerId
* @param sourceFileId
* @param targetContainerId
* @param targetPath
* @return An {@code Map} containing the following elements:
* <li>id: the entity ID of the source container
* <li>project: the entity ID of the modified destination container
* <li>exists: an array of strings representing the object IDs that could not be cloned because they already exist in the destination container
*/
public Map<String,Object> fileClone( String sourceContainerId, String sourceFileId, String targetContainerId, String targetPath ) throws IOException {
List<String> wrap = new ArrayList<>();
wrap.add(sourceFileId);
return fileClone(sourceContainerId, wrap, targetContainerId, targetPath);
}
/**
* Clone a file to the specified path
*
* See https://wiki.dnanexus.com/API-Specification-v1.0.0/Cloning#API-method:-/class-xxxx/clone
*
* @param sourceContainerId
* @param sourceFileId
* @param targetContainerId
* @param targetPath
* @return An {@code Map} containing the following elements:
* <li>id: the entity ID of the source container
* <li>project: the entity ID of the modified destination container
* <li>exists: an array of strings representing the object IDs that could not be cloned because they already exist in the destination container
*/
public Map<String,Object> fileClone( String sourceContainerId, List<String> sourceFileId, String targetContainerId, String targetPath ) throws IOException {
if( sourceContainerId==null || sourceContainerId.isEmpty() ) throw new IllegalStateException("Argument `sourceContainerId` cannot be empty");
if( targetContainerId==null || targetContainerId.isEmpty() ) throw new IllegalStateException("Argument `targetContainerId` cannot be empty");
Map<String,Object> request = new HashMap<>();
request.put("objects", sourceFileId); // list of files to be copied
request.put("project", targetContainerId); // target container ID
request.put("destination", targetPath); // folder where store the file
request.put("parents", true); // create the target folder if do not exist
JsonNode result = api(String.format(String.format("/%s/clone", sourceContainerId)), objToJson(request)).call();
return jsonToObj(result);
}
/**
* See https://wiki.dnanexus.com/API-Specification-v1.0.0/Applets%20and%20Entry%20Points#API-method:-/job/new
*
* @param inputs
* @return
*/
public String jobNew(Map<String,Object> inputs) throws IOException {
JsonNode result = api("/job/new", objToJson(inputs)).call();
return result.get("id").textValue();
}
/**
* See https://wiki.dnanexus.com/API-Specification-v1.0.0/Applets%20and%20Entry%20Points#API-method:-/job-xxxx/describe
*
* @param jobId
*/
public Map jobDescribe(String jobId, String... fields) throws IOException {
Objects.requireNonNull(jobId);
Map inputs = new HashMap();
for( String item : fields ) {
inputs.put(item, true);
}
JsonNode result = api(String.format("/%s/describe",jobId), objToJson(inputs)).call();
return jsonToObj(result);
}
/**
* See https://wiki.dnanexus.com/API-Specification-v1.0.0/Applets%20and%20Entry%20Points#API-method:-/job-xxxx/terminate
*
* @param jobId
*/
public void jobTerminate(String jobId) throws IOException {
Objects.requireNonNull(jobId);
api(String.format("/%s/terminate",jobId)).call();
}
}