package edu.harvard.iq.dataverse.api;
import edu.harvard.iq.dataverse.DOIEZIdServiceBean;
import edu.harvard.iq.dataverse.Dataset;
import edu.harvard.iq.dataverse.DatasetField;
import edu.harvard.iq.dataverse.DatasetFieldServiceBean;
import edu.harvard.iq.dataverse.DatasetFieldType;
import edu.harvard.iq.dataverse.DatasetServiceBean;
import edu.harvard.iq.dataverse.DatasetVersion;
import edu.harvard.iq.dataverse.Dataverse;
import edu.harvard.iq.dataverse.MetadataBlock;
import edu.harvard.iq.dataverse.MetadataBlockServiceBean;
import edu.harvard.iq.dataverse.authorization.DataverseRole;
import edu.harvard.iq.dataverse.authorization.RoleAssignee;
import edu.harvard.iq.dataverse.authorization.users.User;
import edu.harvard.iq.dataverse.engine.command.Command;
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
import edu.harvard.iq.dataverse.engine.command.impl.AssignRoleCommand;
import edu.harvard.iq.dataverse.engine.command.impl.CreateDatasetVersionCommand;
import edu.harvard.iq.dataverse.engine.command.impl.CreatePrivateUrlCommand;
import edu.harvard.iq.dataverse.engine.command.impl.DeleteDatasetCommand;
import edu.harvard.iq.dataverse.engine.command.impl.DeleteDatasetVersionCommand;
import edu.harvard.iq.dataverse.engine.command.impl.DeletePrivateUrlCommand;
import edu.harvard.iq.dataverse.engine.command.impl.DestroyDatasetCommand;
import edu.harvard.iq.dataverse.engine.command.impl.GetDatasetCommand;
import edu.harvard.iq.dataverse.engine.command.impl.GetSpecificPublishedDatasetVersionCommand;
import edu.harvard.iq.dataverse.engine.command.impl.GetDraftDatasetVersionCommand;
import edu.harvard.iq.dataverse.engine.command.impl.GetLatestAccessibleDatasetVersionCommand;
import edu.harvard.iq.dataverse.engine.command.impl.GetLatestPublishedDatasetVersionCommand;
import edu.harvard.iq.dataverse.engine.command.impl.GetPrivateUrlCommand;
import edu.harvard.iq.dataverse.engine.command.impl.ListRoleAssignments;
import edu.harvard.iq.dataverse.engine.command.impl.ListVersionsCommand;
import edu.harvard.iq.dataverse.engine.command.impl.PublishDatasetCommand;
import edu.harvard.iq.dataverse.engine.command.impl.SetDatasetCitationDateCommand;
import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetTargetURLCommand;
import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand;
import edu.harvard.iq.dataverse.export.DDIExportServiceBean;
import edu.harvard.iq.dataverse.export.ExportService;
import edu.harvard.iq.dataverse.export.ddi.DdiExportUtil;
import edu.harvard.iq.dataverse.privateurl.PrivateUrl;
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
import edu.harvard.iq.dataverse.util.SystemConfig;
import edu.harvard.iq.dataverse.util.json.JsonParseException;
import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.EJB;
import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path("datasets")
public class Datasets extends AbstractApiBean {
private static final Logger LOGGER = Logger.getLogger(Datasets.class.getName());
private static final String PERSISTENT_ID_KEY=":persistentId";
@EJB
DatasetServiceBean datasetService;
@EJB
DOIEZIdServiceBean doiEZIdServiceBean;
@EJB
DDIExportServiceBean ddiExportService;
@EJB
SystemConfig systemConfig;
@EJB
DatasetFieldServiceBean datasetfieldService;
@EJB
MetadataBlockServiceBean metadataBlockService;
@EJB
SettingsServiceBean settingsService;
/**
* Used to consolidate the way we parse and handle dataset versions.
* @param <T>
*/
private interface DsVersionHandler<T> {
T handleLatest();
T handleDraft();
T handleSpecific( long major, long minor );
T handleLatestPublished();
}
@GET
@Path("{id}")
public Response getDataset(@PathParam("id") String id) {
return response( req -> {
final Dataset retrieved = execCommand(new GetDatasetCommand(req, findDatasetOrDie(id)));
final DatasetVersion latest = execCommand(new GetLatestAccessibleDatasetVersionCommand(req, retrieved));
final JsonObjectBuilder jsonbuilder = json(retrieved);
return ok(jsonbuilder.add("latestVersion", (latest != null) ? json(latest) : null));
});
}
// TODO:
// This API call should, ideally, call findUserOrDie() and the GetDatasetCommand
// to obtain the dataset that we are trying to export - which would handle
// Auth in the process... For now, Auth isn't necessary - since export ONLY
// WORKS on published datasets, which are open to the world. -- L.A. 4.5
@GET
@Path("/export")
@Produces({"application/xml", "application/json"})
public Response exportDataset(@QueryParam("persistentId") String persistentId, @QueryParam("exporter") String exporter) {
try {
Dataset dataset = datasetService.findByGlobalId(persistentId);
if (dataset == null) {
return error(Response.Status.NOT_FOUND, "A dataset with the persistentId " + persistentId + " could not be found.");
}
ExportService instance = ExportService.getInstance();
String xml = instance.getExportAsString(dataset, exporter);
// I'm wondering if this going to become a performance problem
// with really GIANT datasets,
// the fact that we are passing these exports, blobs of JSON, and,
// especially, DDI XML as complete strings. It would be nicer
// if we could stream instead - and the export service already can
// give it to as as a stream; then we could start sending the
// output to the remote client as soon as we got the first bytes,
// without waiting for the whole thing to be generated and buffered...
// (the way Access API streams its output).
// -- L.A., 4.5
LOGGER.fine("xml to return: " + xml);
String mediaType = MediaType.TEXT_PLAIN;
if (instance.isXMLFormat(exporter)){
mediaType = MediaType.APPLICATION_XML;
}
return Response.ok()
.entity(xml)
.type(mediaType).
build();
} catch (Exception wr) {
return error(Response.Status.FORBIDDEN, "Export Failed");
}
}
@DELETE
@Path("{id}")
public Response deleteDataset( @PathParam("id") String id) {
return response( req -> {
execCommand( new DeleteDatasetCommand(req, findDatasetOrDie(id)));
return ok("Dataset " + id + " deleted");
});
}
@DELETE
@Path("{id}/destroy")
public Response destroyDataset( @PathParam("id") String id) {
return response( req -> {
execCommand( new DestroyDatasetCommand(findDatasetOrDie(id), req) );
return ok("Dataset " + id + " destroyed");
});
}
@PUT
@Path("{id}/citationdate")
public Response setCitationDate( @PathParam("id") String id, String dsfTypeName) {
return response( req -> {
if ( dsfTypeName.trim().isEmpty() ){
return badRequest("Please provide a dataset field type in the requst body.");
}
DatasetFieldType dsfType = null;
if (!":publicationDate".equals(dsfTypeName)) {
dsfType = datasetFieldSvc.findByName(dsfTypeName);
if (dsfType == null) {
return badRequest("Dataset Field Type Name " + dsfTypeName + " not found.");
}
}
execCommand(new SetDatasetCitationDateCommand(req, findDatasetOrDie(id), dsfType));
return ok("Citation Date for dataset " + id + " set to: " + (dsfType != null ? dsfType.getDisplayName() : "default"));
});
}
@DELETE
@Path("{id}/citationdate")
public Response useDefaultCitationDate( @PathParam("id") String id) {
return response( req -> {
execCommand(new SetDatasetCitationDateCommand(req, findDatasetOrDie(id), null));
return ok("Citation Date for dataset " + id + " set to default");
});
}
@GET
@Path("{id}/versions")
public Response listVersions( @PathParam("id") String id ) {
return response( req -> {
return ok(
execCommand(
new ListVersionsCommand(req, findDatasetOrDie(id)) )
.stream()
.map( d -> json(d) )
.collect(toJsonArray()));});
}
@GET
@Path("{id}/versions/{versionId}")
public Response getVersion( @PathParam("id") String datasetId, @PathParam("versionId") String versionId) {
return response( req -> {
DatasetVersion dsv = getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId));
return (dsv == null || dsv.getId() == null) ? notFound("Dataset version not found")
: ok(json(dsv));
});
}
@GET
@Path("{id}/versions/{versionId}/files")
public Response getVersionFiles( @PathParam("id") String datasetId, @PathParam("versionId") String versionId) {
return response( req -> ok( jsonFileMetadatas(
getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId)).getFileMetadatas())) );
}
@GET
@Path("{id}/versions/{versionId}/metadata")
public Response getVersionMetadata( @PathParam("id") String datasetId, @PathParam("versionId") String versionId) {
return response( req -> ok(
jsonByBlocks(
getDatasetVersionOrDie(req, versionId, findDatasetOrDie(datasetId) )
.getDatasetFields())));
}
@GET
@Path("{id}/versions/{versionNumber}/metadata/{block}")
public Response getVersionMetadataBlock( @PathParam("id") String datasetId,
@PathParam("versionNumber") String versionNumber,
@PathParam("block") String blockName ) {
return response( req -> {
DatasetVersion dsv = getDatasetVersionOrDie(req, versionNumber, findDatasetOrDie(datasetId) );
Map<MetadataBlock, List<DatasetField>> fieldsByBlock = DatasetField.groupByBlock(dsv.getDatasetFields());
for ( Map.Entry<MetadataBlock, List<DatasetField>> p : fieldsByBlock.entrySet() ) {
if ( p.getKey().getName().equals(blockName) ) {
return ok( json(p.getKey(), p.getValue()) );
}
}
return notFound("metadata block named " + blockName + " not found");
});
}
@DELETE
@Path("{id}/versions/{versionId}")
public Response deleteDraftVersion( @PathParam("id") String id, @PathParam("versionId") String versionId ){
if ( ! ":draft".equals(versionId) ) {
return badRequest("Only the :draft version can be deleted");
}
return response( req -> {
execCommand( new DeleteDatasetVersionCommand(req, findDatasetOrDie(id)) );
return ok("Draft version of dataset " + id + " deleted");
});
}
@GET
@Path("{id}/modifyRegistration")
public Response updateDatasetTargetURL(@PathParam("id") String id ) {
return response( req -> {
execCommand(new UpdateDatasetTargetURLCommand(findDatasetOrDie(id), req));
return ok("Dataset " + id + " target url updated");
});
}
@GET
@Path("/modifyRegistrationAll")
public Response updateDatasetTargetURLAll() {
return response( req -> {
datasetService.findAll().forEach( ds -> {
try {
execCommand(new UpdateDatasetTargetURLCommand(findDatasetOrDie(ds.getId().toString()), req));
} catch (WrappedResponse ex) {
Logger.getLogger(Datasets.class.getName()).log(Level.SEVERE, null, ex);
}
});
return ok("Update All Dataset target url completed");
});
}
@PUT
@Path("{id}/versions/{versionId}")
public Response updateDraftVersion( String jsonBody, @PathParam("id") String id, @PathParam("versionId") String versionId ){
if ( ! ":draft".equals(versionId) ) {
return error( Response.Status.BAD_REQUEST, "Only the :draft version can be updated");
}
try ( StringReader rdr = new StringReader(jsonBody) ) {
DataverseRequest req = createDataverseRequest(findUserOrDie());
Dataset ds = findDatasetOrDie(id);
JsonObject json = Json.createReader(rdr).readObject();
DatasetVersion incomingVersion = jsonParser().parseDatasetVersion(json);
// clear possibly stale fields from the incoming dataset version.
// creation and modification dates are updated by the commands.
incomingVersion.setId(null);
incomingVersion.setVersionNumber(null);
incomingVersion.setMinorVersionNumber(null);
incomingVersion.setVersionState(DatasetVersion.VersionState.DRAFT);
incomingVersion.setDataset(ds);
incomingVersion.setCreateTime(null);
incomingVersion.setLastUpdateTime(null);
boolean updateDraft = ds.getLatestVersion().isDraft();
DatasetVersion managedVersion = execCommand( updateDraft
? new UpdateDatasetVersionCommand(req, incomingVersion)
: new CreateDatasetVersionCommand(req, ds, incomingVersion));
return ok( json(managedVersion) );
} catch (JsonParseException ex) {
LOGGER.log(Level.SEVERE, "Semantic error parsing dataset version Json: " + ex.getMessage(), ex);
return error( Response.Status.BAD_REQUEST, "Error parsing dataset version: " + ex.getMessage() );
} catch (WrappedResponse ex) {
return ex.getResponse();
}
}
@GET
@Path("{id}/actions/:publish")
public Response publishDataset( @PathParam("id") String id, @QueryParam("type") String type ) {
try {
if ( type == null ) {
return error( Response.Status.BAD_REQUEST, "Missing 'type' parameter (either 'major' or 'minor').");
}
type = type.toLowerCase();
boolean isMinor;
switch ( type ) {
case "minor": isMinor = true; break;
case "major": isMinor = false; break;
default: return error( Response.Status.BAD_REQUEST, "Illegal 'type' parameter value '" + type + "'. It needs to be either 'major' or 'minor'.");
}
long dsId;
try {
dsId = Long.parseLong(id);
} catch ( NumberFormatException nfe ) {
return error( Response.Status.BAD_REQUEST, "Bad dataset id. Please provide a number.");
}
Dataset ds = datasetService.find(dsId);
return ( ds == null ) ? notFound("Can't find dataset with id '" + id + "'")
: ok( json(execCommand(new PublishDatasetCommand(ds,
createDataverseRequest(findAuthenticatedUserOrDie()),
isMinor))) );
} catch (WrappedResponse ex) {
return ex.getResponse();
}
}
@GET
@Path("{id}/links")
public Response getLinks(@PathParam("id") String idSupplied ) {
try {
User u = findUserOrDie();
if (!u.isSuperuser()) {
return error(Response.Status.FORBIDDEN, "Not a superuser");
}
Dataset dataset = findDatasetOrDie(idSupplied);
long datasetId = dataset.getId();
List<Dataverse> dvsThatLinkToThisDatasetId = dataverseSvc.findDataversesThatLinkToThisDatasetId(datasetId);
JsonArrayBuilder dataversesThatLinkToThisDatasetIdBuilder = Json.createArrayBuilder();
for (Dataverse dataverse : dvsThatLinkToThisDatasetId) {
dataversesThatLinkToThisDatasetIdBuilder.add(dataverse.getAlias() + " (id " + dataverse.getId() + ")");
}
JsonObjectBuilder response = Json.createObjectBuilder();
response.add("dataverses that link to dataset id " + datasetId, dataversesThatLinkToThisDatasetIdBuilder);
return ok(response);
} catch (WrappedResponse wr) {
return wr.getResponse();
}
}
/**
* @todo Implement this for real as part of
* https://github.com/IQSS/dataverse/issues/2579
*/
@GET
@Path("ddi")
@Produces({"application/xml", "application/json"})
@Deprecated
public Response getDdi(@QueryParam("id") long id, @QueryParam("persistentId") String persistentId, @QueryParam("dto") boolean dto) {
boolean ddiExportEnabled = systemConfig.isDdiExportEnabled();
if (!ddiExportEnabled) {
return error(Response.Status.FORBIDDEN, "Disabled");
}
try {
User u = findUserOrDie();
if (!u.isSuperuser()) {
return error(Response.Status.FORBIDDEN, "Not a superuser");
}
LOGGER.fine("looking up " + persistentId);
Dataset dataset = datasetService.findByGlobalId(persistentId);
if (dataset == null) {
return error(Response.Status.NOT_FOUND, "A dataset with the persistentId " + persistentId + " could not be found.");
}
String xml = "<codeBook>XML_BEING_COOKED</codeBook>";
if (dto) {
/**
* @todo We can only assume that this should not be hard-coded
* to getLatestVersion
*/
final JsonObjectBuilder datasetAsJson = jsonAsDatasetDto(dataset.getLatestVersion());
xml = DdiExportUtil.datasetDtoAsJson2ddi(datasetAsJson.toString());
} else {
OutputStream outputStream = new ByteArrayOutputStream();
ddiExportService.exportDataset(dataset.getId(), outputStream, null, null);
xml = outputStream.toString();
}
LOGGER.fine("xml to return: " + xml);
return Response.ok()
.entity(xml)
.type(MediaType.APPLICATION_XML).
build();
} catch (WrappedResponse wr) {
return wr.getResponse();
}
}
/**
* @todo Make this real. Currently only used for API testing. Copied from
* the equivalent API endpoint for dataverses and simplified with values
* hard coded.
*/
@POST
@Path("{identifier}/assignments")
public Response createAssignment(String userOrGroup, @PathParam("identifier") String id, @QueryParam("key") String apiKey) {
boolean apiTestingOnly = true;
if (apiTestingOnly) {
return error(Response.Status.FORBIDDEN, "This is only for API tests.");
}
try {
Dataset dataset = findDatasetOrDie(id);
RoleAssignee assignee = findAssignee(userOrGroup);
if (assignee == null) {
return error(Response.Status.BAD_REQUEST, "Assignee not found");
}
DataverseRole theRole = rolesSvc.findBuiltinRoleByAlias("admin");
String privateUrlToken = null;
return ok(
json(execCommand(new AssignRoleCommand(assignee, theRole, dataset, createDataverseRequest(findUserOrDie()), privateUrlToken))));
} catch (WrappedResponse ex) {
LOGGER.log(Level.WARNING, "Can''t create assignment: {0}", ex.getMessage());
return ex.getResponse();
}
}
@GET
@Path("{identifier}/assignments")
public Response getAssignments(@PathParam("identifier") String id) {
return response( req ->
ok( execCommand(
new ListRoleAssignments(req, findDatasetOrDie(id)))
.stream().map(ra->json(ra)).collect(toJsonArray())) );
}
@GET
@Path("{id}/privateUrl")
public Response getPrivateUrlData(@PathParam("id") String idSupplied) {
return response( req -> {
PrivateUrl privateUrl = execCommand(new GetPrivateUrlCommand(req, findDatasetOrDie(idSupplied)));
return (privateUrl != null) ? ok(json(privateUrl))
: error(Response.Status.NOT_FOUND, "Private URL not found.");
});
}
@POST
@Path("{id}/privateUrl")
public Response createPrivateUrl(@PathParam("id") String idSupplied) {
return response( req ->
ok(json(execCommand(
new CreatePrivateUrlCommand(req, findDatasetOrDie(idSupplied))))));
}
@DELETE
@Path("{id}/privateUrl")
public Response deletePrivateUrl(@PathParam("id") String idSupplied) {
return response( req -> {
Dataset dataset = findDatasetOrDie(idSupplied);
PrivateUrl privateUrl = execCommand(new GetPrivateUrlCommand(req, dataset));
if (privateUrl != null) {
execCommand(new DeletePrivateUrlCommand(req, dataset));
return ok("Private URL deleted.");
} else {
return notFound("No Private URL to delete.");
}
});
}
private Dataset findDatasetOrDie( String id ) throws WrappedResponse {
Dataset dataset;
if ( id.equals(PERSISTENT_ID_KEY) ) {
String persistentId = getRequestParameter(PERSISTENT_ID_KEY.substring(1));
if ( persistentId == null ) {
throw new WrappedResponse(
badRequest("When accessing a dataset based on persistent id, "
+ "a " + PERSISTENT_ID_KEY.substring(1) + " query parameter "
+ "must be present"));
}
dataset = datasetService.findByGlobalId(persistentId);
if (dataset == null) {
throw new WrappedResponse( notFound("dataset " + persistentId + " not found") );
}
return dataset;
} else {
try {
dataset = datasetService.find( Long.parseLong(id) );
if (dataset == null) {
throw new WrappedResponse( notFound("dataset " + id + " not found") );
}
return dataset;
} catch ( NumberFormatException nfe ) {
throw new WrappedResponse(
badRequest("Bad dataset id number: '" + id + "'"));
}
}
}
private <T> T handleVersion( String versionId, DsVersionHandler<T> hdl )
throws WrappedResponse {
switch (versionId) {
case ":latest": return hdl.handleLatest();
case ":draft": return hdl.handleDraft();
case ":latest-published": return hdl.handleLatestPublished();
default:
try {
String[] versions = versionId.split("\\.");
switch (versions.length) {
case 1:
return hdl.handleSpecific(Long.parseLong(versions[0]), (long)0.0);
case 2:
return hdl.handleSpecific( Long.parseLong(versions[0]), Long.parseLong(versions[1]) );
default:
throw new WrappedResponse(error( Response.Status.BAD_REQUEST, "Illegal version identifier '" + versionId + "'"));
}
} catch ( NumberFormatException nfe ) {
throw new WrappedResponse( error( Response.Status.BAD_REQUEST, "Illegal version identifier '" + versionId + "'") );
}
}
}
private DatasetVersion getDatasetVersionOrDie( final DataverseRequest req, String versionNumber, final Dataset ds ) throws WrappedResponse {
DatasetVersion dsv = execCommand( handleVersion(versionNumber, new DsVersionHandler<Command<DatasetVersion>>(){
@Override
public Command<DatasetVersion> handleLatest() {
return new GetLatestAccessibleDatasetVersionCommand(req, ds);
}
@Override
public Command<DatasetVersion> handleDraft() {
return new GetDraftDatasetVersionCommand(req, ds);
}
@Override
public Command<DatasetVersion> handleSpecific(long major, long minor) {
return new GetSpecificPublishedDatasetVersionCommand(req, ds, major, minor);
}
@Override
public Command<DatasetVersion> handleLatestPublished() {
return new GetLatestPublishedDatasetVersionCommand(req, ds);
}
}));
if ( dsv == null || dsv.getId() == null ) {
throw new WrappedResponse( notFound("Dataset version " + versionNumber + " of dataset " + ds.getId() + " not found") );
}
return dsv;
}
}