Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

5028 Add dataset level external tools #6059

Merged
merged 30 commits into from
Sep 13, 2019
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f699fd6
Added placeholder Explore btn to dataset pg to be wired up to backend…
mheppler Jul 25, 2019
48d1181
add dataset level external tools #5028
pdurbin Jul 26, 2019
0390e3c
return scope in listing, GET tool by id #5028
pdurbin Jul 26, 2019
6fe2996
adding release notes for dataset explore
djbrooke Jul 31, 2019
e9dad22
Merge branch 'develop' into 5028-dataset-explore-btn
sekmiller Jul 31, 2019
d8f7a96
Merge branch 'develop' into 5028-dataset-explore-btn
sekmiller Aug 5, 2019
c0df37d
removing release note because it's handled by flyway
djbrooke Aug 8, 2019
162207e
get tests passing #5028
pdurbin Aug 26, 2019
319141a
enforce datasetId or datasetPid requirement #5028
pdurbin Aug 27, 2019
3d522aa
allow content type to be null #5028
pdurbin Aug 27, 2019
cc99058
support file PIDs #5028
pdurbin Aug 27, 2019
952c30b
Merge branch 'develop' into 5028-dataset-explore-btn #5028
pdurbin Aug 27, 2019
336a424
adjust docs #5028
pdurbin Aug 27, 2019
8075283
Merge branch 'develop' into 5028-dataset-explore-btn #5028
pdurbin Aug 29, 2019
94f8127
fix typo in method name (remove "scope") #5028
pdurbin Aug 29, 2019
e92c8f1
reduce code duplication #5028
pdurbin Aug 29, 2019
969d5ed
fix assertion in test #5028
pdurbin Aug 29, 2019
f8e1e0e
prevent 500 error if invalid type is supplied #5028
pdurbin Aug 30, 2019
801bf56
rename SQL script to reflect bump to 4.16 #5028
pdurbin Sep 5, 2019
e2e34d3
improve external tools documentation #5028
pdurbin Sep 9, 2019
1578595
Link to the Admin Guide page on external tools #5028
pdurbin Sep 10, 2019
50d6cda
Merge branch 'develop' into 5028-dataset-explore-btn #5028
pdurbin Sep 10, 2019
c89819a
move Building External Tools to API Guide #5028
pdurbin Sep 10, 2019
4897de4
add lots more content for external tool makers #5028
pdurbin Sep 12, 2019
7828dea
ignore python virutal environments (venv)
pdurbin Sep 12, 2019
760d670
Merge branch 'develop' into 5028-dataset-explore-btn #5028
pdurbin Sep 12, 2019
6dd0cfc
rename flyway script # 5028
pdurbin Sep 12, 2019
519662a
get deployment working again for new installations #6165
pdurbin Sep 12, 2019
09766f1
Merge branch '6165-cannot-deploy' into 5028-dataset-explore-btn #5028
pdurbin Sep 12, 2019
06105e8
move testing methods to /api/admin/test #5028 #4137
pdurbin Sep 12, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conf/docker-aio/run-test-suite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ fi

# Please note the "dataverse.test.baseurl" is set to run for "all-in-one" Docker environment.
# TODO: Rather than hard-coding the list of "IT" classes here, add a profile to pom.xml.
mvn test -Dtest=DataversesIT,DatasetsIT,SwordIT,AdminIT,BuiltinUsersIT,UsersIT,UtilIT,ConfirmEmailIT,FileMetadataIT,FilesIT,SearchIT,InReviewWorkflowIT,HarvestingServerIT,MoveIT,MakeDataCountApiIT,FileTypeDetectionIT,EditDDIIT -Ddataverse.test.baseurl=$dvurl
mvn test -Dtest=DataversesIT,DatasetsIT,SwordIT,AdminIT,BuiltinUsersIT,UsersIT,UtilIT,ConfirmEmailIT,FileMetadataIT,FilesIT,SearchIT,InReviewWorkflowIT,HarvestingServerIT,MoveIT,MakeDataCountApiIT,FileTypeDetectionIT,EditDDIIT,ExternalToolsIT -Ddataverse.test.baseurl=$dvurl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"displayName": "Awesome Tool",
"description": "The most awesome tool.",
"type": "explore",
"scope": "file",
"contentType": "text/tab-separated-values",
"toolUrl": "https://awesometool.com",
"toolParameters": {
Expand Down
54 changes: 54 additions & 0 deletions doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,33 @@ Normally published datasets should not be deleted, but there exists a "destroy"

Calling the destroy endpoint is permanent and irreversible. It will remove the dataset and its datafiles, then re-index the parent dataverse in Solr. This endpoint requires the API token of a superuser.

.. _list-external-tools-for-a-dataset-api:

List External Tools for a Dataset
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Some installations of Dataverse have external tools enabled for datasets and there are two types:

- explore
- configure

.. note:: See :ref:`curl-examples-and-environment-variables` if you are unfamiliar with the use of ``export`` below.

.. code-block:: bash

export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export PERSISTENT_ID=doi:10.5072/FK2/J8SJZB
export TYPE=explore

curl -H X-Dataverse-key:$API_TOKEN \""$SERVER_URL/api/datasets/:persistentId/externalTools?persistentId=$PERSISTENT_ID&type=$TYPE"\"

The fully expanded example above (without environment variables) looks like this:

.. code-block:: bash

curl -H X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx "https://demo.dataverse.org/api/datasets/:persistentId/externalTools?persistentId=doi:10.5072/FK2/J8SJZB&type=explore"

Files
-----

Expand Down Expand Up @@ -1023,6 +1050,33 @@ Starting the release 4.10 the size of the saved original file (for an ingested t

Note the optional "limit" parameter. Without it, the API will attempt to populate the sizes for all the saved originals that don't have them in the database yet. Otherwise it will do so for the first N such datafiles.

.. _list-external-tools-for-a-file-api:

List External Tools for a File
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Some installations of Dataverse have external tools enabled for files and there are two types:

- explore
- configure

.. note:: See :ref:`curl-examples-and-environment-variables` if you are unfamiliar with the use of ``export`` below.

.. code-block:: bash

export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export PERSISTENT_ID=doi:10.5072/FK2/J8SJZB
export TYPE=explore

curl -H X-Dataverse-key:$API_TOKEN \""$SERVER_URL/api/files/:persistentId/externalTools?persistentId=$PERSISTENT_ID&type=$TYPE"\"

The fully expanded example above (without environment variables) looks like this:

.. code-block:: bash

curl -H X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx "https://demo.dataverse.org/api/files/:persistentId/externalTools?persistentId=doi:10.5072/FK2/J8SJZB&type=explore"

Builtin Users
-------------

Expand Down
33 changes: 28 additions & 5 deletions doc/sphinx-guides/source/installation/external-tools.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ External tools can provide additional features that are not part of Dataverse it
Inventory of External Tools
---------------------------

Support for external tools is just getting off the ground but the following tools have been successfully integrated with Dataverse:
The following tools have been successfully integrated with Dataverse:

- TwoRavens: a system of interlocking statistical tools for data exploration, analysis, and meta-analysis: http://2ra.vn. See the :doc:`/user/data-exploration/tworavens` section of the User Guide for more information on TwoRavens from the user perspective and the :doc:`r-rapache-tworavens` section of the Installation Guide.

Expand All @@ -35,14 +35,18 @@ External tools must be expressed in an external tool manifest file, a specific J

``type`` is required and must be ``explore`` or ``configure`` to make the tool appear under a button called "Explore" or "Configure", respectively.

External tools can operate on any file, including tabular files that have been created by successful ingestion. (For more on ingest, see the :doc:`/user/tabulardataingest/ingestprocess` of the User Guide.) The optional ``contentType`` entry specifies the mimetype a tool works on. (Not providing this parameter makes the tool work on ingested tabular files and is equivalent to specifying the ``contentType`` as "text/tab-separated-values".)
``scope`` is required and must be ``file`` or ``dataset`` to make the tool appear at the file level or dataset level.

File level tools can operate on any file, including tabular files that have been created by successful ingestion. (For more on ingest, see the :doc:`/user/tabulardataingest/ingestprocess` of the User Guide.) The optional ``contentType`` entry specifies the mimetype a tool works on.

In the example above, a mix of required and optional reserved words appear that can be used to insert dynamic values into tools. The supported values are:

- ``{fileId}`` (required) - The Dataverse database ID of a file the external tool has been launched on.
- ``{siteUrl}`` (optional) - The URL of the Dataverse installation that hosts the file with the fileId above.
- ``{fileId}`` (required for file tools, or ``{filePid}``) - The database ID of a file from which the external tool has been launched.
- ``{filePid}`` (required for file tools, or ``{fileId}``) - The Persistent ID (DOI or Handle) of a file (if available) from which the external tool has been launched.
- ``{siteUrl}`` (optional) - The URL of the Dataverse installation from which the tool was launched.
- ``{apiToken}`` (optional) - The Dataverse API token of the user launching the external tool, if available.
- ``{datasetId}`` (optional) - The ID of the dataset containing the file.
- ``{datasetId}`` (required for dataset tools, or ``{datasetPid}``) - The database ID of the dataset.
- ``{datasetPid}`` (required for dataset tools, or ``{datasetId}``) - The Persistent ID (DOI or Handle) of the dataset.
- ``{datasetVersion}`` (optional) - The friendly version number ( or \:draft ) of the dataset version the tool is being launched from.

Making an External Tool Available in Dataverse
Expand All @@ -59,13 +63,32 @@ To list all the external tools that are available in Dataverse:

``curl http://localhost:8080/api/admin/externalTools``

Showing an External Tool in Dataverse
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose / need for this API? (besides having it in a test?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's for sysadmins to check which external tools have been installed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, for curl http://localhost:8080/api/admin/externalTools, which lists the tools, I'm asking specifically about this new one that takes an id. i.e if you have the id already then you already know about the tool. Doesn't seem to add any value, but maybe I'm missing something.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can imagine a somewhat paranoid sysadmin or tool maker adding a tool and then wanting to double check that correct values were saved in the database. For example, here I'm asserting that the tool that was just added is a dataset level tool but you could make assertions on other things:

    Response getTool = UtilIT.getExternalTool(id);
    getTool.prettyPrint();
    getTool.then().assertThat()
            .body("data.scope", CoreMatchers.equalTo("dataset"))
            .statusCode(OK.getStatusCode());

To me, this is all basic CRUD stuff. We're doing a read of a single item. I meant to add this back when I first implemented external tools but forgot about it. I'm trying to correct an oversight, something I forgot to add.

-------------------------------------

To show one of the external tools that are available in Dataverse, pass its database id:

``export TOOL_ID=1``

``curl http://localhost:8080/api/admin/externalTools/$TOOL_ID``

Removing an External Tool Available in Dataverse
------------------------------------------------

Assuming the external tool database id is "1", remove it with the following command:

``curl -X DELETE http://localhost:8080/api/admin/externalTools/1``

List External Tools for a Dataset
---------------------------------

See :ref:`list-external-tools-for-a-dataset-api`

List External Tools for a File
------------------------------

See :ref:`list-external-tools-for-a-file-api`

Writing Your Own External Tool
------------------------------

Expand Down
5 changes: 5 additions & 0 deletions doc/sphinx-guides/source/user/data-exploration/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Data Exploration Guide

Note that the installation of Dataverse you are using may have additional or different tools configured. Developers interested in creating tools should refer to the :doc:`/installation/external-tools` section of the Installation Guide.

API users can determine if external tools are configured at the dataset for file level by following the instructions below.

- :ref:`list-external-tools-for-a-dataset-api`
- :ref:`list-external-tools-for-a-file-api`

Contents:

.. toctree::
Expand Down
31 changes: 29 additions & 2 deletions src/main/java/edu/harvard/iq/dataverse/DatasetPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
import edu.harvard.iq.dataverse.authorization.Permission;
import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean;
import edu.harvard.iq.dataverse.authorization.users.ApiToken;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser;
import edu.harvard.iq.dataverse.authorization.users.User;
import edu.harvard.iq.dataverse.branding.BrandingUtil;
import edu.harvard.iq.dataverse.dataaccess.StorageIO;
import edu.harvard.iq.dataverse.dataaccess.ImageThumbConverter;
Expand Down Expand Up @@ -99,6 +101,7 @@
import edu.harvard.iq.dataverse.externaltools.ExternalTool;
import edu.harvard.iq.dataverse.externaltools.ExternalToolServiceBean;
import edu.harvard.iq.dataverse.export.SchemaDotOrgExporter;
import edu.harvard.iq.dataverse.externaltools.ExternalToolHandler;
import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean;
import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean.MakeDataCountEntry;
import java.util.Collections;
Expand Down Expand Up @@ -135,6 +138,7 @@
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.primefaces.PrimeFaces;
import org.primefaces.model.DefaultTreeNode;
import org.primefaces.model.TreeNode;

Expand Down Expand Up @@ -312,10 +316,15 @@ public void setShowIngestSuccess(boolean showIngestSuccess) {
this.showIngestSuccess = showIngestSuccess;
}

// TODO: Consider renaming "configureTools" to "fileConfigureTools".
List<ExternalTool> configureTools = new ArrayList<>();
// TODO: Consider renaming "exploreTools" to "fileExploreTools".
List<ExternalTool> exploreTools = new ArrayList<>();
// TODO: Consider renaming "configureToolsByFileId" to "fileConfigureToolsByFileId".
Map<Long, List<ExternalTool>> configureToolsByFileId = new HashMap<>();
// TODO: Consider renaming "exploreToolsByFileId" to "fileExploreToolsByFileId".
Map<Long, List<ExternalTool>> exploreToolsByFileId = new HashMap<>();
private List<ExternalTool> datasetExploreTools;

public Boolean isHasRsyncScript() {
return hasRsyncScript;
Expand Down Expand Up @@ -2014,8 +2023,9 @@ private String init(boolean initFull) {
JsfHelper.addSuccessMessage(BundleUtil.getStringFromBundle("dataset.unlocked.ingest.message"));
}

configureTools = externalToolService.findByType(ExternalTool.Type.CONFIGURE);
exploreTools = externalToolService.findByType(ExternalTool.Type.EXPLORE);
configureTools = externalToolService.findFileToolsByType(ExternalTool.Type.CONFIGURE);
exploreTools = externalToolService.findFileToolsByType(ExternalTool.Type.EXPLORE);
datasetExploreTools = externalToolService.findDatasetToolsByType(ExternalTool.Type.EXPLORE);
rowsPerPage = 10;


Expand Down Expand Up @@ -5052,6 +5062,10 @@ public List<ExternalTool> getCachedToolsForDataFile(Long fileId, ExternalTool.Ty
return cachedTools;
}

public List<ExternalTool> getDatasetExploreTools() {
return datasetExploreTools;
}

Boolean thisLatestReleasedVersion = null;

public boolean isThisLatestReleasedVersion() {
Expand Down Expand Up @@ -5200,4 +5214,17 @@ public int compare(FileMetadata o1, FileMetadata o2) {
return type1.compareTo(type2);
}
};

public void explore(ExternalTool externalTool) {
ApiToken apiToken = null;
User user = session.getUser();
if (user instanceof AuthenticatedUser) {
apiToken = authService.findApiTokenByUser((AuthenticatedUser) user);
}
ExternalToolHandler externalToolHandler = new ExternalToolHandler(externalTool, dataset, apiToken);
String toolUrl = externalToolHandler.getToolUrlWithQueryParams();
logger.fine("Exploring with " + toolUrl);
PrimeFaces.current().executeScript("window.open('"+toolUrl + "', target='_blank');");
}

}
4 changes: 2 additions & 2 deletions src/main/java/edu/harvard/iq/dataverse/FilePage.java
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ public String init() {
if (file.isTabularData()) {
contentType=DataFileServiceBean.MIME_TYPE_TSV_ALT;
}
configureTools = externalToolService.findByType(ExternalTool.Type.CONFIGURE, contentType);
exploreTools = externalToolService.findByType(ExternalTool.Type.EXPLORE, contentType);
configureTools = externalToolService.findFileToolsByTypeAndContentType(ExternalTool.Type.CONFIGURE, contentType);
exploreTools = externalToolService.findFileToolsByTypeAndContentType(ExternalTool.Type.EXPLORE, contentType);

} else {

Expand Down
29 changes: 29 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/Datasets.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,16 @@
import edu.harvard.iq.dataverse.privateurl.PrivateUrl;
import edu.harvard.iq.dataverse.S3PackageImporter;
import static edu.harvard.iq.dataverse.api.AbstractApiBean.error;
import edu.harvard.iq.dataverse.authorization.users.ApiToken;
import edu.harvard.iq.dataverse.batch.util.LoggingUtil;
import edu.harvard.iq.dataverse.dataaccess.StorageIO;
import edu.harvard.iq.dataverse.engine.command.exception.CommandException;
import edu.harvard.iq.dataverse.engine.command.exception.PermissionException;
import edu.harvard.iq.dataverse.engine.command.exception.UnforcedCommandException;
import edu.harvard.iq.dataverse.engine.command.impl.DeleteDataFileCommand;
import edu.harvard.iq.dataverse.engine.command.impl.UpdateDvObjectPIDMetadataCommand;
import edu.harvard.iq.dataverse.externaltools.ExternalTool;
import edu.harvard.iq.dataverse.externaltools.ExternalToolHandler;
import edu.harvard.iq.dataverse.makedatacount.DatasetExternalCitations;
import edu.harvard.iq.dataverse.makedatacount.DatasetExternalCitationsServiceBean;
import edu.harvard.iq.dataverse.makedatacount.DatasetMetrics;
Expand Down Expand Up @@ -1893,5 +1896,31 @@ public Response getMakeDataCountMetric(@PathParam("id") String idSupplied, @Path
}
}

@GET
@Path("{id}/externalTools")
public Response getExternalTools(@PathParam("id") String idSupplied, @QueryParam("type") String typeSupplied) {
ExternalTool.Type type;
try {
type = ExternalTool.Type.fromString(typeSupplied);
} catch (IllegalArgumentException ex) {
return error(BAD_REQUEST, ex.getLocalizedMessage());
}
Dataset dataset;
try {
dataset = findDatasetOrDie(idSupplied);
JsonArrayBuilder tools = Json.createArrayBuilder();
List<ExternalTool> datasetTools = externalToolService.findDatasetToolsByType(type);
for (ExternalTool tool : datasetTools) {
ApiToken apiToken = externalToolService.getApiToken(getRequestApiKey());
ExternalToolHandler externalToolHandler = new ExternalToolHandler(tool, dataset, apiToken);
JsonObjectBuilder toolToJson = externalToolService.getToolAsJsonWithQueryParameters(externalToolHandler);
tools.add(toolToJson);
}
return ok(tools);
} catch (WrappedResponse wr) {
return wr.getResponse();
}
}

}

35 changes: 14 additions & 21 deletions src/main/java/edu/harvard/iq/dataverse/api/ExternalTools.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package edu.harvard.iq.dataverse.api;

import edu.harvard.iq.dataverse.DataFile;
import edu.harvard.iq.dataverse.actionlogging.ActionLogRecord;
import static edu.harvard.iq.dataverse.api.AbstractApiBean.error;
import edu.harvard.iq.dataverse.externaltools.ExternalTool;
import edu.harvard.iq.dataverse.externaltools.ExternalToolServiceBean;
import java.util.List;
import java.util.logging.Logger;
import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.ws.rs.DELETE;
Expand All @@ -19,6 +18,8 @@
@Path("admin/externalTools")
public class ExternalTools extends AbstractApiBean {

private static final Logger logger = Logger.getLogger(ExternalTools.class.getCanonicalName());

@GET
public Response getExternalTools() {
JsonArrayBuilder jab = Json.createArrayBuilder();
Expand All @@ -28,6 +29,17 @@ public Response getExternalTools() {
return ok(jab);
}

@GET
@Path("{id}")
public Response getExternalTool(@PathParam("id") long externalToolIdFromUser) {
ExternalTool externalTool = externalToolService.findById(externalToolIdFromUser);
if (externalTool != null) {
return ok(externalTool.toJson());
} else {
return error(BAD_REQUEST, "Could not find external tool with id of " + externalToolIdFromUser);
}
}

@POST
public Response addExternalTool(String manifest) {
try {
Expand All @@ -53,23 +65,4 @@ public Response deleteExternalTool(@PathParam("id") long externalToolIdFromUser)
}
}

// TODO: Rather than only supporting looking up files by their database IDs, consider supporting persistent identifiers.
@GET
@Path("file/{id}")
public Response getExternalToolsByFile(@PathParam("id") Long fileIdFromUser) {
DataFile dataFile = fileSvc.find(fileIdFromUser);
if (dataFile == null) {
return error(BAD_REQUEST, "Could not find datafile with id " + fileIdFromUser);
}
JsonArrayBuilder tools = Json.createArrayBuilder();

List<ExternalTool> allExternalTools = externalToolService.findAll();
List<ExternalTool> toolsByFile = ExternalToolServiceBean.findExternalToolsByFile(allExternalTools, dataFile);
for (ExternalTool tool : toolsByFile) {
tools.add(tool.toJson());
}

return ok(tools);
}

}
Loading